P1 — κ Rule Engine — Agent Prompts

Copy-paste-ready prompts for agents tackling each of the 20 sub-tasks in Phase 1 κ Rule Engine.

Phase 1 starts at R81 per docs/5-time/roadmap.md. These prompts are staged during R76.P1 (2026-04-18) so that when R81 opens, every sub-task is a zero-friction T3 dispatch.

Canonical spec: task-breakdown.md §Phase 1 Concept doc: docs/3-world/physics/laws/rule-engine.md Algorithm extraction: docs/reference/extractions/kappa-rule-engine-extraction.md Master bootstrap prompt: agent-bootstrap.md Executor rules: CLAUDE.md — §3 worktree, §5 gate, §6 5-step chain, §7 writeback

Design invariants preserved in every sub-task:

  1. 64-bit signed integer arithmetic — no floats anywhere
  2. Deterministic execution — no RNG / clock / async I/O / network / filesystem in rule bodies
  3. First-match-wins within a rule; specificity-sorted across rules
  4. Collect-then-apply mutations — no side effects during evaluation
  5. AST cap 10,000 nodes per rule; MAX_INTEGER_OPS=10_000, MAX_CALL_DEPTH=16, MAX_ARG_COUNT=8
  6. Chevrotain for lexer + parser per ADR-006
  7. Rule version hash feeds θ consensus votes and ι fork ids
  8. Test-corpus parity required for π governance activation

Scope bound: do not graduate the prompts to dependency-less parallel PRs. The sub-task graph has hard prerequisites; respect the Depends-on field in each section.

Group summary

Task ID Title Depends on Effort Unblocks
P1.1.1 Basis Point Arithmetic S P1.1.2, P1.1.3, P1.3.2
P1.1.2 Determinism Verification Harness P1.1.1 S (guardrail for all of P1)
P1.1.3 BPS Constants + Overflow Protection P1.1.1 S P1.3.2, P1.4.3
P1.2.1 Lexer / Tokenizer M P1.2.2
P1.2.2 Parser (Tokens → AST) P1.2.1 L P1.2.3, P1.2.4, P1.3.1, P1.5.4
P1.2.3 AST Validator P1.2.2 M P1.2.4
P1.2.4 Rule Loader / Registry P1.2.3 M P1.4.1
P1.3.1 Core Evaluation Loop P1.2.2, P1.1.1 L P1.3.2, P1.3.3, P1.3.4, P1.4.1, P1.5.5
P1.3.2 Built-in Functions P1.3.1, P1.1.1 M P1.3.1 consumers
P1.3.3 State Access Layer P1.3.1 M P1.4.1
P1.3.4 Policy Gating / Pre-guards P1.3.1 M P1.4.1
P1.4.1 Admission Evaluator P1.3.1, P1.3.4, P1.2.4 L P1.4.2, P1.4.3, P1.4.4
P1.4.2 Denial Reason Taxonomy P1.4.1 S downstream consumers
P1.4.3 Admission Budgets P1.3.1 M P1.4.1
P1.4.4 Tool-Lock Integration Spec P1.4.1, P1.4.2, P1.4.3 M α wiring in R81+
P1.5.1 Version Hash Computation P1.2.2, P1.5.4 S P1.5.2, P1.5.5, θ consensus (Phase 3)
P1.5.2 Rule Migration P1.5.1, P1.3.1, P1.5.5 L π governance wiring
P1.5.3 Activation Epoch + Rollback P1.5.1, P1.5.2 M operational rollout
P1.5.4 Canonical Serialization P1.2.2 M P1.5.1
P1.5.5 Test Corpus Parity Harness P1.3.1, P1.5.1 L P1.5.2

P1.1.1 — Basis Point Arithmetic

Spec source: task-breakdown.md §P1.1.1 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §3 (8 Built-in Functions — BPS operations) + §4 (BPS Integer Math) Worktree: feature/p1-1-1-integer-math Branch command: git worktree add .worktrees/claude/p1-1-1-integer-math -b feature/p1-1-1-integer-math origin/main Estimated effort: S (Small — 1–2 hours) Depends on:Unblocks: P1.1.2 (determinism harness wraps these helpers), P1.1.3 (constants + safe variants wrap these), P1.3.2 (built-ins delegate here)

Files to create

  • src/domains/rules/integer-math.ts — Basis-point helper library
  • src/domains/rules/__tests__/integer-math.test.ts — 100% branch coverage tests

Acceptance criteria

  • All arithmetic uses 64-bit signed integers, no floating point anywhere
  • bps_mul(value, bps)(value * bps) / 10000 (floor division)
  • bps_div(value, bps)(value * 10000) / bps (floor division)
  • apply_bps(value, bps)value - bps_mul(value, bps) (decay variant)
  • decay(value, rate_bps, epochs) → multi-epoch compounded decay with per-step floor
  • Overflow detection: reject inputs where value * bps would exceed 2^63 - 1
  • Underflow: result never goes below 0 for non-negative inputs
  • Division by zero: explicit error, not silent wrap
  • 100% branch coverage in tests

Pre-flight reading

  • CLAUDE.md — worktree (§3), gate (§5), 5-step chain (§6), writeback (§7)
  • docs/guides/implementation/task-breakdown.md §P1.1.1
  • docs/3-world/physics/laws/rule-engine.md §Integer-only arithmetic + §Basis-point arithmetic examples
  • docs/reference/extractions/kappa-rule-engine-extraction.md §3 + §4

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.1.1 — Basis Point Arithmetic
Implement the integer-only basis-point helper library that underpins every κ rule computation.

FILES TO READ FIRST:
1. CLAUDE.md (worktree §3, gate §5, 5-step chain §6, writeback §7)
2. docs/guides/implementation/task-breakdown.md §P1.1.1
3. docs/3-world/physics/laws/rule-engine.md (Integer-only arithmetic + examples)
4. docs/reference/extractions/kappa-rule-engine-extraction.md §3 (BPS constants) + §4 (BPS Integer Math with overflow protection)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-1-1-integer-math -b feature/p1-1-1-integer-math origin/main
cd .worktrees/claude/p1-1-1-integer-math

FILES TO CREATE:
- src/domains/rules/integer-math.ts
  * bps_mul(value: bigint, bps: bigint): bigint  — (value * bps) / 10000n, floor division
  * bps_div(value: bigint, bps: bigint): bigint  — (value * 10000n) / bps, floor division, throws on bps==0
  * apply_bps(value: bigint, bps: bigint): bigint — value - bps_mul(value, bps)
  * decay(value: bigint, rate_bps: bigint, epochs: bigint): bigint — compound decay with per-step floor
  * safe_mul(a: bigint, b: bigint): bigint — throws OverflowError if |a*b| > 2^63-1
  * safe_div(a: bigint, b: bigint): bigint — throws DivisionByZeroError if b==0
  * Export typed errors: OverflowError, DivisionByZeroError, UnderflowError
  * All functions accept and return bigint (to make 64-bit explicit in TS — JS Number is 53-bit)
  * Zero floating-point anywhere: no Number literals like 3.14, no `/` with Number, no Math.*
  * Zero async I/O: all functions synchronous and pure
  * Zero RNG / clock reads

- src/domains/rules/__tests__/integer-math.test.ts
  * Test bps_mul: [(1000, 500) → 50], [(10000, 10000) → 10000], [(0, any) → 0]
  * Test bps_div: [(5000, 2500) → 20000], [(1000, 2000) → 5000], divide-by-zero throws
  * Test apply_bps: [(1000, 150) → 985] (1.5% decay of 1000)
  * Test decay: [(1000, 150, 2) → 970] (compounded two epochs of 1.5%)
  * Test overflow: safe_mul(2^62, 2^62) throws OverflowError
  * Test underflow: apply_bps never goes below 0 for non-negative inputs
  * Verify 100% branch coverage via istanbul/c8

ACCEPTANCE CRITERIA (headline):
✓ All arithmetic uses bigint; no Number, no Math.*
✓ bps_mul, bps_div, apply_bps, decay implemented
✓ safe_mul / safe_div throw on overflow / div-by-zero
✓ 100% branch coverage

SUCCESS CHECK:
cd .worktrees/claude/p1-1-1-integer-math && npm run build && npm run lint && npm test

WRITEBACK (after success, per CLAUDE.md §7):
task_update(id="P1.1.1", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.1.1
branch: feature/p1-1-1-integer-math
worktree: .worktrees/claude/p1-1-1-integer-math
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Implemented bigint-based bps helpers (bps_mul, bps_div, apply_bps, decay) with overflow detection and divide-by-zero guards. 100% branch coverage.
blockers: none")

FORBIDDENS:
✗ No `number` type for quantities — always `bigint`
✗ No Math.* or Date.* or crypto.randomBytes
✗ No async in this file
✗ Do not edit main checkout (CLAUDE.md §3)
✗ Do not skip any of build / lint / test (CLAUDE.md §5)

NEXT:
P1.1.2 — Determinism Verification Harness (wraps these helpers under fuzz + property tests)

Verification checklist (for reviewer agent)

  • All signatures use bigint, not number
  • No use of Math.*, Date.*, Math.random(), or crypto.randomBytes
  • Overflow detection via safe_mul tested with representative boundary inputs
  • Division-by-zero throws typed error, not silent NaN / Infinity
  • 100% branch coverage reported
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.1.1
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.1.1
  branch: feature/p1-1-1-integer-math
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "Implemented bigint-based basis-point arithmetic (bps_mul, bps_div, apply_bps, decay) with overflow detection (safe_mul) and divide-by-zero guards (safe_div). All arithmetic is pure, deterministic, 64-bit signed integer. 100% branch coverage."
  blockers: []

Common gotchas

  • TypeScript number is 53-bit float under the hood — if you use number for quantities, you silently lose precision above 2^53. Use bigint throughout; the DSL’s integer literal type maps to bigint, not number.
  • Floor division in JS/TS is not obviousBigInt division / truncates toward zero (which is floor only for non-negative results). For negative dividends, you need explicit Math.floor equivalent via if (a % b !== 0n && ((a < 0n) !== (b < 0n))) .... The extraction says “truncate toward zero (not floor)” for safe_div — but κ rule bodies never produce negatives from a non-negative value, so truncate-toward-zero is equivalent to floor in practice. Document this explicitly.
  • Do not introduce a basis-point type wrapper in this PR — this is the lowest-level helper file. Types like Bps = bigint & {__brand: "Bps"} are a P1.1.3 concern.

P1.1.2 — Determinism Verification Harness

Spec source: task-breakdown.md §P1.1.2 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §4 (BPS Integer Math) + §5 (Rule Execution Flow) Worktree: feature/p1-1-2-determinism-harness Branch command: git worktree add .worktrees/claude/p1-1-2-determinism-harness -b feature/p1-1-2-determinism-harness origin/main Estimated effort: S (Small — 1–2 hours) Depends on: P1.1.1 Unblocks: guardrail for every compute sub-task in P1.2–P1.5

Files to create

  • src/domains/rules/__tests__/determinism.test.ts — Property + fuzz harness

Acceptance criteria

  • Property test: for any two runs with identical inputs, outputs are bit-identical
  • No Math.random(), Date.now(), process.hrtime(), or equivalent in src/domains/rules/**
  • No async I/O in computation path
  • Fuzz test: 10,000 random input pairs produce identical results under both call orderings
  • Static analysis: grep check rejects any use of Math.* or Date.* outside tests
  • Test runs in under 10 seconds in CI (to stay under default Jest timeout)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.1.2
  • docs/3-world/physics/laws/rule-engine.md §Forbidden operations
  • src/domains/rules/integer-math.ts (from P1.1.1)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.1.2 — Determinism Verification Harness
Build the test-only harness that proves κ computations are deterministic and free of forbidden operations.

FILES TO READ FIRST:
1. CLAUDE.md (§3 worktree, §5 gate, §7 writeback)
2. docs/guides/implementation/task-breakdown.md §P1.1.2
3. docs/3-world/physics/laws/rule-engine.md §Forbidden operations
4. src/domains/rules/integer-math.ts (P1.1.1 output — this harness exercises it)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-1-2-determinism-harness -b feature/p1-1-2-determinism-harness origin/main
cd .worktrees/claude/p1-1-2-determinism-harness

FILES TO CREATE:
- src/domains/rules/__tests__/determinism.test.ts
  * Property test block: for each of {bps_mul, bps_div, apply_bps, decay, safe_mul, safe_div},
    assert that calling the function twice with identical args produces identical outputs
    (use fast-check or hand-rolled shrinking harness with 1000 iterations per function)
  * Fuzz test block: generate 10,000 random (value, bps) tuples using a seeded PRNG,
    run each through apply_bps and decay, collect output arrays; re-run with same seed,
    assert arrays are byte-identical
  * Forbidden-ops static scanner:
    - Read src/domains/rules/**/*.ts via fs.readFileSync (test-side only — tests can use fs)
    - Grep for patterns: /\bMath\./, /\bDate\./, /setTimeout|setInterval/, /Math\.random/, /crypto\.randomBytes/
    - Fail test if any match found (excluding tests/ and test files)
  * Performance assertion: full suite < 10s wall clock

ACCEPTANCE CRITERIA (headline):
✓ Property test over all P1.1.1 helpers, 1000 iter each
✓ Fuzz test 10k tuples identical across two seeded runs
✓ Static scan rejects Math.* / Date.* / randomness in src/domains/rules/**
✓ Suite < 10s

SUCCESS CHECK:
cd .worktrees/claude/p1-1-2-determinism-harness && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.1.2", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.1.2
branch: feature/p1-1-2-determinism-harness
worktree: .worktrees/claude/p1-1-2-determinism-harness
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Added property + fuzz harness proving P1.1.1 helpers are deterministic. Static scanner rejects Math.*, Date.*, randomness in src/domains/rules/**.
blockers: none")

FORBIDDENS:
✗ Do not use a non-seeded RNG for fuzz input generation — the fuzz test must be deterministic itself
✗ Do not import into src/domains/rules/ from this test file (tests read, don't modify)
✗ Do not edit main checkout

NEXT:
P1.1.3 — BPS Constants + Overflow Protection (adds named constants + branded types)

Verification checklist (for reviewer agent)

  • Property test uses fast-check or equivalent seeded generator
  • Fuzz test is reproducible — two runs produce identical outputs
  • Static scanner covers Math., Date., randomness, timers
  • Performance budget enforced
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.1.2
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.1.2
  branch: feature/p1-1-2-determinism-harness
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "Test-only determinism harness covering P1.1.1 helpers: property tests (1000 iter per function), fuzz tests (10k tuples identical across two seeded runs), static scanner rejecting Math.* / Date.* / randomness in src/domains/rules/**. Runs in under 10 seconds."
  blockers: []

Common gotchas

  • The fuzz test itself must be seeded — otherwise it is non-deterministic and will flake in CI. Use a seeded PRNG like seedrandom or a manual linear congruential generator, pinned to a hardcoded seed.
  • fs.readFileSync in tests is fine — tests can read files; the forbidden-ops rule applies to src/ not to test files.
  • Jest default timeout is 5 seconds — if your fuzz loop slows it, either raise the timeout explicitly (jest.setTimeout(15000)) or bucket iterations into smaller describes.

P1.1.3 — BPS Constants + Overflow Protection

Spec source: task-breakdown.md §P1.1.3 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §3 (BPS Constants) + §4 (Overflow Protection) Worktree: feature/p1-1-3-bps-constants Branch command: git worktree add .worktrees/claude/p1-1-3-bps-constants -b feature/p1-1-3-bps-constants origin/main Estimated effort: S (Small — 1–2 hours) Depends on: P1.1.1 Unblocks: P1.3.2 (built-ins consume constants), P1.4.3 (budget module consumes limit constants)

Files to create

  • src/domains/rules/bps-constants.ts
  • src/domains/rules/__tests__/bps-constants.test.ts

Acceptance criteria

  • Exported constants: BPS_100_PERCENT=10000n, BPS_50_PERCENT=5000n, BPS_1_PERCENT=100n
  • Domain decay rates: DECAY_EXECUTION=500n, DECAY_COMMISSIONING=300n, DECAY_ARBITRATION=1000n, DECAY_GOVERNANCE=200n, DECAY_SOCIAL=100n
  • Penalty constants: DAMAGE_MINOR=1500n, DAMAGE_MODERATE=3000n, DAMAGE_SEVERE=5000n, DAMAGE_CRITICAL=8000n, DAMAGE_FRAUD=10000n
  • safe_mul(a, b) returns typed overflow error when |a| > MAX_INT64 / |b|
  • safe_div(a, b) returns typed divide-by-zero error when b == 0n
  • Constants are as const, not let or mutable
  • Brand type Bps = bigint & {__brand: "Bps"} exported for type safety

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.1.3
  • docs/3-world/physics/laws/rule-engine.md §Basis-point arithmetic
  • docs/reference/extractions/kappa-rule-engine-extraction.md §3 + §4
  • src/domains/rules/integer-math.ts (P1.1.1)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.1.3 — BPS Constants + Overflow Protection
Export the named basis-point constants + branded types + safe arithmetic variants on top of P1.1.1.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.1.3
3. docs/3-world/physics/laws/rule-engine.md §Basis-point arithmetic examples
4. docs/reference/extractions/kappa-rule-engine-extraction.md §3 + §4
5. src/domains/rules/integer-math.ts (P1.1.1)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-1-3-bps-constants -b feature/p1-1-3-bps-constants origin/main
cd .worktrees/claude/p1-1-3-bps-constants

FILES TO CREATE:
- src/domains/rules/bps-constants.ts
  * Named constants (as const):
    - BPS_100_PERCENT = 10000n, BPS_50_PERCENT = 5000n, BPS_1_PERCENT = 100n
    - DECAY_EXECUTION=500n, DECAY_COMMISSIONING=300n, DECAY_ARBITRATION=1000n,
      DECAY_GOVERNANCE=200n, DECAY_SOCIAL=100n
    - DAMAGE_MINOR=1500n, DAMAGE_MODERATE=3000n, DAMAGE_SEVERE=5000n,
      DAMAGE_CRITICAL=8000n, DAMAGE_FRAUD=10000n
    - MAX_INT64 = 9223372036854775807n
  * Branded type: export type Bps = bigint & {__brand: "Bps"}
  * Helper: export const bps = (n: bigint | number): Bps => {...} — validates range [0, 10000]
  * Re-export safe_mul / safe_div from integer-math.ts with explicit overflow semantics for consumers
    (or thin wrappers that throw on out-of-bounds inputs)

- src/domains/rules/__tests__/bps-constants.test.ts
  * Test each named constant equals its expected numeric value
  * Test bps() helper: accepts valid inputs, throws for out-of-range (<0 or >10000)
  * Test safe_mul wrapper: throws OverflowError on (2^62, 2^62)
  * Test safe_div wrapper: throws DivisionByZeroError on b=0n
  * Test Bps brand type: confirm value carries brand at compile time (dtslint-style or tsd)

ACCEPTANCE CRITERIA (headline):
✓ 13 named constants (BPS_*, DECAY_*, DAMAGE_*) as bigint, frozen
✓ Bps branded type exported
✓ bps() helper validates range
✓ safe_mul / safe_div re-exports throw on bad inputs

SUCCESS CHECK:
cd .worktrees/claude/p1-1-3-bps-constants && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.1.3", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.1.3
branch: feature/p1-1-3-bps-constants
worktree: .worktrees/claude/p1-1-3-bps-constants
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Added bps-constants module with 13 named constants (BPS_*/DECAY_*/DAMAGE_*), Bps branded type, bps() validator, safe_mul/safe_div wrappers.
blockers: none")

FORBIDDENS:
✗ Do not declare constants as `let` or mutable — they must be `as const`
✗ Do not redefine bps_mul/bps_div here — this module layers on top of P1.1.1, not replaces it
✗ Do not edit main checkout

NEXT:
P1.2.1 — Lexer / Tokenizer (first Chevrotain step)

Verification checklist (for reviewer agent)

  • All constants are bigint (100n, not 100)
  • All constants are as const / frozen
  • Bps brand type exported and used in function signatures where appropriate
  • bps() helper validates range at runtime
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.1.3
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.1.3
  branch: feature/p1-1-3-bps-constants
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "Module exports 13 named basis-point constants (BPS_*, DECAY_*, DAMAGE_*) as frozen bigint, plus Bps branded type, bps() range validator, and safe_mul/safe_div wrappers. All values match the extraction and rule-engine.md."
  blockers: []

Common gotchas

  • Brand types don’t survive JSON round-trip — if a value is serialized to JSON and back, the brand is erased at the type level. Add a re-validation step at deserialization boundaries.
  • Constants must be bigint literals, not BigInt(100) calls — the as const on a function call doesn’t freeze the return value; use 100n suffix.
  • Do not import anything from src/server.ts or src/config.ts — this module is a leaf; it has no runtime dependencies outside integer-math.ts.

P1.2.1 — Lexer / Tokenizer

Spec source: task-breakdown.md §P1.2.1 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §1 (Full EBNF Grammar) Worktree: feature/p1-2-1-lexer Branch command: git worktree add .worktrees/claude/p1-2-1-lexer -b feature/p1-2-1-lexer origin/main Estimated effort: M (Medium — 4–8 hours) Depends on:Unblocks: P1.2.2 (Parser consumes tokens)

Files to create

  • src/domains/rules/lexer.ts
  • src/domains/rules/__tests__/lexer.test.ts

Acceptance criteria

  • Uses Chevrotain library; pin exact version in package.json (e.g., "chevrotain": "11.0.3")
  • Token types: KEYWORD, IDENTIFIER, INTEGER, STRING, OPERATOR, DELIMITER, EOF
  • Keywords tokenized: rule, guards, effects, when, then, if, else, and, or, not, true, false, admit, reject, admission, transition, consequence, promotion
  • Operators: ==, !=, >, <, >=, <=, +, -, *, /, %
  • Variables: prefix $ with dot-path — $actor.reputation.execution
  • Line/column tracking for every token for error messages
  • Rejects floating-point literals (3.14 is a syntax error, reported with position)
  • Rejects underscore-separated integers (1_000_000 is invalid)
  • Unicode identifiers supported (XID_Start / XID_Continue) for future i18n

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.2.1
  • docs/3-world/physics/laws/rule-engine.md §DSL grammar
  • docs/reference/extractions/kappa-rule-engine-extraction.md §1
  • docs/architecture/decisions/ADR-006-dsl-grammar.md (Chevrotain ratification)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.2.1 — Lexer / Tokenizer
Build the Chevrotain-based tokenizer for the κ DSL. This is the first parser-stack step.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.2.1
3. docs/3-world/physics/laws/rule-engine.md §DSL grammar (EBNF fragment)
4. docs/reference/extractions/kappa-rule-engine-extraction.md §1 (Full EBNF Grammar — use this as the canonical superset)
5. docs/architecture/decisions/ADR-006-dsl-grammar.md
6. https://chevrotain.io/docs/tutorial/step1_lexing.html (ref — already memorized by the agent)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-2-1-lexer -b feature/p1-2-1-lexer origin/main
cd .worktrees/claude/p1-2-1-lexer

FIRST: install chevrotain (pinned)
npm install chevrotain@11.0.3

FILES TO CREATE:
- src/domains/rules/lexer.ts
  * Use Chevrotain's createToken + Lexer
  * Define tokens:
    - Whitespace (skipped)
    - Keywords (one createToken per keyword, matching in priority order)
    - Identifier (must come after keywords to avoid shadowing)
    - Variable ($-prefixed with dot-path)
    - IntegerLiteral (/-?[0-9]+/)
    - StringLiteral (double-quoted, escapes)
    - Operators: Eq (==), NotEq (!=), Lte (<=), Gte (>=), Lt (<), Gt (>), Plus (+), Minus (-), Mul (*), Div (/), Mod (%), Arrow (->)
    - Delimiters: LBrace, RBrace, LParen, RParen, Comma, Colon, Dot
  * Export tokenize(input: string): ILexingResult
  * Pass through Chevrotain's line/column tracking
  * Explicit rejection rules:
    - /-?[0-9]+\.[0-9]+/ → FloatRejected token that throws at lex time (or in validator layer)
    - /-?[0-9]+(_[0-9]+)+/ → UnderscoreIntegerRejected token
  * Error recovery: use Chevrotain's built-in token recovery

- src/domains/rules/__tests__/lexer.test.ts
  * Test keyword recognition: "rule AcceptCommitment { ... }" tokenizes rule/identifier/brace
  * Test variable: "$actor.reputation.execution" produces VariableToken with dotpath
  * Test integer: "123", "-5", "0" all tokenize; "3.14" produces error token
  * Test operators: all 12 operators tokenize correctly in isolation
  * Test line/column: error token for "3.14" carries correct startLine/startColumn
  * Test unicode identifier: "règle_日本語" tokenizes as IdentifierToken
  * Test a full realistic snippet: the AcceptCommitment example from rule-engine.md

ACCEPTANCE CRITERIA (headline):
✓ Chevrotain pinned in package.json
✓ 7 token categories + 18 keywords + 12 operators
✓ Variable = $ + dotpath; Integer without decimals; String with escapes
✓ Line/column tracking for every token
✓ Float literals and underscore-integers rejected with position

SUCCESS CHECK:
cd .worktrees/claude/p1-2-1-lexer && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.2.1", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.2.1
branch: feature/p1-2-1-lexer
worktree: .worktrees/claude/p1-2-1-lexer
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Chevrotain lexer for κ DSL. 18 keywords, 12 operators, 7 token categories. Float + underscore-integer literals rejected with line/column.
blockers: none")

FORBIDDENS:
✗ Do not roll a hand-written lexer — Chevrotain per ADR-006
✗ Do not import Chevrotain from a newer major version — pin 11.0.x
✗ Do not edit main checkout

NEXT:
P1.2.2 — Parser (Tokens → AST) — wraps this lexer with Chevrotain's CstParser

Verification checklist (for reviewer agent)

  • Chevrotain version pinned in package.json
  • Keyword tokens defined before Identifier to ensure longest-match
  • Variable token uses $ prefix explicitly
  • Float literals produce error with line/column
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.2.1
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.2.1
  branch: feature/p1-2-1-lexer
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "Chevrotain-based lexer for κ DSL. Tokens: 18 keywords, 12 operators, 7 categories (Keyword, Identifier, Variable, Integer, String, Operator, Delimiter). Line/column tracked. Float and underscore-integer literals rejected with position info. Chevrotain 11.0.3 pinned in package.json."
  blockers: []

Common gotchas

  • Chevrotain token ordering matters for longest-match — keywords must be declared before generic Identifier; if you reverse the order, “rule” tokenizes as Identifier not Keyword.
  • Variable $ handling — Chevrotain’s default identifier pattern doesn’t allow $ prefix; write a custom pattern like /\$[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*/.
  • Unicode identifier patterns are verbose in regex — use /[\p{XID_Start}][\p{XID_Continue}]*/u with the u flag. Without the flag, Unicode categories don’t work.
  • Skip whitespace only in Lexer mode — do NOT skip comments silently at this layer; leave comment handling explicit in the parser if needed. For Phase 1, comments are simply not in the grammar.

P1.2.2 — Parser (Tokens → AST)

Spec source: task-breakdown.md §P1.2.2 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §1 + §2 Worktree: feature/p1-2-2-parser Branch command: git worktree add .worktrees/claude/p1-2-2-parser -b feature/p1-2-2-parser origin/main Estimated effort: L (Large — 1–2 days) Depends on: P1.2.1 Unblocks: P1.2.3 (validator walks AST), P1.2.4 (registry uses parsed rules), P1.3.1 (engine evaluates AST), P1.5.4 (canonical serialization)

Files to create

  • src/domains/rules/parser.ts
  • src/domains/rules/__tests__/parser.test.ts

Acceptance criteria

  • Chevrotain CstParser or EmbeddedActionsParser built on top of P1.2.1 lexer
  • Parses the 4 rule types: Admission, StateTransition, Consequence, Promotion
  • AST node types match extraction §2: RuleNode, GuardClause, EffectCall, BinaryOp, UnaryOp, LogicalOp, IntLiteral, BoolLiteral, StringLiteral, VarRef, FuncCall
  • Operator precedence: NOT > AND > OR; *///% > +/-; comparison > logical
  • Guard blocks: guards { <clauses> }; clause (Expression | else) -> (admit | reject STRING)
  • Effect blocks: effects { <calls> }; call IDENTIFIER ( ArgList )
  • Error recovery: recoveryEnabled: true; reports first 5 errors, doesn’t crash on malformed input
  • Round-trip test: parse(serialize(parse(x))) == parse(x) (structural equality)
  • AST cap enforcement: rejects any single rule with > 10,000 AST nodes at parse time

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.2.2
  • docs/reference/extractions/kappa-rule-engine-extraction.md §1 (EBNF) + §2 (AST node types)
  • docs/3-world/physics/laws/rule-engine.md §Worked rule (AcceptCommitment example)
  • docs/architecture/decisions/ADR-006-dsl-grammar.md
  • src/domains/rules/lexer.ts (P1.2.1)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.2.2 — Parser (Tokens → AST)
Build the Chevrotain parser that turns a token stream from P1.2.1 into a typed AST.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.2.2
3. docs/reference/extractions/kappa-rule-engine-extraction.md §1 (EBNF — the canonical superset grammar) + §2 (AST Node Types)
4. docs/3-world/physics/laws/rule-engine.md §Worked rule (AcceptCommitment) for a realistic fixture
5. docs/architecture/decisions/ADR-006-dsl-grammar.md
6. src/domains/rules/lexer.ts (P1.2.1)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-2-2-parser -b feature/p1-2-2-parser origin/main
cd .worktrees/claude/p1-2-2-parser

FILES TO CREATE:
- src/domains/rules/parser.ts
  * Export AST node types matching extraction §2 (RuleNode, GuardClause, EffectCall, BinaryOp, UnaryOp, LogicalOp, IntLiteral, BoolLiteral, StringLiteral, VarRef, FuncCall)
  * Each node carries {type, location: {startLine, startColumn, endLine, endColumn}} plus type-specific fields
  * Use Chevrotain's EmbeddedActionsParser (cleaner than CST→AST visitor for this shape)
  * Grammar rules (matching extraction §1 EBNF):
    - ruleset:  rule*
    - rule:     "rule" IDENTIFIER "{" guardBlock effectBlock "}"
    - guardBlock:  "guards" "{" guardClause+ "}"
    - guardClause: (expression | "else") "->" action
    - action:   "admit" | "reject" STRING
    - effectBlock: "effects" "{" effectCall+ "}"
    - effectCall: IDENTIFIER "(" argList? ")"
    - expression:   orExpr
    - orExpr:       andExpr ("or" andExpr)*
    - andExpr:      notExpr ("and" notExpr)*
    - notExpr:      "not"? comparison
    - comparison:   additive (compOp additive)?
    - additive:     multiplicative (("+"|"-") multiplicative)*
    - multiplicative: unary (("*"|"/"|"%") unary)*
    - unary:        "-"? primary
    - primary:      INTEGER | "true" | "false" | variable | funcCall | "(" expression ")"
    - variable:     "$" dotPath
    - funcCall:     IDENTIFIER "(" argList? ")"
    - argList:      expression ("," expression)*
  * AST cap check: count nodes recursively, reject rule with > 10000
  * Error recovery: pass recoveryEnabled: true; collect up to 5 parse errors before giving up

- src/domains/rules/__tests__/parser.test.ts
  * Fixture 1: parse the AcceptCommitment rule from rule-engine.md and assert AST shape
  * Fixture 2: parse a rule with all operator types and assert precedence
    Input:   $a + $b * $c == 10 and not $d
    Expected: And(Eq(Add($a, Mul($b, $c)), 10), Not(Var(d)))
  * Fixture 3: malformed input "rule X { guards { -> admit } }" (missing expression) → 1 error, no crash
  * Fixture 4: 10,000-deep nested expression → AST cap error
  * Fixture 5: round-trip: parse(canonicalize(parse(original))) structurally equals parse(original)

ACCEPTANCE CRITERIA (headline):
✓ All 11 AST node types exported
✓ Operator precedence as specified
✓ 4 rule types parsed (Admission / StateTransition / Consequence / Promotion are identified by rule-name convention or attribute — document in the PR)
✓ recoveryEnabled: true; first 5 errors reported
✓ AST cap at 10k nodes enforced at parse time
✓ Round-trip property holds

SUCCESS CHECK:
cd .worktrees/claude/p1-2-2-parser && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.2.2", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.2.2
branch: feature/p1-2-2-parser
worktree: .worktrees/claude/p1-2-2-parser
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Chevrotain EmbeddedActionsParser producing typed AST matching extraction §2. Grammar matches EBNF superset in extraction §1. Error recovery with 5-error cap; AST cap at 10000 nodes.
blockers: none")

FORBIDDENS:
✗ Do not define AST nodes as classes with behavior — they are pure data (plain objects or records)
✗ Do not perform semantic validation here — that is P1.2.3's job
✗ Do not collapse operator precedence into a single rule — use the stratified grammar per extraction §1
✗ Do not edit main checkout

NEXT:
P1.2.3 — AST Validator (walks the AST to reject forbidden ops / type errors)

Verification checklist (for reviewer agent)

  • All 11 AST node types exported with type discriminant (type: "BinaryOp", etc.)
  • Operator precedence stratified in grammar (not via precedence table hack)
  • recoveryEnabled: true in parser config
  • AST cap enforced via recursive node counter
  • Round-trip test passes
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.2.2
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.2.2
  branch: feature/p1-2-2-parser
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "Chevrotain EmbeddedActionsParser built on P1.2.1 lexer. Emits typed AST matching extraction §2 (11 node types: RuleNode, GuardClause, EffectCall, BinaryOp, UnaryOp, LogicalOp, IntLiteral, BoolLiteral, StringLiteral, VarRef, FuncCall). Grammar matches EBNF superset from extraction §1; operator precedence stratified via orExpr  andExpr  notExpr  comparison  additive  multiplicative  unary  primary. Error recovery with 5-error cap. AST cap at 10000 nodes enforced at parse time. Round-trip property tested."
  blockers: []

Common gotchas

  • Chevrotain’s EmbeddedActionsParser vs CstParser — for a read-only AST shape, EmbeddedActions is simpler (direct return of node objects); CST + visitor is needed if you plan multiple passes. Pick one and stick with it.
  • Left recursion — Chevrotain is LL(k), so left-recursive grammar rules must be rewritten as iteration. The EBNF uses { ... } for repetition which maps cleanly to MANY rules — use that, not OR([...]) on recursive references.
  • AST cap placement — count nodes AFTER the full AST is built; counting mid-parse requires threading state through Chevrotain’s parser DSL which is brittle. Walk the final tree with a simple recursive counter.

P1.2.3 — AST Validator

Spec source: task-breakdown.md §P1.2.3 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §2 (AST) + §5 (Rule Execution Flow) Worktree: feature/p1-2-3-validator Branch command: git worktree add .worktrees/claude/p1-2-3-validator -b feature/p1-2-3-validator origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.2.2 Unblocks: P1.2.4 (registry invokes validator before indexing)

Files to create

  • src/domains/rules/validator.ts
  • src/domains/rules/__tests__/validator.test.ts

Acceptance criteria

  • Rejects rules that read local state (clock, filesystem, network, process)
  • Rejects rules with randomness (except VRF-input references — $vrf_output)
  • Rejects rules with side effects (HTTP calls, file writes, stdout)
  • Rejects rules that mutate input events
  • Type checking: operands compatible with operators (int + string rejected)
  • Scope checking: variables defined before use (no forward references)
  • Cycle detection: no infinite recursion in rule references
  • Axiom pre-check: rejects rules that violate AX-01–AX-07 at load time
  • Returns structured ValidationResult with all errors, not just first

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.2.3
  • docs/3-world/physics/laws/rule-engine.md §Forbidden operations + §Constitutional axioms
  • docs/3-world/physics/constitution.md (AX-01–AX-07)
  • src/domains/rules/parser.ts (P1.2.2)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.2.3 — AST Validator
Walk the parsed AST and reject any rule that violates κ's determinism invariants or constitutional axioms.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.2.3
3. docs/3-world/physics/laws/rule-engine.md §Forbidden operations + §Constitutional axioms
4. docs/3-world/physics/constitution.md (AX-01 through AX-07)
5. src/domains/rules/parser.ts (P1.2.2)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-2-3-validator -b feature/p1-2-3-validator origin/main
cd .worktrees/claude/p1-2-3-validator

FILES TO CREATE:
- src/domains/rules/validator.ts
  * Export validate(rule: RuleNode): ValidationResult
  * ValidationResult = {valid: true} | {valid: false, errors: ValidationError[]}
  * Checks (each a separate function, composed):
    1. forbiddenFunctions: reject calls to clock/fs/network/random built-ins
       - Blocklist: "time", "now", "read_file", "http_get", "random", "rand"
       - Allowlist exception: "$vrf_output" variable reads for VRF inputs
    2. sideEffectsInGuard: guard expressions must be read-only (no effect calls)
    3. mutationOfInput: event fields cannot be reassigned
    4. typeCompatibility: int + int OK, int + string = error, comparison between different types = error
       (use a simple type-inference walker: IntLiteral→int, StringLiteral→string, VarRef→unknown-but-tracked)
    5. scopeCheck: all variables must be resolvable in rule context or introduced explicitly
    6. cycleDetection: topological sort over rule-to-rule references; any SCC > 1 is a cycle
    7. axiomCheck: dedicated check per AX-01..AX-07 (documented as stubs that pass for now until π lands — but structured correctly)
  * Each check produces {code, message, path, location} structured errors
  * Aggregate ALL errors across all checks — do not short-circuit

- src/domains/rules/__tests__/validator.test.ts
  * Fixture per check:
    - forbiddenFunctions: rule with "now()" call → error
    - sideEffectsInGuard: "guards { -> admit stake.freeze($a) }" → error
    - mutationOfInput: rule attempting to set $event.status → error
    - typeCompatibility: 5 + "foo" → error
    - scopeCheck: using $undefined → error
    - cycleDetection: rule A references rule B which references rule A → error
    - axiomCheck: AX-01 stub returns pass
  * Happy-path fixture: a valid rule returns {valid: true}
  * Multi-error fixture: rule with 3 issues returns {valid: false, errors: [3 items]}

ACCEPTANCE CRITERIA (headline):
✓ 7 validation checks implemented
✓ Each rejects the target class of violation
✓ Aggregates all errors, does not short-circuit
✓ Structured ValidationError format

SUCCESS CHECK:
cd .worktrees/claude/p1-2-3-validator && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.2.3", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.2.3
branch: feature/p1-2-3-validator
worktree: .worktrees/claude/p1-2-3-validator
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: AST validator with 7 checks: forbidden functions, side effects in guard, input mutation, type compatibility, scope, cycle detection, axioms. Aggregates all errors per pass.
blockers: none")

FORBIDDENS:
✗ Do not short-circuit on first error — aggregate
✗ Do not consult external I/O for axiom check (axioms are static)
✗ Do not mutate the input AST — validator is read-only
✗ Do not edit main checkout

NEXT:
P1.2.4 — Rule Loader / Registry (wraps parser + validator, produces indexed registry)

Verification checklist (for reviewer agent)

  • 7 checks listed and tested separately
  • Multi-error aggregation: one malformed rule can produce > 1 error
  • ValidationError format includes code, message, path, location
  • Axiom check stubs structured for later π wiring
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.2.3
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.2.3
  branch: feature/p1-2-3-validator
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "AST validator module with 7 independent checks: forbidden functions (clock, fs, network, RNG), side effects in guard blocks, input mutation, type compatibility, scope (variable-before-use), cycle detection (topological sort), axiom pre-check (AX-01 to AX-07 stubs). Aggregates all errors across checks, no short-circuit. Structured ValidationError with code/message/path/location."
  blockers: []

Common gotchas

  • Type inference is best-effort in κ — the DSL has no explicit type declarations, so you’re tracking types by propagation. Unknown (e.g., variable types without a binding) must be allowed unless there’s a hard constraint.
  • Cycle detection scope — only named rule-to-rule references form cycles. Within a single rule, the AST is a tree by construction (parser produces it), so no cycles are possible within one rule.
  • Axiom checks are stubs in Phase 1 — the full axiom system activates with π governance. For P1.2.3, implement the structural scaffolding (a named function per axiom) so the wiring is in place when π lands. Returning pass from each is acceptable; document clearly.

P1.2.4 — Rule Loader / Registry

Spec source: task-breakdown.md §P1.2.4 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §8 (process_action) + §7 (TransitionType Enum) Worktree: feature/p1-2-4-registry Branch command: git worktree add .worktrees/claude/p1-2-4-registry -b feature/p1-2-4-registry origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.2.3 Unblocks: P1.4.1 (admission evaluator queries registry)

Files to create

  • src/domains/rules/registry.ts
  • src/domains/rules/__tests__/registry.test.ts

Acceptance criteria

  • loadRuleset(source: string): RuleRegistry — parses + validates + indexes a source file
  • Registry sorts rules by specificity: (a) guard term count descending, (b) declaration order
  • Specificity ties at load time → explicit AmbiguousRulesetError — refuse boot
  • getRule(name: string): RuleNode | null — named lookup
  • getByTransitionType(type: TransitionType): RuleNode[] — indexed lookup; transition types per extraction §7
  • computeVersionHash(): string — stub delegating to P1.5.1 when landed (return placeholder for now)
  • Load-time error aggregation: reports all validator errors in one pass

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.2.4
  • docs/3-world/physics/laws/rule-engine.md §Rule application algorithm
  • docs/reference/extractions/kappa-rule-engine-extraction.md §7 (TransitionType) + §8 (process_action, RULE_REGISTRY)
  • src/domains/rules/parser.ts, src/domains/rules/validator.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.2.4 — Rule Loader / Registry
Build the indexed rule registry that sits above parser + validator and feeds the interpreter.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.2.4
3. docs/3-world/physics/laws/rule-engine.md §Rule application algorithm + §Specificity ordering
4. docs/reference/extractions/kappa-rule-engine-extraction.md §7 (TransitionType 13-value enum) + §8 (RULE_REGISTRY + process_action)
5. src/domains/rules/parser.ts, src/domains/rules/validator.ts

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-2-4-registry -b feature/p1-2-4-registry origin/main
cd .worktrees/claude/p1-2-4-registry

FILES TO CREATE:
- src/domains/rules/registry.ts
  * TransitionType enum from extraction §7 (13 values: COMMITMENT_CREATE, COMMITMENT_ACCEPT, SETTLEMENT_COMPLETE, SETTLEMENT_FAIL, DISPUTE_OPEN, DISPUTE_RESOLVE, GOVERNANCE_PROPOSE, GOVERNANCE_VOTE, IDENTITY_CREATE, IDENTITY_UPDATE, FORK_CREATE, FORK_MERGE, REPUTATION_DECAY)
  * class RuleRegistry:
    - constructor(rules: RuleNode[])  — private; use loadRuleset
    - static loadRuleset(source: string): RuleRegistry
      1. Parse source (P1.2.2)
      2. Validate each rule (P1.2.3); aggregate errors; throw if any
      3. Compute specificity score per rule: count guard terms (operands across OrExpr, AndExpr, comparisons)
      4. Sort rules stable: (specificity desc, declaration order asc)
      5. Detect ties: if two rules have identical specificity AND same applicable transition type, throw AmbiguousRulesetError
      6. Build name index: Map<string, RuleNode>
      7. Build type index: Map<TransitionType, RuleNode[]>
      8. Return new RuleRegistry
    - getRule(name: string): RuleNode | null
    - getByTransitionType(type: TransitionType): RuleNode[]
    - computeVersionHash(): string
      * Phase 1 stub: return `sha256:stub:${ruleset-length-bigint}`
      * Replaced with P1.5.1 implementation when that lands
    - getAll(): RuleNode[]  — sorted by specificity
  * AmbiguousRulesetError with {rule1_name, rule2_name, specificity, transition_type}

- src/domains/rules/__tests__/registry.test.ts
  * loadRuleset happy path: 3-rule source file → registry with 3 entries
  * loadRuleset invalid: source with validator error → aggregated ValidationError thrown
  * Specificity sort: rule A has 4 guard terms, rule B has 2; A comes first
  * Tie detection: two rules, identical specificity + type → AmbiguousRulesetError
  * getRule: name lookup returns correct node
  * getByTransitionType: returns only rules applicable to that transition type (determined by a `@transition` annotation or by rule name prefix convention — document which)
  * computeVersionHash: returns non-empty string

ACCEPTANCE CRITERIA (headline):
✓ loadRuleset = parse + validate + index + sort
✓ Specificity: guard-term count desc, declaration order asc
✓ Tie → AmbiguousRulesetError (refuse boot)
✓ getRule, getByTransitionType, computeVersionHash exported
✓ All 13 TransitionType values enumerated

SUCCESS CHECK:
cd .worktrees/claude/p1-2-4-registry && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.2.4", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.2.4
branch: feature/p1-2-4-registry
worktree: .worktrees/claude/p1-2-4-registry
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: RuleRegistry module: loadRuleset(parse+validate+sort+index), getRule by name, getByTransitionType across 13 transition types, computeVersionHash stub. Specificity ties throw AmbiguousRulesetError.
blockers: none")

FORBIDDENS:
✗ Do not allow registry mutation after construction — immutable
✗ Do not defer tie detection to runtime — catch at load time
✗ Do not hardcode transition types — use the enum from extraction §7
✗ Do not edit main checkout

NEXT:
P1.3.1 — Core Evaluation Loop (interpreter that consumes this registry)

Verification checklist (for reviewer agent)

  • TransitionType enum has exactly 13 values matching extraction §7
  • loadRuleset aggregates ALL validator errors before throwing
  • Specificity sort stable (same specificity → declaration order)
  • AmbiguousRulesetError thrown for true ties
  • Registry is immutable post-construction
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.2.4
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.2.4
  branch: feature/p1-2-4-registry
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "RuleRegistry wraps parser (P1.2.2) + validator (P1.2.3) with indexed lookup. loadRuleset parses + validates + sorts by specificity (guard-term count desc, declaration order asc) + detects ties (AmbiguousRulesetError). Exposes getRule by name and getByTransitionType over the 13-value TransitionType enum from extraction §7. Immutable post-construction. computeVersionHash returns P1.5.1 stub."
  blockers: []

Common gotchas

  • Guard term count for specificity — the concept doc says “guard term count descending.” A “term” is a leaf expression in a comparison or logical combination. Count only top-level operands, not nested sub-expressions; otherwise specificity becomes sensitive to refactoring that should be semantically neutral.
  • Rule-to-transition-type mapping — κ doesn’t natively encode which transition type a rule applies to. Options: (a) naming convention (e.g., rule AcceptCommitment → COMMITMENT_ACCEPT), or (b) explicit annotation (e.g., rule @COMMITMENT_ACCEPT AcceptCommitment). Pick (b) and add @IDENTIFIER handling to the lexer/parser if not already there (coordinate with P1.2.1/P1.2.2 authors if they need a patch).
  • Registry is NOT a singleton — a host process may load multiple rulesets (for testing, migration, parity harness). Do not expose a global registry; always construct via loadRuleset.

P1.3.1 — Core Evaluation Loop

Spec source: task-breakdown.md §P1.3.1 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §5 (Rule Execution Flow) + §6 (Rule Execution Order) Worktree: feature/p1-3-1-engine Branch command: git worktree add .worktrees/claude/p1-3-1-engine -b feature/p1-3-1-engine origin/main Estimated effort: L (Large — 1–2 days) Depends on: P1.2.2, P1.1.1 Unblocks: P1.3.2, P1.3.3, P1.3.4, P1.4.1, P1.5.5

Files to create

  • src/domains/rules/engine.ts
  • src/domains/rules/__tests__/engine.test.ts

Acceptance criteria

  • Evaluates AST nodes recursively with an immutable context
  • Rule execution order: Admission → StateTransition → Consequence → Promotion (fixed, per extraction §6)
  • Within each category: alphabetical by rule name (stable ordering)
  • First-match-wins: once a guard matches, remaining guards in the same rule are skipped
  • Context contains: event, current_state (read-only snapshot), rule_version, epoch, actor binding
  • Returns list of {type, target, field, old_value, new_value} mutations
  • No mutations applied during evaluation (collect-then-apply pattern)
  • Timeout MAX_INTEGER_OPS=10000RuleBudgetExceeded("integer_ops")
  • Depth cap MAX_CALL_DEPTH=16RuleBudgetExceeded("call_depth")
  • Arg cap MAX_ARG_COUNT=8RuleBudgetExceeded("arg_count")

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.3.1
  • docs/3-world/physics/laws/rule-engine.md §Rule application algorithm + §Evaluation budget
  • docs/reference/extractions/kappa-rule-engine-extraction.md §5 + §6
  • src/domains/rules/parser.ts, src/domains/rules/integer-math.ts, src/domains/rules/registry.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.3.1 — Core Evaluation Loop
Implement the deterministic AST evaluator — the heart of κ.

FILES TO READ FIRST:
1. CLAUDE.md (worktree §3, gate §5, 5-step chain §6, writeback §7)
2. docs/guides/implementation/task-breakdown.md §P1.3.1
3. docs/3-world/physics/laws/rule-engine.md §Rule application algorithm + §Evaluation budget + §Default budget constants
4. docs/reference/extractions/kappa-rule-engine-extraction.md §5 (Rule Execution Flow pseudocode) + §6 (Rule Execution Order)
5. src/domains/rules/parser.ts (AST types)
6. src/domains/rules/integer-math.ts (bigint helpers)
7. src/domains/rules/registry.ts (rule lookup)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-3-1-engine -b feature/p1-3-1-engine origin/main
cd .worktrees/claude/p1-3-1-engine

FILES TO CREATE:
- src/domains/rules/engine.ts
  * Context type: { event, state, rule_version, epoch, bindings: Map<string, bigint|string|boolean>, budget: BudgetTracker }
  * BudgetTracker: { integer_ops: number, call_depth: number, current_arg_count: number }
  * Mutation type: { kind: "set"|"emit"|"apply", target: string, field: string, old_value?: any, new_value: any }
  * evaluate(rule: RuleNode, context: Context): RuleResult
    * RuleResult = { status: "admitted", mutations: Mutation[] } | { status: "rejected", reason: string }
    * Execution:
      1. For each guard clause (in order):
         - Evaluate condition (or true if "else")
         - If match: if action is "reject" → rejected; if "admit" → proceed to effects; break guard loop
      2. For each effect call:
         - Build Mutation from effect + current context (no application)
         - Append to mutations list
      3. Return { admitted, mutations }
  * evaluateExpr(expr: Expression, ctx: Context): bigint | string | boolean
    * Recursive walker
    * On every walk: ctx.budget.integer_ops++; check against MAX
    * On every func/method call: ctx.budget.call_depth++; check against MAX; decrement on return
  * executeRuleset(registry: RuleRegistry, event, state, rule_version, epoch): TransitionResult
    * Group rules by category (Admission → StateTransition → Consequence → Promotion — extracted from annotation on each rule or first-token convention)
    * Within each group: sort alphabetically by rule name
    * Execute each rule; collect all mutations
    * Return { all_mutations, per_category_results }
  * Constants exported: MAX_INTEGER_OPS=10000, MAX_CALL_DEPTH=16, MAX_ARG_COUNT=8
  * Error type: RuleBudgetExceeded { which: "integer_ops" | "call_depth" | "arg_count", limit, observed }

- src/domains/rules/__tests__/engine.test.ts
  * Fixture 1: simple rule "guards { $a > 5 -> admit } effects { $result = 42 }"
    - Context with $a=10 → admitted with mutation $result=42
    - Context with $a=3 → rejected, no mutations
  * Fixture 2: multi-guard first-match-wins:
    "guards { $x > 10 -> admit $x < 0 -> reject NEG else -> admit }"
    - $x=15 → admitted; $x=-1 → rejected(NEG); $x=5 → admitted (else)
  * Fixture 3: category ordering:
    - Three rules in registry, one Admission, one Consequence, one Promotion
    - Assert execution order: Admission runs first
  * Fixture 4: AST cap enforcement: manually construct context with budget.integer_ops=9999, one more op → RuleBudgetExceeded
  * Fixture 5: collect-then-apply: assert no state object is mutated during evaluate

ACCEPTANCE CRITERIA (headline):
✓ Recursive AST walker with immutable context
✓ Execution order: Admission → StateTransition → Consequence → Promotion; alpha within group
✓ First-match-wins guard evaluation
✓ Mutations collected, not applied
✓ 3 budget caps enforced with typed errors

SUCCESS CHECK:
cd .worktrees/claude/p1-3-1-engine && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.3.1", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.3.1
branch: feature/p1-3-1-engine
worktree: .worktrees/claude/p1-3-1-engine
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: κ deterministic interpreter. evaluate(rule, ctx) walks AST with budget tracking (3 limits). executeRuleset orders by category (Admission → StateTransition → Consequence → Promotion) then alpha by name. Collect-then-apply mutation pattern.
blockers: none")

FORBIDDENS:
✗ Do not apply mutations during evaluate — collect only
✗ Do not mutate the Context during recursion — return new Context for bindings changes
✗ Do not throw non-RuleBudgetExceeded errors for budget overruns — use the typed error
✗ Do not edit main checkout

NEXT:
P1.3.2 — Built-in Functions (provides min/max/isqrt/etc callable from rules)

Verification checklist (for reviewer agent)

  • evaluate is pure (no writes to inputs)
  • Mutations are returned as a list, not applied in-band
  • All 3 budget caps enforced with typed errors
  • Execution order: Admission → StateTransition → Consequence → Promotion
  • Within category: alphabetical by rule name
  • First-match-wins within one rule’s guard block
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.3.1
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.3.1
  branch: feature/p1-3-1-engine
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "κ deterministic interpreter. evaluate(rule, context) walks AST recursively with budget tracking (MAX_INTEGER_OPS=10000, MAX_CALL_DEPTH=16, MAX_ARG_COUNT=8)  overruns throw RuleBudgetExceeded. executeRuleset groups rules by category (Admission  StateTransition  Consequence  Promotion) then alphabetical within group. First-match-wins guard evaluation; collect-then-apply mutation pattern (no side effects during walk)."
  blockers: []

Common gotchas

  • executeRuleset order is load-bearing for consensus — two arbiters with the same registry + same event must produce bit-identical mutation lists. Any non-determinism here (e.g., Map iteration order in JS is insertion order, but be careful with Object.keys) will break θ consensus.
  • Budget tracking must be threaded, not global — each executeRuleset call gets a fresh BudgetTracker. Sharing a global tracker across invocations leaks counts.
  • Bindings are not the same as state — bindings are rule-local variable introductions (if any); state is the read-only snapshot. Do not collapse them.

P1.3.2 — Built-in Functions

Spec source: task-breakdown.md §P1.3.2 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §3 (8 Built-in Functions) Worktree: feature/p1-3-2-builtins Branch command: git worktree add .worktrees/claude/p1-3-2-builtins -b feature/p1-3-2-builtins origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.3.1, P1.1.1 Unblocks: P1.3.1’s full surface (evaluator consumes built-ins)

Files to create

  • src/domains/rules/builtins.ts
  • src/domains/rules/__tests__/builtins.test.ts

Acceptance criteria

  • min(a, b), max(a, b), abs(a), cap(v, m) — integer-only
  • clamp(v, lo, hi)max(lo, min(v, hi))
  • isqrt(n) — Newton’s method integer square root (pseudocode in extraction §3)
  • ilog2(n) — integer floor of log base 2
  • decay(v, rate_bps) — single-epoch decay; delegates to integer-math.ts
  • diminishing(v, k)(v * k) / (k + v) diminishing-returns transform
  • bps_mul(v, b), bps_div(v, b) — delegate to P1.1.1
  • hash(data) — SHA-256 hex string (Node’s crypto.createHash)
  • vrf_verify(pk, proof, input) — VRF proof verification per ADR-002 (stub OK, returns Bool)
  • All functions pure (no side effects, same input = same output)
  • Each function counts as 1 or more integer ops (budget-cost table documented)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.3.2
  • docs/3-world/physics/laws/rule-engine.md §Built-in functions
  • docs/reference/extractions/kappa-rule-engine-extraction.md §3 (with isqrt + ilog2 pseudocode)
  • docs/architecture/decisions/ADR-002-vrf-implementation.md
  • src/domains/rules/integer-math.ts, src/domains/rules/engine.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.3.2 — Built-in Functions
Implement the 8-to-14 built-in function table callable from rule bodies.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.3.2
3. docs/3-world/physics/laws/rule-engine.md §Built-in functions
4. docs/reference/extractions/kappa-rule-engine-extraction.md §3 (with isqrt + ilog2 pseudocode)
5. docs/architecture/decisions/ADR-002-vrf-implementation.md (VRF stub shape)
6. src/domains/rules/integer-math.ts, src/domains/rules/engine.ts

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-3-2-builtins -b feature/p1-3-2-builtins origin/main
cd .worktrees/claude/p1-3-2-builtins

FILES TO CREATE:
- src/domains/rules/builtins.ts
  * export type BuiltinTable = Map<string, (args: (bigint|string|boolean)[]) => (bigint|string|boolean)>
  * Registered built-ins:
    - "min" / "max" — 2-arg bigint → bigint
    - "abs" / "cap" — 1-arg bigint → bigint (cap: 2-arg)
    - "clamp" — 3-arg bigint → bigint
    - "isqrt" — 1-arg bigint → bigint (Newton's method; reject negative)
    - "ilog2" — 1-arg bigint → bigint (bit_length - 1; defined as 0 for n≤0 per extraction §3)
    - "decay" — 2-arg (bigint, bigint) → bigint; delegates to integer-math.decay(v, rate, 1n)
    - "diminishing" — 2-arg (bigint, bigint) → bigint; (v*k) / (k+v)
    - "bps_mul" / "bps_div" — 2-arg bigint → bigint; delegates to integer-math
    - "hash" — 1-arg string → string (SHA-256 hex); uses Node crypto.createHash("sha256")
    - "vrf_verify" — 3-arg (pk: string, proof: string, input: string) → bool; stub returns true for a canonical test-vector, false otherwise
  * Each function validates arg count and types; throws BuiltinTypeError on mismatch
  * Budget-cost table (exported as BUILTIN_COSTS):
    - min/max/abs/cap/clamp: 1 integer_op each
    - isqrt/ilog2/decay/diminishing/bps_mul/bps_div: 5 integer_ops each (reflect non-trivial compute)
    - hash: 100 integer_ops (SHA-256 is not cheap)
    - vrf_verify: 500 integer_ops (expensive even as stub, so the budget reflects production shape)
  * Register-in-engine helper: bindBuiltins(ctx: Context): Context — returns a context where bindings table is populated with the BuiltinTable lookup

- src/domains/rules/__tests__/builtins.test.ts
  * Test each arithmetic builtin against 3+ inputs with known outputs
  * isqrt: [100 → 10], [101 → 10], [0 → 0], [negative throws]
  * ilog2: [8 → 3], [1024 → 10], [0 → 0]
  * hash("hello world") → SHA-256 hex (well-known value)
  * vrf_verify with canonical test-vector → true; with garbage → false
  * Type mismatch: min(5n, "foo") → BuiltinTypeError
  * Determinism: hash of same input twice → identical

ACCEPTANCE CRITERIA (headline):
✓ 13 built-in functions implemented (extraction §3 table + concept doc table union)
✓ All pure and deterministic
✓ Budget costs table documented
✓ Type validation per call

SUCCESS CHECK:
cd .worktrees/claude/p1-3-2-builtins && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.3.2", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.3.2
branch: feature/p1-3-2-builtins
worktree: .worktrees/claude/p1-3-2-builtins
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: 13 built-in functions (arithmetic, bps, crypto, vrf stub) callable from DSL rules. All pure, deterministic, bigint-based. Budget-cost table exported.
blockers: none")

FORBIDDENS:
✗ Do not use Math.sqrt, Math.log2, or any Number-based math — bigint only
✗ Do not call fetch/http/fs — builtins are pure
✗ Do not emit events or log from builtins — side-effect-free
✗ Do not edit main checkout

NEXT:
P1.3.3 — State Access Layer (read-only snapshot handing)

Verification checklist (for reviewer agent)

  • All bigint arithmetic, no Number
  • isqrt uses Newton’s method pseudocode from extraction §3
  • hash uses Node crypto.createHash(“sha256”)
  • vrf_verify stub returns bool, shape matches ADR-002
  • Budget-cost table exported
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.3.2
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.3.2
  branch: feature/p1-3-2-builtins
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "13 κ built-in functions: min/max/abs/cap/clamp (arithmetic), isqrt (Newton's integer sqrt), ilog2 (bit-length), decay/diminishing/bps_mul/bps_div (bps arithmetic), hash (SHA-256), vrf_verify (stub per ADR-002). All pure, deterministic, bigint-typed. Budget-cost table (BUILTIN_COSTS) exported and consumed by engine.ts budget tracker."
  blockers: []

Common gotchas

  • Newton’s method for bigint isqrt — the standard n=Number; x=n/2 bootstrap overflows for large bigints. Start with x = 1n << (bitLength(n) / 2n) instead.
  • crypto.createHash returns a hex string of unpredictable length — no, it’s always 64 hex chars for SHA-256. But verify with a fixture — drift in Node versions has been observed for other algorithms.
  • VRF stub validation — the stub must still reject obviously malformed inputs (wrong lengths, non-hex strings) to avoid false confidence in test fixtures. Stub ≠ completely permissive.

P1.3.3 — State Access Layer

Spec source: task-breakdown.md §P1.3.3 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §10 (ReadOnlyState Interface) Worktree: feature/p1-3-3-state-access Branch command: git worktree add .worktrees/claude/p1-3-3-state-access -b feature/p1-3-3-state-access origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.3.1 Unblocks: P1.4.1 (admission evaluator consumes state snapshots)

Files to create

  • src/domains/rules/state-access.ts
  • src/domains/rules/__tests__/state-access.test.ts

Acceptance criteria

  • Read-only state snapshot (frozen object or COW proxy)
  • State keys: reputation[node][domain], tokens[node], stake[node], epoch, event_count, fork_id, rule_version
  • with_binding(name, value) returns new context; original unchanged
  • No direct database access from rules — state is pre-loaded by the host
  • State diff output: {key, old_value, new_value} for each mutation
  • Merkle proof generation hook-point for state reads (stub wire into η)
  • Mutation attempts throw ReadOnlyStateError immediately

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.3.3
  • docs/3-world/physics/laws/rule-engine.md §State Access Pattern (if present; otherwise just the forbidden-ops section)
  • docs/reference/extractions/kappa-rule-engine-extraction.md §10
  • src/domains/rules/engine.ts
  • src/domains/proof/ (η Proof Store — for merkle hook shape)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.3.3 — State Access Layer
Build the read-only state snapshot that κ rules see during evaluation.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.3.3
3. docs/reference/extractions/kappa-rule-engine-extraction.md §10 (ReadOnlyState Interface)
4. docs/3-world/physics/laws/rule-engine.md §Forbidden operations (motivates immutability)
5. src/domains/rules/engine.ts (Context type)
6. src/domains/proof/ (for Merkle proof hook shape)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-3-3-state-access -b feature/p1-3-3-state-access origin/main
cd .worktrees/claude/p1-3-3-state-access

FILES TO CREATE:
- src/domains/rules/state-access.ts
  * interface ReadOnlyState {
      reputation: Map<NodeId, Map<Domain, bigint>>
      tokens:     Map<NodeId, TokenRecord[]>
      stakes:     Map<NodeId, bigint>
      epoch:      bigint
      event_count: bigint
      fork_id:    string      // hex
      rule_version: string    // sha256 hex
      with_binding(name: string, value: bigint|string|boolean): ReadOnlyState
    }
  * Implementation: class ReadOnlyStateImpl
    - Private fields matching the interface
    - All Maps are frozen Maps (use a Map wrapper that throws on set/delete)
    - with_binding returns a NEW ReadOnlyStateImpl; original unchanged
    - getReputation(node, domain): bigint — throws if not found? No — returns 0n per κ convention
    - getStake(node): bigint — returns 0n if missing
    - getTokens(node): TokenRecord[] — returns [] if missing
  * Mutation attempts (direct property assign, Map.set on frozen) → throw ReadOnlyStateError
  * Diff helper: computeDiff(before: ReadOnlyState, after: ReadOnlyState): StateDiff[]
    * StateDiff: { key: string, old_value: any, new_value: any }
    * Walks both states, enumerates changed keys
    * Deterministic key ordering (sorted)
  * Merkle-proof hook stub:
    * generateReadProof(state, key): ReadProof — stub returns {state_root: state.merkle_root, key, value, proof_path: []}
    * Production wiring lands in η integration PR

- src/domains/rules/__tests__/state-access.test.ts
  * Fixture 1: construct state with 3 nodes; getReputation returns values; missing key returns 0n
  * Fixture 2: mutation attempt via proxy → throws ReadOnlyStateError
  * Fixture 3: with_binding creates new state; original state's bindings unchanged
  * Fixture 4: computeDiff of two snapshots returns sorted StateDiff array
  * Fixture 5: generateReadProof returns stub proof with correct key + value
  * Determinism: serialize state to canonical form twice → byte-identical

ACCEPTANCE CRITERIA (headline):
✓ ReadOnlyState interface + impl (frozen Maps, throw on mutate)
✓ with_binding is immutable (returns new state)
✓ 7 required keys (reputation, tokens, stakes, epoch, event_count, fork_id, rule_version)
✓ computeDiff produces sorted key-by-key diff
✓ Merkle proof hook stubbed for η

SUCCESS CHECK:
cd .worktrees/claude/p1-3-3-state-access && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.3.3", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.3.3
branch: feature/p1-3-3-state-access
worktree: .worktrees/claude/p1-3-3-state-access
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: ReadOnlyState interface + impl. Frozen Maps, throws on mutation. with_binding immutable. 7 keys. Merkle read-proof stubbed for η.
blockers: none")

FORBIDDENS:
✗ Do not allow state.reputation.set(...) to succeed — wrap Maps
✗ Do not expose DB handles or prepared statements — state is pre-loaded
✗ Do not perform async I/O in getters — synchronous only
✗ Do not edit main checkout

NEXT:
P1.3.4 — Policy Gating (pre-guard pipeline)

Verification checklist (for reviewer agent)

  • Frozen Map wrapper prevents mutation
  • with_binding creates new instance (test assertion: original === original after call)
  • All 7 state keys exposed
  • computeDiff is deterministic (sorted key order)
  • Merkle proof stub has production-shape interface
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.3.3
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.3.3
  branch: feature/p1-3-3-state-access
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "ReadOnlyState layer for κ rules. Interface + frozen-Map impl with 7 keys (reputation, tokens, stakes, epoch, event_count, fork_id, rule_version). with_binding returns new state (immutable). computeDiff emits sorted StateDiff array for audit. Merkle read-proof hook stubbed to integrate with η Proof Store in a follow-up PR."
  blockers: []

Common gotchas

  • JS Object.freeze is shallow — a frozen Map’s .set still works because Map isn’t a plain object. Use a Proxy or a custom class wrapping Map that throws on set/delete/clear.
  • with_binding must be O(1) — avoid copying the entire state on every call. Use structural sharing or a persistent data structure for bindings.
  • computeDiff key ordering is deterministic across implementations — sort by codepoint, not locale collation. Otherwise consensus fails across nodes with different locales.

P1.3.4 — Policy Gating / Pre-guards

Spec source: task-breakdown.md §P1.3.4 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §9 (Policy Gating: check_policy) Worktree: feature/p1-3-4-policy-gate Branch command: git worktree add .worktrees/claude/p1-3-4-policy-gate -b feature/p1-3-4-policy-gate origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.3.1 Unblocks: P1.4.1 (admission evaluator runs policies before named rules)

Files to create

  • src/domains/rules/policy-gate.ts
  • src/domains/rules/__tests__/policy-gate.test.ts

Acceptance criteria

  • Policy enum P1–P13 per extraction §9
  • Each policy is a pure DSL expression (reuses P1.2.2 parser + P1.3.1 evaluator)
  • check_policy(id, actor, context){admitted, reason?}
  • check_all_policies(action, actor, context) → short-circuits on first failure
  • Policies run BEFORE named rule evaluation
  • Policies share evaluation budget with named rules (same 10k op cap)
  • Each policy has rejection reason pre-registered (no dynamic strings)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.3.4
  • docs/reference/extractions/kappa-rule-engine-extraction.md §9
  • src/domains/rules/engine.ts, src/domains/rules/parser.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.3.4 — Policy Gating / Pre-guards
Build the P1–P13 policy pre-guard pipeline that runs before named rules.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.3.4
3. docs/reference/extractions/kappa-rule-engine-extraction.md §9 (Policy Gating pseudocode)
4. src/domains/rules/engine.ts (evaluator reused)
5. src/domains/rules/parser.ts (for parsing policy DSL)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-3-4-policy-gate -b feature/p1-3-4-policy-gate origin/main
cd .worktrees/claude/p1-3-4-policy-gate

FILES TO CREATE:
- src/domains/rules/policy-gate.ts
  * enum PolicyId { P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13 }
  * interface Policy {
      id: PolicyId
      predicate_source: string    // DSL expression text
      predicate_ast: Expression   // parsed once at load time
      rejection_reason: string    // pre-registered
      applicable_actions: string[] // which action names this policy gates
    }
  * POLICIES: Record<PolicyId, Policy> — seed with stub predicates per extraction §9
    (Each policy gets a placeholder predicate text like "true" that always admits; substantive policies land per future round.)
  * check_policy(id: PolicyId, actor, context): PolicyResult
    * PolicyResult = { admitted: true } | { admitted: false, reason: string }
    * Inject actor binding, evaluate predicate via engine.ts
    * On any evaluation error (including budget exceeded) → { admitted: false, reason: "POLICY_EVAL_ERROR" }
  * check_all_policies(action_name: string, actor, context): PolicyResult
    * Filter POLICIES by applicable_actions
    * Run each in order (ascending PolicyId)
    * Short-circuit on first admit=false

- src/domains/rules/__tests__/policy-gate.test.ts
  * Fixture: P1 policy with predicate "$actor.reputation.execution >= 100"
    - Actor rep 150 → admitted
    - Actor rep 50 → rejected with P1's reason
  * Fixture: check_all_policies with P1, P2 both applicable; P1 fails → returns P1 reason (P2 not evaluated)
  * Fixture: all 13 policies loadable without parse errors
  * Fixture: policy that hits evaluation budget → {admitted: false, reason: "POLICY_EVAL_ERROR"}

ACCEPTANCE CRITERIA (headline):
✓ PolicyId enum with P1..P13
✓ POLICIES table with stubs seeded
✓ check_policy runs predicate via engine
✓ check_all_policies short-circuits on first failure
✓ Budget shared with named rules

SUCCESS CHECK:
cd .worktrees/claude/p1-3-4-policy-gate && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.3.4", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.3.4
branch: feature/p1-3-4-policy-gate
worktree: .worktrees/claude/p1-3-4-policy-gate
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: P1-P13 policy pre-guard table with predicate ASTs parsed once at load. check_policy + check_all_policies (short-circuit on first fail). Shares budget with named rules.
blockers: none")

FORBIDDENS:
✗ Do not define policy predicates as JS functions — they are DSL expressions parsed via P1.2.2
✗ Do not allow dynamic rejection reasons — each policy's reason is pre-registered
✗ Do not edit main checkout

NEXT:
P1.4.1 — Admission Evaluator (runs policy gate + named rules)

Verification checklist (for reviewer agent)

  • 13 policies in POLICIES table
  • Predicates parsed once, cached as AST
  • check_all_policies short-circuits properly
  • Rejection reasons pre-registered (no dynamic strings)
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.3.4
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.3.4
  branch: feature/p1-3-4-policy-gate
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "Policy gating module per extraction §9. PolicyId enum P1-P13 with predicate ASTs parsed once at load. check_policy evaluates via engine.ts, check_all_policies short-circuits on first failure. Budget is shared with named rules (same MAX_INTEGER_OPS). Rejection reasons pre-registered per policy (static strings, no dynamic formatting)."
  blockers: []

Common gotchas

  • Policies are not rules — they run before rules, with a simpler API (expression → bool). Don’t reuse RuleNode here; use a dedicated Policy interface.
  • Applicable-actions filtering is load-bearing — without it, every action pays the cost of every policy. Filter early.
  • Stub predicates should default-admit — if you seed with false, the test ruleset denies everything at boot, which breaks evaluator tests upstream.

P1.4.1 — Admission Evaluator

Spec source: task-breakdown.md §P1.4.1 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §8 (process_action) + §9 (Policy Gating) Worktree: feature/p1-4-1-admission Branch command: git worktree add .worktrees/claude/p1-4-1-admission -b feature/p1-4-1-admission origin/main Estimated effort: L (Large — 1–2 days) Depends on: P1.3.1, P1.3.4, P1.2.4 Unblocks: P1.4.2, P1.4.3, P1.4.4

Files to create

  • src/domains/rules/admission.ts
  • src/domains/rules/__tests__/admission.test.ts

Acceptance criteria

  • evaluateAdmission({caller, tool, mode, rep_snapshot, rule_version}): AdmissionResult
  • Returns {admitted: true, effect_mutations: [...]} | {admitted: false, reason: DenialReason}
  • Runs policy pre-guards first (P1.3.4), then named rules (P1.3.1)
  • Rule version stamp on every admission record
  • Pure function — no DB writes, no network calls
  • Timing independent of input values (constant-time comparison for sensitive fields)
  • Integration test: ≥20 representative (caller, tool, mode) tuples with expected verdicts

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.4.1
  • docs/3-world/physics/laws/rule-engine.md §Admission layer
  • docs/spec/s10-admission.md (authoritative admission spec)
  • docs/reference/extractions/kappa-rule-engine-extraction.md §8 + §9
  • src/domains/rules/engine.ts, policy-gate.ts, registry.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.4.1 — Admission Evaluator
Compose policy-gate + named-rules into the admission verdict function.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.4.1
3. docs/3-world/physics/laws/rule-engine.md §Admission layer
4. docs/spec/s10-admission.md (authoritative admission rules)
5. docs/reference/extractions/kappa-rule-engine-extraction.md §8 + §9
6. src/domains/rules/engine.ts, policy-gate.ts, registry.ts

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-4-1-admission -b feature/p1-4-1-admission origin/main
cd .worktrees/claude/p1-4-1-admission

FILES TO CREATE:
- src/domains/rules/admission.ts
  * interface AdmissionRequest {
      caller: NodeId
      tool: string
      mode: "normal" | "readonly" | "admin"
      rep_snapshot: ReadOnlyState
      rule_version: string
    }
  * type AdmissionResult =
      | { admitted: true, effect_mutations: Mutation[], rule_version: string }
      | { admitted: false, reason: DenialReason, rule_version: string }
  * evaluateAdmission(req: AdmissionRequest, registry: RuleRegistry): AdmissionResult
    Steps:
      1. Rule version check: if req.rule_version !== registry.computeVersionHash() → { denied, rule_version_mismatch }
      2. Run check_all_policies(req.tool, req.caller, context) — short-circuit on fail
      3. Build context with binding { $actor: req.caller, $tool: req.tool, $mode: req.mode }
      4. Look up applicable rules via registry.getByTransitionType or by tool name convention
      5. Run each in priority order (registry already sorted); first admit wins
      6. Return result with mutations or denial reason
  * Exports the constant-time comparison helper for rule-version strings (prevents timing side channels)

- src/domains/rules/__tests__/admission.test.ts
  * 20+ representative tuple tests:
    (caller=alice, tool=create_task, mode=normal) → expected admit with mutations
    (caller=bob_low_rep, tool=arbitrate, mode=normal) → expected deny(policy:P1)
    (caller=admin, tool=delete_all, mode=admin) → admit
    (caller=alice, tool=create_task, mode=normal, rule_version=wrong) → deny(rule_version_mismatch)
    ... (continue to ≥20 diverse fixtures)
  * Determinism: same request → same result (bit-identical)
  * Pure function: no DB writes side-effect — run in test sandbox with mocked DB
  * Timing independence: evaluate 100 requests with varied rule_version inputs; assert std-dev of evaluation time < 2x mean (loose but meaningful)

ACCEPTANCE CRITERIA (headline):
✓ evaluateAdmission composes rule-version check + policies + rules
✓ AdmissionRequest + AdmissionResult typed
✓ Rule version stamped on every result
✓ Pure, no I/O
✓ ≥20 tuple fixtures tested

SUCCESS CHECK:
cd .worktrees/claude/p1-4-1-admission && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.4.1", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.4.1
branch: feature/p1-4-1-admission
worktree: .worktrees/claude/p1-4-1-admission
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: evaluateAdmission composes rule-version check + P1-P13 policy gate + priority-sorted named rules. Returns AdmissionResult with mutations or typed DenialReason. Rule version stamped; constant-time comparison for version strings.
blockers: none")

FORBIDDENS:
✗ Do not perform DB writes inside evaluateAdmission — it's pure
✗ Do not vary execution time by input — avoid short-circuit on sensitive fields without constant-time compare
✗ Do not skip rule-version check — it's load-bearing for consensus
✗ Do not edit main checkout

NEXT:
P1.4.2 — Denial Reason Taxonomy (typed DenialReason union used here)

Verification checklist (for reviewer agent)

  • Pure function (no DB, no fetch, no fs)
  • Rule-version check before anything else
  • Policies run before named rules
  • 20+ fixtures covering admit, deny-policy, deny-rule-version, deny-no-match
  • Rule version in every result
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.4.1
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.4.1
  branch: feature/p1-4-1-admission
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "evaluateAdmission(request, registry) composes (1) constant-time rule-version check, (2) P1.3.4 policy gate, (3) P1.3.1 priority-sorted named rules. Returns AdmissionResult tagged with rule_version. Pure; no DB/fs/network. 20+ tuple fixtures covering admit, policy-denial, rule-version-mismatch, no-match paths."
  blockers: []

Common gotchas

  • Timing independence is easy to get wrong — even return x === y is not guaranteed constant-time in V8. Use a hand-rolled loop-over-bytes XOR-accumulate for rule-version comparison.
  • Rule-version check before policies — if you run policies first and only then find the version is wrong, you’ve leaked policy-evaluation timing to an attacker with a bad version. Version check FIRST.
  • Mutations are collected but not applied here either — admission returns the mutation list; application happens at β (task pipeline) commit time.

P1.4.2 — Denial Reason Taxonomy

Spec source: task-breakdown.md §P1.4.2 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §5 (Rule Execution Flow return values) Worktree: feature/p1-4-2-denial-reasons Branch command: git worktree add .worktrees/claude/p1-4-2-denial-reasons -b feature/p1-4-2-denial-reasons origin/main Estimated effort: S (Small — 1–2 hours) Depends on: P1.4.1 Unblocks: downstream consumers (audit, β tool-lock logging)

Files to create

  • src/domains/rules/denial-reasons.ts
  • src/domains/rules/__tests__/denial-reasons.test.ts

Acceptance criteria

  • Typed discriminated union: no_rule_matched, budget:integer_ops, budget:call_depth, budget:arg_count, effect_invariant_violated, axiom_violation:AX-01..AX-07, policy:P1..P13, rule_version_mismatch, ambiguous_ruleset
  • Each reason carries a structured details payload (no freeform strings)
  • Reason codes stable across upgrades (additive-only changes; no renumbering)
  • toString(reason) produces operator-readable rendering
  • JSON serialization preserves discriminant tag

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.4.2
  • docs/3-world/physics/laws/rule-engine.md §Rule application algorithm + §Evaluation budget
  • src/domains/rules/admission.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.4.2 — Denial Reason Taxonomy
Define the structured DenialReason discriminated union used by every κ rejection path.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.4.2
3. docs/3-world/physics/laws/rule-engine.md §Rule application algorithm + §Evaluation budget
4. src/domains/rules/admission.ts (P1.4.1 consumer)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-4-2-denial-reasons -b feature/p1-4-2-denial-reasons origin/main
cd .worktrees/claude/p1-4-2-denial-reasons

FILES TO CREATE:
- src/domains/rules/denial-reasons.ts
  * export type DenialReason =
      | { kind: "no_rule_matched", transition_type?: TransitionType }
      | { kind: "budget", which: "integer_ops"|"call_depth"|"arg_count", limit: number, observed: number }
      | { kind: "effect_invariant_violated", rule_name: string, invariant_id: string, details: string }
      | { kind: "axiom_violation", axiom: "AX-01"|"AX-02"|"AX-03"|"AX-04"|"AX-05"|"AX-06"|"AX-07", rule_name: string }
      | { kind: "policy", policy_id: PolicyId, reason: string }
      | { kind: "rule_version_mismatch", expected: string, got: string }
      | { kind: "ambiguous_ruleset", rule1: string, rule2: string, specificity: number }
  * export function renderDenialReason(r: DenialReason): string
    - Deterministic string rendering for operator logs
    - Example: renderDenialReason({kind: "budget", which: "call_depth", limit: 16, observed: 17}) → "budget:call_depth (limit=16, observed=17)"
  * export function serializeDenialReason(r: DenialReason): string
    - Canonical JSON (sorted keys)
  * export function parseDenialReason(json: string): DenialReason
    - Inverse of serialize; validates discriminant tag

- src/domains/rules/__tests__/denial-reasons.test.ts
  * Test rendering for each variant
  * Test serialize/parse round-trip for each variant
  * Test that adding a new variant doesn't break existing rendering (TypeScript exhaustive-switch via `_: never`)
  * Test JSON canonical form: identical DenialReason → identical JSON bytes

ACCEPTANCE CRITERIA (headline):
✓ All 7 variant kinds typed
✓ Each variant has structured details, no freeform strings in payload
✓ renderDenialReason + serialize/parse round-trip
✓ Exhaustive-switch compile check

SUCCESS CHECK:
cd .worktrees/claude/p1-4-2-denial-reasons && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.4.2", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.4.2
branch: feature/p1-4-2-denial-reasons
worktree: .worktrees/claude/p1-4-2-denial-reasons
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: DenialReason discriminated union (7 variants: no_rule_matched, budget, effect_invariant_violated, axiom_violation, policy, rule_version_mismatch, ambiguous_ruleset). renderDenialReason + serialize/parse. Canonical JSON.
blockers: none")

FORBIDDENS:
✗ Do not add a freeform `message: string` field — all details structured
✗ Do not renumber axioms AX-01..AX-07 — stability across upgrades required
✗ Do not edit main checkout

NEXT:
P1.4.3 — Admission Budgets (enforces the limits referenced here)

Verification checklist (for reviewer agent)

  • Union has 7 discriminated variants
  • Each variant’s payload is fully structured
  • Exhaustive-switch compile test passes
  • serialize/parse round-trip test passes
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.4.2
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.4.2
  branch: feature/p1-4-2-denial-reasons
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "DenialReason discriminated union with 7 variants (no_rule_matched, budget, effect_invariant_violated, axiom_violation, policy, rule_version_mismatch, ambiguous_ruleset), each with fully-structured payload. renderDenialReason for operator logs, serialize/parse with canonical JSON. Exhaustive-switch compile check confirms variant coverage."
  blockers: []

Common gotchas

  • Exhaustive-switch check requires : never fallthroughswitch (r.kind) { case "budget": ... default: const _exh: never = r; throw new Error(...); }. Without this, adding a new variant silently omits handling.
  • Canonical JSON for serialize — sort keys, no whitespace. Otherwise different platforms produce different bytes.
  • Do not expose kind as an enum — discriminant strings are simpler to version and don’t require migration when variants are added.

P1.4.3 — Admission Budgets

Spec source: task-breakdown.md §P1.4.3 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §5 (Rule Execution Flow budget tracking) Worktree: feature/p1-4-3-budget Branch command: git worktree add .worktrees/claude/p1-4-3-budget -b feature/p1-4-3-budget origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.3.1 Unblocks: P1.4.1 (admission uses budget tracker instance)

Files to create

  • src/domains/rules/budget.ts
  • src/domains/rules/__tests__/budget.test.ts

Acceptance criteria

  • Budget tracker class with counters: integer_ops, call_depth, current_arg_count
  • Limits: MAX_INTEGER_OPS=10_000, MAX_CALL_DEPTH=16, MAX_ARG_COUNT=8
  • On exceed: throw RuleBudgetExceeded with which-counter-fired field
  • Instrumentation hooks: emit budget.tick events for α’s audit layer (count only)
  • Budget state resets per-rule (no leaking across rules)
  • Limits are part of the rule version hash (via P1.5.1 — document the dependency)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.4.3
  • docs/3-world/physics/laws/rule-engine.md §Evaluation budget + §Default budget constants
  • src/domains/rules/engine.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.4.3 — Admission Budgets
Implement the BudgetTracker that enforces MAX_INTEGER_OPS / MAX_CALL_DEPTH / MAX_ARG_COUNT.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.4.3
3. docs/3-world/physics/laws/rule-engine.md §Evaluation budget + §Default budget constants
4. src/domains/rules/engine.ts (where BudgetTracker is referenced)
5. src/domains/rules/denial-reasons.ts (budget denial variant)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-4-3-budget -b feature/p1-4-3-budget origin/main
cd .worktrees/claude/p1-4-3-budget

FILES TO CREATE:
- src/domains/rules/budget.ts
  * class BudgetTracker {
      readonly limits: { integer_ops: number, call_depth: number, arg_count: number }
      private integer_ops = 0
      private call_depth = 0
      private current_arg_count = 0
      private listeners: Array<(event: BudgetTick) => void> = []

      constructor(limits?: Partial<Limits>) — defaults to 10000/16/8
      tickIntegerOp(): void — increment; if > limit, throw
      pushCall(arg_count: number): void — increment call_depth; check arg_count; if overflow, throw
      popCall(): void — decrement call_depth
      reset(): void — zero all counters
      subscribe(listener: (event: BudgetTick) => void): () => void — returns unsubscribe
      snapshot(): BudgetSnapshot — for observability
    }
  * BudgetTick = { kind: "integer_op" | "call_push" | "call_pop", at: bigint, counter_snapshot: BudgetSnapshot }
    (Note: "at" here is a logical op counter, NOT wall-clock time — determinism.)
  * RuleBudgetExceeded error class with { which: "integer_ops"|"call_depth"|"arg_count", limit, observed }
  * Constants exported: DEFAULT_LIMITS = { integer_ops: 10000, call_depth: 16, arg_count: 8 }

- src/domains/rules/__tests__/budget.test.ts
  * Fixture: default tracker; 10000 ticks OK; 10001st throws RuleBudgetExceeded("integer_ops", 10000, 10001)
  * Fixture: call_depth push 16x OK; 17th push throws
  * Fixture: pushCall(9) throws arg_count
  * Fixture: reset() zeroes all counters
  * Fixture: listener receives one BudgetTick per operation, deterministic order
  * Fixture: "at" field is monotonic within a single rule evaluation

ACCEPTANCE CRITERIA (headline):
✓ 3 counters, 3 limits, 3 exceed paths
✓ reset() per-rule
✓ Listener/observer pattern for α audit
✓ "at" field logical, not wall-clock

SUCCESS CHECK:
cd .worktrees/claude/p1-4-3-budget && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.4.3", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.4.3
branch: feature/p1-4-3-budget
worktree: .worktrees/claude/p1-4-3-budget
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: BudgetTracker class with 3 counters (integer_ops, call_depth, arg_count). RuleBudgetExceeded thrown on overflow. Listener hooks for α audit. Deterministic (no wall-clock).
blockers: none")

FORBIDDENS:
✗ Do not include Date.now() in BudgetTick — use a logical counter
✗ Do not share one BudgetTracker across rules — reset per rule
✗ Do not allow listeners to mutate the tracker from callbacks (could cascade)
✗ Do not edit main checkout

NEXT:
P1.4.4 — Tool-Lock Integration Spec (registers admission evaluator at α's tool-lock stage)

Verification checklist (for reviewer agent)

  • 3 counters, 3 limits enforced
  • RuleBudgetExceeded carries structured fields
  • reset() zeroes all counters
  • Listeners do not mutate tracker state
  • No wall-clock time in tick events
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.4.3
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.4.3
  branch: feature/p1-4-3-budget
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "BudgetTracker class with 3 counters and 3 limits (integer_ops=10000, call_depth=16, arg_count=8). RuleBudgetExceeded thrown on any overflow. Listener hooks emit BudgetTick with logical op counter (not wall-clock, for determinism). reset() per-rule prevents leaking across rules."
  blockers: []

Common gotchas

  • Date.now() in BudgetTick would poison determinism — use a monotonic logical counter that increments on every tick regardless of wall-clock.
  • Listeners are observers, not gates — they cannot deny operations. If they need to influence admission, they go through the rule engine.
  • reset() vs fresh instance — cheaper to reset than to construct a new instance per rule; document the decision in the code.

P1.4.4 — Tool-Lock Integration Spec

Spec source: task-breakdown.md §P1.4.4 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §8 (process_action) + docs/2-plugin/middleware.md (α 5-stage wrapper) Worktree: feature/p1-4-4-tool-lock-adapter Branch command: git worktree add .worktrees/claude/p1-4-4-tool-lock-adapter -b feature/p1-4-4-tool-lock-adapter origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.4.1, P1.4.2, P1.4.3 Unblocks: α wiring in R81+ (separate PR — this is spec, not registration)

Files to create

  • src/domains/rules/tool-lock-adapter.ts
  • src/domains/rules/__tests__/tool-lock-adapter.test.ts

Acceptance criteria

  • createToolLockAdapter(registry): MiddlewareStage factory
  • Output is a stage-1 middleware function matching α’s 5-stage wrapper contract
  • Admission denials short-circuit the middleware chain (stages 2–5 skipped)
  • Denials emit structured event to audit layer before returning
  • Integration test wires a test ruleset into a test server; verifies admission decisions end-to-end
  • Zero registration with server boot in R76 — this is just the adapter; α wiring is deferred

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.4.4
  • docs/3-world/physics/laws/rule-engine.md §Admission layer
  • docs/spec/s10-admission.md
  • docs/2-plugin/middleware.md (5-stage middleware wrapper contract)
  • src/server.ts (for current middleware signature reference)
  • src/domains/rules/admission.ts (P1.4.1)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.4.4 — Tool-Lock Integration Spec
Write the adapter that converts the κ admission evaluator into an α stage-1 middleware handler.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.4.4
3. docs/3-world/physics/laws/rule-engine.md §Admission layer
4. docs/spec/s10-admission.md
5. docs/2-plugin/middleware.md (5-stage wrapper: tool-lock → schema-validate → audit-enter → dispatch → audit-exit)
6. src/server.ts (reference for middleware signature; DO NOT modify)
7. src/domains/rules/admission.ts (P1.4.1)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-4-4-tool-lock-adapter -b feature/p1-4-4-tool-lock-adapter origin/main
cd .worktrees/claude/p1-4-4-tool-lock-adapter

FILES TO CREATE:
- src/domains/rules/tool-lock-adapter.ts
  * type MiddlewareRequest = { caller: NodeId, tool: string, args: unknown, mode: Mode, audit: AuditHandle }
  * type MiddlewareStage = (req: MiddlewareRequest, next: () => Promise<unknown>) => Promise<unknown>
  * createToolLockAdapter(registry: RuleRegistry, options?: {on_deny?: (reason: DenialReason) => void}): MiddlewareStage
    Behavior:
      1. On each tool call, synthesize an AdmissionRequest from the MiddlewareRequest
      2. Call evaluateAdmission from P1.4.1
      3. If admitted: invoke next() and return its result
      4. If denied:
         - Emit structured audit event {type: "admission_deny", reason, caller, tool, timestamp: req.audit.clock()}
         - Call options.on_deny callback if provided
         - Throw ToolAdmissionDeniedError wrapping the DenialReason
      5. Stages 2–5 are never invoked on denial
  * ToolAdmissionDeniedError class extending Error with {reason: DenialReason, http_status: 403}

- src/domains/rules/__tests__/tool-lock-adapter.test.ts
  * Fixture 1: mock registry; admit path → next() is called, return propagates
  * Fixture 2: deny path → ToolAdmissionDeniedError thrown; next() is NOT called
  * Fixture 3: on_deny callback invoked exactly once
  * Fixture 4: audit event emitted with correct fields
  * Fixture 5: rule_version_mismatch denies with expected reason
  * NOTE: do not register with src/server.ts in this PR — that's an R81+ wiring task

ACCEPTANCE CRITERIA (headline):
✓ createToolLockAdapter factory
✓ Adapter signature matches α 5-stage middleware
✓ Denial short-circuits stages 2-5
✓ Audit event emitted on deny
✓ NOT wired into src/server.ts in this PR

SUCCESS CHECK:
cd .worktrees/claude/p1-4-4-tool-lock-adapter && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.4.4", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.4.4
branch: feature/p1-4-4-tool-lock-adapter
worktree: .worktrees/claude/p1-4-4-tool-lock-adapter
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: createToolLockAdapter factory. Converts evaluateAdmission into a stage-1 middleware. Denial short-circuits stages 2-5; emits audit event; throws ToolAdmissionDeniedError. NOT wired into server.ts (deferred to R81+ α integration task).
blockers: none")

FORBIDDENS:
✗ Do not modify src/server.ts — that's a separate α task
✗ Do not register the adapter globally — factory returns a stage; consumer registers
✗ Do not call next() on denial path
✗ Do not edit main checkout

NEXT:
P1.5.1 — Version Hash Computation (wire registry.computeVersionHash into the real hash)

Verification checklist (for reviewer agent)

  • Factory returns a function matching MiddlewareStage signature
  • Deny path does NOT call next()
  • Audit event emitted with caller/tool/reason fields
  • src/server.ts is unchanged
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.4.4
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.4.4
  branch: feature/p1-4-4-tool-lock-adapter
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "createToolLockAdapter(registry, options) factory. Converts P1.4.1 evaluateAdmission into an α stage-1 middleware function. On admit: calls next() and propagates. On deny: emits structured audit event, invokes on_deny callback, throws ToolAdmissionDeniedError. Stages 2-5 never reached on denial. Adapter is not auto-registered with src/server.ts; α wiring is a separate R81+ task."
  blockers: []

Common gotchas

  • Middleware signature may drift during R81+ α evolution — pin the exact signature used at the time of this landing, and add a compile-time assertion against src/server.ts’s type.
  • Audit event emission must be synchronous at admission time — async leaves a window where the denial wouldn’t be recorded. Use the synchronous audit handle path.
  • http_status: 403 on the error is spec-indicative — the actual transport (MCP JSON-RPC) doesn’t use HTTP codes, but downstream integrations may translate — keep the field for clarity.

P1.5.1 — Version Hash Computation

Spec source: task-breakdown.md §P1.5.1 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §1 (EBNF) + docs/3-world/physics/laws/rule-engine.md §Rule versioning Worktree: feature/p1-5-1-version-hash Branch command: git worktree add .worktrees/claude/p1-5-1-version-hash -b feature/p1-5-1-version-hash origin/main Estimated effort: S (Small — 1–2 hours) Depends on: P1.2.2, P1.5.4 Unblocks: P1.5.2, P1.5.5, θ consensus integration in Phase 3

Files to create

  • src/domains/rules/versioning.ts
  • src/domains/rules/__tests__/versioning.test.ts

Acceptance criteria

  • computeVersionHash(ruleset, engine_version): string returns hex SHA-256
  • Input: canonical_serialization(all_rules) || engine_version
  • Canonical serialization via P1.5.4 (sorted-key deterministic JSON)
  • Version stored in event metadata: {rule_version: "sha256:abc..."}
  • Version mismatch detection: events with wrong rule_version are rejected via P1.4.2 taxonomy code rule_version_mismatch
  • Test: two logically-equivalent but differently-ordered rulesets produce identical hash

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.5.1
  • docs/3-world/physics/laws/rule-engine.md §Rule versioning
  • docs/reference/extractions/kappa-rule-engine-extraction.md §1
  • src/domains/rules/canonical.ts (P1.5.4)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.5.1 — Version Hash Computation
Implement the rule_version_hash that θ consensus and ι fork ids sign.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.5.1
3. docs/3-world/physics/laws/rule-engine.md §Rule versioning
4. src/domains/rules/canonical.ts (P1.5.4 canonical serializer)
5. src/domains/rules/registry.ts

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-5-1-version-hash -b feature/p1-5-1-version-hash origin/main
cd .worktrees/claude/p1-5-1-version-hash

FILES TO CREATE:
- src/domains/rules/versioning.ts
  * computeVersionHash(ruleset: RuleNode[], engine_version: string): string
    * Use canonicalize from P1.5.4 on ruleset
    * Concatenate with engine_version
    * Hash with SHA-256 (Node crypto)
    * Return "sha256:" + hex
  * ENGINE_VERSION constant — e.g., "kappa-engine/1.0.0" — incremented when engine binary changes
  * wireVersionHash(registry: RuleRegistry): RuleRegistry — patches registry.computeVersionHash to use this impl
  * verifyRuleVersion(expected: string, ruleset: RuleNode[]): boolean — constant-time compare

- src/domains/rules/__tests__/versioning.test.ts
  * Fixture 1: two differently-ordered but semantically-identical rulesets produce identical hash
  * Fixture 2: adding one character to a rule body changes the hash
  * Fixture 3: engine_version change with same ruleset → different hash
  * Fixture 4: verifyRuleVersion constant-time (timing assertion loose)
  * Fixture 5: patched registry.computeVersionHash returns the SHA-256 form

ACCEPTANCE CRITERIA (headline):
✓ SHA-256 hex computed over canonical(ruleset) || engine_version
✓ Format "sha256:<hex>"
✓ wireVersionHash patches registry
✓ Constant-time compare

SUCCESS CHECK:
cd .worktrees/claude/p1-5-1-version-hash && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.5.1", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.5.1
branch: feature/p1-5-1-version-hash
worktree: .worktrees/claude/p1-5-1-version-hash
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: computeVersionHash using SHA-256 over canonicalize(ruleset) || ENGINE_VERSION. Patched into RuleRegistry. Constant-time verify. Produces same hash for differently-ordered equivalent rulesets.
blockers: none")

FORBIDDENS:
✗ Do not use MD5 or SHA-1 — SHA-256 only
✗ Do not skip the engine_version concatenation — it's load-bearing
✗ Do not return raw bytes — hex + prefix "sha256:"
✗ Do not edit main checkout

NEXT:
P1.5.2 — Rule Migration (uses this hash to detect version drift)

Verification checklist (for reviewer agent)

  • SHA-256 hex output with “sha256:” prefix
  • Canonical serialization via P1.5.4
  • engine_version mixed into hash
  • Constant-time verify helper
  • Two equivalent rulesets → same hash
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.5.1
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.5.1
  branch: feature/p1-5-1-version-hash
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "computeVersionHash(ruleset, engine_version)  SHA-256 over canonicalize(ruleset) || engine_version, output format 'sha256:<hex>'. wireVersionHash patches RuleRegistry.computeVersionHash. verifyRuleVersion does constant-time comparison. Test fixtures confirm differently-ordered equivalent rulesets produce identical hash and engine_version change produces different hash."
  blockers: []

Common gotchas

  • ENGINE_VERSION must bump on any semantic change — if you change how decay rounds, bump the version. Silent rounding changes are the entire motivation for this hash.
  • Prefix matters — “sha256:” lets future migrations to different hash functions coexist without format ambiguity. Don’t drop the prefix for “efficiency.”
  • Constant-time compare is load-bearing — without it, an attacker can learn the rule_version byte-by-byte via timing.

P1.5.2 — Rule Migration

Spec source: task-breakdown.md §P1.5.2 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §5 (Rule Execution Flow) + docs/3-world/physics/laws/rule-engine.md §Test corpus parity requirement Worktree: feature/p1-5-2-migration Branch command: git worktree add .worktrees/claude/p1-5-2-migration -b feature/p1-5-2-migration origin/main Estimated effort: L (Large — 1–2 days) Depends on: P1.5.1, P1.3.1, P1.5.5 Unblocks: π governance wiring (separate future task)

Files to create

  • src/domains/rules/migration.ts
  • src/domains/rules/__tests__/migration.test.ts

Acceptance criteria

  • migrateRuleset(old, new, corpus): MigrationResult — runs parity harness (P1.5.5)
  • Test corpus: ≥100 representative events
  • Activation epoch: new rules take effect at epoch N+1 (not immediately)
  • Parity requirement: h_old == h_new for every corpus event both versions admit
  • Divergence set must match proposal’s declared scope or migration is rejected
  • Rollback: if parity fails, old ruleset remains active; proposal marked rejected:parity
  • Fork trigger: nodes that reject migration automatically fork (link to ι — deferred wiring)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.5.2
  • docs/3-world/physics/laws/rule-engine.md §Test corpus parity requirement
  • docs/3-world/physics/enforcement/governance.md §versioning (when landed)
  • src/domains/rules/versioning.ts, parity-harness.ts, engine.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.5.2 — Rule Migration
Orchestrate the migration flow from one ruleset version to another with parity + activation epoch + rollback.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.5.2
3. docs/3-world/physics/laws/rule-engine.md §Test corpus parity requirement + §Rule versioning
4. docs/3-world/physics/enforcement/governance.md §versioning (may not exist yet — stub reference)
5. src/domains/rules/versioning.ts, parity-harness.ts (P1.5.5), engine.ts, activation.ts (P1.5.3)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-5-2-migration -b feature/p1-5-2-migration origin/main
cd .worktrees/claude/p1-5-2-migration

FILES TO CREATE:
- src/domains/rules/migration.ts
  * interface MigrationProposal {
      old_version: string
      new_version: string
      new_ruleset: RuleNode[]
      declared_divergence_scope: EventIdPattern[]   // events where behavior change is expected
      target_epoch: bigint
    }
  * type MigrationResult =
      | { status: "accepted", activation_token: ActivationToken, parity_report: ParityReport }
      | { status: "rejected:parity", divergence_exceeds_scope: EventId[], parity_report: ParityReport }
      | { status: "rejected:version_mismatch", expected: string, got: string }
  * migrateRuleset(proposal: MigrationProposal, test_corpus: Event[], current_epoch: bigint): MigrationResult
    1. Load old + new ruleset, compute version hashes
    2. Verify old_version matches current active version
    3. Run P1.5.5 parity harness over test_corpus
    4. If divergence set ⊆ declared_divergence_scope: accept + schedule activation for target_epoch (must be > current_epoch)
    5. Else: reject with status: "rejected:parity"
  * onRejection: emit fork-trigger hook (stub — full ι wiring in Phase 5)

- src/domains/rules/__tests__/migration.test.ts
  * Fixture: 100-event corpus, identical behavior under old + new → accepted
  * Fixture: divergence within declared scope → accepted
  * Fixture: divergence outside declared scope → rejected:parity
  * Fixture: wrong old_version hash → rejected:version_mismatch
  * Fixture: target_epoch == current_epoch → rejected (must be strictly greater)
  * Fixture: on-rejection fork-trigger callback invoked exactly once

ACCEPTANCE CRITERIA (headline):
✓ migrateRuleset full orchestration
✓ 100+ event corpus required
✓ Activation at epoch N+1 (strictly)
✓ Parity → accepted; divergence outside scope → rejected:parity
✓ Fork hook stubbed

SUCCESS CHECK:
cd .worktrees/claude/p1-5-2-migration && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.5.2", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.5.2
branch: feature/p1-5-2-migration
worktree: .worktrees/claude/p1-5-2-migration
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Rule migration orchestrator. Runs parity harness over corpus, enforces declared-scope divergence, schedules activation at strictly-greater target epoch. Fork-trigger hook stubbed for ι.
blockers: none")

FORBIDDENS:
✗ Do not activate migration at the same epoch — must be N+1 minimum
✗ Do not auto-accept on parity if declared_divergence_scope is empty — empty scope means strict parity required
✗ Do not edit main checkout

NEXT:
P1.5.3 — Activation Epoch + Rollback

Verification checklist (for reviewer agent)

  • 100+ corpus events required
  • target_epoch strictly greater than current
  • Divergence scope check bidirectional (admit→reject AND reject→admit)
  • Fork hook invoked on rejection
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.5.2
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.5.2
  branch: feature/p1-5-2-migration
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "migrateRuleset(proposal, corpus, current_epoch) orchestrates: version-hash verification, parity harness run (P1.5.5), declared-divergence-scope check, activation scheduling at strictly-greater target epoch. Returns tagged MigrationResult (accepted | rejected:parity | rejected:version_mismatch). Fork-trigger hook stubbed for Phase 5 ι integration."
  blockers: []

Common gotchas

  • Empty declared_divergence_scope means strict parity — some migrations genuinely don’t change any observable behavior (e.g., refactoring). Empty scope should be the default and should be strictly enforced: any divergence at all is rejection.
  • Corpus must be curated, not randomly generated — a random corpus has no guarantee of exercising the rules that matter. Ship a hand-curated baseline corpus and let π governance extend it.
  • Fork trigger is not a direct call — it’s a hook that Phase 5 ι wiring picks up. Don’t block the migration flow on the fork actually being created.

P1.5.3 — Activation Epoch + Rollback

Spec source: task-breakdown.md §P1.5.3 Extraction reference: docs/reference/extractions/kappa-rule-engine-extraction.md §5 + docs/3-world/physics/laws/rule-engine.md §Rule versioning Worktree: feature/p1-5-3-activation Branch command: git worktree add .worktrees/claude/p1-5-3-activation -b feature/p1-5-3-activation origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.5.1, P1.5.2 Unblocks: operational rollout

Files to create

  • src/domains/rules/activation.ts
  • src/domains/rules/__tests__/activation.test.ts

Acceptance criteria

  • scheduleActivation(new_version, target_epoch) — target_epoch must be current_epoch + 1 minimum
  • applyActivation(token, current_epoch) — applies only when current_epoch >= target_epoch
  • rollback(version) — reinstates prior version; emits rollback event
  • Activation journal: append-only log of (epoch, version_hash, cause) tuples
  • Rollback does not retroactively invalidate events admitted under rolled-back version
  • Rollback during dispute window triggers π governance review hook (hook name only — π not implemented)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.5.3
  • docs/spec/s11-rule-engine.md
  • docs/3-world/physics/laws/rule-engine.md §Rule versioning
  • src/domains/rules/versioning.ts, migration.ts

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.5.3 — Activation Epoch + Rollback
Implement activation scheduling, apply, and rollback flows with an append-only journal.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.5.3
3. docs/spec/s11-rule-engine.md (authoritative rule engine spec)
4. docs/3-world/physics/laws/rule-engine.md §Rule versioning
5. src/domains/rules/versioning.ts, migration.ts

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-5-3-activation -b feature/p1-5-3-activation origin/main
cd .worktrees/claude/p1-5-3-activation

FILES TO CREATE:
- src/domains/rules/activation.ts
  * interface ActivationToken { new_version: string, target_epoch: bigint, proposal_id: string }
  * interface JournalEntry { epoch: bigint, version_hash: string, cause: "initial" | "migration" | "rollback" }
  * class ActivationJournal:
      private entries: JournalEntry[] = []
      append(entry: JournalEntry): void — appends; rejects non-monotonic epoch
      current(): JournalEntry — last entry
      at(epoch: bigint): JournalEntry — entry active at a given epoch
      all(): JournalEntry[] — read-only copy
  * scheduleActivation(journal: ActivationJournal, new_version: string, target_epoch: bigint, current_epoch: bigint): ActivationToken
    - Require target_epoch > current_epoch
  * applyActivation(token: ActivationToken, journal: ActivationJournal, current_epoch: bigint): void
    - Require current_epoch >= token.target_epoch
    - Append to journal with cause: "migration"
  * rollback(journal: ActivationJournal, target_version: string, current_epoch: bigint, dispute_window_open: boolean): void
    - Look up target_version in journal (must exist prior to current entry)
    - Append new JournalEntry {epoch: current_epoch, version_hash: target_version, cause: "rollback"}
    - If dispute_window_open: call governance_review_hook() — stub for π wiring

- src/domains/rules/__tests__/activation.test.ts
  * Fixture 1: schedule with target_epoch = current_epoch → throws
  * Fixture 2: apply before target_epoch → throws
  * Fixture 3: apply at/after target_epoch → succeeds; journal entry added
  * Fixture 4: rollback to prior version → journal entry added; pre-rollback events still valid
  * Fixture 5: rollback during dispute window → governance_review_hook invoked
  * Fixture 6: non-monotonic epoch in journal.append → throws

ACCEPTANCE CRITERIA (headline):
✓ ActivationJournal append-only, monotonic epochs
✓ scheduleActivation / applyActivation / rollback APIs
✓ Rollback does not invalidate prior events
✓ Governance review hook stub for π

SUCCESS CHECK:
cd .worktrees/claude/p1-5-3-activation && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.5.3", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.5.3
branch: feature/p1-5-3-activation
worktree: .worktrees/claude/p1-5-3-activation
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Activation journal (append-only, monotonic epochs) + scheduleActivation / applyActivation / rollback flows. Rollback does not retroactively invalidate events; dispute-window rollbacks trigger governance_review_hook (stubbed).
blockers: none")

FORBIDDENS:
✗ Do not allow journal mutation of existing entries
✗ Do not apply activation at target_epoch - 1 (must be >=)
✗ Do not invalidate past events on rollback — they stand
✗ Do not edit main checkout

NEXT:
P1.5.4 — Canonical Serialization (foundation for P1.5.1)

Verification checklist (for reviewer agent)

  • ActivationJournal append-only + monotonic-epoch check
  • scheduleActivation requires strict >
  • applyActivation requires >=
  • Rollback does NOT mutate past entries
  • Governance review hook invoked only during dispute window
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.5.3
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.5.3
  branch: feature/p1-5-3-activation
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "ActivationJournal (append-only log of epoch, version_hash, cause tuples) + scheduleActivation (target epoch strictly greater than current) + applyActivation (runs when current epoch reaches target) + rollback (reinstates prior version, past events stand). Dispute-window rollbacks trigger governance_review_hook stub for future π integration."
  blockers: []

Common gotchas

  • Journal append must be synchronous — an async append leaves a window where two rollbacks race. Keep journal operations synchronous.
  • Past events are not retroactively invalidated — events admitted under version v1 remain valid even after rollback to v0. The journal tracks “which version was active at which epoch,” not “which version should have been active.”
  • Governance review hook is stubbed — log that it was called; actual π wiring comes later. Don’t call it unconditionally.

P1.5.4 — Canonical Serialization

Spec source: task-breakdown.md §P1.5.4 Extraction reference: docs/3-world/physics/laws/rule-engine.md §Rule versioning (“canonical serialization of the rule bodies”) Worktree: feature/p1-5-4-canonical Branch command: git worktree add .worktrees/claude/p1-5-4-canonical -b feature/p1-5-4-canonical origin/main Estimated effort: M (Medium — 4–8 hours) Depends on: P1.2.2 Unblocks: P1.5.1 (hash consumes canonical form)

Files to create

  • src/domains/rules/canonical.ts
  • src/domains/rules/__tests__/canonical.test.ts

Acceptance criteria

  • canonicalize(ast_or_ruleset): string — byte-identical output on any platform
  • Keys sorted alphabetically at every object level
  • No whitespace (single-line JSON)
  • Integer literals preserved exactly (no 1e3 normalization, no leading zeros)
  • String escapes use canonical JSON form (\", \n, \u00XX)
  • Property test: canonicalize(parse(canonicalize(parse(x)))) == canonicalize(parse(x)) — idempotent round-trip
  • No locale dependence (sort uses codepoint order, not locale-aware collation)

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.5.4
  • docs/3-world/physics/laws/rule-engine.md §Rule versioning
  • src/domains/rules/parser.ts (AST types)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.5.4 — Canonical Serialization
Produce byte-identical JSON output for κ rulesets across platforms. This is the input to P1.5.1 version hashing.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.5.4
3. docs/3-world/physics/laws/rule-engine.md §Rule versioning (canonical-serialization requirement)
4. src/domains/rules/parser.ts (AST types)

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-5-4-canonical -b feature/p1-5-4-canonical origin/main
cd .worktrees/claude/p1-5-4-canonical

FILES TO CREATE:
- src/domains/rules/canonical.ts
  * canonicalize(value: AstNode | RuleNode | RuleNode[]): string
    - Walk the tree
    - At each object: sort keys by codepoint order (Array.prototype.sort default on strings is codepoint-based)
    - Emit single-line JSON, no whitespace
    - Integer literals: write as decimal string, no leading zeros, no exponent (13n → "13")
    - Strings: JSON-escape using canonical form: \" \\ \/ \b \f \n \r \t \uXXXX for non-printable
    - Never use String.prototype.localeCompare or Intl.Collator
  * byteLength(value): number — test helper returning Buffer.byteLength(canonicalize(value))

- src/domains/rules/__tests__/canonical.test.ts
  * Fixture 1: object {b:1, a:2} → '{"a":2,"b":1}' (sorted)
  * Fixture 2: bigint 13n → '13' (no 'n' suffix, no exponent)
  * Fixture 3: string 'hello\nworld' → '"hello\\nworld"' (escaped)
  * Fixture 4: nested: [{c:1}, {a:2}] → '[{"c":1},{"a":2}]'
  * Fixture 5: a parsed rule from P1.2.2 → canonical string; re-parse via P1.2.2 + re-canonicalize → identical bytes
  * Fixture 6: locale independence: force Turkish locale in Node via Intl; confirm output unchanged
  * Fixture 7: property test 100 random AST shapes; canonicalize twice is idempotent

ACCEPTANCE CRITERIA (headline):
✓ Sorted keys at every level
✓ Single-line, no whitespace
✓ bigint → decimal string
✓ Idempotent round-trip
✓ Locale-independent

SUCCESS CHECK:
cd .worktrees/claude/p1-5-4-canonical && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.5.4", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.5.4
branch: feature/p1-5-4-canonical
worktree: .worktrees/claude/p1-5-4-canonical
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: canonicalize() produces byte-identical JSON for κ rulesets. Sorted keys (codepoint), no whitespace, bigint → decimal, canonical escapes. Locale-independent. Idempotent round-trip via parser.
blockers: none")

FORBIDDENS:
✗ Do not use JSON.stringify — its key ordering is insertion-based and unpredictable after mutations
✗ Do not use localeCompare — breaks cross-locale determinism
✗ Do not include ES-style syntax like single quotes or trailing commas — this is JSON, strictly
✗ Do not edit main checkout

NEXT:
P1.5.5 — Test Corpus Parity Harness (uses this canonicalizer for effect hashing)

Verification checklist (for reviewer agent)

  • Keys sorted using codepoint order, not localeCompare
  • bigint serialized as decimal string without ‘n’ suffix
  • String escapes produce canonical JSON
  • Idempotent round-trip test passes
  • Locale-independence fixture passes
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.5.4
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.5.4
  branch: feature/p1-5-4-canonical
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "canonicalize(value) produces byte-identical single-line JSON regardless of platform. Keys sorted by codepoint order. bigint rendered as decimal string. Strings escaped with canonical JSON form (\\uXXXX for non-printable). Locale-independent (no Intl.Collator). Idempotent round-trip via P1.2.2 parser."
  blockers: []

Common gotchas

  • JSON.stringify with a replacer can sort keys but is non-idempotent on nested objects — roll your own walker; don’t rely on replacer.
  • Codepoint sort ≠ lexical sort for multi-byte characters'ñ' < 'o' by codepoint; sort strings as UTF-16 code units, which is what JS Array.sort does by default.
  • Trailing zeros on decimals — if ever adding fixed-point fractions, write them as "0.500" or "0.5" consistently and pick one forever. For Phase 1, there are no fractions: only integers.

P1.5.5 — Test Corpus Parity Harness

Spec source: task-breakdown.md §P1.5.5 Extraction reference: docs/3-world/physics/laws/rule-engine.md §Test corpus parity requirement Worktree: feature/p1-5-5-parity-harness Branch command: git worktree add .worktrees/claude/p1-5-5-parity-harness -b feature/p1-5-5-parity-harness origin/main Estimated effort: L (Large — 1–2 days) Depends on: P1.3.1, P1.5.1 Unblocks: P1.5.2 (migration uses this harness)

Files to create

  • src/domains/rules/parity-harness.ts
  • src/domains/rules/__tests__/parity-harness.test.ts

Acceptance criteria

  • runParity({old_ruleset, new_ruleset, corpus}): ParityReport
  • Per event: compute effect-set hash h = SHA-256(canonical(effects)) under both versions
  • Report categorizes events: both_admit_same, both_admit_diverge, old_admit_new_reject, old_reject_new_admit, both_reject
  • Pass condition: both_admit_diverge == [] AND (old_admit_new_reject ∪ old_reject_new_admit) ⊆ declared scope
  • Default corpus of ≥100 hand-curated events shipped with the harness
  • Deterministic: identical inputs → identical report bytes
  • Performance: 10k corpus events in < 5 seconds

Pre-flight reading

  • CLAUDE.md
  • docs/guides/implementation/task-breakdown.md §P1.5.5
  • docs/3-world/physics/laws/rule-engine.md §Test corpus parity requirement
  • src/domains/rules/engine.ts, canonical.ts (P1.5.4), versioning.ts (P1.5.1)

Ready-to-paste agent prompt

You are a Phase 1 builder agent for Colibri (κ Rule Engine).

TASK: P1.5.5 — Test Corpus Parity Harness
Build the harness that runs a corpus through two rulesets and categorizes divergences.

FILES TO READ FIRST:
1. CLAUDE.md
2. docs/guides/implementation/task-breakdown.md §P1.5.5
3. docs/3-world/physics/laws/rule-engine.md §Test corpus parity requirement
4. src/domains/rules/engine.ts, canonical.ts, versioning.ts

WORKTREE SETUP:
git fetch origin
git worktree add .worktrees/claude/p1-5-5-parity-harness -b feature/p1-5-5-parity-harness origin/main
cd .worktrees/claude/p1-5-5-parity-harness

FILES TO CREATE:
- src/domains/rules/parity-harness.ts
  * interface ParityInput {
      old_ruleset: RuleNode[]
      new_ruleset: RuleNode[]
      corpus: Event[]
      declared_divergence_scope: EventIdPattern[]
    }
  * interface ParityReport {
      both_admit_same: EventId[]
      both_admit_diverge: EventId[]        // MUST be empty for pass
      old_admit_new_reject: EventId[]
      old_reject_new_admit: EventId[]
      both_reject: EventId[]
      pass: boolean
      details_by_event: Map<EventId, {old_result: RuleResult, new_result: RuleResult, old_hash: string, new_hash: string}>
    }
  * runParity(input: ParityInput): ParityReport
    For each event in corpus:
      1. Execute old ruleset → old_result, old_hash = SHA-256(canonical(old_result.mutations))
      2. Execute new ruleset → new_result, new_hash = SHA-256(canonical(new_result.mutations))
      3. Categorize:
         - both admit, old_hash == new_hash → both_admit_same
         - both admit, old_hash != new_hash → both_admit_diverge
         - old admit, new reject → old_admit_new_reject
         - old reject, new admit → old_reject_new_admit
         - both reject → both_reject
      4. Store details
    Pass = (both_admit_diverge.length == 0) AND ((old_admit_new_reject ∪ old_reject_new_admit) ⊆ scope)
  * Default corpus: export DEFAULT_CORPUS with ≥100 events covering admission / state-transition / consequence / promotion / governance / identity / fork scenarios

- src/domains/rules/__tests__/parity-harness.test.ts
  * Fixture: identical rulesets → all events in both_admit_same; pass=true
  * Fixture: new ruleset rejects one event type → old_admit_new_reject populated; pass depends on scope
  * Fixture: new ruleset produces different mutation for one event → both_admit_diverge populated; pass=false
  * Fixture: 10000-event synthetic corpus; perf assertion under 5s
  * Fixture: determinism — run harness twice with same input, compare report JSON

ACCEPTANCE CRITERIA (headline):
✓ 5 category buckets
✓ SHA-256 effect hashes via P1.5.4 canonical
✓ Pass condition implemented
✓ ≥100-event default corpus shipped
✓ 10k corpus < 5s

SUCCESS CHECK:
cd .worktrees/claude/p1-5-5-parity-harness && npm run build && npm run lint && npm test

WRITEBACK (after success):
task_update(id="P1.5.5", status="done", progress=100)
thought_record(session_id="r81-kappa-phase-1", thought_type="reflection",
  content="task_id: P1.5.5
branch: feature/p1-5-5-parity-harness
worktree: .worktrees/claude/p1-5-5-parity-harness
commit: <SHA>
tests: npm run build && npm run lint && npm test
summary: Parity harness runs corpus through two rulesets, buckets into 5 categories, computes effect-hash divergence via P1.5.4 canonical + SHA-256. Default 100+ event corpus shipped. 10k events < 5s.
blockers: none")

FORBIDDENS:
✗ Do not short-circuit on first divergence — the point is the full report
✗ Do not parallelize with workers unless results come back deterministic — serial is simpler
✗ Do not load corpus from disk inside the harness — tests pass it explicitly
✗ Do not edit main checkout

NEXT:
Phase 2 planning — λ Reputation

Verification checklist (for reviewer agent)

  • 5 categories correctly populated
  • Pass condition: both_admit_diverge == [] AND divergence ⊆ scope
  • Default corpus has ≥100 events
  • Performance: 10k events in < 5s
  • Determinism: two runs produce identical report bytes
  • npm run build && npm run lint && npm test pass

Writeback template

task_update:
  task_id: P1.5.5
  status: done
  progress: 100

thought_record:
  session_id: r81-kappa-phase-1
  task_id: P1.5.5
  branch: feature/p1-5-5-parity-harness
  commit_sha: <sha>
  tests_run: ["npm run build", "npm run lint", "npm test"]
  summary: "runParity(input) executes corpus under old + new rulesets, categorizes each event into 5 buckets (both_admit_same, both_admit_diverge, old_admit_new_reject, old_reject_new_admit, both_reject). Pass requires both_admit_diverge empty AND divergence set within declared scope. Default corpus of 100+ hand-curated events ships with module. 10k synthetic events complete in under 5 seconds."
  blockers: []

Common gotchas

  • Effect hashes depend on mutation ordering — the engine (P1.3.1) guarantees alphabetical-within-category. Ensure the harness receives mutations in that order before hashing.
  • Default corpus curation matters more than size — 100 well-chosen events beat 1000 random ones. Cover every rule-execution category: admission pass/fail, each TransitionType, each built-in function in use.
  • The harness itself must be deterministic — no parallel workers (thread scheduling is non-deterministic), no wall-clock, no random corpus generation at runtime.

Back to index

← task-prompts/index.md ← task-breakdown.md §Phase 1

See also


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.