mai/curie/coder-mig152-clone-dedupe
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 94310ba498 |
feat(submissions): Composer Slice E — specialist bases + base-swap content survival (m/paliad#141)
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
|
|||
| f963b0df34 |
feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
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
|
|||
| 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 |
|||
| 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.
|
|||
| 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.
|
|||
| 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. |
|||
| 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).
|
|||
| 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. |
|||
| 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. |
|||
|
|
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).
|
||
|
|
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.) |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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}.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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 |