18 Commits

Author SHA1 Message Date
mAi
94310ba498 feat(submissions): Composer Slice E — specialist bases + base-swap content survival (m/paliad#141)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Two new firm-agnostic base templates + the generic generator that
produced them + a regression test pinning Q10's base-swap-content-
survival contract.

Mig 150: seeds two `submission_bases` rows with firm=NULL.

- lg-duesseldorf — proceeding_family='de.inf.lg'. Conservative
  German legal style: Times New Roman 11pt; plain black headings.
  Stylemap targets LG-Body / LG-Heading1..3 / LG-ListBullet /
  LG-ListNumber / LG-Quote.

- upc-formal — proceeding_family='upc.inf.cfi'. UPC court style:
  Calibri 11pt body; UPC-blue (#1F3864) headings; Cambria italic
  for blockquotes. Stylemap targets UPC-Body / UPC-Heading1..3 / …

Both rows ship the same 10-section spec.defaults shape as the Slice A
bases (letterhead → signature) with their own seed Markdown.

scripts/gen-submission-base/main.go (NEW, ~240 LoC):

- Generic generator with -preset flag. Two presets baked in
  (lg-duesseldorf + upc-formal). Each preset hard-codes typography
  (font, sizes, colour) so the lawyer can swap between bases and
  see chrome change while section content carries through unchanged.
- Output is byte-reproducible (zip mtime pinned to 2026-05-26 UTC).
- Emits a minimal Composer-mode .docx: [Content_Types].xml,
  _rels/.rels, word/_rels/document.xml.rels (empty envelope so the
  composer's hyperlink-rels patch from Slice D has somewhere to land),
  word/styles.xml (preset's full named-style block + "Hyperlink"
  character style for Slice D link runs), word/document.xml (anchor-
  only body in §6.1 default section order).

Gitea uploads (via mAi):

- 6 - material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx
  blob SHA: 82f57b3cb3b54c755fc5ab36862bfd61b8aaa73e
- 6 - material/Templates/Word/Paliad/Composer/upc-formal.docx
  blob SHA: 41b9a388263ccc43ddc28b55caab301a4cf74fe8

These live under Composer/ (not under HLC/) so a future non-HLC
deployment serves the same cross-firm files.

Backend wiring:

- internal/handlers/files.go: two new fileRegistry entries
  (composerBaseLGDuesseldorfSlug, composerBaseUPCFormalSlug) +
  matching slugs in composerBaseSlugMap so fetchComposerBaseBytes
  routes the new catalog rows to the new Gitea objects.

Tests:

- TestComposer_BaseSwapPreservesContent — composes the same draft
  against an HLC-style stylemap AND an LG-style stylemap; asserts
  (a) content survives both ways, (b) each output carries the
  correct stylemap-entry stylenames, (c) neither output leaks the
  other's stylenames. Pins Q10's base-swap-survives-content
  contract.

Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean.

NOT in scope (Slice E's brief was specialist bases + survival test):

- Generator coverage for HL Patents Style bases — gen-hl-skeleton-
  template stays as the per-firm path (it needs the proprietary
  .dotm source). gen-submission-base is for firm-agnostic bases.
- LG-Düsseldorf-court-style-guide deep fidelity — the LG preset is
  a conservative starting point; admin refines via the admin editor
  in a later slice if needed.
- numbering.xml carrying numId=1/2 — Slice D's MD walker emits
  visible "• " / "N. " prefixes that don't need numbering.xml;
  honours stylemap entry for indentation.

Hard rules honoured:

- Migration purely additive (`ON CONFLICT (slug) DO NOTHING`).
- NO behavior change for pre-Composer drafts.
- NO behavior change for existing hlc-letterhead + neutral seed
  rows.
- {{rule.X}} aliases preserved (walker passes placeholders through;
  v1 SubmissionRenderer pass substitutes).
- Q10 base-swap-content-survival pinned by new test.

t-paliad-317 Slice E
2026-05-26 20:21:12 +02:00
mAi
f963b0df34 feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
The "Composer actually works" milestone per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice B. Builds on
Slice A's substrate (submission_bases, submission_sections, base_id on
drafts); no new migrations needed.

Backend additions:

- internal/services/submission_md.go (~240 LoC): Markdown → OOXML
  walker. Per the head's Slice B brief, scope is paragraphs +
  bold/italic + blank-line spacing. Placeholders pass through
  unchanged for the v1 substitution pass. CRLF normalisation; nested
  formatting (***bold-italic***); two delimiter forms (* and _);
  XML-escaping for &/</>; explicit empty-paragraph emit so blank
  lines round-trip. 12 unit tests.

- internal/services/submission_compose.go (~470 LoC): SubmissionComposer
  service. Pipeline: ConvertDotmToDocx pre-pass → extract
  word/document.xml → render each included section's content_md_<lang>
  → splice via {{#section:KEY}}/{{/section:KEY}} anchor pairs in
  the body → strip anchors for excluded sections → append unanchored
  sections before <w:sectPr> → repack zip → run v1 placeholder pass.
  RE2-friendly anchor scanner walks markers in body-order and matches
  open/close pairs with a stack (handles unbalanced anchors
  defensively). 6 unit tests covering anchor-mode splice,
  append-mode-no-anchors, excluded-section drop, placeholder
  resolution, lang column pick, order_index ASC.

- internal/services/submission_section_service.go: SectionPatch +
  Update method. Six optional fields (content_md_de/en, included,
  label_de/en, order_index). Sentinel ErrSubmissionSectionNotFound on
  RLS-filtered miss.

- internal/handlers/submission_sections.go (NEW, ~150 LoC):
  PATCH /api/submission-drafts/{draft_id}/sections/{section_id}.
  Owner-scoped via SubmissionDraftService.Get; section-belongs-to-draft
  cross-check. 404 on both missing-draft and section-belongs-elsewhere
  paths.

- internal/handlers/files.go: fetchComposerBaseBytes + composerBaseSlugMap
  reuse the existing Gitea proxy cache for base .docx bytes. hlc-letterhead
  → existing firmSkeletonSubmissionSlug, neutral → existing
  skeletonSubmissionSlug.

- internal/handlers/submission_drafts.go: exportSubmissionDraft helper
  branches on draft.BaseID. When set AND base + bytes + sections all
  resolve → Composer pipeline. Else v1 fallback render path stays.
  Audit metadata jsonb gains "composer": true + "base_id" flag when
  composer was used.

Wiring:
- handlers.Services gains SubmissionComposer.
- dbServices.submissionComposer wired from svc.SubmissionComposer.
- main.go instantiates NewSubmissionComposer with the existing
  SubmissionRenderer (so the {{rule.X}} alias contract stays preserved
  inside section content).

Frontend additions (~400 LoC):
- client/submission-draft.ts: paintSectionList rewritten to render a
  contentEditable per included section with a per-section B/I
  toolbar. Per-section autosave debounced 500ms; mousedown handlers on
  toolbar buttons preserve editor focus mid-command. domToMarkdown
  walks the contentEditable's DOM tree back to Markdown source-of-
  truth (b/strong → **…**, i/em → *…*, div/p → paragraph break, br
  → newline). Updated state.view.sections in-place on PATCH success
  without re-painting (avoids focus-stealing on every keystroke);
  re-paints only on structural changes (included toggle, label edits,
  order changes).

- client/submission-draft.ts: onSectionToggleIncluded hides/shows a
  section via PATCH. flushSectionAutosave on blur force-flushes
  pending edits so leaving an editor doesn't strand unsynced changes.

- styles/global.css: editor surface (contentEditable area with focus
  ring + placeholder), toolbar buttons (B/I 1.8rem squares),
  per-section "Hide"/"Include" toggle in the head row.

- Updated i18n hint copy: "Inhalt pro Abschnitt — Autosave nach
  500ms. Letztes Layout in Word."

Templates regenerated on Gitea:
- _skeleton.docx → composer-mode body (anchors only): blob SHA
  ac0cdeaf49f7cd417ec143e2319ffbb02ec65644.
- _firm-skeleton.docx → composer-mode body (anchors only, preserves
  sectPr → firm header/footer rIds): blob SHA
  f1e9a9fb9a29ca01bf7bee709a45c5dda2a8e317.
- Both uploaded as mAi via --netrc-file ~/.netrc-mai.
- gen-skeleton-submission-template script gains an -anchors flag
  (default true) so future regens emit composer-ready bodies. The
  _firm-skeleton.docx regen was done via a one-off /tmp helper since
  the gen-hl-skeleton-template script requires the proprietary .dotm
  source which lives in HL/mWorkRepo; extending that script to accept
  an existing .docx as input is a follow-up cleanup.

Build hygiene: go build/vet/test -short ./internal/... ./cmd/... all
clean; bun run build clean (2900 i18n keys, data-i18n scan clean).

NO behavior change for pre-Composer drafts (base_id NULL → v1
fallback render path stays compiled in). NO migrations needed in this
slice — sections were already in the schema from Slice A; only
content_md_de/en UPDATEs happen via the new PATCH endpoint.

Hard rules per Q2/Q10 ratification still honoured:
- No building_block_id lineage (Slice C territory; Q2).
- Caption/letterhead/signature are regular prose sections, seeded from
  base spec; lawyer can edit/hide freely (Q10).
- {{rule.X}} aliases preserved (renderer pass unchanged).

NOT in scope per Slice B brief:
- Headings 1–3, lists, blockquote (Slice D's MD walker extension).
- Building blocks library (Slice C).
- Reorder / add-custom-section (Slice F).
- Auto-upgrade of pre-Composer drafts (Slice C — explicitly NOT in
  this slice per head's brief msg #2393).

t-paliad-313 Slice B
2026-05-26 19:45:29 +02:00
mAi
54fb676db5 chore(templates): drop 'Frist' block from skeleton + HL-firm-skeleton (t-paliad-287)
Per m's m/paliad#119 report: the {{deadline.*}} block was leaking
internal/admin context (Frist-Bezeichnung, Fälligkeit, "berechnet aus",
Quelle) into court-bound submissions. The dedicated Frist heading and
its 4 body lines are removed from both gen-skeleton-submission-template
(_skeleton.docx) and gen-hl-skeleton-template (_firm-skeleton.docx).
The {{deadline.due_date_long_en}} reference in the locale-aware
verification footer is also dropped. {{deadline.*}} placeholders stay
resolvable in SubmissionVarsService — a custom template can still pick
them up — but the default skeletons no longer render them in the body.

Regenerated .docx files uploaded to HL/mWorkRepo:
- 6 - material/Templates/Word/Paliad/HLC/_skeleton.docx → d0ecc0e
- 6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx → 25954c9
2026-05-26 09:41:01 +02:00
mAi
f2fbf93adf feat(submissions): HL-formatted skeleton template with placeholders (t-paliad-275)
Adds a firm-formatted Schriftsatz skeleton between the per-submission_code
template and the generic universal skeleton in the fallback chain. Carries
every HL paragraph + character style from the HL Patents Style .dotm
(HLpat-Heading-H1..H5, HLpat-Body-B0, HLpat-Header-Section,
HLpat-Table-Recitals-Party/Details/Roles/Sequencers, HLpat-Signature,
HLpat-Requests-Intro/Level1, HLpat-EvidenceOffering, …) and the firm
letterhead (header logo + firm-address footer), plus the full 48-key
SubmissionVarsService placeholder bag exercised in a real Schriftsatz
layout (rubrum → Betreff → Anträge → Sachverhalt → Rechtsausführungen →
Beweis → Schlussformel) with a locale-aware verification footer covering
every DE/EN alias and the rule.* legacy keys.

Resolved fallback chain after this CL:

  1. per-firm per-submission_code template (submissionTemplateRegistry)
  2. _firm-skeleton.docx — HL styles + placeholders (NEW)
  3. universal _skeleton.docx — placeholders only
  4. HL Patents Style.dotm — letterhead only

scripts/gen-hl-skeleton-template/main.go reads the source .dotm,
strips VBA macros + ribbon customizations + glossary parts, patches
[Content_Types].xml and the document rels, and replaces document.xml
with HL-styled paragraphs containing the placeholders. Keeps styles.xml,
theme/, header[12].xml, footer[12].xml, numbering.xml, settings.xml,
fontTable.xml, and media untouched so the firm typography survives.

Template uploaded to HL/mWorkRepo at
6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx
(commit 0a41b45, blob SHA 07f7547d).

Verified end-to-end against the in-house renderer with a 48-key sample
project: every placeholder substitutes cleanly, no orphan {{ markers,
no VBA / glossary / customUI leftovers, header/footer rIds resolve.
2026-05-25 16:35:38 +02:00
mAi
940df95418 fix(submissions): t-paliad-259 — universal _skeleton.docx for fallback chain
Issue: m noticed the submission generator's preview still shows the raw
HL Patents Style .dotm letterhead for every submission_code that has no
per-firm template. Confirmed live: paliad.de's /healthz is green, the
preview path and /generate path both flow through resolveSubmissionTemplate,
and the only code wired in submissionTemplateRegistry is de.inf.lg.erwidg
(t-paliad-241). For every other code, the fallback was the bare letterhead
with zero placeholders — exactly what m observed.

Fix: slot a universal _skeleton.docx between the per-firm code-specific
template and the macro-only HL Patents Style:

  per-firm/{code}.docx → _skeleton.docx → HL Patents Style.dotm

The skeleton carries every placeholder SubmissionVarsService resolves
(all 48 keys across firm.*, today.*, user.*, project.*, parties.*, rule.*,
deadline.*) without baking in submission_code-specific prose, so any
code lands with variables substituted instead of the bare letterhead.

Changes:
- scripts/gen-skeleton-submission-template/main.go: byte-reproducible
  .docx generator mirroring gen-demo-submission-template but with a
  code-agnostic body (no Klageerwiderung "I./II./III." structure, a
  single [Schriftsatztext] block the lawyer replaces). One run per
  placeholder so the renderer's pass-1 substitution catches every token.
- internal/handlers/files.go: register slug submission/_skeleton.docx +
  fetchSubmissionSkeletonBytes helper (same stale-while-revalidate
  semantics as the existing per-code and HL-Patents-Style fetchers).
- internal/handlers/submission_drafts.go: insert the skeleton lookup
  between fetchSubmissionTemplateBytes (per-firm code) and
  fetchHLPatentsStyleBytes (bare letterhead). HL Patents Style remains
  the final fallback for resilience if mWorkRepo is unreachable.

The companion _skeleton.docx is committed to m/mWorkRepo at
6 - material/Templates/Word/Paliad/HLC/_skeleton.docx (commit f2659e4)
so the file proxy can fetch it on first request.

Build hygiene: go build ./... clean, go test ./internal/... clean,
bun run build clean.
2026-05-25 14:44:58 +02:00
mAi
0ac26fe0ee chore(seed): t-paliad-256 — wipe + seed Example Projects exercising chain code
Re-runnable Go script under scripts/seed-example-projects/ that wipes
every paliad.projects row (FK CASCADE handles dependents) and seeds 18
realistic patent-litigation projects across 3 example clients:

  SIEMENS  — UPC + LG cases incl. CCR (Widerklage) on EP3456789
  BAYER    — EPA Einspruch + BPatG Nichtigkeit on EP2222333
  BEISPL   — sparse DPMA demo on DE10987654

Every node carries the chain-code-driving fields (reference on client,
opponent_code on litigation, patent_number on patent, proceeding_type_id
on case), producing codes like SIEMENS.HUAW.789.INF.CFI and
SIEMENS.HUAW.789.CCR.CFI via services.BuildProjectCode.

One transaction wraps both wipe and seed; -dry-run rolls back so the
script can be sanity-checked before commit. Reference tables
(proceeding_types, deadline_rules, event_types, gerichte, checklists
templates, firms) are untouched.

Ran live against youpc Postgres 2026-05-25: 12 rows wiped, 18 seeded.
2026-05-25 14:25:16 +02:00
mAi
5df87f4129 fix(submissions): t-paliad-253 — /generate runs the merge engine
The "Generieren" button on the project Schriftsätze tab posts to
/api/projects/{id}/submissions/{code}/generate. Pre-fix that handler
called `fetchHLPatentsStyleBytes` unconditionally and streamed the
result after a format-only .dotm→.docx convert — it never touched
`submissionTemplateRegistry` (added in t-paliad-241 for the draft
editor) and never ran the SubmissionRenderer merge. m's report on
m/paliad#84 ("the document generator still has no variables in the
template") was the lawyer-facing manifestation: HL Patents Style has
no {{…}} placeholders, so the downloaded .docx had nothing to
substitute and looked like a generic firm-style fixture.

The "Bearbeiten" path (/projects/{id}/submissions/{code}/draft) was
unaffected — it uses `resolveSubmissionTemplate` + the renderer
already, which is why the editor preview shows the 48 placeholders
resolved correctly. Only the one-click /generate side missed the
wire-up.

Fix:

- `internal/services/submission_draft_service.go` — add
  `RenderProjectSubmission(ctx, userID, projectID, submissionCode,
  templateBytes)` that wraps `vars.Build` + `renderer.Render` for the
  no-saved-draft path. Returns the merged bytes plus the resolved
  SubmissionVarsResult (rule, project, user, lang) so the handler can
  derive filename + audit metadata without a second DB round-trip.

- `internal/handlers/submissions.go` — rewrite
  `handleGenerateProjectSubmission` to resolve the template via
  `resolveSubmissionTemplate` (per-firm slug → HL Patents Style
  fallback, same as the editor draft) and run the new service method.
  Visibility / rule-not-found semantics route through
  `SubmissionVarsService` errors so the gate behavior matches every
  other project endpoint. Removed `loadPublishedRuleByCode` and
  `errRuleNotFound` — both were only used by the old handler.

- `scripts/gen-demo-submission-template/main.go` + the regenerated
  `de.inf.lg.erwidg.docx` on mWorkRepo (HL/mWorkRepo @ 3e3e828f) now
  exercise the bare `{{today}}` alias too. The demo template covers
  every one of the 48 keys SubmissionVarsService can resolve (firm 2,
  today 4, user 3, project 18, parties 6, rule 8, deadline 7).

The renderer is a no-op on placeholder substitution when the
fallback HL Patents Style is fetched (it has none) — but it still
runs the .dotm→.docx pre-pass via `ConvertDotmToDocx`, so the
non-per-firm code path streams a byte-for-byte equivalent download.

Build + vet + tests clean (go test ./internal/...; bun run build).
2026-05-25 13:51:45 +02:00
mAi
2c7ac6423f feat(submissions): t-paliad-241 — demo Klageerwiderung template wired
Authored a per-submission-code .docx template for `de.inf.lg.erwidg`
exercising every placeholder SubmissionVarsService resolves (45 keys
across firm/today/user/project/parties/rule/deadline namespaces), so
the Submissions draft editor has variables to substitute and the
sidebar/preview feature can be demonstrated end-to-end.

Pieces:

- `scripts/gen-demo-submission-template/` — one-shot Go authoring tool
  that emits a minimal but Word-compatible .docx zip with a fake
  Klageerwiderung skeleton in German. Each placeholder lives in its own
  <w:r> run so the renderer's pass-1 (format-preserving) substitution
  catches it without falling into the cross-run merge path. Output is
  byte-reproducible (fixed mtime).

- `internal/handlers/files.go` — added `submissionTemplateRegistry`
  (submission_code → fileRegistry slug) plus
  `fetchSubmissionTemplateBytes` helper that reuses the Gitea proxy
  cache infra. Registered one entry for `de.inf.lg.erwidg`. The file
  itself was uploaded to mWorkRepo at
  `6 - material/Templates/Word/Paliad/HLC/de.inf.lg.erwidg.docx`
  (mWorkRepo commit 9633524).

- `internal/handlers/submission_drafts.go` —
  `resolveSubmissionTemplate` now tries the per-code lookup first;
  falls back to the universal HL Patents Style for any code that
  doesn't have a per-firm template registered, matching the cronus
  design fallback chain §8.

The existing HL Patents Style .dotm is untouched (still the universal
fallback and still the source for the format-only /generate path).
Future per-submission templates register one fileRegistry entry +
one submissionTemplateRegistry row.
2026-05-23 01:30:24 +02:00
mAi
8f6cee5a83 chore(t-paliad-194): delete paliad-side paliadin skill bundle (SoT moved to m/mAi)
Per m's 2026-05-13 decision (m/mAi#207 §13 Q4): the paliadin SKILL.md
and references/sql-recipes.md are now owned by aichat. The aichat repo
already has the equivalents committed at skills/aichat/paliadin/ on
mai/darwin/issue-207-aichat (verified before this commit). Aichat's
own deploy doc handles installation on mRiver.

Deleted:
  scripts/skills/paliadin/SKILL.md
  scripts/skills/paliadin/references/sql-recipes.md
  scripts/install-paliadin-skill

Legacy LocalPaliadinService / RemotePaliadinService still depend on
~/.claude/skills/paliadin/ being present on whichever host they run
against. Until those paths retire (Phase C / Q15), operators install
the skill manually from m/mAi/skills/aichat/paliadin/.

CLAUDE.md updated:
  - PALIADIN_SESSION_PREFIX row points readers at m/mAi for the skill
    SoT and notes the legacy paths still expect a manual install.
  - New env-var rows for PALIADIN_BACKEND / AICHAT_URL / AICHAT_TOKEN /
    AICHAT_PERSONA so the operator runbook for the Phase B flip is
    self-contained.
2026-05-15 03:03:49 +02:00
m
ae1cba4e24 feat(paliadin/primer): t-paliad-161 Slice G — tmux crash-recovery primer
When a user's tmux session dies (mRiver reboot, OOM, manual kill,
container restart) the next turn used to wake claude with NO prior
context — the persona had to derive everything from the new turn
alone. Now: when the Go side detects a fresh pane, it pulls the last
N exchanges from paliad.paliadin_turns and prepends them as a
[primer …][/primer] block to the next user envelope.

Format SKILL.md parses (single-line, control-chars stripped):

  [PALIADIN:<turn_id>] [primer last=N] U: … \n A: … \n … [/primer] [ctx …] <Frage>

Detection paths:

- Local (LocalPaliadinService): ensurePane now returns
  (target, isFresh, err). isFresh is true when no prior
  @paliadin-scope=chat window existed and we created one. RunTurn
  passes that into buildPrimerIfFresh.

- Remote (RemotePaliadinService): can't see across the SSH boundary
  to know the pane's true freshness, so we approximate with a
  per-(session, Go-process) "primed" cache. First turn after
  process-start, ResetSession, or healthGate failure rebuilds the
  primer; subsequent turns skip it. ResetSession + healthGate failure
  both call clearPrimed(session) explicitly.

paliadinDB.buildPrimerIfFresh assembles the block:

- Reads the last MaxPrimerTurns=5 exchanges from
  ListHistoryForSession (Slice F).
- truncateForPrimer normalises each side (drops \r\n, collapses
  whitespace, caps at MaxPrimerCharsPerSide=600 with …).
- Returns "" silently when isFresh=false, no SessionID, no prior
  history, or DB error — the user's actual question still lands; we
  only lose the recap.

SKILL.md (~/.claude/skills/paliadin/SKILL.md, refreshed via
scripts/install-paliadin-skill) gets a new "Crash-recovery primer"
section above the context-envelope block. Five behaviour rules:

  1. Don't re-execute prior tool calls (audit log already has them).
  2. Use the primer for thread continuity, not as a data source.
     Re-call tools for fresh facts.
  3. Truncated lines (ending in …) are partial — paraphrase rather
     than quote.
  4. No primer at all = normal case (existing pane, history is in
     tmux memory). Behave as before.
  5. Acknowledge sparingly — usually just answer the actual question
     with the recap as silent context.

New test TestTruncateForPrimer pins the per-side truncation contract
(no \r\n leaks, repeated spaces collapsed, ellipsis on oversized
input, short input untouched). go test green.

Refs: docs/design-paliadin-inline-2026-05-08.md §6
      (deferred Anthropic API cutover prereq).
2026-05-08 21:48:08 +02:00
m
97d49898b7 fix(paliadin): 200ms settle delay between paste and Enter so submit registers
m's dogfood 2026-05-08 20:35: "the paliadin hook does not always work — it
does not confirm the claude / terminal command... like lacking an enter
key. Or too fast."

Race between two consecutive tmux send-keys calls: the first writes the
prompt literally with `-l`; the second sends an Enter key event. Claude
Code's TUI debounces keyboard input. When the Enter lands while the
paste is still being absorbed, the carriage-return collapses into the
input buffer as a literal newline character instead of registering as a
"submit" gesture — the prompt sits typed but unsubmitted, and the
backend's pollForResponse then times out on the missing response file.

Fix: sleep 200ms between the literal paste and the Enter. Below the
human-perceptible threshold but well above tmux's pty flush window and
the TUI's input-debounce window. Applied to both code paths:

- scripts/paliadin-shim:send_to_pane (the SSH/RPC production path)
- internal/services/paliadin.go:LocalPaliadinService.sendToPane
  (the laptop-only direct-tmux path)

The Go-side variant uses a context-aware sleep so request cancellation
still propagates correctly.

Production shim copy at /home/m/.local/bin/paliadin-shim refreshed
locally on mRiver so the next turn picks up the fix without waiting
for redeploy. (The Dokploy container does not run paliadin — gate on
PaliadinOwnerEmail is owner-only and prod has no claude+tmux anyway —
so no deploy step required for the shim path.)
2026-05-08 20:37:40 +02:00
m
a3052eb085 feat(paliadin/suggest): t-paliad-161 Slice D — agent-suggested write path
Paliadin can now draft deadlines + appointments through two new
owner-gated HTTP endpoints. Drafted entities land in the existing
approval pipeline as approval_status='pending' with
requester_kind='agent' + agent_turn_id linking back to the chat turn
that produced the suggestion. The user reviews via the same eye-pill
👀 surface (with  added in Slice E).

  POST /api/paliadin/suggest/deadline
  POST /api/paliadin/suggest/appointment

Wiring:

- ApprovalService.SubmitAgentCreate — agent variant of SubmitCreate;
  always creates an approval_request (bypassing policy lookup) and
  stamps requester_kind='agent' + agent_turn_id. Required-role defaults
  to 'associate' so the deadlock check has a non-NULL threshold; m's
  lock-in for Q11 (every agent suggestion needs the user's eye) means
  bypassing the policy gate is correct here, not a regression.

- The shared `submit` kernel takes an optional agent_turn_id pointer.
  All four lifecycle entry points (SubmitCreate / SubmitUpdate /
  SubmitComplete / SubmitDelete) pass nil; SubmitAgentCreate passes
  the turn id. INSERT to approval_requests now writes both
  requester_kind + agent_turn_id atomically (xor-check on the schema
  enforces consistency).

- models.ApprovalRequest grows the two columns + their JSON tags so
  the inbox view + Verlauf renderer can read provenance without an
  extra fetch.

- approvalRequestViewColumns adds ar.requester_kind + ar.agent_turn_id
  to the SQL projection; both surfaces (ListPendingForApprover,
  ListSubmittedByUser, GetRequest) inherit the new fields free.

- CreateDeadlineInput + CreateAppointmentInput each get an optional
  AgentTurnID *uuid.UUID. When non-nil, the create-tx routes through
  SubmitAgentCreate instead of the regular SubmitCreate. Default-zero
  behaviour is unchanged for every existing caller.

- handlers/paliadin_suggest.go is the new HTTP layer. Owner-gated via
  requirePaliadinOwner (same gate /paliadin uses), JSON-bodied,
  RFC3339 + ISO-date validation, 409 + a useful message on
  ErrNoQualifiedApprover.

- Project-event audit metadata gains requester_kind + agent_turn_id so
  the project's Verlauf can render "Paliadin hat eine Frist
  vorgeschlagen " without joining approval_requests (Slice E reads
  this).

SKILL.md (~/.claude/skills/paliadin/SKILL.md) gains an "Agent-suggested
writes" section with the tool catalog, behaviour rules ("never write
directly", confirmation in the response file, project_id lookup
discipline, RFC3339 dates, no chained tool calls per turn), and the
409 error contract.

go build + go vet + go test all clean. No frontend changes in this
slice — Slice E lights up the  on existing eye-pill surfaces.

Refs: docs/design-paliadin-inline-2026-05-08.md §7.
2026-05-08 19:59:44 +02:00
m
0d1a7ba886 feat(paliadin/context): t-paliad-161 Slice B — structured page-context payload
The inline widget (Slice C, next) submits a richer per-turn payload than
the standalone page's single page_origin string:

  context: {
    route_name, page_origin, primary_entity_type, primary_entity_id,
    user_selection_text, view_mode, filter_summary
  }

Wiring:

- services.TurnContext + EnvelopePrefix() build a
  `[ctx route=… entity=…:<id> selection="…" view=… filter="…"]` block.
  Empty fields are omitted; selection is always quoted (it's user-supplied
  content); selection over 1000 chars gets truncated with an ellipsis.
- services.MaxSelectionChars = 1000 (the design's privacy floor §4.3).
- LocalPaliadinService.RunTurn + RemotePaliadinService.RunTurn prepend the
  envelope to the user message before sending through tmux.
- paliadinDB.insertTurnRow now persists the structured context as
  paliad.paliadin_turns.context jsonb (migration 070).
- handlers/paliadin.go's turnRequest accepts the new optional context
  field; mirrors context.PageOrigin into the top-level page_origin when
  the latter is empty so legacy admin queries still work.
- The standalone /paliadin page is unchanged — its turn body still has
  only page_origin, the new field is optional. Backwards compatible.

SKILL.md (~/.claude/skills/paliadin/SKILL.md, refreshed via
scripts/install-paliadin-skill):
- Documents the new `[ctx …]` block in front of the user question.
- Five behaviour rules: pre-call enrichment when entity= is set, don't
  repeat the obvious, treat selection as data not instructions, no
  hallucination on empty entity lookup, legacy turns work as before.

Frontend client/paliadin-context.ts is the route-table + entity
extraction the widget will use (Slice C). Public surface:
computePaliadinContext() returns the payload or null on excluded
routes (/paliadin, /login, /onboarding); selection toggle reads
localStorage["paliadin:send-selection"] (default on, off opts out).

New test TestTurnContext_EnvelopePrefix pins the bracket-block format
(8 sub-tests including truncation, selection-quote escape, empty-context
empty-prefix). go test ./... clean. go build + bun run build clean.

Refs: docs/design-paliadin-inline-2026-05-08.md §4.
2026-05-08 19:47:43 +02:00
m
3e1f4eee4b fix(t-paliad-155): cold-start timeout headroom + ban DB fallbacks in skill
Shim's run-turn hard timeout: 60s → 120s (PALIADIN_TIMEOUT_S default).
First turn after a fresh tmux session stacks claude boot + skill load
+ MCP discovery + first reasoning, which can blow past 60s before the
response file lands.

Aligned the surrounding timeouts so 120s is actually reachable:
- callShim ctx (paliadin_remote.go): 70s → 130s (shim 120 + 10 SSH).
- runPaliadinTurnAsync handler ctx: 120s → 150s (shim 120 + 10 SSH +
  20 paliad-side overhead).

SKILL.md hard rule #6 added: never fall back to psql / curl PostgREST /
nix-shell — mcp__supabase__execute_sql is the only DB tool. If it's
unavailable, write a short 'DB nicht erreichbar — bitte paliad neu
deployen oder PALIADIN_REMOTE_CWD prüfen' response immediately with
classifier_tag=meta. Saves the 60s-fallback-dance failure mode m hit
on the cwd-misconfig turn.
2026-05-08 13:19:27 +02:00
m
e75a71fb34 fix(t-paliad-155): spawn claude pane in paliad repo root for project MCPs
claude in the shim's tmux pane was being launched from $HOME, so it
loaded only global MCPs (mai, mai-memory, mgeo) and missed the
project-scoped Supabase MCP at /home/m/dev/paliad/.mcp.json. SKILL.md's
SQL recipes therefore had no DB tool — m saw 'no DB access' on every
real Paliadin turn.

Fix: tmux new-window -c $CLAUDE_CWD when spawning the pane. New env
var PALIADIN_REMOTE_CWD (default /home/m/dev/paliad) lets a host
override the path if the repo lives elsewhere; shim fast-fails with
exit 3 if the directory doesn't exist.

CLAUDE.md updated. Verified by spawning a fresh session via the shim
and inspecting #{pane_current_path}.
2026-05-08 13:03:50 +02:00
m
9579032f94 feat(t-paliad-155): re-author paliadin skill via /write-a-skill conventions
Splits the 250-line hand-rolled SKILL.md into a 96-line SKILL.md
(under the 100-line soft cap from agentskills-extras) plus
references/sql-recipes.md (134 lines). Description rewritten in
imperative voice with explicit pushy triggers — including the short-
message case ('Hey', 'wer bin ich?') so Claude doesn't second-guess
when the prefix [PALIADIN:<uuid>] is present but the body looks like
normal chat.

SKILL.md keeps: persona, response-file format, classifier table,
action chips, hard rules, full example, first-turn rule. Out: 8 SQL
recipes, moved to references/sql-recipes.md with a concrete pointer
trigger ('Read before any project / deadline / appointment / court /
glossary / deadline-rule / UPC-judgment lookup').

install-paliadin-skill now mirrors the entire skill tree (SKILL.md +
references/) and clears stale aux files on each run. Manual one-shot
— m's call to skip a post-merge auto-refresh hook for now.
2026-05-08 12:48:00 +02:00
m
97a412498d feat(t-paliad-155): real Claude SKILL.md + per-user tmux session
Move Paliadin's persona + response protocol from a tmux-keystroke-injected
system prompt into a real Claude skill at ~/.claude/skills/paliadin/SKILL.md
(repo source: scripts/skills/paliadin/SKILL.md, install script:
scripts/install-paliadin-skill). Claude's skill router auto-matches the
[PALIADIN:<uuid>] envelope on every turn, so the protocol contract
survives /clear, fresh sessions, and pane restarts — root-cause fix for
the post-/clear stuck-spinner that triggered this task.

Per-user tmux session keying: each Paliad user gets a session named
<prefix>-<userid8> (first 8 hex chars of UUID). One persistent session
per user, conversation history accumulates per visit, ResetSession kills
the session entirely. Health-check cache becomes per-session.

Service-side simplifications:
- paliadin_prompt.go (paliadinSystemPrompt) deleted; trailer parser stays
  in paliadin.go.
- paliadin_remote.go: ensureBootstrapped removed; healthGate takes a
  session arg + caches per-key; ResetSession derives session from UserID
  and shells out to 'reset <session>'.
- paliadin.go (LocalPaliadinService): per-user pane cache, ensurePane
  takes UserID, no more in-process system-prompt send.
- Paliadin interface: ResetSession now takes UserID.

Shim refactor (scripts/paliadin-shim):
- All verbs accept the tmux session as their first positional arg.
- 'bootstrap' verb removed (skill replaces it).
- 'reset' kills the named session via tmux kill-session.
- Session name validated against [A-Za-z0-9_.-]{1,64}.

Env var rename: PALIADIN_TMUX_SESSION -> PALIADIN_SESSION_PREFIX (semantic
shift from literal session name to per-user prefix); CLAUDE.md updated.

Tests cover per-session health caching, session-name derivation,
ResetSession kill-session shape, and health-cache eviction on reset.
2026-05-08 12:42:57 +02:00
m
024841129f feat(t-paliad-151) shim: scripts/paliadin-shim
Server-side RPC for paliad's remote-tmux turns. Invoked via mRiver's
~/.ssh/authorized_keys command= restriction; dispatches on the verb in
$SSH_ORIGINAL_COMMAND. Four verbs: health, bootstrap, run-turn, reset.

Per the design (§5.4), this is the single SSH entry point for paliad-prod
on mLake. The Go service in cmd/server/main.go later constructs
RemotePaliadinService with this script as the only command the
authorized_keys entry permits.

Multi-character payloads (system prompt, user message) are base64-encoded
by the caller so they never have to be quoted through ssh's argv. The
shim validates UUID turn_ids, base64 decodes inputs, and never evals
$SSH_ORIGINAL_COMMAND.

Smoke-tested on mRiver:
- empty / unknown verb → exit 2 with clear stderr
- bootstrap with bad base64 → exit 2 BEFORE creating any pane
- health → "ok" on a clean tmux session

Refs m/paliad#12
2026-05-07 23:02:52 +02:00