Skip to content

Commit 4fb7d9d

Browse files
author
Paul Kyle
committed
release: v0.8.5
1 parent 2afd055 commit 4fb7d9d

84 files changed

Lines changed: 8220 additions & 2403 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# CI pipeline for Palinode — runs on every push and pull_request to any branch.
2+
#
3+
# Jobs:
4+
# 1. unit-tests — fast feedback on core logic (no external services)
5+
# 2. integration — tests/integration/ (may need Ollama; continue-on-error)
6+
# 3. security-scan — bandit (code) + pip-audit (dependencies)
7+
8+
name: CI
9+
10+
env:
11+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
12+
13+
on:
14+
push:
15+
pull_request:
16+
17+
jobs:
18+
# ---------------------------------------------------------------------------
19+
# Unit tests — should never need network access or Ollama.
20+
# All embeddings / LLM calls are mocked in the test suite.
21+
# ---------------------------------------------------------------------------
22+
unit-tests:
23+
runs-on: ubuntu-latest
24+
25+
strategy:
26+
matrix:
27+
python-version: ["3.11", "3.12"]
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v5
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
cache: "pip"
37+
38+
- name: Install dependencies
39+
run: |
40+
python -m pip install --upgrade pip
41+
pip install -e ".[dev]"
42+
43+
- name: Assert palinode resolves to the checked-out tree
44+
# Regression guard for editable installs: palinode.__file__ must
45+
# resolve under GITHUB_WORKSPACE, not some other site-packages path.
46+
run: |
47+
RESOLVED=$(python -c "import palinode; print(palinode.__file__)")
48+
echo "palinode.__file__ = $RESOLVED"
49+
if [[ "$RESOLVED" != "$GITHUB_WORKSPACE"/* ]]; then
50+
echo "ERROR: palinode resolves outside the workspace ($GITHUB_WORKSPACE)"
51+
echo " Got: $RESOLVED"
52+
exit 1
53+
fi
54+
55+
- name: Run unit tests (excluding integration)
56+
run: python -m pytest tests/ -x -q --ignore=tests/integration --ignore=tests/live
57+
58+
# ---------------------------------------------------------------------------
59+
# Integration tests — run against tests/integration/.
60+
#
61+
# These tests do not require Ollama directly (embeddings are stubbed), but
62+
# they do spin up FastAPI in-process and exercise the full save/search loop
63+
# against a real SQLite database in a temp directory.
64+
#
65+
# continue-on-error: true — any test tagged @pytest.mark.slow that needs
66+
# a live Ollama instance will fail here; that is expected in CI.
67+
# Run the full suite locally against a host with Ollama for full coverage.
68+
# ---------------------------------------------------------------------------
69+
integration-tests:
70+
runs-on: ubuntu-latest
71+
72+
env:
73+
PALINODE_DIR: /tmp/palinode-ci-test
74+
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- name: Set up Python
79+
uses: actions/setup-python@v5
80+
with:
81+
python-version: "3.11"
82+
cache: "pip"
83+
84+
- name: Install dependencies
85+
run: |
86+
python -m pip install --upgrade pip
87+
pip install -e ".[dev]"
88+
89+
- name: Run integration tests
90+
# Integration tests that need Ollama will be skipped in CI;
91+
# run locally against a host with Ollama for full Ollama-backed coverage.
92+
run: python -m pytest tests/integration/ -x -q
93+
continue-on-error: true
94+
95+
# ---------------------------------------------------------------------------
96+
# Security scans — informational (continue-on-error: true on pip-audit).
97+
#
98+
# bandit: static analysis for common Python security issues
99+
# pip-audit: checks installed packages against known vulnerability databases
100+
# ---------------------------------------------------------------------------
101+
security-scan:
102+
runs-on: ubuntu-latest
103+
104+
steps:
105+
- uses: actions/checkout@v4
106+
107+
- name: Set up Python
108+
uses: actions/setup-python@v5
109+
with:
110+
python-version: "3.11"
111+
cache: "pip"
112+
113+
- name: Install dependencies
114+
run: |
115+
python -m pip install --upgrade pip
116+
pip install -e ".[dev]"
117+
pip install bandit pip-audit
118+
119+
- name: Run bandit (static security analysis)
120+
# -r: recursive, -ll: medium+ severity, -q: quiet output
121+
run: bandit -r palinode/ -ll -q
122+
123+
- name: Run pip-audit (dependency vulnerability check)
124+
# continue-on-error: known-vulnerability lists drift; treat as informational
125+
run: pip-audit
126+
continue-on-error: true

.github/workflows/main-ci.yml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Rationale: Option B (post-merge sweep) from dev#198.
2+
# Option A (require branch-up-to-date before merge) is enforced
3+
# in GitHub repo settings → Branches → main branch protection.
4+
# This file is the backstop if that check is bypassed (admin merge, etc.).
5+
#
6+
# Triggered only on push to main (not on PRs — those are covered by ci.yml).
7+
# On any failure, opens a GitHub issue to flag the regression.
8+
9+
name: Main CI (post-merge sweep)
10+
11+
env:
12+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
13+
14+
on:
15+
push:
16+
branches: [main]
17+
18+
jobs:
19+
# ---------------------------------------------------------------------------
20+
# Unit tests — mirrors ci.yml; catches interaction bugs that slip through
21+
# independent-PR CI (the failure mode documented in #198).
22+
# ---------------------------------------------------------------------------
23+
unit-tests:
24+
runs-on: ubuntu-latest
25+
26+
strategy:
27+
matrix:
28+
python-version: ["3.11", "3.12"]
29+
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Set up Python ${{ matrix.python-version }}
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
cache: "pip"
38+
39+
- name: Install dependencies
40+
run: |
41+
python -m pip install --upgrade pip
42+
pip install -e ".[dev]"
43+
44+
- name: Assert palinode resolves to the checked-out tree
45+
run: |
46+
RESOLVED=$(python -c "import palinode; print(palinode.__file__)")
47+
echo "palinode.__file__ = $RESOLVED"
48+
if [[ "$RESOLVED" != "$GITHUB_WORKSPACE"/* ]]; then
49+
echo "ERROR: palinode resolves outside the workspace ($GITHUB_WORKSPACE)"
50+
echo " Got: $RESOLVED"
51+
exit 1
52+
fi
53+
54+
- name: Run unit tests (excluding integration)
55+
run: python -m pytest tests/ -x -q --ignore=tests/integration --ignore=tests/live
56+
57+
# ---------------------------------------------------------------------------
58+
# Integration tests — informational backstop on main.
59+
# continue-on-error: true because Ollama is not available in CI runners.
60+
# ---------------------------------------------------------------------------
61+
integration-tests:
62+
runs-on: ubuntu-latest
63+
64+
env:
65+
PALINODE_DIR: /tmp/palinode-ci-test
66+
67+
steps:
68+
- uses: actions/checkout@v4
69+
70+
- name: Set up Python
71+
uses: actions/setup-python@v5
72+
with:
73+
python-version: "3.11"
74+
cache: "pip"
75+
76+
- name: Install dependencies
77+
run: |
78+
python -m pip install --upgrade pip
79+
pip install -e ".[dev]"
80+
81+
- name: Run integration tests
82+
run: python -m pytest tests/integration/ -x -q
83+
continue-on-error: true
84+
85+
# ---------------------------------------------------------------------------
86+
# Security scan — same as ci.yml.
87+
# ---------------------------------------------------------------------------
88+
security-scan:
89+
runs-on: ubuntu-latest
90+
91+
steps:
92+
- uses: actions/checkout@v4
93+
94+
- name: Set up Python
95+
uses: actions/setup-python@v5
96+
with:
97+
python-version: "3.11"
98+
cache: "pip"
99+
100+
- name: Install dependencies
101+
run: |
102+
python -m pip install --upgrade pip
103+
pip install -e ".[dev]"
104+
pip install bandit pip-audit
105+
106+
- name: Run bandit (static security analysis)
107+
run: bandit -r palinode/ -ll -q
108+
109+
- name: Run pip-audit (dependency vulnerability check)
110+
run: pip-audit
111+
continue-on-error: true
112+
113+
# ---------------------------------------------------------------------------
114+
# Regression reporter — fires only when a job above fails.
115+
# Opens a GitHub issue so the regression is visible outside the Actions UI.
116+
# ---------------------------------------------------------------------------
117+
report-regression:
118+
runs-on: ubuntu-latest
119+
needs: [unit-tests, integration-tests, security-scan]
120+
if: failure()
121+
122+
steps:
123+
- name: Report regression
124+
if: failure()
125+
env:
126+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
127+
run: |
128+
gh issue create \
129+
--title "CI regression on main: ${{ github.sha }}" \
130+
--body "Commit ${{ github.sha }} broke CI on main. Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
131+
--label "bug"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ nohup.out
6161

6262
# Launch-posts working draft — local-only, not for git
6363
artifacts/launch-posts.md
64+
65+
# Test-rig deploy-key material (.claude/plans/test-rigs/) — never commit secrets
66+
.claude/plans/test-rigs/group_vars/all.vault.yml
67+
.claude/plans/palinode-test-env/group_vars/all.vault.yml
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# ADR-011: Deterministic Slash Commands
2+
3+
**Status:** Accepted
4+
**Date:** 2026-04-28
5+
**Context:** Issue #138 — formalizes de-facto practice already observed in `.claude/commands/`
6+
**Relates to:** ADR-001 (Tools Over Pipeline)
7+
8+
## Decision
9+
10+
User-facing slash commands MUST map to a **single, named tool** with a **fixed argument shape**. The LLM is allowed to synthesize the *content* of arguments but must not choose the *tool*, decide whether to invoke a tool at all, or vary the type/shape of arguments based on context.
11+
12+
| Allowed | Not allowed |
13+
|---|---|
14+
| Agent writes the `summary` text for `palinode_session_end` | Agent decides whether to call `palinode_session_end` or `palinode_save` |
15+
| Agent picks which facts to list as `decisions` | Agent decides whether to save at all |
16+
| Agent writes the body of a `ProjectSnapshot` | Agent picks between `ProjectSnapshot` and `Decision` type |
17+
| Agent fills in `project` from the current CLAUDE.md | Agent skips saving because "nothing important happened" |
18+
19+
## Context
20+
21+
Palinode provides slash commands as shortcut entry points for common memory operations. As of 2026-04-28 the canonical commands are:
22+
23+
| Command | File | Tool | Fixed argument shape |
24+
|---|---|---|---|
25+
| `/wrap` | `.claude/commands/wrap.md` | `palinode_session_end` | `summary`, `decisions`, `blockers`, `project` |
26+
| `/save` | `.claude/commands/save.md` | `palinode_save` | `type=ProjectSnapshot`, `content`, `project` |
27+
| `/ps` | `.claude/commands/ps.md` | `palinode_save` (back-compat alias for `/save`) | same as `/save` |
28+
29+
Each command file ends with an explicit "**This command is deterministic.**" statement naming the single tool it calls and what it does *not* do.
30+
31+
The pattern emerged organically. This ADR formalizes it as a load-bearing constraint rather than a style preference.
32+
33+
## Rationale
34+
35+
### 1. Trust via repetition
36+
37+
When a user types `/wrap`, they expect the same tool to fire every time — unconditionally. Smart-dispatch (letting the LLM decide whether to use `palinode_session_end` or `palinode_save`, or whether to skip saving entirely because the session looked short) breaks user mental models. The value of `/wrap` is precisely that the user does not have to think; they just type it and know what happened.
38+
39+
Predictability is the contract. An agent that fires the right tool 95% of the time provides weaker guarantees than one that fires *a specific tool* 100% of the time — even if the 5% of smart-dispatch choices would technically be "better."
40+
41+
### 2. ADR-001 alignment
42+
43+
ADR-001 established the principle: **LLM proposes content, deterministic Python disposes.** That principle governs the LLM→executor boundary (the LLM proposes KEEP/MERGE/ARCHIVE ops; the executor applies them deterministically without re-evaluating the proposals).
44+
45+
The same principle applies at the user→tool boundary. The LLM synthesizes the *content* of the tool call (what to write in `summary`, which facts to surface in `decisions`). It does not re-evaluate *which tool to call* or *whether to call one at all*. That decision was made by the user when they typed `/wrap`.
46+
47+
### 3. Provenance
48+
49+
Git blame on memory only works when tool selection is deterministic and visible in the commit trail. When `/wrap` always calls `palinode_session_end`, every session end produces a predictable commit pattern. When `/save` always writes a `ProjectSnapshot`, snapshots are auditable by type. Allowing the LLM to vary tool choice breaks the "what fired and why" trail that makes palinode memory trustworthy across sessions.
50+
51+
### 4. Failure surface
52+
53+
When something goes wrong, "I typed `/wrap` and it called the wrong tool" is an obvious, debuggable failure. "The LLM decided to call `palinode_save` instead of `palinode_session_end` because of context" is a silent divergence that is nearly impossible to diagnose post-hoc. The failure surface of deterministic dispatch is narrow and visible; the failure surface of smart-dispatch is wide and latent.
54+
55+
## Alternatives considered
56+
57+
### Smart-dispatch: let the LLM pick the best tool for context
58+
59+
Rejected. The benefit (theoretically better tool selection for unusual cases) is smaller than the cost (broken user mental models, unpredictable git trail, hard-to-debug failures). If the user wants `palinode_save` instead of `palinode_session_end`, they type `/save`. The slash commands are the user's dispatch mechanism; the LLM's job is content synthesis, not routing.
60+
61+
### Parameterized commands: one command, many modes
62+
63+
Rejected for the existing three commands. `/wrap` and `/save` are already separate because they serve different purposes (end-of-session structured capture vs. mid-session snapshot). Merging them into a single `/memory` command with a mode flag would require the LLM to choose the mode — which is exactly the smart-dispatch problem in disguise.
64+
65+
### No slash commands at all: always use tools directly
66+
67+
Deferred. Tools-first is the right long-term direction (ADR-001). But slash commands provide a UX shorthand that reduces friction for common workflows and are especially useful in non-coding contexts (Cursor, Obsidian, conversational sessions). The commands remain valid as long as they are deterministic wrappers.
68+
69+
## Consequences
70+
71+
### Requirements for new slash commands
72+
73+
Any new slash command added to `.claude/commands/` MUST:
74+
75+
1. Name exactly one tool in its command file.
76+
2. Specify the fixed argument shape (which fields, what types/values).
77+
3. End with a "**This command is deterministic.**" statement that names the single tool and explicitly contrasts with the command that does *not* apply (e.g., "For X, use `/other-command` instead").
78+
4. Not contain any conditional logic, branching, or context-dependent tool selection.
79+
80+
### Compliance audit (as of 2026-04-28)
81+
82+
All three existing commands comply:
83+
84+
- `/wrap` (`.claude/commands/wrap.md`): always `palinode_session_end`. Explicitly states "Do not call any other tool."
85+
- `/save` (`.claude/commands/save.md`): always `palinode_save` with `type=ProjectSnapshot`. Explicitly states "always `palinode_save`, always `ProjectSnapshot`."
86+
- `/ps` (`.claude/commands/ps.md`): back-compat alias, identical behaviour to `/save`. Deprecated label included.
87+
88+
No violations found. The "This command is deterministic." footer in each file is the inline enforcement signal — any future command file missing it fails the review.
89+
90+
### Scope
91+
92+
This ADR governs user-facing slash commands (`.claude/commands/`). It does not govern:
93+
94+
- Internal agent-to-agent calls (those are governed by ADR-001 and ADR-010).
95+
- The MCP tool implementations themselves (they may have internal branching).
96+
- CLI commands, which are multi-dispatch by design and governed by ADR-010's parity contract.
97+
98+
## References
99+
100+
- ADR-001 (Tools Over Pipeline) — the source principle: LLM proposes content, deterministic Python disposes.
101+
- ADR-010 (Cross-Surface Parity Contract) — the complementary discipline for CLI/MCP/API parity.
102+
- `.claude/commands/wrap.md` — canonical example of a deterministic wrap command.
103+
- `.claude/commands/save.md` — canonical example of a deterministic snapshot command.
104+
- `.claude/commands/ps.md` — back-compat alias demonstrating that even deprecated commands carry the determinism guarantee.
105+
- Issue #138 — the tracking issue that prompted this formalization.

0 commit comments

Comments
 (0)