Skip to content

Demo companies: three staged clients, 30-day histories, runbook, daily warm pulse - #186

Merged
ericlovold merged 15 commits into
mainfrom
claude/sanction-mcp-vision-vyn7iy
Jul 12, 2026
Merged

Demo companies: three staged clients, 30-day histories, runbook, daily warm pulse#186
ericlovold merged 15 commits into
mainfrom
claude/sanction-mcp-vision-vyn7iy

Conversation

@ericlovold

@ericlovold ericlovold commented Jul 12, 2026

Copy link
Copy Markdown
Owner

What this is

The demo-companies arc (docs/plans/demo-companies.md), PR2+PR3+prod-hardening — the driver's PR1 landed in #184. Three fictional clients now live in production under one Demo HQ, built entirely through the public REST API:

  • Meridian Analytics (internal governance): departments as pools, Engineering at ~87% of its pooled token cap, a seat denied over its own line, escalations staged.
  • Coastline Digital (agency): channels as pools, tagged spend, bookings as outcomes, one channel throttled live by its cost-per-outcome ceiling, one frozen.
  • Harbor & Wren LLP (regulated): an already-expired contractor seat failing closed 401, clearance-3 vault injection, a $450 retainer + docusign.send waiting on the partner.

In this PR

  • scripts/demo/: history --days N (API-driven days + quarantined direct-DB backdate that keeps WalletBudgetCounter coherent; requires explicit DATABASE_URL), prime (API-only ceiling arming for DB-less targets — how prod got staged), Coastline + Harbor & Wren manifests, HQ keys in a shared store, date-suffixed outcome dedupe for daily re-pulses.
  • docs/DEMO-RUNBOOK.md: per-client click-paths with wow moments, the admin subtree tour, caveats learned by running it.
  • .github/workflows/demo-pulse.yml: daily warm pulse against prod from the DEMO_KEYS_JSON secret. Every pulse asserts the engine's decisions, so a red run doubles as a production smoke alarm. Scheduled workflows fire from the default branch — merging this is what activates the cron.

Verification

  • Fresh local stack: 3 seeds + 3×30-day histories + 3 pulses, zero expectation mismatches
  • Audit chain verifies across backdated decisions (valid: true, 32 entries); sequential simulation over the real month flips 4/10 decisions under a $50 draft cap
  • Production: all three companies seeded + pulsed live; Bluebird's $12 charge escalates COST_PER_OUTCOME_CEILING post-prime; the audit page (the Prod patch: audit-page crash fix + red-CI fix, sanction-mcp 0.6.0, demo-company driver, MCP vision specs #184 fix) renders vault-injection rows at HTTP 200
  • npm run check green

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added demo scenarios for Coastline Digital and Harbor & Wren LLP alongside Meridian.
    • Expanded demo tooling with multi-persona support plus new history, status, and prime commands.
    • Enabled advanced demo behaviors including staged outcomes, expired seats, frozen budgets, escalations, restricted credentials, and backdated history.
    • Added an automated “Demo Pulse” run (manual + daily) for ongoing demo health checks.
  • Documentation
    • Updated the demo runbook with end-to-end walkthroughs and operational caveats.
    • Refreshed the demo backlog status and remaining tasks.
  • Chores
    • Prevented demo local key files from being committed and added tsx for development.

claude added 14 commits July 11, 2026 23:03
Captures Eric's 2026-07-11 direction — Apollo-grade connector enable,
org scan for shadow AI / seats, autofill seat-plan recommendations,
manual path over the same roster — as a phased build spec, verified
against shipped primitives (SEATS-1, provision ladder, subtree caps,
PACK-1, KILL-1, the promoted hosted-remote-MCP item).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
…rding spec

Eric's 2026-07-11 strategy addition: after connect + scan, the org gets
monitoring + recommendations with zero governance assigned. Track C adds
Policy.enforcementMode (enforce|observe, shadow decisions logged, nothing
blocked) and a monitoring digest whose upgrade recommendations replay the
org's own observed contexts via the shipped simulation engine. Migration
flips pool-by-pool from observe to enforce; readiness ladder becomes the
migration UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
… on TS2554

sanctionTool<T> returns T, so a zero-param inner execute types the wrapped
execute zero-param too; calling it with {} failed tsc. Every main CI run
since #178 was failing on this line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
…lease

npm still serves 0.4.0; the announced 0.5.0 was never published (and
server.json had stayed at 0.4.0, so the MCP registry would have been
mispinned even if it had been). Jump straight to 0.6.0 per Eric: bump
package.json, server.json (both fields), the serverInfo version, rebuild
the committed bundle, and update the changelog's two version claims so
the public record names the version that will actually exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
Eric's 2026-07-12 direction: fully-loaded mock client accounts he can
click through and demo — data passed A→B through the real engine, not
fixtures. Three personas mapped to the three buyer stories; driver =
seed/pulse/history over pure REST; admin oversight via the shipped
subtree views; /login-by-mgmt-key as the per-company demo door.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
…feed

Outside a tenant transaction the joined CredentialVault row is hidden by
RLS and Prisma returns null for the required relation — a child pool's
injection crashed the org owner's Audit page. Found live by the demo-
company driver's first full run. Regression test pins it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
PR1 of docs/plans/demo-companies.md. REST-only (the customer's own
surface): seed builds HQ → company → pools → seats → vault with per-
wallet plus-addressed owner emails and wide HQ guardrails (ancestor
policies fold into every decision); pulse drives a self-verifying
day-in-the-life — token burn to ~87% of Engineering's pooled cap, a
seat denied over its own line, the spend ladder end to end, an owner
approval redeemed through the grant loop, exec→inject — and leaves two
escalations pending so every demo opens with something to approve.
pulse --watch polls the staged escalations and completes them live as
the owner clicks approve. Keys land in a gitignored key store.

Verified end-to-end against local Postgres: all expectations green,
audit export verifies (signed, chain-valid), dashboard render-checked
via headless Chromium.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
…ision-vyn7iy

# Conflicts:
#	docs/BACKLOG.md
#	sdk/src/adapters.test.ts
history <persona> --days N drives each backdated day through the real
engine, then shifts timestamps with a quarantined direct-DB pass
(scripts/demo/backdate.ts) that also resets WalletBudgetCounter so
enforcement state stays coherent; requires an explicit DATABASE_URL so
it can never silently touch an API-only target.

Coastline Digital stages the agency story: channels as pools with
attribution tags, bookings as outcomes, one channel throttled live by
its cost-per-outcome ceiling (min_outcomes cold-start guard holds it
silent until the 3rd booking lands), one channel frozen on stage
(WALLET_FROZEN). Harbor & Wren LLP stages the regulated story: an
already-expired contractor seat failing closed 401, clearance-3 vault
injection, tight ladders, a $450 retainer + docusign.send left pending.
HQ keys move to a shared store so all personas nest under one root.

Verified against a fresh local stack: 3 seeds + 3 backdated months +
3 pulses with zero expectation mismatches; sequential simulation over
the real month flips 4/10 decisions under a $50 cap; the audit chain
verifies across 32 decisions including backdated ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
docs/DEMO-RUNBOOK.md: bootstrap commands, the three client click-paths
with the wow moments marked (live approve→grant→complete, the CPO
throttle, the fail-closed contractor, the verifiable evidence export),
the HQ admin tour, and the caveats learned by running it (freeze
denials don't persist as rows; open on Pools, not Overview).

.github/workflows/demo-pulse.yml: daily cron re-pulses all three
companies against prod from the DEMO_KEYS_JSON secret — dashboards stay
warm and, because every pulse asserts the engine's decisions, a red run
is a daily production smoke alarm. Outcome dedupe keys now carry a date
suffix so daily re-pulses record new bookings instead of deduping into
day one (validated with a same-day re-pulse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
Production has no DATABASE_URL access from the driver, so the CPO
ceiling's cold-start guard (min_outcomes) kept Bluebird silent. prime
stages it through the API alone: today's approved spends supply the
ratio's numerator and occurred_at-backdated bookings satisfy the
outcome floor. Verified live: post-prime, Bluebird's $12 charge
escalates COST_PER_OUTCOME_CEILING on getsanction.com.

Also widens the key-store gitignore to the .local-keys/ stash directory
(the previous tip briefly committed local-container-only dev keys —
worthless outside this sandbox, and rewritten out of the branch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sanction Ready Ready Preview, Comment Jul 12, 2026 4:38pm

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 82ba89ff-9d80-4cb8-bf1c-188847daf3f5

📥 Commits

Reviewing files that changed from the base of the PR and between aaf947b and 599f7bc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • .github/workflows/demo-pulse.yml
  • docs/DEMO-RUNBOOK.md
  • package.json
  • scripts/demo/lib.ts
  • scripts/demo/personas/coastline.ts
  • scripts/demo/personas/harborwren.ts
  • scripts/demo/personas/meridian.ts
  • scripts/demo/run.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • .github/workflows/demo-pulse.yml
  • scripts/demo/personas/meridian.ts
  • scripts/demo/lib.ts
  • docs/DEMO-RUNBOOK.md
  • scripts/demo/personas/coastline.ts
  • scripts/demo/personas/harborwren.ts
  • scripts/demo/run.ts

📝 Walkthrough

Walkthrough

The demo system now models three companies with generated histories, staged outcomes, expired seats, frozen pools, and pulse expectations. The runner adds HQ key persistence, priming, history, status, and multi-persona support, with scheduled GitHub Actions execution and supporting operational documentation.

Changes

Demo fleet lifecycle

Layer / File(s) Summary
Persona manifests and scenario contracts
scripts/demo/lib.ts, scripts/demo/personas/*
Manifest types support history, outcomes, priming, expired seats, frozen pools, and HQ keys; Meridian, Coastline, and Harbor & Wren define scenario-specific histories and pulse states.
Multi-persona runner lifecycle
scripts/demo/run.ts
Wallet provisioning, seat expiration, shared validation, priming, history execution, pulse checks, and CLI commands are expanded for all three personas.
Database history backdating
scripts/demo/backdate.ts
Selected demo records are timestamp-shifted transactionally, outcome timing is aligned, and wallet budget counters are reset.
Scheduled demo operations and runbook
.github/workflows/demo-pulse.yml, .gitignore, docs/BACKLOG.md, docs/DEMO-RUNBOOK.md, package.json
Scheduled and manual pulse execution restores secret-backed demo keys, while dependency setup and documentation cover operation, walkthroughs, administration, and caveats.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant DemoRunner
  participant KeyStore
  participant SanctionAPI
  GitHubActions->>KeyStore: restore DEMO_KEYS_JSON files
  GitHubActions->>DemoRunner: run pulse for meridian, coastline, and harborwren
  DemoRunner->>SanctionAPI: stage and verify tokens, spends, outcomes, and injections
  SanctionAPI-->>DemoRunner: return decisions and outcomes
  DemoRunner-->>GitHubActions: report expectation mismatches
Loading

Possibly related PRs

  • ericlovold/sanction#184: Introduces the demo driver that this multi-persona seeding, history, and pulse flow extends.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change set: three demo companies, historical staging, a runbook, and the daily warm pulse workflow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sanction-mcp-vision-vyn7iy

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
scripts/demo/personas/meridian.ts (1)

86-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated "rough weekday" formula into a shared helper.

(day + 3) % 7 < 5 is copy-pasted identically in coastline.ts and harborwren.ts. Consider exporting a single isWeekday(day: number) from lib.ts so the three personas can't silently drift.

♻️ Proposed refactor
// lib.ts
+export function isWeekday(dayIndex: number): boolean {
+  return (dayIndex + 3) % 7 < 5
+}
// meridian.ts / coastline.ts / harborwren.ts
-    const weekday = (day + 3) % 7 < 5
+    const weekday = isWeekday(day)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/demo/personas/meridian.ts` around lines 86 - 113, Extract the shared
rough-weekday calculation into an exported isWeekday(day: number) helper in
lib.ts, then update history in meridian.ts and the corresponding coastline.ts
and harborwren.ts logic to call it instead of evaluating (day + 3) % 7 < 5
directly. Preserve the existing weekday behavior.
scripts/demo/run.ts (1)

42-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse createWallet for pool provisioning too.

The pool block (unchanged lines 105-115) duplicates the same POST /wallets + response-shape handling that createWallet now encapsulates. Since both paths must stay contract-compatible with the same API, keeping them in sync manually risks silent drift.

♻️ Proposed refactor
-      const { status, json } = await call<{ id: string; management_key: string }>("/wallets", {
-        auth: { mgmt: company.mgmtKey },
-        body: { name: pool.name, owner_email: demoEmail(`${persona.key}-${pool.name.split("/").pop()}`), parent_id: company.walletId },
-      })
-      if (status !== 201 && status !== 200) fail(`create pool "${pool.name}" → ${status} ${JSON.stringify(json)}`)
-      keys.pools[pool.name] = { walletId: json.id, mgmtKey: json.management_key }
+      keys.pools[pool.name] = await createWallet(
+        pool.name,
+        demoEmail(`${persona.key}-${pool.name.split("/").pop()}`),
+        company,
+      )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/demo/run.ts` around lines 42 - 56, The pool provisioning block should
reuse the existing createWallet function instead of duplicating the POST
/wallets request and response handling. Replace its manual wallet creation logic
with a createWallet call, passing the pool name, email, and parent credentials
as appropriate, while preserving the current provisioning behavior.
scripts/demo/backdate.ts (1)

22-78: 🚀 Performance & Scalability | 🔵 Trivial

Solid transactional design; consider index coverage for the scan predicates.

The transaction wrapping, rollback-on-error, and the OutcomeEvent.createdAt = occurredAt alignment plus WalletBudgetCounter reset are all sound given the stated fabrication-lite constraints. One operational note: every UPDATE here filters on "agentId" = ANY($2) AND "createdAt" >= $3 (or "walletId" = ANY($1) AND "createdAt" >= $2) against AuthorizationRequest, TokenLog, PendingApproval, Grant, and OutcomeEvent — worth confirming these columns are indexed (composite (agentId, createdAt) / (walletId, createdAt)) so repeated backdate passes stay cheap as these production tables grow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/demo/backdate.ts` around lines 22 - 78, Confirm and add composite
indexes covering the backdate scan predicates used by backdateWindow: (agentId,
createdAt) on AuthorizationRequest, TokenLog, PendingApproval, and Grant, and
(walletId, createdAt) on OutcomeEvent. Use the project’s existing schema or
migration conventions and avoid changing the transactional update behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/demo-pulse.yml:
- Line 28: Update the workflow step using actions/checkout@v4 to disable
credential persistence with persist-credentials: false, and add job-level
permissions granting only contents: read.
- Around line 18-21: Add a workflow-level concurrency group to the
scheduled/manual workflow so same-day runs cannot queue or overlap. In
scripts/demo/run.ts, check the date-suffixed outcome dedupe state before
processing and skip execution when that date was already processed, preserving
normal processing for new dates.
- Line 56: Update the demo pulse workflow around the `npx -y tsx` invocation to
install dependencies with `npm ci` first and execute the lockfile-pinned local
`tsx` dependency via the offline npm execution path. Add `tsx` as a direct
dependency and ensure it is recorded in the lockfile, replacing the runtime
registry fetch.

In `@docs/DEMO-RUNBOOK.md`:
- Around line 15-18: Update the bootstrap instructions in the demo runbook to
document the persona-specific required order: run seed, then history for
database-backed targets or prime for DB-less targets, and only then pulse.
Ensure the sequence explicitly includes prime before pulse for Coastline and
related DB-less personas.

In `@scripts/demo/personas/coastline.ts`:
- Around line 150-167: The Bluebird seed must retain at least three qualifying
bookings as the 30-day window advances, and the prime comment must use the
correct $180 spend total. Update the Bluebird `prime` outcomes to provide
ongoing in-window coverage without requiring `prime` to be rerun, and correct
the explanatory math from approximately $204 to $180.

In `@scripts/demo/run.ts`:
- Around line 2-20: Document the dispatched prime command in the header command
list and the CLI usage/error string near the command dispatcher. Describe its
expected persona argument and API-only behavior consistently with the existing
command descriptions, while preserving the current prime dispatch
implementation.
- Around line 353-377: The history() loop needs resumable progress handling
before backdateWindow() so failed runs do not replay completed days or leave
current-time rows stranded. Add a persistent completion marker or equivalent
idempotency/cleanup guard keyed by persona and day, consult it before processing
each day, and record completion only after backdateWindow() succeeds; preserve
the existing mismatch failure behavior and outcome deduplication.

---

Nitpick comments:
In `@scripts/demo/backdate.ts`:
- Around line 22-78: Confirm and add composite indexes covering the backdate
scan predicates used by backdateWindow: (agentId, createdAt) on
AuthorizationRequest, TokenLog, PendingApproval, and Grant, and (walletId,
createdAt) on OutcomeEvent. Use the project’s existing schema or migration
conventions and avoid changing the transactional update behavior.

In `@scripts/demo/personas/meridian.ts`:
- Around line 86-113: Extract the shared rough-weekday calculation into an
exported isWeekday(day: number) helper in lib.ts, then update history in
meridian.ts and the corresponding coastline.ts and harborwren.ts logic to call
it instead of evaluating (day + 3) % 7 < 5 directly. Preserve the existing
weekday behavior.

In `@scripts/demo/run.ts`:
- Around line 42-56: The pool provisioning block should reuse the existing
createWallet function instead of duplicating the POST /wallets request and
response handling. Replace its manual wallet creation logic with a createWallet
call, passing the pool name, email, and parent credentials as appropriate, while
preserving the current provisioning behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f60bc4d2-5e63-4c16-ab24-33ddbbbc7e25

📥 Commits

Reviewing files that changed from the base of the PR and between 5782a96 and aaf947b.

📒 Files selected for processing (10)
  • .github/workflows/demo-pulse.yml
  • .gitignore
  • docs/BACKLOG.md
  • docs/DEMO-RUNBOOK.md
  • scripts/demo/backdate.ts
  • scripts/demo/lib.ts
  • scripts/demo/personas/coastline.ts
  • scripts/demo/personas/harborwren.ts
  • scripts/demo/personas/meridian.ts
  • scripts/demo/run.ts

Comment on lines +18 to +21
on:
workflow_dispatch:
schedule:
- cron: "0 13 * * *" # daily, morning US

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workflow file ==\n'
nl -ba .github/workflows/demo-pulse.yml | sed -n '1,220p'

printf '\n== search for demo pulse references ==\n'
rg -n "demo[- ]pulse|production pulse|same-day|budget expectations|concurrency|workflow_dispatch|schedule" . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

printf '\n== workflow-aware structural summary ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/demo-pulse.yml')
text = p.read_text()
print('contains concurrency:', 'concurrency:' in text)
print('contains workflow_dispatch:', 'workflow_dispatch:' in text)
print('contains schedule:', 'schedule:' in text)
PY

Repository: ericlovold/sanction

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workflow file ==\n'
cat -n .github/workflows/demo-pulse.yml | sed -n '1,220p'

printf '\n== search for demo pulse references ==\n'
rg -n "demo[- ]pulse|production pulse|same-day|budget expectations|concurrency|workflow_dispatch|schedule" . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

printf '\n== workflow-aware structural summary ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/demo-pulse.yml')
text = p.read_text()
print('contains concurrency:', 'concurrency:' in text)
print('contains workflow_dispatch:', 'workflow_dispatch:' in text)
print('contains schedule:', 'schedule:' in text)
PY

Repository: ericlovold/sanction

Length of output: 6458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== demo runbook ==\n'
cat -n docs/DEMO-RUNBOOK.md | sed -n '1,220p'

printf '\n== demo plans ==\n'
cat -n docs/plans/demo-companies.md | sed -n '70,120p'

printf '\n== demo pulse script references ==\n'
rg -n "processed date|already-processed|same-day|date|concurrency|pulse|workflow_dispatch|schedule" scripts docs .github/workflows/demo-pulse.yml -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

printf '\n== pulse script outline ==\n'
fd -a "run.ts" scripts/demo

Repository: ericlovold/sanction

Length of output: 26288


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== pulse implementation ==\n'
cat -n scripts/demo/run.ts | sed -n '230,330p'

printf '\n== any date/state guard in demo scripts ==\n'
rg -n "processed date|already-processed|dedupeSuffix|slice\(0, 10\)|re-pulses|same day|manual pulse|skip" scripts/demo scripts -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

Repository: ericlovold/sanction

Length of output: 6760


Prevent duplicate same-day pulses.
This workflow is both scheduled and manually dispatchable, but it has no concurrency lock, and scripts/demo/run.ts only date-suffixes outcome dedupe keys. A queued rerun can still replay tokens, spends, and tools for the same day and trip the budget-denial expectations called out in the runbook. Add workflow concurrency and a processed-date skip in the runner.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/demo-pulse.yml around lines 18 - 21, Add a workflow-level
concurrency group to the scheduled/manual workflow so same-day runs cannot queue
or overlap. In scripts/demo/run.ts, check the date-suffixed outcome dedupe state
before processing and skip execution when that date was already processed,
preserving normal processing for new dates.

Comment thread .github/workflows/demo-pulse.yml
Comment thread .github/workflows/demo-pulse.yml Outdated
Comment thread docs/DEMO-RUNBOOK.md
Comment on lines +150 to +167
// For DB-less targets (prod): arm Bluebird's ceiling through the API alone —
// today's approved spend supplies the ratio's numerator, three backdated
// bookings (occurred_at) satisfy min_outcomes. Windowed: ~$204 / 3 ≈ $68 >
// the $60 ceiling, so the next spend escalates. `history` supersedes this
// when direct DB access exists.
prime: {
spends: [
{ seat: "bluebird-media-agent", action: "purchase", amount_usd: 45, merchant: "Google Ads", category: "marketing", description: "Search defense — week 1 batch", tags: { channel: "bluebird", play: "branded-search" }, expect: "approved" },
{ seat: "bluebird-media-agent", action: "purchase", amount_usd: 45, merchant: "Google Ads", category: "marketing", description: "Search defense — week 2 batch", tags: { channel: "bluebird", play: "branded-search" }, expect: "approved" },
{ seat: "bluebird-media-agent", action: "purchase", amount_usd: 45, merchant: "Google Ads", category: "marketing", description: "Search defense — week 3 batch", tags: { channel: "bluebird", play: "branded-search" }, expect: "approved" },
{ seat: "bluebird-media-agent", action: "purchase", amount_usd: 45, merchant: "Google Ads", category: "marketing", description: "Search defense — week 4 batch", tags: { channel: "bluebird", play: "branded-search" }, expect: "approved" },
],
outcomes: [
{ seat: "bluebird-media-agent", kind: "booking", value_usd: 180, play: "branded-search", dedupe_key: "bluebird-booking-p20", days_ago: 20 },
{ seat: "bluebird-media-agent", kind: "booking", value_usd: 180, play: "branded-search", dedupe_key: "bluebird-booking-p10", days_ago: 10 },
{ seat: "bluebird-media-agent", kind: "booking", value_usd: 180, play: "branded-search", dedupe_key: "bluebird-booking-p1", days_ago: 1 },
],
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the scheduled workflow / runbook re-runs `prime` periodically
fd -e yml -e md . .github docs 2>/dev/null | xargs -I{} sh -c 'echo "== {} =="; grep -n -i "prime" {}'

Repository: ericlovold/sanction

Length of output: 1244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== coastline.ts ==\n'
wc -l scripts/demo/personas/coastline.ts
sed -n '130,190p' scripts/demo/personas/coastline.ts

printf '\n== demo-pulse.yml ==\n'
wc -l .github/workflows/demo-pulse.yml
sed -n '1,220p' .github/workflows/demo-pulse.yml

printf '\n== DEMO-RUNBOOK.md ==\n'
wc -l docs/DEMO-RUNBOOK.md
sed -n '1,120p' docs/DEMO-RUNBOOK.md

Repository: ericlovold/sanction

Length of output: 11845


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== coastline.ts excerpt ==\n'
sed -n '1,260p' scripts/demo/personas/coastline.ts

printf '\n== demo-pulse workflow ==\n'
sed -n '1,240p' .github/workflows/demo-pulse.yml

printf '\n== runbook mentions ==\n'
grep -RniE 'prime|pulse|coastline|bluebird' docs/DEMO-RUNBOOK.md docs 2>/dev/null | sed -n '1,120p'

Repository: ericlovold/sanction

Length of output: 15312


🌐 Web query:

ericlovold/sanction scripts/demo/personas/coastline.ts demo-pulse.yml DEMO-RUNBOOK prime bluebird

💡 Result:

The references provided in your query correspond to a development environment using the Coasts tool [1][2] and the Sanction MCP server [3][4][5], which is authored by Eric Lovold [6][7]. Based on the structure of the paths and files provided: 1. ericlovold/sanction: This refers to the Sanction project, an open-source MCP (Model Context Protocol) server designed to act as an agent wallet, credential vault, and governance layer for autonomous AI agents [3][4][5]. 2. demo/personas/coastline.ts: This is a TypeScript file likely located within the demo folder of the Sanction repository or a related project. Given the context of "coastline," it is highly probable that this script is used to define or simulate agent personas or interaction patterns within a Coasts-based development environment [1][2]. 3. demo-pulse.yml: This is a configuration file, likely used for pulse-based monitoring or demonstration setups, often found in projects integrating with PulseMCP or similar monitoring/orchestration systems [5][8]. 4. DEMO-RUNBOOK: This typically refers to a markdown or text document (e.g., RUNBOOK.md) used to guide a live demonstration, detailing the sequence of commands, check-lists, and recovery steps [9]. 5. prime bluebird: These appear to be specific labels, environment identifiers, or internal names used for the demo scenario (e.g., "prime" might indicate a primary environment or persona, and "bluebird" could be the codename for a specific service or simulation branch) [9][10]. To run this demonstration, you would typically follow the procedures outlined in the DEMO-RUNBOOK and utilize the coast CLI tools (e.g., coast run, coast assign) to provision the necessary containers for the Sanction server and its demo components [1][10][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "cost_per_outcome_(ceiling|window_days|min_outcomes)|outcome.*ceiling|min_outcomes|pending" scripts docs .github -g '!**/*.json'

Repository: ericlovold/sanction

Length of output: 9842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== spend rule ==\n'
sed -n '1,260p' scripts/lib.ts 2>/dev/null || true
sed -n '1,260p' lib/rules/spend.ts 2>/dev/null || true

printf '\n== outcomes context ==\n'
sed -n '1,260p' lib/outcomes.ts 2>/dev/null || true

printf '\n== coastline usage ==\n'
rg -n "bluebird-booking|prime:|pulse:" scripts/demo/personas/coastline.ts scripts/demo/run.ts docs/DEMO-RUNBOOK.md docs/plans/demo-companies.md

Repository: ericlovold/sanction

Length of output: 11536


Keep Bluebird’s CPO seed from expiring
The seeded Bluebird bookings are added once, and the daily pulse only replays spend. After ~10 days, the oldest booking drops out of the 30-day window, min_outcomes falls below 3, and Bluebird stops escalating again until prime is rerun. The comment’s math is also off: 4 × $45 = $180, not $204.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/demo/personas/coastline.ts` around lines 150 - 167, The Bluebird seed
must retain at least three qualifying bookings as the 30-day window advances,
and the prime comment must use the correct $180 spend total. Update the Bluebird
`prime` outcomes to provide ongoing in-window coverage without requiring `prime`
to be rerun, and correct the explanatory math from approximately $204 to $180.

Comment thread scripts/demo/run.ts
Comment thread scripts/demo/run.ts
Comment on lines +353 to +377
async function history(persona: Persona, days: number) {
if (!persona.history) fail(`persona "${persona.key}" has no history generator`)
const keys = loadKeys(persona.key)
if (!keys.company) fail(`no keys for "${persona.key}" — run seed first`)
const { backdateWindow } = await import("./backdate")
const agentIds = Object.values(keys.seats).map((s) => s.agentId)
const walletIds = [keys.company.walletId, ...Object.values(keys.pools).map((p) => p.walletId)]

console.log(`Generating ${days - 1} backdated days for ${persona.company} (oldest first)…`)
for (let day = days - 1; day >= 1; day--) {
const plan = persona.history(day)
const nothing = !plan.tokens.length && !plan.spends.length && !(plan.outcomes?.length ?? 0)
if (nothing) continue
const batchStart = new Date(Date.now() - 2000)
const occurredAt = new Date(Date.now() - day * 24 * 3600 * 1000).toISOString()
const c = makeChecker()
await runTokens(keys, plan.tokens, c)
await runSpends(keys, plan.spends, c, { stagePending: false })
if (plan.outcomes?.length) await runOutcomes(keys, plan.outcomes, c, { occurredAt })
if (c.mismatches()) fail(`day -${day}: ${c.mismatches()} expectation mismatch(es) — stopping before backdate`)
const shifted = await backdateWindow({ since: batchStart, days: day, agentIds, walletIds })
console.log(` day -${day}: ${shifted.authRows} decisions + ${shifted.tokenRows} token logs shifted`)
}
console.log(`History complete. Run pulse for today's staged state.`)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files | rg '^(scripts/demo/run\.ts|scripts/demo/.*backdate|scripts/demo/.*keys|scripts/demo/.*history|scripts/demo/.*token|scripts/demo/.*spend|scripts/demo/.*outcome)'

printf '\n== outline run.ts ==\n'
ast-grep outline scripts/demo/run.ts --view expanded

printf '\n== targeted lines run.ts ==\n'
sed -n '300,430p' scripts/demo/run.ts

printf '\n== search helper defs/usages ==\n'
rg -n 'function (runTokens|runSpends|runOutcomes|history|backdateWindow|loadKeys|saveKeys|dedupe|stagePending|mismatches|fail\()' scripts/demo -S

Repository: ericlovold/sanction

Length of output: 8322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backdate.ts ==\n'
wc -l scripts/demo/backdate.ts scripts/demo/lib.ts scripts/demo/run.ts
printf '\n-- backdate.ts outline --\n'
ast-grep outline scripts/demo/backdate.ts --view expanded
printf '\n-- lib.ts outline --\n'
ast-grep outline scripts/demo/lib.ts --view expanded

printf '\n== backdate.ts contents ==\n'
sed -n '1,220p' scripts/demo/backdate.ts

printf '\n== lib.ts contents (relevant) ==\n'
sed -n '1,240p' scripts/demo/lib.ts

printf '\n== helper functions in run.ts ==\n'
sed -n '180,280p' scripts/demo/run.ts

printf '\n== search for progress/dedupe/history state ==\n'
rg -n 'pending|progress|resume|dedupe|dedup|history|last-completed|completed day|sidecar|backdateWindow|occurredAt' scripts/demo -S

Repository: ericlovold/sanction

Length of output: 23548


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search for models and endpoints ==\n'
rg -n 'TokenLog|AuthorizationRequest|OutcomeEvent|dedupe_key|dedupeKey|PendingApproval|Grant|/tokens|/authorize|/outcomes' . -S --glob '!scripts/demo/**'

printf '\n== possible schema files ==\n'
git ls-files | rg '(^prisma/|schema\.prisma$|drizzle|migrations|packages/.*/schema|src/.*/schema|server/.*/schema)'

printf '\n== targeted unique constraint search ==\n'
rg -n 'unique|@@unique|`@unique`|dedupe' . -S --glob '!scripts/demo/**'

Repository: ericlovold/sanction

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tokens route outline + relevant lines ==\n'
ast-grep outline app/api/v1/tokens/route.ts --view expanded
sed -n '1,220p' app/api/v1/tokens/route.ts

printf '\n== authorize route outline + relevant lines ==\n'
ast-grep outline app/api/v1/authorize/route.ts --view expanded
sed -n '1,260p' app/api/v1/authorize/route.ts

printf '\n== persona history generators ==\n'
for f in scripts/demo/personas/*.ts; do
  echo "--- $f"
  rg -n 'history:\s*\(|dedupe_key|tokens:|spends:|outcomes:' "$f" -n -A40 -B8
done

Repository: ericlovold/sanction

Length of output: 35644


Make history() resumable before backdating scripts/demo/run.ts:353-377

A failure before backdateWindow() leaves that day’s token logs and authorization rows at “now”, and reruns start from days - 1 again with no progress marker. outcomes are already idempotent via dedupe_key, but the failed run still leaves the original row stranded if the shift never happens. Persist the last completed day or add a cleanup/idempotency guard so retries don’t replay earlier days and leave mixed timestamps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/demo/run.ts` around lines 353 - 377, The history() loop needs
resumable progress handling before backdateWindow() so failed runs do not replay
completed days or leave current-time rows stranded. Add a persistent completion
marker or equivalent idempotency/cleanup guard keyed by persona and day, consult
it before processing each day, and record completion only after backdateWindow()
succeeds; preserve the existing mismatch failure behavior and outcome
deduplication.

Applied: checkout persist-credentials:false + contents:read permissions
(prod keys sit next to executed code); tsx pinned in the lockfile and
run via npm ci + npx --no-install (no registry fetch-and-execute);
workflow concurrency group (no overlapping pulses); prime documented in
the CLI header/usage; pool creation reuses createWallet; shared
isWeekday helper across personas; runbook states the per-target order
(seed → history|prime → pulse).

The real find: Bluebird's CPO arming decays — a throttled channel
approves nothing, so its windowed spend only ages out and the throttle
disarms in ~10 days (seeded bookings left the 30-day window even
sooner). Tightened prime bookings to 5/3/1 days ago (~25-day hold),
fixed the ratio math for fresh targets (4×$48=$192 → $64 > $60),
date-scoped the prime dedupe keys so re-priming mints fresh rows, and
documented the red-run→re-prime maintenance beat. The decay itself is
honest engine behavior, so it's operated around, not patched over.

Skipped with reason: history() resumability (fresh-target staging tool;
documented recovery is wipe/reseed — the mismatch guard already stops
before any backdate) and backdate index additions (AuthorizationRequest,
TokenLog, PendingApproval, Grant already carry (agentId, createdAt)
indexes; OutcomeEvent has (walletId, kind, occurredAt) — demo-scale).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjvLUvHRKVDKbYgZEHWMQ
@ericlovold
ericlovold merged commit 1246e79 into main Jul 12, 2026
8 checks passed
ericlovold pushed a commit that referenced this pull request Jul 12, 2026
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.

2 participants