SLOPSHOPPER

mod

Hybrid: the classic UserPromptSubmit hook runs everywhere; the module loads only where function hooks are on (Claude Code >= 2.1.287, or…

newguardcommandstatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /my-mod-status ⎿ mod: my-mod · 9 tool calls ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ mod: my-mod · 9 tool calls
README

SquiFlow (internal development codename)

Current product version: v0.0.1 (locked until the complete production-capable product gate)

Implementation state: narrow independently earned backend/API slices after the 2026-09-17 purge. Phase 0A remains qualified; the former 0B Parties implementation is retired history.

The repository codename is not the public product identity. Every deployed presentation obtains its name, legal identity, theme and links from validated branding configuration; the checked-in runtime has no codename branding fallback. Earlier code and retired phase records remain in Git history as evidence and context, not current implementation authority.

Read in this order

  1. README.IMPLEMENTATION.md — current implementation truth.
  2. docs/product/PRODUCT_IDENTITY_AND_VERSIONING.md — codename, public identity and locked-version contract.
  3. docs/decisions/CURRENT_IMPLEMENTATION_PURGE_2026-09-17.md — purge decision and removed inventory.
  4. docs/implementation/PHASE_GATE_PRODUCTION_HONESTY.md — scope and quality contract.
  5. docs/implementation/PHASE_GATE_EVIDENCE_REGRESSION_AND_TRANSITIONS.md — evidence permanence and requalification contract.
  6. docs/implementation/phases/phase-0/0A_BASELINE_STATUS.md — qualified reset baseline.
  7. docs/review/APPLICATION_BASELINE_IMPLEMENTATION_SOURCE_REVIEW.md — proven-source routing for future implementation.
  8. docs/domain/BUSINESS_TERMS.md, docs/decisions/CURRENT_DECISIONS.md, and OPEN_DECISIONS.md — accepted and unresolved product meaning.
  9. The focused owner for the responsibility being changed.

Source precedence

focused canonical owner
        ↓
accepted/current decision record
        ↓
current implementation/gate record
        ↓
historical review/source-study/branch/MR material

Repository state proves what is implemented. Architecture documents can define future ownership and accepted direction without implying that a project or runtime exists.

Current implementation boundary

production projects: 12
test projects:       12
executable hosts:    2
solution/build/test contract: present
active runtime responsibilities: public application bootstrap; classified OpenAPI v1 description; JWT access-token validation; authenticated account and tenant membership resolution; Finbuckle route-candidate resolution; pinned-model OpenFGA workspace/customer/order authorization; account/tenancy/customer/order-draft PostgreSQL persistence with tenant RLS; liveness and bounded dependency readiness; Autofac root composition and dormant tenant-keyed runtime mechanics; ordered one-shot DB migration
active host-neutral responsibilities: bounded feature graph validation and effective-selection compilation; immutable customer organization/program context; optionally attributed priced order-draft creation, read, browse and abandonment with caller-scoped semantic idempotency
BLOCKED: none

The deleted PartyKind enum and Parties implementation are not current implementation. The current solution/build/test files belong to independently earned ApplicationProfiles, Branding, IdentityAccess, Tenancy, Customers, Orders, CoreApi and DbMigrator slices and do not revive the retired 0B shape. Customer organization/program association on a draft is attribution, not account billing or settlement authority. The tenant-keyed Autofac registry is internal mechanics; no production request acquires it, and durable Tenant Application Profile authority remains absent.

Architecture direction

  • C# and modern .NET are the application direction; .NET 10 is the current toolchain baseline (pinned by global.json).
  • Avalonia Workstation and Blazor tenant Web remain accepted presentation directions when those surfaces are implemented.
  • ASP.NET Core remains the server-host foundation when server hosts are implemented.
  • The product starts as a modular monolith; ordinary capability communication is in-process.
  • One capability owns one source implementation of its business meaning.
  • PostgreSQL remains selected central transactional storage and SQLite/WAL remains selected Workstation local storage when those responsibilities are introduced.
  • ZITADEL remains the selected identity platform; CoreApi enforces its configured OIDC/JWT issuer and audience contract, while live-provider/login/session evidence remains pending. OpenFGA is the active pinned-model tenant workspace/customer/order permission provider. OpenBao/Vault-style key management remains a future direction, not a current runtime.
  • Guard remains a future supervision/recovery boundary, not business authority.
  • Projects, processes, providers, interfaces, and protocols are earned by a real boundary.

Development rule

Scope is a choice; honesty is not.

real useful responsibility
        ↓
inspect applicable proven implementations
        ↓
declare production intent and exact scope
        ↓
NOT_INTRODUCED / PRODUCTION_HONEST / BLOCKED
        ↓
implement the smallest useful production-honest slice
        ↓
prove claims with falsifiable evidence
        ↓
retain recurring regression guards
        ↓
qualify only with BLOCKED = none

Production-honest does not mean writing every mechanism from scratch. Before custom infrastructure, inspect the source review and record whether a focused dependency, bounded source adaptation, reusable tests/algorithms, or reference-only decision fits the active responsibility. The repository keeps ownership of its business meaning, authority, security, compatibility, and evidence.

Gate and verification status

Phase 0A is the current qualified baseline. The former 0B qualification proved properties of code that has since been deliberately purged, so those claims and CI results are historical and no longer describe the repository.

The repository-owned verification entry point is ./eng/verify.sh. It restores, verifies formatting, builds in Release configuration and runs the complete test suite, including real PostgreSQL Testcontainers integration tests. .github/workflows/verify.yml is the thin GitHub Actions wrapper around the same command. A workflow definition is not evidence that a remote run passed; report remote CI evidence only after inspecting that run.

Phase 0 and the already-concrete Phase 1 trust boundary retain architecture direction. Phase 2–10 remain direction-only NOT_INTRODUCED planning until real work earns detail.

Versioning

The product version is exactly v0.0.1 and remains locked there until the complete product is explicitly qualified as capable of its accepted real-production responsibilities. Git revisions identify builds and changes without creating alternate product versions. /api/v1 and similar identifiers version a contract family; they are not the product version. Historical documents may retain the version that identified their original decision or qualification event.

Source 3 files
hooks/register.ts 62 lines
1import type { Hook, Register } from 'claude-code'
2
3import { show, statusLine, type Host } from './host'
4import { newCounters, STATUS_PATH, statusText, type Counters } from './status'
5
6type Dollar = Parameters<Hook<'session.start'>>[0]
7
8/** What one session keeps: the counters for the status file, the project root and the start time. */
9type Session = { readonly counters: Counters; root?: string; startedMs: number }
10
11/** A top-level function declaration, so `import type { Hook, Register } from 'claude-code'
12
13import { show, statusLine, type Host } from './host'
14 may be passed to it; a refused write is ignored (the status file is a courtesy). */
15async function flush($: Dollar, s: Session): Promise<void> {
16  if (s.root === undefined) return
17  try {
18    await $.fs.write(`${s.root}/${STATUS_PATH}`, statusText(s.counters, await $.clock.now(), s.startedMs))
19  } catch {
20    // never fail a hook over the status file
21  }
22}
23
24/**
25 * A mod scaffolded by ruflo-plugin-creator (ADR-404 patterns):
26 * - observe: `const r = await next(e); …; return r` on tool.call;
27 * - every `$` call literal, wrapped in a host adapter, refusal-tolerant;
28 * - namespaced command (`my-mod-status`), never a built-in's name;
29 * - the classic fallback handed over through MY_MOD_ACTIVE;
30 * - a status file (status.ts) the console reads: keep `calls` a number.
31 * Hot reload re-runs register and session.start: keep anything that must
32 * survive a reload in `$.store` or `$.state`, not in these variables.
33 */
34export const register: Register = (on, options) => {
35  const showLine = options.statusLine !== false
36  const s: Session = { counters: newCounters(), startedMs: 0 }
37
38  on('session.start', async ($, e, next) => {
39    await $.env.set('MY_MOD_ACTIVE', '1').catch(() => undefined)
40    await $.command.register({ name: 'my-mod-status', description: 'my-mod: tool calls this session' }).catch(() => undefined)
41    try {
42      s.root = (await $.session.root()) as string | undefined
43      s.startedMs = await $.clock.now()
44    } catch {
45      // no project root: the mod still runs, it just writes no status file
46    }
47    await flush($, s)
48    return next(e)
49  })
50
51  on('tool.call', async ($, e, next) => {
52    const result = await next(e)
53    s.counters.calls++
54    const host: Host = { status: text => $.ui.status(text) }
55    if (showLine) show(host, s.counters.calls)
56    await flush($, s)
57    return result
58  })
59
60  on('command.run', { command: 'my-mod-status' }, () => ({ text: statusLine(s.counters.calls) }))
61}
62
hooks/host.ts 23 lines
1/**
2 * What the pure logic needs from the engine, as plain functions. The engine
3 * reads what a module calls off its source, so `$` never leaves a hook: the
4 * hook builds this from literal `$.noun.method(...)` calls (register.ts).
5 */
6export type Host = {
7  readonly status: (text: string | undefined) => void
8}
9
10/** The tool-call tally and the line it shows; pure, testable without the engine. */
11export function statusLine(calls: number): string {
12  return `my-mod · ${calls} tool call${calls === 1 ? '' : 's'}`
13}
14
15/** Draws through the host; a refused `ui.status` (an admin may withhold it) is ignored. */
16export function show(host: Host, calls: number): void {
17  try {
18    host.status(statusLine(calls))
19  } catch {
20    // never fail a hook over the status line
21  }
22}
23
hooks/status.ts 41 lines
1/**
2 * The status contract (ADR-446): the file the console's Mods section reads. It scans `.claude-flow` for `<name>-mod` folders (the folder name
3 * must match /^[a-z0-9][a-z0-9-]{0,40}-mod$/) and keeps a file only when `version` is 1. Anything else you write is ignored by the console.
4 */
5export const STATUS_PATH = '.claude-flow/my-mod/status.json'
6
7/** What the console reads; `calls` is a number (not a per-tool object), `lastDenied` is a short reason, left out until the mod refuses something. */
8export type ModStatus = {
9  version: 1
10  updatedMs: number
11  startedMs: number
12  modVersion: string
13  summary: string
14  guard: boolean
15  calls: number
16  blocked: number
17  lastDenied?: string
18}
19
20/** The counters the mod keeps for one session. This template guards nothing, so `blocked` stays 0 and `guard` is false. */
21export type Counters = { calls: number; blocked: number; lastDenied?: string }
22
23export const newCounters = (): Counters => ({ calls: 0, blocked: 0 })
24
25/** Builds the document; pure, so a test can assert the shape without the engine. */
26export function modStatus(c: Counters, nowMs: number, startedMs: number): ModStatus {
27  return {
28    version: 1,
29    updatedMs: nowMs,
30    startedMs,
31    modVersion: '0.1.0',
32    summary: 'counts tool calls; guards nothing',
33    guard: false,
34    calls: c.calls,
35    blocked: c.blocked,
36    ...(c.lastDenied !== undefined && { lastDenied: c.lastDenied }),
37  }
38}
39
40export const statusText = (c: Counters, nowMs: number, startedMs: number): string => `${JSON.stringify(modStatus(c, nowMs, startedMs), null, 2)}\n`
41