Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99bb3526db | |||
| bb8cb8d014 |
@@ -1,160 +0,0 @@
|
||||
# Maven project dictionary for the direct-prose skill.
|
||||
#
|
||||
# These terms override every word preference in the skill's word-choice tables.
|
||||
# Each entry exists because the name drifted in real docs or real answers, not
|
||||
# because the word looked improvable.
|
||||
#
|
||||
# Format and the rule for adding a term: ~/.claude/skills/direct-prose/references/modes.md
|
||||
|
||||
terms:
|
||||
resident_model:
|
||||
name: resident model
|
||||
meaning: the one always-warm Qwen3-1.7B llama-server that both routes and phrases
|
||||
avoid:
|
||||
- the model
|
||||
- the LLM
|
||||
- the 1.7B
|
||||
- the phraser model
|
||||
examples:
|
||||
good: The resident model emits GBNF-constrained JSON.
|
||||
bad: The 1.7B emits GBNF-constrained JSON.
|
||||
|
||||
router:
|
||||
name: router
|
||||
meaning: the stage that turns an utterance into a Decision with one of 7 intents
|
||||
avoid:
|
||||
- orchestrator
|
||||
- intent classifier
|
||||
- dispatcher
|
||||
|
||||
classifier:
|
||||
name: classifier
|
||||
meaning: the embedder nearest-neighbour path that runs when the router is off or errors
|
||||
avoid:
|
||||
- the fallback
|
||||
- the floor
|
||||
- the old router
|
||||
examples:
|
||||
good: A router error falls through to the classifier.
|
||||
bad: A router error falls through to the floor.
|
||||
|
||||
cascade:
|
||||
name: cascade
|
||||
meaning: the ordered path stage 0, then router, then classifier
|
||||
avoid:
|
||||
- the pipeline
|
||||
- the chain
|
||||
- the fallback chain
|
||||
|
||||
stage_0:
|
||||
name: stage 0
|
||||
meaning: the deterministic rules that answer before the resident model is called
|
||||
avoid:
|
||||
- the fast path
|
||||
- bypass
|
||||
- deterministic assist
|
||||
- preemption
|
||||
examples:
|
||||
good: Stage 0 routes agenda questions to IntentQuery.
|
||||
bad: The bypass routes agenda questions to IntentQuery.
|
||||
|
||||
query_source:
|
||||
name: query source
|
||||
meaning: one entry in querySources, which either claims a turn or passes
|
||||
avoid:
|
||||
- arm
|
||||
- handler
|
||||
- branch
|
||||
- answerer
|
||||
examples:
|
||||
good: Kiwix is the last query source before the model answers from memory.
|
||||
bad: Kiwix is the last arm before the model answers from memory.
|
||||
|
||||
personal_boundary:
|
||||
name: personal boundary
|
||||
meaning: the query source that stops a question about him from reaching the world
|
||||
avoid:
|
||||
- the boundary
|
||||
- the privacy gate
|
||||
- the personal filter
|
||||
|
||||
clarify:
|
||||
name: clarify
|
||||
meaning: the turn outcome where Maven asks instead of acting
|
||||
avoid:
|
||||
- refusal
|
||||
- rejection
|
||||
- punt
|
||||
examples:
|
||||
good: The gate produced two false clarifies.
|
||||
bad: The gate produced two false refusals.
|
||||
|
||||
fact:
|
||||
name: fact
|
||||
meaning: a keyed, supersedable row in the fact store
|
||||
avoid:
|
||||
- memory entry
|
||||
- datum
|
||||
- record
|
||||
|
||||
note:
|
||||
name: note
|
||||
meaning: free text he captured, indexed for recall
|
||||
avoid:
|
||||
- memo
|
||||
- entry
|
||||
|
||||
memory:
|
||||
name: memory
|
||||
meaning: the embedded index over notes and facts that backs recall
|
||||
avoid:
|
||||
- RAG store
|
||||
- vector db
|
||||
- long-term memory
|
||||
|
||||
nudge:
|
||||
name: nudge
|
||||
meaning: one proactive message the digestion worker proposes and the dispatcher sends
|
||||
avoid:
|
||||
- suggestion
|
||||
- proposal
|
||||
- proactive prompt
|
||||
- reminder
|
||||
examples:
|
||||
good: A fact can close the nudge that asked for it.
|
||||
bad: A fact can close the suggestion that asked for it.
|
||||
|
||||
digestion_worker:
|
||||
name: digestion worker
|
||||
meaning: the background engine that consolidates memory and proposes nudges
|
||||
avoid:
|
||||
- digestion tick
|
||||
- background engine
|
||||
- reflection loop
|
||||
|
||||
reach:
|
||||
name: reach
|
||||
meaning: an outbound channel Maven speaks through, such as telegram, ntfy or voice
|
||||
avoid:
|
||||
- sink
|
||||
- delivery channel
|
||||
- notification backend
|
||||
|
||||
ecosystem:
|
||||
name: ecosystem
|
||||
meaning: Nexus, Praxis and Hexis together
|
||||
avoid:
|
||||
- the services
|
||||
- the integrations
|
||||
- upstream
|
||||
|
||||
act:
|
||||
name: act
|
||||
meaning: the intent that runs a capability through Hexis
|
||||
avoid:
|
||||
- action
|
||||
- command
|
||||
- execution
|
||||
examples:
|
||||
good: An act with no allowlisted fn is gated to a clarify.
|
||||
bad: An action with no allowlisted fn is gated to a clarify.
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=.claude/prose-dictionary.yaml; [ -f \"$f\" ] && jq -Rs '{hookSpecificOutput:{hookEventName:\"SessionStart\",additionalContext:(\"Project prose dictionary. These terms override every word preference in the direct-prose output style. Use the name, never the avoid list.\\n\\n\"+.)}}' \"$f\" 2>/dev/null || true",
|
||||
"statusMessage": "Loading prose dictionary"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=HANDOFF.md; [ -f \"$f\" ] && jq -Rs '{hookSpecificOutput:{hookEventName:\"SessionStart\",additionalContext:(\"An unconsumed HANDOFF.md is present. Run the pickup skill before anything else: read it, read the Vikunja task it names, restate the assumption set in at most five bullets, and wait for the user to confirm before writing code. It is a claim from the previous session, not truth. Delete it once consumed.\\n\\n\"+.)}}' \"$f\" 2>/dev/null || true",
|
||||
"statusMessage": "Loading handoff"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: pickup
|
||||
description: Start a work session on a Maven task. Runs task start, reads the brief and the disposable handoff, restates the assumption set, and waits for correction before touching code. Use at the start of any session that continues earlier work, when the user says "pickup", "continue", "resume", or names a Vikunja task id.
|
||||
---
|
||||
|
||||
# Pickup
|
||||
|
||||
The point of this skill is the pause in step 5. Every wasted session in this repo
|
||||
started with an agent that inferred the goal instead of stating it back.
|
||||
|
||||
## 0. Get on the branch
|
||||
|
||||
```sh
|
||||
task start <vikunja-id>
|
||||
```
|
||||
|
||||
`~/.local/bin/task` owns the branch, the identity and the PR. It cuts
|
||||
`task/<id>-<slug>` off `origin/master` and sets the commit author to the `claude`
|
||||
gitea user. It writes `TASK.md` from the Vikunja task, and pulls any waiting
|
||||
review comments into `.task/review-comments.md`. Do not hand-roll any of that.
|
||||
|
||||
`TASK.md` is the brief and it is immutable. If it says a PR already exists, this
|
||||
is a review-fix session and not new work. Read the comments first.
|
||||
|
||||
## 1. Read the handoff
|
||||
|
||||
`HANDOFF.md` at the repo root, if it exists. It is gitignored, it belongs to one
|
||||
session, and it holds only what is needed to resume. Treat it as a claim from the
|
||||
previous agent, not as truth. It can be stale or wrong.
|
||||
|
||||
If there is no handoff, that is normal. It means the last session closed clean.
|
||||
|
||||
## 2. Read the durable state
|
||||
|
||||
In this order, and stop as soon as you have enough:
|
||||
|
||||
- The Vikunja task, by id. Project Maven is ID 2, MCP at `http://localhost:9100/mcp`.
|
||||
The task description and its comments hold the goal, the constraints, and the
|
||||
assumption ledger. This outranks the handoff on every conflict.
|
||||
- `CLAUDE.md`, the section that covers the area you are about to touch.
|
||||
- The one file under `docs/` that owns the area. Check its `Last verified` line.
|
||||
If the sha is behind the code you are reading, say so in step 4 and trust the code.
|
||||
|
||||
Do not read the dated files under `docs/evals/`. They are measurements from one day,
|
||||
never updated. Read one only when you need the number it recorded.
|
||||
|
||||
If no task id is known, ask for one before doing anything else. Work without a task
|
||||
is work nobody can resume.
|
||||
|
||||
## 3. Look at the ground
|
||||
|
||||
`git status`, `git log --oneline -5`, and the diff on the current branch. What the
|
||||
repo says beats what any document says.
|
||||
|
||||
## 4. Restate, then stop
|
||||
|
||||
Write at most five bullets and stop. Do not write code, do not open files to "check
|
||||
one thing first", do not start with a small safe change.
|
||||
|
||||
```
|
||||
Task: V-359, one line.
|
||||
Done: what is already on the branch.
|
||||
Next: the one thing this session does.
|
||||
Constraints: what would make this wrong.
|
||||
Assuming: the beliefs that, if false, waste the session.
|
||||
```
|
||||
|
||||
Then ask: is this right? Wait for the answer.
|
||||
|
||||
A corrected assumption goes into the Vikunja task as a comment, not into the handoff.
|
||||
The handoff dies tonight. The task does not.
|
||||
|
||||
## 5. Then begin
|
||||
|
||||
- Delete `HANDOFF.md`. It has been consumed and must not outlive this step.
|
||||
- On master, cut the branch: `scripts/task-branch.sh <id> <slug>`.
|
||||
- One task per session. When context passes roughly half, run `/wrap` rather than
|
||||
pushing on. A compacted session is a session that forgot why it made a choice.
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
name: wrap
|
||||
description: Close a Maven work session cleanly. Runs the tests, updates the durable docs, commits in reviewable slices with the Vikunja ref, pushes so the PR opens, records state in Vikunja, and leaves a disposable handoff only if work remains. Use when the user says "wrap", "wrap up", "done for now", or when context passes roughly half.
|
||||
---
|
||||
|
||||
# Wrap
|
||||
|
||||
Run every step. A partial wrap is worse than none, because the next session trusts
|
||||
the parts that did run.
|
||||
|
||||
## 1. Prove it works
|
||||
|
||||
`make test`. If something fails, fix it or say plainly in the handoff and in Vikunja
|
||||
that it fails, with the output. Never wrap on an untested claim.
|
||||
|
||||
## 2. Update the durable docs
|
||||
|
||||
Ask what a future agent would have to learn the hard way, and write that down.
|
||||
|
||||
- `CLAUDE.md` when a fact an agent needs before touching code has changed: routing
|
||||
behaviour, a measured number, a flag default, a constraint. A commit that changed
|
||||
routing or phrasing without touching the matching CLAUDE.md section is a bug.
|
||||
Correct stale text in place. Do not append a new paragraph next to the wrong one.
|
||||
- `AGENTS.md` when the recipe to build, run or preview changed.
|
||||
- The one file under `docs/` that owns the area, plus its `Last verified: <date> @ <sha>`
|
||||
line. Only a doc directly under `docs/` carries that line.
|
||||
- A new dated file under `docs/evals/` when you measured something. Never edit an
|
||||
existing dated file. A newer measurement is a new file, and the living doc points
|
||||
at it.
|
||||
|
||||
Nothing that must survive tonight goes anywhere else. Not into the handoff, not into
|
||||
a commit message, not into a comment in the code.
|
||||
|
||||
## 3. Commit in slices
|
||||
|
||||
Under 300 changed lines per commit in non-markdown files, enforced by `.githooks/pre-commit`.
|
||||
Markdown is exempt and may land as one batch.
|
||||
|
||||
Each commit is one idea, subject in the repo's voice, lowercase area prefix, and it
|
||||
ends with the Vikunja ref:
|
||||
|
||||
```
|
||||
router: narrow the single-token rule (V-359)
|
||||
```
|
||||
|
||||
If a change genuinely cannot split under 300 lines, say why in the commit body before
|
||||
reaching for `--no-verify`.
|
||||
|
||||
## 4. Land it
|
||||
|
||||
```sh
|
||||
task pr
|
||||
```
|
||||
|
||||
It refuses a dirty tree, pushes, opens or refreshes the PR against the repo default
|
||||
branch, labels the Vikunja task in-review, comments the PR url on it, and pushes an
|
||||
ntfy. Do not push by hand and do not call `tea` yourself.
|
||||
|
||||
## 5. Record what `task pr` cannot know
|
||||
|
||||
Comment on the Vikunja task: what you measured, what is still open. List every
|
||||
assumption that turned out to be wrong. If the session found new work, create a task
|
||||
for it now rather than describing it in prose.
|
||||
|
||||
This step is what makes the handoff disposable.
|
||||
|
||||
## 6. Leave the handoff, or leave none
|
||||
|
||||
If the task is finished, delete `HANDOFF.md` and stop. An empty root is the correct
|
||||
end state.
|
||||
|
||||
If work remains, write `HANDOFF.md` with nothing but what the next agent needs to
|
||||
resume, and no history:
|
||||
|
||||
```markdown
|
||||
# Handoff — <date>
|
||||
|
||||
Task: V-359 <one line>
|
||||
Branch: task/359-<slug>, cut from master
|
||||
|
||||
## Where I stopped
|
||||
<two sentences, mid-thought detail that is nowhere else>
|
||||
|
||||
## Next action
|
||||
<the single concrete next step>
|
||||
|
||||
## Do not
|
||||
<the trap I nearly fell into, or the approach already ruled out>
|
||||
```
|
||||
|
||||
Nothing else goes in it. No summary of what landed, that is in git and Vikunja. No
|
||||
design rationale, that is in `docs/`. No fact an agent needs on any task, that is in
|
||||
`CLAUDE.md`. If a line in the handoff would still matter next week, it is in the wrong
|
||||
file.
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Every commit names the Vikunja task it belongs to.
|
||||
#
|
||||
# router: narrow the single-token rule (V-359)
|
||||
#
|
||||
# V- and not #, because Gitea autolinks #359 to a Gitea issue, which is a
|
||||
# different tracker and a wrong link.
|
||||
#
|
||||
# Exempt: merges, reverts, fixup/squash, and the initial commit.
|
||||
|
||||
msg_file=$1
|
||||
subject=$(sed -n '1p' "$msg_file")
|
||||
|
||||
case "$subject" in
|
||||
Merge\ *|Revert\ *|fixup!\ *|squash!\ *|amend!\ *) exit 0 ;;
|
||||
esac
|
||||
|
||||
if [ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if printf '%s' "$subject" | grep -qE '\(V-[0-9]+\)$'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "commit-msg: subject must end with a Vikunja task ref." >&2
|
||||
echo " got: $subject" >&2
|
||||
echo " want: router: narrow the single-token rule (V-359)" >&2
|
||||
echo " No task yet? Create one. Work without a task is work nobody can resume." >&2
|
||||
exit 1
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Two guards, both bypassable with --no-verify when you mean it.
|
||||
# 1. master is not a working branch.
|
||||
# 2. a code commit stays under 300 changed lines.
|
||||
# Markdown is exempt from the size cap on purpose: docs land as one batch.
|
||||
|
||||
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
|
||||
case "$branch" in
|
||||
master|main)
|
||||
echo "pre-commit: refusing to commit on $branch." >&2
|
||||
echo " task start <vikunja-id> # branch off origin/master, write TASK.md" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Added + deleted lines across staged files that are not markdown.
|
||||
# numstat prints "-\t-\t<path>" for binaries; those count 0 and that is fine,
|
||||
# a binary blob is not the kind of diff this cap exists to stop.
|
||||
loc=$(git diff --cached --numstat -- . ':(exclude)*.md' |
|
||||
awk '$1 ~ /^[0-9]+$/ { a += $1 } $2 ~ /^[0-9]+$/ { d += $2 } END { print a + d + 0 }')
|
||||
|
||||
if [ "$loc" -gt 300 ]; then
|
||||
echo "pre-commit: $loc changed lines in non-markdown files, cap is 300." >&2
|
||||
echo " Split it. Each commit should be one reviewable idea." >&2
|
||||
echo " git reset <path> to unstage, or --no-verify if this genuinely cannot split." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
+2
-11
@@ -50,16 +50,7 @@ opencode.json
|
||||
# Test coverage output
|
||||
coverage.out
|
||||
|
||||
# Agent worktrees and local agent state. The workflow itself is tracked: the
|
||||
# hooks, the skills and the prose dictionary are how a session behaves, so they
|
||||
# get reviewed like code. Everything else under .claude/ is scratch.
|
||||
/.claude/*
|
||||
!/.claude/settings.json
|
||||
!/.claude/prose-dictionary.yaml
|
||||
!/.claude/skills/
|
||||
|
||||
# The disposable handoff. One session, then deleted. Never committed:
|
||||
# anything worth keeping belongs in Vikunja, CLAUDE.md or docs/.
|
||||
/HANDOFF.md
|
||||
# Agent worktrees and local agent state
|
||||
.claude/
|
||||
/models/stt
|
||||
/models/tts
|
||||
|
||||
@@ -213,60 +213,3 @@ data pans on a phone. Local preview + headless screenshot recipe is in `AGENTS.m
|
||||
|
||||
This repo is project **Maven** (ID 2) in Vikunja. MCP: `http://localhost:9100/mcp` (or
|
||||
`http://192.168.1.104:9100/mcp` from workpc). Feature/bug/deploy tasks go there.
|
||||
|
||||
Vikunja is the durable task store. A task holds the goal, the constraints and the
|
||||
assumption ledger. Work without a task id is work nobody can resume, so a session that
|
||||
has no id asks for one before it starts.
|
||||
|
||||
## Session workflow
|
||||
|
||||
`~/.local/bin/task` owns the branch, the commit identity and the PR. One task, one
|
||||
session, one PR.
|
||||
|
||||
```sh
|
||||
task start <vikunja-id> # branch off origin/master, write TASK.md, fetch review comments
|
||||
task pr # push, open or refresh the PR, label Vikunja, notify
|
||||
task comments # re-pull this branch's review comments into .task/
|
||||
```
|
||||
|
||||
Around that, `/pickup` opens a session and `/wrap` closes it. Wrap at roughly half
|
||||
context rather than letting the session compact.
|
||||
|
||||
Five stores, and each one owns something the others must not hold:
|
||||
|
||||
| Store | Holds | Lifetime |
|
||||
|---|---|---|
|
||||
| Vikunja task | goal, constraints, assumption ledger, status | durable |
|
||||
| `CLAUDE.md`, `AGENTS.md` | what an agent must know before touching code | durable |
|
||||
| `docs/` | design, measurements, decisions | durable |
|
||||
| `TASK.md` | the brief for this branch, written by `task start`, immutable | one branch |
|
||||
| `HANDOFF.md` | only what the next agent needs to resume | one session |
|
||||
|
||||
`TASK.md` and `.task/` are excluded through `.git/info/exclude`. `HANDOFF.md` is
|
||||
gitignored and injected at session start. If a line in the handoff would still matter
|
||||
next week, it is in the wrong file.
|
||||
|
||||
Docs are tiered by path, so staleness is visible from the filename. Files directly under
|
||||
`docs/` are living and carry a `Last verified: <date> @ <sha>` line. Files under
|
||||
`docs/evals/` are dated measurements and are never edited after the day, so a newer
|
||||
number is a new file. Files under `docs/archive/` are dead and read by nobody by default.
|
||||
|
||||
## Git guards
|
||||
|
||||
Two hooks in `.githooks/`, tracked, wired with `core.hooksPath`. Fresh clone:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
- `pre-commit` refuses master, and refuses more than 300 changed lines in non-markdown
|
||||
files. Markdown is exempt and may land as one batch.
|
||||
- `commit-msg` requires the subject to end with `(V-<id>)`. `V-` and not `#`, because
|
||||
Gitea autolinks `#123` to a Gitea issue, which is the wrong tracker.
|
||||
|
||||
Two more guards live outside the repo, in `~/.claude/hooks/`. `diff-budget.sh` blocks
|
||||
further edits past 600 changed lines on a `task/` branch. `prose_lint_hook.py` checks
|
||||
prose on every write. Both measure against `origin/master`, so a local master that is
|
||||
ahead of the remote makes the diff budget read high.
|
||||
|
||||
`--no-verify` exists. Using it means saying why in the commit body.
|
||||
|
||||
+12
-8
@@ -107,14 +107,18 @@ func (h *reactiveHandler) confirmResolvers(ctx context.Context) []confirmResolve
|
||||
return pr != nil && !h.now().After(pr.expiry)
|
||||
},
|
||||
yes: func() string {
|
||||
// Only record the acceptance. The tick loop reads accepted
|
||||
// routines and nudges on their own interval. Building a
|
||||
// reminder here made a routine fire exactly once (Vikunja #366).
|
||||
if err := h.dataStore.AcceptProposedRoutine(ctx, pr.routineID, h.now()); err != nil {
|
||||
log.Printf("voice: accept proposed routine: %v", err)
|
||||
return "не получилось запомнить рутину."
|
||||
}
|
||||
return "буду напоминать."
|
||||
// Voice does NOT accept (Vikunja #367). Accepting hands the
|
||||
// tick loop a standing new reason to speak, which is the same
|
||||
// tier as enabling a tool — and DESIGN.md § "surface caps
|
||||
// authority" says a room mic, reachable by anyone present, is
|
||||
// structurally incapable of layer 3. So a spoken "да" leaves
|
||||
// the row 'proposed' and points at the authed page, where the
|
||||
// accept button is gated at step-up. The convenience of
|
||||
// answering out loud stays; the authority does not move.
|
||||
//
|
||||
// Acceptance itself is recorded by /routines, and the tick
|
||||
// loop nudges on the interval from there (Vikunja #366).
|
||||
return "поняла — подтверди на странице рутин, и начну напоминать."
|
||||
},
|
||||
no: func() string {
|
||||
if err := h.dataStore.DismissProposedRoutine(ctx, pr.routineID); err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/kami/maven/internal/config"
|
||||
"github.com/kami/maven/internal/delivery"
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/loop"
|
||||
"github.com/kami/maven/internal/pattern"
|
||||
"github.com/kami/maven/internal/store"
|
||||
@@ -283,3 +284,81 @@ func TestTickProposalCooldownSpacesAnnouncements(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestVoiceYesDoesNotAcceptRoutine — Vikunja #367. Accepting a routine hands
|
||||
// the tick loop a standing new reason to speak, which DESIGN.md puts at layer
|
||||
// 3, and voice is structurally incapable of layer 3. A spoken "да" must park
|
||||
// the decision for the authed page, not flip the row itself.
|
||||
func TestVoiceYesDoesNotAcceptRoutine(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := refNow()
|
||||
seedRefillEvents(t, st, ctx, now, pattern.MinEvents-1)
|
||||
|
||||
h := &reactiveHandler{api: ipc.NewStoreAPI(st), dataStore: st, now: func() time.Time { return now }}
|
||||
|
||||
// The MinEvents'th event is the one that makes the pattern detectable, and
|
||||
// it goes through the voice path so the proposal is parked for a y/n.
|
||||
last := now.Add(time.Duration(pattern.MinEvents-1) * 7 * 24 * time.Hour)
|
||||
factID, err := st.WriteFact(ctx, last, store.KindSelf, "cat_water", "refill", "voice", 1.0, sql.NullInt64{})
|
||||
if err != nil {
|
||||
t.Fatalf("write fact: %v", err)
|
||||
}
|
||||
if phrase := h.detectPattern(ctx, factID, "cat_water", "refill", last); phrase == "" {
|
||||
t.Fatal("expected a parked routine proposal")
|
||||
}
|
||||
|
||||
reply, handled := h.resolveConfirm(ctx, "да")
|
||||
if !handled {
|
||||
t.Fatal("the spoken yes should be consumed by the routine confirm")
|
||||
}
|
||||
if !strings.Contains(reply, "рутин") {
|
||||
t.Fatalf("reply should send him to the routines page, got %q", reply)
|
||||
}
|
||||
|
||||
rows, err := st.ListProposedRoutinesByStatus(ctx, store.RoutineAccepted)
|
||||
if err != nil {
|
||||
t.Fatalf("list accepted: %v", err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Fatalf("voice accepted a routine: %+v", rows)
|
||||
}
|
||||
proposed, err := st.ListProposedRoutinesByStatus(ctx, store.RoutineProposed)
|
||||
if err != nil {
|
||||
t.Fatalf("list proposed: %v", err)
|
||||
}
|
||||
if len(proposed) != 1 {
|
||||
t.Fatalf("proposed routines = %d, want 1 (still waiting for the page)", len(proposed))
|
||||
}
|
||||
}
|
||||
|
||||
// TestVoiceNoStillDismissesRoutine — declining does not move the boundary
|
||||
// outward, so voice keeps it. Only acceptance is gated.
|
||||
func TestVoiceNoStillDismissesRoutine(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := refNow()
|
||||
seedRefillEvents(t, st, ctx, now, pattern.MinEvents-1)
|
||||
|
||||
h := &reactiveHandler{api: ipc.NewStoreAPI(st), dataStore: st, now: func() time.Time { return now }}
|
||||
|
||||
last := now.Add(time.Duration(pattern.MinEvents-1) * 7 * 24 * time.Hour)
|
||||
factID, err := st.WriteFact(ctx, last, store.KindSelf, "cat_water", "refill", "voice", 1.0, sql.NullInt64{})
|
||||
if err != nil {
|
||||
t.Fatalf("write fact: %v", err)
|
||||
}
|
||||
if phrase := h.detectPattern(ctx, factID, "cat_water", "refill", last); phrase == "" {
|
||||
t.Fatal("expected a parked routine proposal")
|
||||
}
|
||||
|
||||
if _, handled := h.resolveConfirm(ctx, "нет"); !handled {
|
||||
t.Fatal("the spoken no should be consumed by the routine confirm")
|
||||
}
|
||||
rows, err := st.ListProposedRoutinesByStatus(ctx, store.RoutineDismissed)
|
||||
if err != nil {
|
||||
t.Fatalf("list dismissed: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("dismissed routines = %d, want 1", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -1210,13 +1210,10 @@ func routineRows(rs []ipc.ProposedRoutine) []routineRow {
|
||||
return out
|
||||
}
|
||||
|
||||
// acceptRoutine creates the recurring reminder for a proposal, then marks the
|
||||
// proposal accepted and links the reminder to it. Weekly patterns get a cron
|
||||
// expression; any other interval fires once.
|
||||
//
|
||||
// TODO(vikunja#46): this mirrors the voice accept path in cmd/mavend/voice.go.
|
||||
// When the tick loop learns to read accepted proposals directly, both callers
|
||||
// should hand off to one place in core instead of each building a reminder.
|
||||
// acceptRoutine marks a proposal accepted. This page is the ONLY surface that
|
||||
// may do it (Vikunja #367): accepting gives the tick loop a standing new
|
||||
// reason to speak, which DESIGN.md puts at layer 3, and the button here is
|
||||
// behind step-up. Voice can park the question and dismiss, never accept.
|
||||
func acceptRoutine(ctx context.Context, core ipc.CoreAPI, id int64) error {
|
||||
proposed, err := core.ListProposedRoutines(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -534,8 +534,14 @@ func (p *LLMPhraser) PhraseReminder(ctx context.Context, d loop.ReminderDecision
|
||||
text = "reminder"
|
||||
}
|
||||
|
||||
// Russian, like the other two prompts (Vikunja #404). Asking a model for a
|
||||
// Russian reply in English is asking it to switch languages mid-prompt,
|
||||
// and a 1.7B sometimes answers in the language it was asked in. The
|
||||
// persona rules and the JSON contract are not repeated here: this call
|
||||
// goes through chat(), so nudgeSystem already states both, and a second
|
||||
// statement of the same contract is one more thing that can drift.
|
||||
prompt := fmt.Sprintf(
|
||||
`The user set a reminder: "%s". Rephrase it briefly as a gentle nudge. Respond as JSON: {"response": "...", "mood": "..."}`,
|
||||
`Он поставил напоминание: "%s". Скажи это своими словами, коротко и мягко — одно предложение.`,
|
||||
text,
|
||||
)
|
||||
resp, err := p.chat(ctx, prompt)
|
||||
@@ -754,7 +760,7 @@ func (p *LLMPhraser) querySystemPrompt() string {
|
||||
base := "Ты отвечаешь ему по источникам, которые тебе дали. Отвечай ТОЛЬКО по ним: всё, что ты говоришь, должно быть написано в источниках. " +
|
||||
"Если ответа в них нет — так и скажи и на этом остановись; не добавляй ничего из своих знаний и не догадывайся. " +
|
||||
"Не приплетай прошлые реплики разговора. " +
|
||||
"Отвечай по-русски, коротко и своими словами, начинай с \"вот что я нашла: \". О себе — в женском роде, глаголы в прошедшем времени с окончанием -ла. Он мужчина, обращайся к нему на \"ты\". Respond ONLY with valid JSON: {\"response\": \"...\", \"mood\": \"neutral\"}."
|
||||
"Отвечай по-русски, коротко и своими словами, начинай с \"вот что я нашла: \". О себе — в женском роде, глаголы в прошедшем времени с окончанием -ла. Он мужчина, обращайся к нему на \"ты\". Отвечай ТОЛЬКО одним объектом JSON: {\"response\": \"...\", \"mood\": \"neutral\"}."
|
||||
return persona.Prepend(p.cfg.ContextBlock, base)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@ func KnowledgePrompt() string {
|
||||
// No self-introduction here: the shared persona block already says who she
|
||||
// is, and this line used to disagree with it — a different name ("Мавена")
|
||||
// and a masculine noun ("ассистент") in front of a feminine persona.
|
||||
return `Ответь кратко из своих знаний. Если не знаешь — скажи "не знаю". Не выдумывай. Respond ONLY with valid JSON: {"response": "...", "mood": "neutral"}.`
|
||||
return `Ответь кратко из своих знаний. Если не знаешь — скажи "не знаю". Не выдумывай. Отвечай ТОЛЬКО одним объектом JSON: {"response": "...", "mood": "neutral"}.`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user