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 1Concept doc:docs/3-world/physics/laws/rule-engine.mdAlgorithm extraction:docs/reference/extractions/kappa-rule-engine-extraction.mdMaster bootstrap prompt:agent-bootstrap.mdExecutor rules:CLAUDE.md— §3 worktree, §5 gate, §6 5-step chain, §7 writebackDesign invariants preserved in every sub-task:
- 64-bit signed integer arithmetic — no floats anywhere
- Deterministic execution — no RNG / clock / async I/O / network / filesystem in rule bodies
- First-match-wins within a rule; specificity-sorted across rules
- Collect-then-apply mutations — no side effects during evaluation
- AST cap 10,000 nodes per rule;
MAX_INTEGER_OPS=10_000,MAX_CALL_DEPTH=16,MAX_ARG_COUNT=8- Chevrotain for lexer + parser per ADR-006
- Rule version hash feeds θ consensus votes and ι fork ids
- 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 librarysrc/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 * bpswould 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.1docs/3-world/physics/laws/rule-engine.md§Integer-only arithmetic + §Basis-point arithmetic examplesdocs/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, notnumber - No use of
Math.*,Date.*,Math.random(), orcrypto.randomBytes - Overflow detection via
safe_multested 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 testpass
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
numberis 53-bit float under the hood — if you usenumberfor quantities, you silently lose precision above 2^53. Usebigintthroughout; the DSL’s integer literal type maps to bigint, not number. - Floor division in JS/TS is not obvious —
BigIntdivision/truncates toward zero (which is floor only for non-negative results). For negative dividends, you need explicitMath.floorequivalent viaif (a % b !== 0n && ((a < 0n) !== (b < 0n))) .... The extraction says “truncate toward zero (not floor)” forsafe_div— but κ rule bodies never produce negatives from a non-negativevalue, 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 insrc/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.*orDate.*outside tests - Test runs in under 10 seconds in CI (to stay under default Jest timeout)
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.1.2docs/3-world/physics/laws/rule-engine.md§Forbidden operationssrc/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 testpass
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
seedrandomor 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.tssrc/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 whenb == 0n- Constants are
as const, notletor mutable - Brand type
Bps = bigint & {__brand: "Bps"}exported for type safety
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.1.3docs/3-world/physics/laws/rule-engine.md§Basis-point arithmeticdocs/reference/extractions/kappa-rule-engine-extraction.md§3 + §4src/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, not100) - All constants are
as const/ frozen - Bps brand type exported and used in function signatures where appropriate
bps()helper validates range at runtimenpm run build && npm run lint && npm testpass
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
bigintliterals, notBigInt(100)calls — theas conston a function call doesn’t freeze the return value; use100nsuffix. - Do not import anything from
src/server.tsorsrc/config.ts— this module is a leaf; it has no runtime dependencies outsideinteger-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.tssrc/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.14is a syntax error, reported with position) - Rejects underscore-separated integers (
1_000_000is invalid) - Unicode identifiers supported (XID_Start / XID_Continue) for future i18n
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.2.1docs/3-world/physics/laws/rule-engine.md§DSL grammardocs/reference/extractions/kappa-rule-engine-extraction.md§1docs/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 testpass
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}]*/uwith theuflag. 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.tssrc/domains/rules/__tests__/parser.test.ts
Acceptance criteria
- Chevrotain
CstParserorEmbeddedActionsParserbuilt 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> }; callIDENTIFIER ( 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.mddocs/guides/implementation/task-breakdown.md§P1.2.2docs/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.mdsrc/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: truein parser config- AST cap enforced via recursive node counter
- Round-trip test passes
npm run build && npm run lint && npm testpass
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
EmbeddedActionsParservsCstParser— 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 toMANYrules — use that, notOR([...])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.tssrc/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 + stringrejected) - 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
ValidationResultwith all errors, not just first
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.2.3docs/3-world/physics/laws/rule-engine.md§Forbidden operations + §Constitutional axiomsdocs/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 testpass
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
passfrom 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.tssrc/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 lookupgetByTransitionType(type: TransitionType): RuleNode[]— indexed lookup; transition types per extraction §7computeVersionHash(): 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.mddocs/guides/implementation/task-breakdown.md§P1.2.4docs/3-world/physics/laws/rule-engine.md§Rule application algorithmdocs/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 testpass
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@IDENTIFIERhandling 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.tssrc/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=10000→RuleBudgetExceeded("integer_ops") - Depth cap
MAX_CALL_DEPTH=16→RuleBudgetExceeded("call_depth") - Arg cap
MAX_ARG_COUNT=8→RuleBudgetExceeded("arg_count")
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.3.1docs/3-world/physics/laws/rule-engine.md§Rule application algorithm + §Evaluation budgetdocs/reference/extractions/kappa-rule-engine-extraction.md§5 + §6src/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 testpass
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
executeRulesetorder 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 withObject.keys) will break θ consensus.- Budget tracking must be threaded, not global — each
executeRulesetcall gets a freshBudgetTracker. 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.tssrc/domains/rules/__tests__/builtins.test.ts
Acceptance criteria
min(a, b),max(a, b),abs(a),cap(v, m)— integer-onlyclamp(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 2decay(v, rate_bps)— single-epoch decay; delegates tointeger-math.tsdiminishing(v, k)—(v * k) / (k + v)diminishing-returns transformbps_mul(v, b),bps_div(v, b)— delegate to P1.1.1hash(data)— SHA-256 hex string (Node’scrypto.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.mddocs/guides/implementation/task-breakdown.md§P1.3.2docs/3-world/physics/laws/rule-engine.md§Built-in functionsdocs/reference/extractions/kappa-rule-engine-extraction.md§3 (with isqrt + ilog2 pseudocode)docs/architecture/decisions/ADR-002-vrf-implementation.mdsrc/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 testpass
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/2bootstrap overflows for large bigints. Start withx = 1n << (bitLength(n) / 2n)instead. crypto.createHashreturns 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.tssrc/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
ReadOnlyStateErrorimmediately
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.3.3docs/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§10src/domains/rules/engine.tssrc/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 testpass
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.freezeis shallow — a frozen Map’s.setstill works because Map isn’t a plain object. Use aProxyor a custom class wrapping Map that throws onset/delete/clear. with_bindingmust be O(1) — avoid copying the entire state on every call. Use structural sharing or a persistent data structure for bindings.computeDiffkey 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.tssrc/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.mddocs/guides/implementation/task-breakdown.md§P1.3.4docs/reference/extractions/kappa-rule-engine-extraction.md§9src/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 testpass
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.tssrc/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.mddocs/guides/implementation/task-breakdown.md§P1.4.1docs/3-world/physics/laws/rule-engine.md§Admission layerdocs/spec/s10-admission.md(authoritative admission spec)docs/reference/extractions/kappa-rule-engine-extraction.md§8 + §9src/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 testpass
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 === yis 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.tssrc/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
detailspayload (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.mddocs/guides/implementation/task-breakdown.md§P1.4.2docs/3-world/physics/laws/rule-engine.md§Rule application algorithm + §Evaluation budgetsrc/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 testpass
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
: neverfallthrough —switch (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
kindas 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.tssrc/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
RuleBudgetExceededwith which-counter-fired field - Instrumentation hooks: emit
budget.tickevents 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.mddocs/guides/implementation/task-breakdown.md§P1.4.3docs/3-world/physics/laws/rule-engine.md§Evaluation budget + §Default budget constantssrc/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 testpass
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.tssrc/domains/rules/__tests__/tool-lock-adapter.test.ts
Acceptance criteria
createToolLockAdapter(registry): MiddlewareStagefactory- 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.mddocs/guides/implementation/task-breakdown.md§P1.4.4docs/3-world/physics/laws/rule-engine.md§Admission layerdocs/spec/s10-admission.mddocs/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 testpass
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: 403on 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.tssrc/domains/rules/__tests__/versioning.test.ts
Acceptance criteria
computeVersionHash(ruleset, engine_version): stringreturns 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_versionare rejected via P1.4.2 taxonomy coderule_version_mismatch - Test: two logically-equivalent but differently-ordered rulesets produce identical hash
Pre-flight reading
CLAUDE.mddocs/guides/implementation/task-breakdown.md§P1.5.1docs/3-world/physics/laws/rule-engine.md§Rule versioningdocs/reference/extractions/kappa-rule-engine-extraction.md§1src/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 testpass
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
decayrounds, 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.tssrc/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_newfor 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.mddocs/guides/implementation/task-breakdown.md§P1.5.2docs/3-world/physics/laws/rule-engine.md§Test corpus parity requirementdocs/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 testpass
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.tssrc/domains/rules/__tests__/activation.test.ts
Acceptance criteria
scheduleActivation(new_version, target_epoch)— target_epoch must becurrent_epoch + 1minimumapplyActivation(token, current_epoch)— applies only whencurrent_epoch >= target_epochrollback(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.mddocs/guides/implementation/task-breakdown.md§P1.5.3docs/spec/s11-rule-engine.mddocs/3-world/physics/laws/rule-engine.md§Rule versioningsrc/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 testpass
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.tssrc/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
1e3normalization, 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.mddocs/guides/implementation/task-breakdown.md§P1.5.4docs/3-world/physics/laws/rule-engine.md§Rule versioningsrc/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 testpass
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.stringifywith 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.tssrc/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.mddocs/guides/implementation/task-breakdown.md§P1.5.5docs/3-world/physics/laws/rule-engine.md§Test corpus parity requirementsrc/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 testpass
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
- agent-bootstrap.md — Master bootstrap prompt (read FIRST)
- first-7-prs.md — Phase 0 critical-path reference (not Phase 1)
- agent-handoff-protocol.md — Multi-agent handoff spec
- task-breakdown.md — Canonical 63-task breakdown (all phases)
- docs/3-world/physics/laws/rule-engine.md — κ concept doc
- docs/reference/extractions/kappa-rule-engine-extraction.md — Algorithm extraction
- docs/architecture/decisions/ADR-006-dsl-grammar.md — Chevrotain ratification
- docs/5-time/roadmap.md — Phase 1 start (R81)