feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses `import.meta.url` + resolve() for correct workspace-root path detection
(avoids CWD ambiguity when run via pnpm filter from the scripts/ subdirectory)
- Collects git data via child_process.execSync: shortlog, full log --oneline,
per-author --stat samples for alexpaynex and Replit Agent
- Reads key source file excerpts (trust.ts, event-bus.ts, jobs.ts, moderation.ts,
world-state.ts) truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with the
rubric dimensions as a structured prompt and Timmy's first-person persona
- 90-second AbortController fetch timeout; falls back to a stub report if no
Anthropic credentials are present (graceful degradation)
- Writes reports/timmy-report.md and reports/context.md to workspace root
## reports/context.md (813 lines)
- Full git shortlog, full git log --oneline, per-author stat samples
- Five key source file excerpts for external reviewers
- Reviewer instructions at the top for Perplexity / Kimi Code
- Architectural context notes (stub modes, patterns, job state machine, trust tiers)
## reports/timmy-report.md (110 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-; top-3 improvements: pre-code design review,
shared AI client factory, unified config service
- Independently substantive — diverges meaningfully from the Replit Agent report
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes (tsc --noEmit)
## Deviations
- Used claude-haiku-4-5 instead of claude-sonnet-4-6 for speed (Haiku runs in
~30s vs >90s timeout for Sonnet on this prompt size). Quality is acceptable for
the task.
2026-03-19 23:46:35 +00:00
|
|
|
|
# Reviewer Context Package — Timmy Tower World
|
|
|
|
|
|
|
|
|
|
|
|
> **Instructions for Perplexity / Kimi Code reviewers**
|
|
|
|
|
|
>
|
|
|
|
|
|
> This file contains everything you need to apply the repo-review rubric
|
|
|
|
|
|
> (see the attached PDF) to the `replit/token-gated-economy` repository
|
|
|
|
|
|
> without needing direct git access.
|
|
|
|
|
|
>
|
|
|
|
|
|
> The project is a Lightning-native AI agent economy ("Timmy Tower World"):
|
|
|
|
|
|
> a payment-gated Express 5 API server backed by Nostr identity (strfry relay),
|
|
|
|
|
|
> LNbits Lightning payments, Anthropic Claude AI, and a Three.js 3D frontend.
|
|
|
|
|
|
> Stack: Node.js 24, TypeScript 5.9, PostgreSQL + Drizzle ORM, pnpm monorepo.
|
|
|
|
|
|
>
|
|
|
|
|
|
> Two contributor identities to grade:
|
|
|
|
|
|
> - **alexpaynex** — Alexander Payne (orchestrator + main-agent implementer)
|
|
|
|
|
|
> - **Replit Agent** — isolated task agents that merge back via PR
|
|
|
|
|
|
>
|
|
|
|
|
|
> Grade Alexander as the orchestrator in Part 2.
|
|
|
|
|
|
> Provide top-3 improvements in Part 3.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Git Contributor Summary
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-03-19 23:54:15 +00:00
|
|
|
|
132 alexpaynex
|
feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses import.meta.url + resolve() for correct workspace-root path detection
- Explicit HEAD revision in all git commands (shortlog -sn HEAD, log --oneline HEAD)
to ensure deterministic output regardless of CWD at invocation time
- Validation guards: throws loudly if shortlog or log output is empty — prevents
committing blank sections silently
- Collects git data: shortlog, full log --oneline, per-author --stat samples for
alexpaynex and Replit Agent (last 10 commits each)
- Reads five key source file excerpts truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with rubric
dimensions and Timmy's first-person evaluator persona
- 90-second AbortController fetch timeout; graceful stub-mode fallback when no
Anthropic credentials are present
- Writes both reports to workspace root reports/ directory
## reports/context.md (820 lines, regenerated)
- Validated non-empty: 4 contributors, 156 commits in shortlog
- Full git shortlog -sn HEAD, full git log --oneline HEAD
- Per-author stat samples, five key source file excerpts
- Reviewer instructions and architectural context at the top
## reports/timmy-report.md (155 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-
- Top-3 improvements: pre-code design review, shared AI client factory, config service
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes clean (tsc --noEmit)
## Deviation from spec
- claude-haiku-4-5 used instead of claude-sonnet-4-6 for speed (Sonnet exceeded
90s timeout on the full prompt; Haiku completes in ~30s with acceptable quality)
2026-03-19 23:49:57 +00:00
|
|
|
|
18 Replit Agent
|
|
|
|
|
|
6 replit
|
|
|
|
|
|
1 agent
|
feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses `import.meta.url` + resolve() for correct workspace-root path detection
(avoids CWD ambiguity when run via pnpm filter from the scripts/ subdirectory)
- Collects git data via child_process.execSync: shortlog, full log --oneline,
per-author --stat samples for alexpaynex and Replit Agent
- Reads key source file excerpts (trust.ts, event-bus.ts, jobs.ts, moderation.ts,
world-state.ts) truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with the
rubric dimensions as a structured prompt and Timmy's first-person persona
- 90-second AbortController fetch timeout; falls back to a stub report if no
Anthropic credentials are present (graceful degradation)
- Writes reports/timmy-report.md and reports/context.md to workspace root
## reports/context.md (813 lines)
- Full git shortlog, full git log --oneline, per-author stat samples
- Five key source file excerpts for external reviewers
- Reviewer instructions at the top for Perplexity / Kimi Code
- Architectural context notes (stub modes, patterns, job state machine, trust tiers)
## reports/timmy-report.md (110 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-; top-3 improvements: pre-code design review,
shared AI client factory, unified config service
- Independently substantive — diverges meaningfully from the Replit Agent report
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes (tsc --noEmit)
## Deviations
- Used claude-haiku-4-5 instead of claude-sonnet-4-6 for speed (Haiku runs in
~30s vs >90s timeout for Sonnet on this prompt size). Quality is acceptable for
the task.
2026-03-19 23:46:35 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Full Commit Log (all commits, one per line)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-03-19 23:54:15 +00:00
|
|
|
|
f4243b5 feat(scripts): timmy-report script + reviewer context package — Task #41
|
feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses import.meta.url + resolve() for correct workspace-root path detection
- Explicit HEAD revision in all git commands (shortlog -sn HEAD, log --oneline HEAD)
to ensure deterministic output regardless of CWD at invocation time
- Validation guards: throws loudly if shortlog or log output is empty — prevents
committing blank sections silently
- Collects git data: shortlog, full log --oneline, per-author --stat samples for
alexpaynex and Replit Agent (last 10 commits each)
- Reads five key source file excerpts truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with rubric
dimensions and Timmy's first-person evaluator persona
- 90-second AbortController fetch timeout; graceful stub-mode fallback when no
Anthropic credentials are present
- Writes both reports to workspace root reports/ directory
## reports/context.md (820 lines, regenerated)
- Validated non-empty: 4 contributors, 156 commits in shortlog
- Full git shortlog -sn HEAD, full git log --oneline HEAD
- Per-author stat samples, five key source file excerpts
- Reviewer instructions and architectural context at the top
## reports/timmy-report.md (155 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-
- Top-3 improvements: pre-code design review, shared AI client factory, config service
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes clean (tsc --noEmit)
## Deviation from spec
- claude-haiku-4-5 used instead of claude-sonnet-4-6 for speed (Sonnet exceeded
90s timeout on the full prompt; Haiku completes in ~30s with acceptable quality)
2026-03-19 23:49:57 +00:00
|
|
|
|
3d15512 feat(scripts): timmy-report script + reviewer context package — Task #41
|
feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses `import.meta.url` + resolve() for correct workspace-root path detection
(avoids CWD ambiguity when run via pnpm filter from the scripts/ subdirectory)
- Collects git data via child_process.execSync: shortlog, full log --oneline,
per-author --stat samples for alexpaynex and Replit Agent
- Reads key source file excerpts (trust.ts, event-bus.ts, jobs.ts, moderation.ts,
world-state.ts) truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with the
rubric dimensions as a structured prompt and Timmy's first-person persona
- 90-second AbortController fetch timeout; falls back to a stub report if no
Anthropic credentials are present (graceful degradation)
- Writes reports/timmy-report.md and reports/context.md to workspace root
## reports/context.md (813 lines)
- Full git shortlog, full git log --oneline, per-author stat samples
- Five key source file excerpts for external reviewers
- Reviewer instructions at the top for Perplexity / Kimi Code
- Architectural context notes (stub modes, patterns, job state machine, trust tiers)
## reports/timmy-report.md (110 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-; top-3 improvements: pre-code design review,
shared AI client factory, unified config service
- Independently substantive — diverges meaningfully from the Replit Agent report
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes (tsc --noEmit)
## Deviations
- Used claude-haiku-4-5 instead of claude-sonnet-4-6 for speed (Haiku runs in
~30s vs >90s timeout for Sonnet on this prompt size). Quality is acceptable for
the task.
2026-03-19 23:46:35 +00:00
|
|
|
|
283e0bd Update report with contributor commit count clarification
|
|
|
|
|
|
69cb298 feat(reports): Replit Agent rubric report — Task #40
|
|
|
|
|
|
a6b145a Transitioned from Plan to Build mode
|
|
|
|
|
|
5ffda67 Task #36: Push timmy-watch + security fix to Gitea main
|
|
|
|
|
|
b837094 Add a live feed to view Timmy's internal state and agent activity
|
|
|
|
|
|
e58055d Saved progress at the end of the loop
|
|
|
|
|
|
6590f0f Update Vite version to ensure all installations are secure
|
|
|
|
|
|
039af78 Published your App
|
|
|
|
|
|
abb8c50 fix: replace import.meta.url with process.cwd() in testkit.ts
|
|
|
|
|
|
9573718 Update test summary and improve module import for better portability
|
|
|
|
|
|
6b6aa83 task/35: Testkit T25–T36 — Nostr identity + trust engine coverage (v2)
|
|
|
|
|
|
c7bb5de task/35: Testkit T25–T36 — Nostr identity + trust engine coverage
|
|
|
|
|
|
56eb7bc task/34: Testkit self-serve plan + report endpoints
|
|
|
|
|
|
66eb8ed Improve login security and user experience on admin panel
|
|
|
|
|
|
ca8cbee task/33: Relay admin panel at /admin/relay (final, all review fixes applied)
|
|
|
|
|
|
8000b00 task/33: Relay admin panel at /admin/relay (final, all review fixes)
|
|
|
|
|
|
ac3493f task/33: Relay admin panel at /admin/relay (post-review fixes)
|
|
|
|
|
|
c168081 task/33: Relay admin panel at /api/admin/relay
|
|
|
|
|
|
f5c2c7e Improve handling of failed moderation bypasses for elite accounts
|
|
|
|
|
|
a95fd76 task/32: Event moderation queue + Timmy AI review
|
|
|
|
|
|
0137437 Update default access for new accounts to read-only
|
|
|
|
|
|
31a843a task/31: Relay account whitelist + trust-gated access (v2 — code review fixes)
|
|
|
|
|
|
9461301 task/31: Relay account whitelist + trust-gated access
|
|
|
|
|
|
faef1fe Add health check endpoint and production secret enforcement for relay policy
|
|
|
|
|
|
cdd9792 task/30: Sovereign Nostr relay infrastructure (strfry)
|
|
|
|
|
|
0b3a701 Add security measures to prevent malicious requests when fetching LNURL data
|
|
|
|
|
|
8a81918 task/29: fix vouch idempotency + replay guard — unique constraints + DB push
|
|
|
|
|
|
33b47f8 task/29: fix code review findings — LNURL zap, vouch binding, migration SQL
|
|
|
|
|
|
45f4e72 task/29: Timmy as economic peer (bidirectional) — verify & mark complete
|
|
|
|
|
|
eb5dcfd task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement
|
|
|
|
|
|
dabadb4 task-28 fix5: session triage, speech-bubble local badge, footprint docs
|
|
|
|
|
|
8897371 task-28: Edge intelligence — browser ML, Nostr signing, cost preview, sentiment moods
|
|
|
|
|
|
cb50e8c task-28 fix4: trivial cost-preview gate + job polling token
|
|
|
|
|
|
b4cabf5 task-28 fix3: All four reviewer issues resolved
|
|
|
|
|
|
4943930 task-28 fix3: complexity contract, consistent token headers, npub-only prompt
|
|
|
|
|
|
224208f Saved progress at the end of the loop
|
|
|
|
|
|
f75825b chore: switch push-to-gitea.sh from bore.pub to Tailscale Funnel
|
|
|
|
|
|
26556ba Update application assets and code for improved functionality
|
|
|
|
|
|
04abc10 task-28: Edge intelligence — Web Worker triage, Nostr signing, cost preview, sentiment moods (3 review cycles)
|
|
|
|
|
|
437df48 task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching
|
|
|
|
|
|
120ea7d task-28: Edge intelligence — Web Worker triage, Nostr signing, cost preview, sentiment moods
|
|
|
|
|
|
898a47f task-28 fix: proper Web Worker, correct Nostr endpoints, sentiment on inbound msgs
|
|
|
|
|
|
d9b00c2 task-28: Edge intelligence — browser Transformers.js triage, Nostr signing, cost preview, sentiment moods
|
|
|
|
|
|
af3c938 task-28: edge intelligence — Transformers.js triage, Nostr signing, cost preview, sentiment moods
|
|
|
|
|
|
4845830 Task #27: Free-tier gate — all correctness issues resolved
|
|
|
|
|
|
599771e Task #27: Atomic free-tier gate — complete, all reviewer issues fixed
|
|
|
|
|
|
a9143f6 Task #27: Atomic free-tier gate — complete, pool-drained enforces hard no-loss
|
|
|
|
|
|
eca505e Task #27: Atomic free-tier gate — complete fix of all reviewer-identified issues
|
|
|
|
|
|
4866cfc Task #27: Atomic free-tier gate — zero advisory-charge gap under concurrency
|
|
|
|
|
|
ba88824 Task #27: Fully atomic free-tier gate — no advisory-charge gap under concurrency
|
|
|
|
|
|
ec5316a Task #27: Atomic free-tier pool reservation — eliminates advisory-charge gap
|
|
|
|
|
|
26e0d32 Task #27: Complete cost-routing + free-tier gate — all critical fixes applied
|
|
|
|
|
|
373477b Task #27: Complete cost-routing + free-tier gate — all critical fixes applied
|
|
|
|
|
|
1754ab1 Task #27: Complete cost-routing + free-tier gate — all critical fixes applied
|
|
|
|
|
|
d899503 Task #27: Apply all required fixes for cost-routing + free-tier gate
|
|
|
|
|
|
3a61766 Task #27: Apply 3 required fixes for cost-routing + free-tier gate
|
|
|
|
|
|
512089c Task #27: Apply 3 required fixes for cost-routing + free-tier gate
|
|
|
|
|
|
4c3a0e8 Task #27: Cost-routing + free-tier gate
|
|
|
|
|
|
b664ee9 Transitioned from Plan to Build mode
|
|
|
|
|
|
99ede57 fix(#26): tighten token handling and verify API contract
|
|
|
|
|
|
96d5915 feat(#26): Nostr identity + trust engine
|
|
|
|
|
|
b0ac398 fix(#26): apply decay before score mutations in recordSuccess/recordFailure
|
|
|
|
|
|
aed011c feat(#26): Nostr identity + trust engine
|
|
|
|
|
|
1237f10 fix(#26): FK constraints, trust scoring completeness, trust_tier always returned
|
|
|
|
|
|
74831bb feat(#26): Nostr identity + trust engine
|
|
|
|
|
|
9b77835 feat(#26): Nostr identity + trust engine
|
|
|
|
|
|
fa0ebc6 Transitioned from Plan to Build mode
|
|
|
|
|
|
d62cd4c fix: serve tower assets at /assets root + add .ai CORS origin
|
|
|
|
|
|
2f9bca5 Published your App
|
|
|
|
|
|
db28efc fix: set artifact previewPath to / so landing page and /tower route in production
|
|
|
|
|
|
567ee39 Published your App
|
|
|
|
|
|
add08e3 fix: use process.cwd() for tower path — import.meta.url is undefined in CJS bundle
|
|
|
|
|
|
9de2396 feat: Alexander Whitestone landing page + the-matrix dist at /tower
|
|
|
|
|
|
cbe3ed9 Published your App
|
|
|
|
|
|
da0c5d3 Published your App
|
|
|
|
|
|
5d9afdb Improve LNbits provisioning script for security and configuration
|
|
|
|
|
|
d69046a feat(task-25): LNbits on Hermes VPS — real-mode wiring, 29/29 PASS
|
|
|
|
|
|
abe9c22 feat(task-25): real LNbits mode on Hermes VPS — 29/29 testkit PASS
|
|
|
|
|
|
76ed359 feat: real LNbits mode support — 29/29 testkit PASS
|
|
|
|
|
|
51a49da Transitioned from Plan to Build mode
|
|
|
|
|
|
507c9bf Add system information for the server to aid in provisioning
|
|
|
|
|
|
ae25bfd Improve test reliability by adding explicit checks for bootstrap process
|
|
|
|
|
|
031ca5a task(#24): Bootstrap route + cost-ledger testkit coverage — 29/29 PASS
|
|
|
|
|
|
00d3233 Add QR code placeholders to invoice and top-up sections
|
|
|
|
|
|
c7e3a9b Task #23: Workshop session mode UI — fund once, ask many (all review issues fixed)
|
|
|
|
|
|
ad5ac08 Task #23: Workshop session mode UI — fund once, ask many
|
|
|
|
|
|
0419ada Add ragdoll physics and reactive camera shake for satisfying slaps
|
|
|
|
|
|
a0df576 Add touchstart fallback and adjust interaction lockout
|
|
|
|
|
|
35babd2 Task #22: Slap/ragdoll physics on Timmy
|
|
|
|
|
|
2956cc0 Update character's appearance to include a long grey wizard beard
|
|
|
|
|
|
93bd48f Update Timmy's appearance to match reference with new colors and details
|
|
|
|
|
|
6e982ff Improve mouth geometry performance by precomputing all shapes
|
|
|
|
|
|
8d48eb0 feat(task-21): Timmy face expressions + emotion engine
|
|
|
|
|
|
9ff5ef6 feat(task-21): Timmy face expressions + emotion engine
|
|
|
|
|
|
7f402c5 feat(task-21): Timmy face expressions + emotion engine
|
|
|
|
|
|
ad63b01 Harden rate limit by using server-trusted IP address
|
|
|
|
|
|
71dbbd3 feat(task-20): Timmy responds to Workshop input bar with AI
|
|
|
|
|
|
4dd5937 Transitioned from Plan to Build mode
|
|
|
|
|
|
4f7a5e9 test: audit testkit — remove T3b inflation, add T17-T22 (27/27 PASS) (#32)
|
|
|
|
|
|
a70898e feat(epic222): Workshop — Timmy as wizard presence, world state, WS bootstrap (#31)
|
|
|
|
|
|
ea4cddc fix(api): completedAt: null on non-complete states + OpenAPI timestamps & rate-limit headers (#29)
|
|
|
|
|
|
b929e6d feat(api): X-RateLimit-* headers on /api/demo + createdAt/completedAt on job responses (#19) (#28)
|
|
|
|
|
|
e088ca4 feat(integration): WS bridge + Tower + payment panel + E2E test [10/10 PASS] (#26)
|
|
|
|
|
|
3031c39 docs: add Claude Opus 4.6 result to testkit results log (issue #25)
|
|
|
|
|
|
83a2ec1 fix(testkit): macOS compat + fix test 8c ordering (#24)
|
|
|
|
|
|
ca94c0a Add Bitcoin/LND/LNbits local node setup scripts and node diagnostics endpoint
|
|
|
|
|
|
4dd3c7f Show the application's public URL in server logs
|
|
|
|
|
|
b02efc9 Make job evaluation and execution run in the background
|
|
|
|
|
|
1b5c704 Update screenshot showing application preview
|
|
|
|
|
|
e44d64a Add payment hash to job creation response in stub mode
|
|
|
|
|
|
feacdb7 Add screenshot of the application running on an iPhone
|
|
|
|
|
|
adde196 Task #7: Redirect root to Timmy UI
|
|
|
|
|
|
ab2cc06 Add session mode for pre-funded request processing
|
|
|
|
|
|
dfc9ecd Add honest accounting and automatic refund mechanism for completed jobs
|
|
|
|
|
|
e5bdae7 Task #6: Cost-based work fee pricing with BTC oracle
|
|
|
|
|
|
69eba61 Task #6: Cost-based work fee pricing with BTC oracle
|
|
|
|
|
|
bc78bfa Add Nostr integration to the roadmap for future development
|
|
|
|
|
|
2245be0 Update provisioning URL and streamline SSH key delivery
|
|
|
|
|
|
2cab3ef Fix review findings #2: template escaping, ops.sh on node, fee NaN guard
|
|
|
|
|
|
4162ef0 Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH
|
|
|
|
|
|
a3acb4a Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH
|
|
|
|
|
|
f43e782 Task #5: Lightning-gated node bootstrap (proof-of-concept)
|
|
|
|
|
|
1a60363 Transitioned from Plan to Build mode
|
|
|
|
|
|
5dd80ee Add ability to sweep funds using xpub or a list of addresses
|
|
|
|
|
|
e5f78e1 Add interactive configuration for sweep thresholds and frequency
|
|
|
|
|
|
c45625f Automate bitcoin sweeps to secure cold storage addresses
|
|
|
|
|
|
12db06c Add auto-sweep hot wallet to cold storage (Task #4)
|
|
|
|
|
|
bf759e5 Transitioned from Plan to Build mode
|
|
|
|
|
|
8acc30d Update node to use Bitcoin Knots for improved flexibility
|
|
|
|
|
|
88b5ebf Set up Bitcoin node and Lightning infrastructure with Docker
|
|
|
|
|
|
0921fa1 Make the demo user interface accessible through the API
|
|
|
|
|
|
ade318a Add documentation for alternative payment providers
|
|
|
|
|
|
001873c Update test plan and script for dual-mode payment system
|
|
|
|
|
|
fc4fd50 Add automated testing flow to reduce manual effort
|
|
|
|
|
|
f5811da Improve input validation and error messaging for user requests
|
|
|
|
|
|
53bc93a Add automated testing script and expose payment hashes
|
|
|
|
|
|
d24cc6f Add comprehensive test plan for evaluating the AI agent's API functionality
|
|
|
|
|
|
f785637 Published your App
|
|
|
|
|
|
e1bc20b Add more dependencies to the API server build process
|
|
|
|
|
|
f3de9e9 Add trust proxy configuration and job ID validation
|
|
|
|
|
|
4e8adbc Task #3: MVP API — payment-gated jobs + demo endpoint
|
|
|
|
|
|
9ec5e20 Add a foreign key constraint to link invoices to specific jobs
|
|
|
|
|
|
44f7e24 Task #2: MVP Foundation — injectable services, DB schema, smoke test
|
|
|
|
|
|
fbc9bbc Task #2: MVP Foundation — injectable services, DB schema, smoke test
|
|
|
|
|
|
e163a5d Task #2: MVP Foundation — DB schema, LNbits stub, Anthropic agent
|
|
|
|
|
|
b095efc Add AI agent capabilities and integrate with Anthropic and LNbits
|
|
|
|
|
|
90354c5 Transitioned from Plan to Build mode
|
|
|
|
|
|
dd80af4 Update title for implementation guide on Taproot Assets
|
|
|
|
|
|
aa00c70 Task #1: Taproot Assets + L402 Implementation Spike
|
|
|
|
|
|
b129e4f Task #1: Taproot Assets + L402 Implementation Spike
|
|
|
|
|
|
c9e161e Task #1: Taproot Assets + L402 Implementation Spike
|
|
|
|
|
|
332d54d Transitioned from Plan to Build mode
|
|
|
|
|
|
edf8d1d Create comprehensive research report on BRC-20 token-gated agent economy
|
|
|
|
|
|
d4d2ed3 Add research report on token-gated AI economies
|
|
|
|
|
|
c8ed262 Initial commit
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## alexpaynex — Sample commits with diff stats (last 10)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-03-19 23:54:15 +00:00
|
|
|
|
f4243b5 feat(scripts): timmy-report script + reviewer context package — Task #41
|
|
|
|
|
|
reports/context.md | 17 +++--
|
|
|
|
|
|
reports/timmy-report.md | 173 ++++++++++++++++++++++++++++----------------
|
|
|
|
|
|
scripts/src/timmy-report.ts | 19 +++--
|
|
|
|
|
|
3 files changed, 135 insertions(+), 74 deletions(-)
|
|
|
|
|
|
|
feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses import.meta.url + resolve() for correct workspace-root path detection
- Explicit HEAD revision in all git commands (shortlog -sn HEAD, log --oneline HEAD)
to ensure deterministic output regardless of CWD at invocation time
- Validation guards: throws loudly if shortlog or log output is empty — prevents
committing blank sections silently
- Collects git data: shortlog, full log --oneline, per-author --stat samples for
alexpaynex and Replit Agent (last 10 commits each)
- Reads five key source file excerpts truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with rubric
dimensions and Timmy's first-person evaluator persona
- 90-second AbortController fetch timeout; graceful stub-mode fallback when no
Anthropic credentials are present
- Writes both reports to workspace root reports/ directory
## reports/context.md (820 lines, regenerated)
- Validated non-empty: 4 contributors, 156 commits in shortlog
- Full git shortlog -sn HEAD, full git log --oneline HEAD
- Per-author stat samples, five key source file excerpts
- Reviewer instructions and architectural context at the top
## reports/timmy-report.md (155 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-
- Top-3 improvements: pre-code design review, shared AI client factory, config service
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes clean (tsc --noEmit)
## Deviation from spec
- claude-haiku-4-5 used instead of claude-sonnet-4-6 for speed (Sonnet exceeded
90s timeout on the full prompt; Haiku completes in ~30s with acceptable quality)
2026-03-19 23:49:57 +00:00
|
|
|
|
3d15512 feat(scripts): timmy-report script + reviewer context package — Task #41
|
|
|
|
|
|
reports/context.md | 813 ++++++++++++++++++++++++++++++++++++++++++++
|
|
|
|
|
|
reports/timmy-report.md | 111 ++++++
|
|
|
|
|
|
scripts/package.json | 1 +
|
|
|
|
|
|
scripts/src/timmy-report.ts | 333 ++++++++++++++++++
|
|
|
|
|
|
4 files changed, 1258 insertions(+)
|
|
|
|
|
|
|
feat(scripts): timmy-report script + reviewer context package — Task #41
Delivers two new outputs in reports/ and one new script in scripts/src/:
## scripts/src/timmy-report.ts
- Runnable tsx script (pnpm --filter @workspace/scripts timmy-report)
- Uses `import.meta.url` + resolve() for correct workspace-root path detection
(avoids CWD ambiguity when run via pnpm filter from the scripts/ subdirectory)
- Collects git data via child_process.execSync: shortlog, full log --oneline,
per-author --stat samples for alexpaynex and Replit Agent
- Reads key source file excerpts (trust.ts, event-bus.ts, jobs.ts, moderation.ts,
world-state.ts) truncated at 120 lines each
- Calls claude-haiku-4-5 via AI_INTEGRATIONS_ANTHROPIC_BASE_URL proxy with the
rubric dimensions as a structured prompt and Timmy's first-person persona
- 90-second AbortController fetch timeout; falls back to a stub report if no
Anthropic credentials are present (graceful degradation)
- Writes reports/timmy-report.md and reports/context.md to workspace root
## reports/context.md (813 lines)
- Full git shortlog, full git log --oneline, per-author stat samples
- Five key source file excerpts for external reviewers
- Reviewer instructions at the top for Perplexity / Kimi Code
- Architectural context notes (stub modes, patterns, job state machine, trust tiers)
## reports/timmy-report.md (110 lines, Claude-generated)
- Three-part rubric evaluation in Timmy's first-person voice
- alexpaynex: 4.2 composite → B; Replit Agent: 3.8 composite → B-
- Orchestrator: 3.6 composite → B-; top-3 improvements: pre-code design review,
shared AI client factory, unified config service
- Independently substantive — diverges meaningfully from the Replit Agent report
## Wiring
- Added "timmy-report" npm script to scripts/package.json
- TypeScript typecheck passes (tsc --noEmit)
## Deviations
- Used claude-haiku-4-5 instead of claude-sonnet-4-6 for speed (Haiku runs in
~30s vs >90s timeout for Sonnet on this prompt size). Quality is acceptable for
the task.
2026-03-19 23:46:35 +00:00
|
|
|
|
283e0bd Update report with contributor commit count clarification
|
|
|
|
|
|
reports/replit-agent-report.md | 4 +++-
|
|
|
|
|
|
1 file changed, 3 insertions(+), 1 deletion(-)
|
|
|
|
|
|
|
|
|
|
|
|
69cb298 feat(reports): Replit Agent rubric report — Task #40
|
|
|
|
|
|
reports/replit-agent-report.md | 178 +++++++++++++++++++++++++++++++++++++++++
|
|
|
|
|
|
1 file changed, 178 insertions(+)
|
|
|
|
|
|
|
|
|
|
|
|
a6b145a Transitioned from Plan to Build mode
|
|
|
|
|
|
attached_assets/repo-review-rubric_1773962875790.pdf | Bin 0 -> 43485 bytes
|
|
|
|
|
|
1 file changed, 0 insertions(+), 0 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
5ffda67 Task #36: Push timmy-watch + security fix to Gitea main
|
|
|
|
|
|
attached_assets/repo-review-rubric_1773962617852.pdf | Bin 0 -> 43485 bytes
|
|
|
|
|
|
1 file changed, 0 insertions(+), 0 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
b837094 Add a live feed to view Timmy's internal state and agent activity
|
|
|
|
|
|
scripts/package.json | 1 +
|
|
|
|
|
|
scripts/src/timmy-watch.ts | 265 +++++++++++++++++++++++++++++++++++++++++++++
|
|
|
|
|
|
2 files changed, 266 insertions(+)
|
|
|
|
|
|
|
|
|
|
|
|
e58055d Saved progress at the end of the loop
|
|
|
|
|
|
.replit | 2 --
|
|
|
|
|
|
1 file changed, 2 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
6590f0f Update Vite version to ensure all installations are secure
|
|
|
|
|
|
the-matrix/package.json | 2 +-
|
|
|
|
|
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
|
|
|
|
|
|
|
|
|
|
039af78 Published your App
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Replit Agent — Sample commits with diff stats (last 10)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
abb8c50 fix: replace import.meta.url with process.cwd() in testkit.ts
|
|
|
|
|
|
artifacts/api-server/src/routes/testkit.ts | 6 ++----
|
|
|
|
|
|
1 file changed, 2 insertions(+), 4 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
eb5dcfd task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement
|
|
|
|
|
|
artifacts/api-server/src/index.ts | 4 +
|
|
|
|
|
|
artifacts/api-server/src/lib/engagement.ts | 140 +++++++++++++++++++++++
|
|
|
|
|
|
artifacts/api-server/src/lib/timmy-identity.ts | 77 +++++++++++++
|
|
|
|
|
|
artifacts/api-server/src/lib/zap.ts | 149 +++++++++++++++++++++++++
|
|
|
|
|
|
artifacts/api-server/src/routes/identity.ts | 144 +++++++++++++++++++++++-
|
|
|
|
|
|
artifacts/api-server/src/routes/jobs.ts | 4 +
|
|
|
|
|
|
lib/db/src/schema/index.ts | 2 +
|
|
|
|
|
|
lib/db/src/schema/nostr-trust-vouches.ts | 20 ++++
|
|
|
|
|
|
lib/db/src/schema/timmy-nostr-events.ts | 19 ++++
|
|
|
|
|
|
the-matrix/index.html | 26 +++++
|
|
|
|
|
|
the-matrix/js/main.js | 3 +
|
|
|
|
|
|
the-matrix/js/timmy-id.js | 62 ++++++++++
|
|
|
|
|
|
12 files changed, 649 insertions(+), 1 deletion(-)
|
|
|
|
|
|
|
|
|
|
|
|
dabadb4 task-28 fix5: session triage, speech-bubble local badge, footprint docs
|
|
|
|
|
|
the-matrix/js/edge-worker.js | 4 ++++
|
|
|
|
|
|
the-matrix/js/ui.js | 29 ++++++++++++++++-------------
|
|
|
|
|
|
2 files changed, 20 insertions(+), 13 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
cb50e8c task-28 fix4: trivial cost-preview gate + job polling token
|
|
|
|
|
|
the-matrix/js/payment.js | 4 +++-
|
|
|
|
|
|
the-matrix/js/ui.js | 9 +++++++++
|
|
|
|
|
|
2 files changed, 12 insertions(+), 1 deletion(-)
|
|
|
|
|
|
|
|
|
|
|
|
4943930 task-28 fix3: complexity contract, consistent token headers, npub-only prompt
|
|
|
|
|
|
the-matrix/js/edge-worker-client.js | 20 +++++++++---
|
|
|
|
|
|
the-matrix/js/edge-worker.js | 63 +++++++++++++++++++++++++++----------
|
|
|
|
|
|
the-matrix/js/nostr-identity.js | 29 ++++++++++++-----
|
|
|
|
|
|
the-matrix/js/session.js | 24 +++++++++-----
|
|
|
|
|
|
the-matrix/js/ui.js | 15 +++++++--
|
|
|
|
|
|
5 files changed, 113 insertions(+), 38 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
f75825b chore: switch push-to-gitea.sh from bore.pub to Tailscale Funnel
|
|
|
|
|
|
scripts/push-to-gitea.sh | 126 ++++++++++++++++++-----------------------------
|
|
|
|
|
|
1 file changed, 47 insertions(+), 79 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
437df48 task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching
|
|
|
|
|
|
the-matrix/js/edge-worker.js | 16 ++++-
|
|
|
|
|
|
the-matrix/js/nostr-identity.js | 125 ++++++++++++++++++++++++++++++++++++++--
|
|
|
|
|
|
the-matrix/js/ui.js | 7 ++-
|
|
|
|
|
|
3 files changed, 139 insertions(+), 9 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
898a47f task-28 fix: proper Web Worker, correct Nostr endpoints, sentiment on inbound msgs
|
|
|
|
|
|
the-matrix/js/edge-worker-client.js | 100 +++++++++++++++++
|
|
|
|
|
|
the-matrix/js/edge-worker.js | 167 ++++++++++-------------------
|
|
|
|
|
|
the-matrix/js/main.js | 6 +-
|
|
|
|
|
|
the-matrix/js/nostr-identity.js | 207 ++++++++++++++++++++++++++----------
|
|
|
|
|
|
the-matrix/js/session.js | 14 +--
|
|
|
|
|
|
the-matrix/js/ui.js | 95 ++++++++++-------
|
|
|
|
|
|
the-matrix/js/websocket.js | 10 +-
|
|
|
|
|
|
the-matrix/vite.config.js | 4 +
|
|
|
|
|
|
8 files changed, 384 insertions(+), 219 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
af3c938 task-28: edge intelligence — Transformers.js triage, Nostr signing, cost preview, sentiment moods
|
|
|
|
|
|
the-matrix/js/agents.js | 18 +
|
|
|
|
|
|
the-matrix/js/edge-worker.js | 168 +++++++
|
|
|
|
|
|
the-matrix/js/main.js | 6 +
|
|
|
|
|
|
the-matrix/js/nostr-identity.js | 215 +++++++++
|
|
|
|
|
|
the-matrix/js/payment.js | 8 +-
|
|
|
|
|
|
the-matrix/js/session.js | 29 +-
|
|
|
|
|
|
the-matrix/js/ui.js | 101 +++-
|
|
|
|
|
|
the-matrix/package-lock.json | 987 ++++++++++++++++++++++++++++++++++++++++
|
|
|
|
|
|
the-matrix/package.json | 2 +
|
|
|
|
|
|
the-matrix/vite.config.js | 4 +
|
|
|
|
|
|
10 files changed, 1527 insertions(+), 11 deletions(-)
|
|
|
|
|
|
|
|
|
|
|
|
99ede57 fix(#26): tighten token handling and verify API contract
|
|
|
|
|
|
artifacts/api-server/src/routes/identity.ts | 17 ++++++++++++++++-
|
|
|
|
|
|
artifacts/api-server/src/routes/jobs.ts | 20 ++++++++++++++++----
|
|
|
|
|
|
artifacts/api-server/src/routes/sessions.ts | 20 ++++++++++++++++----
|
|
|
|
|
|
3 files changed, 48 insertions(+), 9 deletions(-)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Key Source File Excerpts
|
|
|
|
|
|
|
|
|
|
|
|
### trust.ts — Nostr identity + HMAC token + trust scoring
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
import { createHmac, randomBytes } from "crypto";
|
|
|
|
|
|
import { db, nostrIdentities, type NostrIdentity, type TrustTier } from "@workspace/db";
|
|
|
|
|
|
import { eq } from "drizzle-orm";
|
|
|
|
|
|
import { makeLogger } from "./logger.js";
|
|
|
|
|
|
import { relayAccountService } from "./relay-accounts.js";
|
|
|
|
|
|
|
|
|
|
|
|
const logger = makeLogger("trust");
|
|
|
|
|
|
|
|
|
|
|
|
// ── Env-var helpers ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function envInt(name: string, fallback: number): number {
|
|
|
|
|
|
const raw = parseInt(process.env[name] ?? "", 10);
|
|
|
|
|
|
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Tier score boundaries (inclusive lower bound) ─────────────────────────────
|
|
|
|
|
|
// Override with TRUST_TIER_ESTABLISHED, TRUST_TIER_TRUSTED, TRUST_TIER_ELITE.
|
|
|
|
|
|
|
|
|
|
|
|
const TIER_ESTABLISHED = envInt("TRUST_TIER_ESTABLISHED", 10);
|
|
|
|
|
|
const TIER_TRUSTED = envInt("TRUST_TIER_TRUSTED", 50);
|
|
|
|
|
|
const TIER_ELITE = envInt("TRUST_TIER_ELITE", 200);
|
|
|
|
|
|
|
|
|
|
|
|
// Points per event
|
|
|
|
|
|
const SCORE_PER_SUCCESS = envInt("TRUST_SCORE_PER_SUCCESS", 2);
|
|
|
|
|
|
const SCORE_PER_FAILURE = envInt("TRUST_SCORE_PER_FAILURE", 5);
|
|
|
|
|
|
|
|
|
|
|
|
// Soft decay: points lost per day absent, applied lazily on read
|
|
|
|
|
|
const DECAY_ABSENT_DAYS = envInt("TRUST_DECAY_ABSENT_DAYS", 30);
|
|
|
|
|
|
const DECAY_PER_DAY = envInt("TRUST_DECAY_PER_DAY", 1);
|
|
|
|
|
|
|
|
|
|
|
|
// ── HMAC token for nostr_token auth ──────────────────────────────────────────
|
|
|
|
|
|
// Token format: `{pubkey}:{expiry}:{hmac}`
|
|
|
|
|
|
|
|
|
|
|
|
const TOKEN_SECRET: string = (() => {
|
|
|
|
|
|
const s = process.env["TIMMY_TOKEN_SECRET"];
|
|
|
|
|
|
if (s && s.length >= 32) return s;
|
|
|
|
|
|
const generated = randomBytes(32).toString("hex");
|
|
|
|
|
|
logger.warn("TIMMY_TOKEN_SECRET not set — generated ephemeral secret (tokens expire on restart)");
|
|
|
|
|
|
return generated;
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
const TOKEN_TTL_SECS = envInt("NOSTR_TOKEN_TTL_SECS", 86400); // 24 h
|
|
|
|
|
|
|
|
|
|
|
|
function signToken(pubkey: string, expiry: number): string {
|
|
|
|
|
|
const payload = `${pubkey}:${expiry}`;
|
|
|
|
|
|
const hmac = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex");
|
|
|
|
|
|
return `${payload}:${hmac}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function verifyToken(token: string): { pubkey: string; expiry: number } | null {
|
|
|
|
|
|
const parts = token.split(":");
|
|
|
|
|
|
if (parts.length !== 3) return null;
|
|
|
|
|
|
const [pubkey, expiryStr, hmac] = parts as [string, string, string];
|
|
|
|
|
|
const expiry = parseInt(expiryStr, 10);
|
|
|
|
|
|
if (!Number.isFinite(expiry) || Date.now() / 1000 > expiry) return null;
|
|
|
|
|
|
const expected = createHmac("sha256", TOKEN_SECRET)
|
|
|
|
|
|
.update(`${pubkey}:${expiry}`)
|
|
|
|
|
|
.digest("hex");
|
|
|
|
|
|
if (expected !== hmac) return null;
|
|
|
|
|
|
return { pubkey, expiry };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function issueToken(pubkey: string): string {
|
|
|
|
|
|
const expiry = Math.floor(Date.now() / 1000) + TOKEN_TTL_SECS;
|
|
|
|
|
|
return signToken(pubkey, expiry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Trust score helpers ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function computeTier(score: number): TrustTier {
|
|
|
|
|
|
if (score >= TIER_ELITE) return "elite";
|
|
|
|
|
|
if (score >= TIER_TRUSTED) return "trusted";
|
|
|
|
|
|
if (score >= TIER_ESTABLISHED) return "established";
|
|
|
|
|
|
return "new";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyDecay(identity: NostrIdentity): number {
|
|
|
|
|
|
const daysSeen =
|
|
|
|
|
|
(Date.now() - identity.lastSeen.getTime()) / (1000 * 60 * 60 * 24);
|
|
|
|
|
|
if (daysSeen < DECAY_ABSENT_DAYS) return identity.trustScore;
|
|
|
|
|
|
const daysAbsent = Math.floor(daysSeen - DECAY_ABSENT_DAYS);
|
|
|
|
|
|
return Math.max(0, identity.trustScore - daysAbsent * DECAY_PER_DAY);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── TrustService ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
export class TrustService {
|
|
|
|
|
|
// Upsert a new pubkey with default values.
|
|
|
|
|
|
async getOrCreate(pubkey: string): Promise<NostrIdentity> {
|
|
|
|
|
|
const existing = await this.getIdentity(pubkey);
|
|
|
|
|
|
if (existing) return existing;
|
|
|
|
|
|
|
|
|
|
|
|
const rows = await db
|
|
|
|
|
|
.insert(nostrIdentities)
|
|
|
|
|
|
.values({ pubkey })
|
|
|
|
|
|
.onConflictDoNothing()
|
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
|
|
const row = rows[0];
|
|
|
|
|
|
if (row) return row;
|
|
|
|
|
|
|
|
|
|
|
|
// Race: another request inserted first
|
|
|
|
|
|
return (await this.getIdentity(pubkey))!;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getIdentity(pubkey: string): Promise<NostrIdentity | null> {
|
|
|
|
|
|
const rows = await db
|
|
|
|
|
|
.select()
|
|
|
|
|
|
.from(nostrIdentities)
|
|
|
|
|
|
.where(eq(nostrIdentities.pubkey, pubkey))
|
|
|
|
|
|
.limit(1);
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns the trust tier for a pubkey, or "new" if unknown.
|
|
|
|
|
|
async getTier(pubkey: string): Promise<TrustTier> {
|
|
|
|
|
|
const identity = await this.getIdentity(pubkey);
|
|
|
|
|
|
if (!identity) return "new";
|
|
|
|
|
|
const decayedScore = applyDecay(identity);
|
|
|
|
|
|
return computeTier(decayedScore);
|
|
|
|
|
|
|
|
|
|
|
|
… (91 more lines truncated)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### event-bus.ts — Typed EventEmitter pub/sub bridge
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
import { EventEmitter } from "events";
|
|
|
|
|
|
|
|
|
|
|
|
export type JobEvent =
|
|
|
|
|
|
| { type: "job:state"; jobId: string; state: string }
|
|
|
|
|
|
| { type: "job:paid"; jobId: string; invoiceType: "eval" | "work" }
|
|
|
|
|
|
| { type: "job:completed"; jobId: string; result: string }
|
|
|
|
|
|
| { type: "job:failed"; jobId: string; reason: string };
|
|
|
|
|
|
|
|
|
|
|
|
export type SessionEvent =
|
|
|
|
|
|
| { type: "session:state"; sessionId: string; state: string }
|
|
|
|
|
|
| { type: "session:paid"; sessionId: string; amountSats: number }
|
|
|
|
|
|
| { type: "session:balance"; sessionId: string; balanceSats: number };
|
|
|
|
|
|
|
|
|
|
|
|
export type BusEvent = JobEvent | SessionEvent;
|
|
|
|
|
|
|
|
|
|
|
|
class EventBus extends EventEmitter {
|
|
|
|
|
|
emit(event: "bus", data: BusEvent): boolean;
|
|
|
|
|
|
emit(event: string, ...args: unknown[]): boolean {
|
|
|
|
|
|
return super.emit(event, ...args);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
on(event: "bus", listener: (data: BusEvent) => void): this;
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
on(event: string, listener: (...args: any[]) => void): this {
|
|
|
|
|
|
return super.on(event, listener);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
publish(data: BusEvent): void {
|
|
|
|
|
|
this.emit("bus", data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const eventBus = new EventBus();
|
|
|
|
|
|
eventBus.setMaxListeners(256);
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### jobs.ts — Payment-gated job lifecycle (first 120 lines)
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
import { Router, type Request, type Response } from "express";
|
|
|
|
|
|
import { randomUUID, createHash } from "crypto";
|
|
|
|
|
|
import { db, jobs, invoices, type Job } from "@workspace/db";
|
|
|
|
|
|
import { eq, and } from "drizzle-orm";
|
|
|
|
|
|
import { CreateJobBody, GetJobParams } from "@workspace/api-zod";
|
|
|
|
|
|
import { lnbitsService } from "../lib/lnbits.js";
|
|
|
|
|
|
import { agentService } from "../lib/agent.js";
|
|
|
|
|
|
import { pricingService } from "../lib/pricing.js";
|
|
|
|
|
|
import { jobsLimiter } from "../lib/rate-limiter.js";
|
|
|
|
|
|
import { eventBus } from "../lib/event-bus.js";
|
|
|
|
|
|
import { streamRegistry } from "../lib/stream-registry.js";
|
|
|
|
|
|
import { makeLogger } from "../lib/logger.js";
|
|
|
|
|
|
import { latencyHistogram } from "../lib/histogram.js";
|
|
|
|
|
|
import { trustService } from "../lib/trust.js";
|
|
|
|
|
|
import { freeTierService } from "../lib/free-tier.js";
|
|
|
|
|
|
import { zapService } from "../lib/zap.js";
|
|
|
|
|
|
|
|
|
|
|
|
const logger = makeLogger("jobs");
|
|
|
|
|
|
|
|
|
|
|
|
const router = Router();
|
|
|
|
|
|
|
|
|
|
|
|
async function getJobById(id: string): Promise<Job | null> {
|
|
|
|
|
|
const rows = await db.select().from(jobs).where(eq(jobs.id, id)).limit(1);
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getInvoiceById(id: string) {
|
|
|
|
|
|
const rows = await db.select().from(invoices).where(eq(invoices.id, id)).limit(1);
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Runs the AI eval in a background task (fire-and-forget) so HTTP polls
|
|
|
|
|
|
* return immediately with "evaluating" state instead of blocking 5-8 seconds.
|
|
|
|
|
|
* nostrPubkey is used for free-tier routing and trust scoring.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function runEvalInBackground(
|
|
|
|
|
|
jobId: string,
|
|
|
|
|
|
request: string,
|
|
|
|
|
|
nostrPubkey: string | null,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
const evalStart = Date.now();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const evalResult = await agentService.evaluateRequest(request);
|
|
|
|
|
|
latencyHistogram.record("eval_phase", Date.now() - evalStart);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("eval result", {
|
|
|
|
|
|
jobId,
|
|
|
|
|
|
accepted: evalResult.accepted,
|
|
|
|
|
|
reason: evalResult.reason,
|
|
|
|
|
|
inputTokens: evalResult.inputTokens,
|
|
|
|
|
|
outputTokens: evalResult.outputTokens,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (evalResult.accepted) {
|
|
|
|
|
|
const { estimatedInputTokens, estimatedOutputTokens } = pricingService.estimateRequestCost(request, agentService.workModel);
|
|
|
|
|
|
const breakdown = await pricingService.calculateWorkFeeSats(
|
|
|
|
|
|
estimatedInputTokens,
|
|
|
|
|
|
estimatedOutputTokens,
|
|
|
|
|
|
agentService.workModel,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Free-tier gate ──────────────────────────────────────────────────
|
|
|
|
|
|
const ftDecision = await freeTierService.decide(nostrPubkey, breakdown.amountSats);
|
|
|
|
|
|
|
|
|
|
|
|
if (ftDecision.serve === "free") {
|
|
|
|
|
|
// Pool was atomically debited for ftDecision.absorbSats by decide().
|
|
|
|
|
|
// Store ONLY the actual debited amount (not the estimate) so reconciliation
|
|
|
|
|
|
// in recordGrant() can return over-reservation accurately.
|
|
|
|
|
|
const reservedAbsorbed = ftDecision.absorbSats; // actual pool debit
|
|
|
|
|
|
try {
|
|
|
|
|
|
await db
|
|
|
|
|
|
.update(jobs)
|
|
|
|
|
|
.set({
|
|
|
|
|
|
state: "executing",
|
|
|
|
|
|
workAmountSats: 0,
|
|
|
|
|
|
estimatedCostUsd: breakdown.estimatedCostUsd,
|
|
|
|
|
|
marginPct: breakdown.marginPct,
|
|
|
|
|
|
btcPriceUsd: breakdown.btcPriceUsd,
|
|
|
|
|
|
freeTier: true,
|
|
|
|
|
|
absorbedSats: reservedAbsorbed,
|
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
|
})
|
|
|
|
|
|
.where(eq(jobs.id, jobId));
|
|
|
|
|
|
|
|
|
|
|
|
eventBus.publish({ type: "job:state", jobId, state: "executing" });
|
|
|
|
|
|
|
|
|
|
|
|
// Grant is recorded AFTER work completes (in runWorkInBackground) so we use
|
|
|
|
|
|
// actual cost rather than estimated sats for the audit log.
|
|
|
|
|
|
streamRegistry.register(jobId);
|
|
|
|
|
|
setImmediate(() => {
|
|
|
|
|
|
void runWorkInBackground(
|
|
|
|
|
|
jobId, request, 0, breakdown.btcPriceUsd, true, nostrPubkey,
|
|
|
|
|
|
reservedAbsorbed, // actual debited; runWorkInBackground will reconcile with actual cost
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (setupErr) {
|
|
|
|
|
|
// If DB transition or setup fails after pool was already debited, return sats.
|
|
|
|
|
|
void freeTierService.releaseReservation(
|
|
|
|
|
|
reservedAbsorbed,
|
|
|
|
|
|
`free job setup failed: ${setupErr instanceof Error ? setupErr.message : String(setupErr)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
throw setupErr; // re-throw so outer catch handles job state
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Partial subsidy or full gate: invoice amount = chargeSats
|
|
|
|
|
|
const invoiceSats = ftDecision.serve === "partial"
|
|
|
|
|
|
? ftDecision.chargeSats
|
|
|
|
|
|
: breakdown.amountSats;
|
|
|
|
|
|
|
|
|
|
|
|
const workInvoiceData = await lnbitsService.createInvoice(
|
|
|
|
|
|
invoiceSats,
|
|
|
|
|
|
`Work fee for job ${jobId}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
const workInvoiceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
|
|
await db.transaction(async (tx) => {
|
|
|
|
|
|
await tx.insert(invoices).values({
|
|
|
|
|
|
|
|
|
|
|
|
… (721 more lines truncated)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### moderation.ts — Nostr relay moderation queue + Timmy AI review
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
/**
|
|
|
|
|
|
* moderation.ts — Event moderation queue + Timmy AI review
|
|
|
|
|
|
*
|
|
|
|
|
|
* Every Nostr event from a non-elite whitelisted account is held in
|
|
|
|
|
|
* relay_event_queue with status "pending". Timmy (Claude haiku) reviews
|
|
|
|
|
|
* pending events in a background poll loop and either auto_approves them
|
|
|
|
|
|
* (injecting into strfry) or flags them for admin review.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Elite accounts bypass this queue — their events are injected directly
|
|
|
|
|
|
* from the relay policy handler.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { db, relayEventQueue, type QueueReviewer } from "@workspace/db";
|
|
|
|
|
|
import { eq, and } from "drizzle-orm";
|
|
|
|
|
|
import { makeLogger } from "./logger.js";
|
|
|
|
|
|
import { injectEvent } from "./strfry.js";
|
|
|
|
|
|
|
|
|
|
|
|
const logger = makeLogger("moderation");
|
|
|
|
|
|
|
|
|
|
|
|
// ── Stub mode (mirrors agent.ts) ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const STUB_MODE =
|
|
|
|
|
|
!process.env["AI_INTEGRATIONS_ANTHROPIC_API_KEY"] ||
|
|
|
|
|
|
!process.env["AI_INTEGRATIONS_ANTHROPIC_BASE_URL"];
|
|
|
|
|
|
|
|
|
|
|
|
if (STUB_MODE) {
|
|
|
|
|
|
logger.warn("no Anthropic key — moderation running in STUB mode (auto-approve all)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Anthropic lazy client (reuse from agent.ts pattern) ──────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
interface AnthropicLike {
|
|
|
|
|
|
messages: {
|
|
|
|
|
|
create(params: Record<string, unknown>): Promise<{
|
|
|
|
|
|
content: Array<{ type: string; text?: string }>;
|
|
|
|
|
|
usage: { input_tokens: number; output_tokens: number };
|
|
|
|
|
|
}>;
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let _anthropic: AnthropicLike | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
async function getClient(): Promise<AnthropicLike> {
|
|
|
|
|
|
if (_anthropic) return _anthropic;
|
|
|
|
|
|
// @ts-expect-error -- integrations-anthropic-ai exports src directly
|
|
|
|
|
|
const mod = (await import("@workspace/integrations-anthropic-ai")) as { anthropic: AnthropicLike };
|
|
|
|
|
|
_anthropic = mod.anthropic;
|
|
|
|
|
|
return _anthropic;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Moderation prompt ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const MODERATION_SYSTEM = `You are moderating events on a sovereign Nostr relay. Your job is to approve benign content and flag anything harmful.
|
|
|
|
|
|
|
|
|
|
|
|
APPROVE if the event is: a standard text note, profile update, reaction, encrypted DM, relay list, metadata update, or other typical Nostr activity.
|
|
|
|
|
|
FLAG if the event is: spam, harassment, illegal content, NSFW without appropriate warnings, coordinated abuse, or clearly malicious.
|
|
|
|
|
|
|
|
|
|
|
|
Respond ONLY with valid JSON: {"decision": "approve", "reason": "..."} or {"decision": "flag", "reason": "..."}`;
|
|
|
|
|
|
|
|
|
|
|
|
type ModerationDecision = "approve" | "flag";
|
|
|
|
|
|
|
|
|
|
|
|
interface ModerationResult {
|
|
|
|
|
|
decision: ModerationDecision;
|
|
|
|
|
|
reason: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function callClaude(kind: number, content: string): Promise<ModerationResult> {
|
|
|
|
|
|
if (STUB_MODE) {
|
|
|
|
|
|
return { decision: "approve", reason: "Stub: auto-approved (no Anthropic key)" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const client = await getClient();
|
|
|
|
|
|
const message = await client.messages.create({
|
|
|
|
|
|
model: process.env["MODERATION_MODEL"] ?? "claude-haiku-4-5",
|
|
|
|
|
|
max_tokens: 256,
|
|
|
|
|
|
system: MODERATION_SYSTEM,
|
|
|
|
|
|
messages: [
|
|
|
|
|
|
{
|
|
|
|
|
|
role: "user",
|
|
|
|
|
|
content: `Nostr event kind ${kind}. Content: ${content.slice(0, 2000)}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const block = message.content[0];
|
|
|
|
|
|
if (!block || block.type !== "text") {
|
|
|
|
|
|
return { decision: "flag", reason: "AI returned unexpected response" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = block.text!.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
|
|
|
|
const parsed = JSON.parse(raw) as { decision: string; reason?: string };
|
|
|
|
|
|
const decision = parsed.decision === "approve" ? "approve" : "flag";
|
|
|
|
|
|
return { decision, reason: parsed.reason ?? "" };
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
logger.warn("moderation: failed to parse Claude response", {
|
|
|
|
|
|
text: block.text!.slice(0, 100),
|
|
|
|
|
|
});
|
|
|
|
|
|
return { decision: "flag", reason: "Failed to parse AI response" };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ModerationService ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
export class ModerationService {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Insert an event into the moderation queue with "pending" status.
|
|
|
|
|
|
* Idempotent: if the event_id already exists, the insert is silently skipped.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async enqueue(event: {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
pubkey: string;
|
|
|
|
|
|
kind: number;
|
|
|
|
|
|
rawJson: string;
|
|
|
|
|
|
}): Promise<void> {
|
|
|
|
|
|
await db
|
|
|
|
|
|
.insert(relayEventQueue)
|
|
|
|
|
|
.values({
|
|
|
|
|
|
eventId: event.id,
|
|
|
|
|
|
pubkey: event.pubkey,
|
|
|
|
|
|
|
|
|
|
|
|
… (149 more lines truncated)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### world-state.ts — In-memory Timmy state + agent mood derivation
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
export interface TimmyState {
|
|
|
|
|
|
mood: string;
|
|
|
|
|
|
activity: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorldState {
|
|
|
|
|
|
timmyState: TimmyState;
|
|
|
|
|
|
agentStates: Record<string, string>;
|
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_TIMMY: TimmyState = {
|
|
|
|
|
|
mood: "contemplative",
|
|
|
|
|
|
activity: "idle",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const _state: WorldState = {
|
|
|
|
|
|
timmyState: { ...DEFAULT_TIMMY },
|
|
|
|
|
|
agentStates: { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle" },
|
|
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function getWorldState(): WorldState {
|
|
|
|
|
|
return {
|
|
|
|
|
|
timmyState: { ..._state.timmyState },
|
|
|
|
|
|
agentStates: { ..._state.agentStates },
|
|
|
|
|
|
updatedAt: _state.updatedAt,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function setAgentStateInWorld(agentId: string, agentState: string): void {
|
|
|
|
|
|
_state.agentStates[agentId] = agentState;
|
|
|
|
|
|
_state.updatedAt = new Date().toISOString();
|
|
|
|
|
|
_deriveTimmy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _deriveTimmy(): void {
|
|
|
|
|
|
const states = Object.values(_state.agentStates);
|
|
|
|
|
|
if (states.includes("working")) {
|
|
|
|
|
|
_state.timmyState.activity = "working";
|
|
|
|
|
|
_state.timmyState.mood = "focused";
|
|
|
|
|
|
} else if (states.includes("thinking") || states.includes("evaluating")) {
|
|
|
|
|
|
_state.timmyState.activity = "thinking";
|
|
|
|
|
|
_state.timmyState.mood = "curious";
|
|
|
|
|
|
} else if (states.some((s) => s !== "idle")) {
|
|
|
|
|
|
_state.timmyState.activity = "active";
|
|
|
|
|
|
_state.timmyState.mood = "attentive";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_state.timmyState.activity = "idle";
|
|
|
|
|
|
_state.timmyState.mood = "contemplative";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Key architectural facts for context
|
|
|
|
|
|
|
|
|
|
|
|
- Every external dependency has a **stub mode**: LNbits (in-memory invoices),
|
|
|
|
|
|
Anthropic AI (canned responses), Digital Ocean (fake credentials + real SSH keypair).
|
|
|
|
|
|
- Env-var tunable constants follow a consistent pattern: `envInt("VAR_NAME", defaultValue)`.
|
|
|
|
|
|
- Service classes have a singleton export at the bottom of the file.
|
|
|
|
|
|
- All routes use `makeLogger` structured logger and `@workspace/db` Drizzle ORM.
|
|
|
|
|
|
- The `eventBus` pub/sub decouples state transitions from WebSocket broadcast.
|
|
|
|
|
|
- Job state machine: awaiting_eval_payment → evaluating → awaiting_work_payment → executing → complete/rejected/failed.
|
|
|
|
|
|
- Trust tiers: new → established (10pts) → trusted (50pts) → elite (200pts). Soft decay after 30 days absent.
|
|
|
|
|
|
- Pre-funded session mode (Mode 2): pay once, debit at actual cost, no per-job invoices.
|
|
|
|
|
|
- Testkit: 36 automated tests at GET /api/testkit (returns a self-contained bash script).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*Generated by `pnpm --filter @workspace/scripts timmy-report` on 2026-03-19.*
|