@rayul/kordis 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,430 @@
1
+ ---
2
+ name: kordis-memory
3
+ description: Use when the user wants an answer from THEIR OWN notes folder rather than from the registry — connecting a notes/vault folder to Kordis, recalling what they previously decided, or expanding one recalled card into its detail. Triggers include "what did I decide about X", "check my notes", "예전에 뭐라고 정했더라", "내 노트에서 찾아줘", "connect my Obsidian vault", "link my notes folder", "볼트 연결", "rebuild the memory index", "kordis memory recall", "무슨 근거로 그렇게 정한 거였지". NOT for finding packages other people published — that is kordis-search.
4
+ type: skill
5
+ allowed-tools: ['Bash(kordis:*)']
6
+ ---
7
+
8
+ # Kordis: Recall Over the User's Own Notes
9
+
10
+ ## Status — experimental, off unless the user turned it on
11
+
12
+ Every command here exists only when `KORDIS_EXPERIMENTAL_MEMORY=1` is set in
13
+ the environment. With the flag unset the whole surface is absent from
14
+ `kordis --help`, and `kordis memory recall …` fails as an unknown command.
15
+
16
+ That absence is deliberate: appearing in help is a promise, and this surface
17
+ is not finished. If the commands are missing, **say the feature is off and
18
+ stop** — do not set the variable on the user's behalf, and do not fall back to
19
+ reading their notes folder yourself with Read/Grep. The point of the feature
20
+ is that a human decides which files feed it; reaching around it defeats
21
+ exactly the thing it exists to do.
22
+
23
+ ## Goal
24
+
25
+ Answer a question from the user's own notes — decisions, specs, meeting
26
+ records — without the notes being copied, moved, uploaded, or rewritten. Kordis
27
+ builds a derived index OUTSIDE the folder (`~/.kordis/memgineering/<hash>/`)
28
+ and treats the notes as the only source of truth. Deleting the index costs a
29
+ rebuild and nothing else.
30
+
31
+ This is the opposite direction from the rest of Kordis: `kordis search` finds
32
+ what OTHER people published; this reads what the USER already wrote.
33
+
34
+ ## The consent rule — read before running anything
35
+
36
+ `kordis memory link` is the one moment a human decides which of their files
37
+ feed the index. Therefore:
38
+
39
+ 1. **Never run `link` unless the user asked for their notes connected in this
40
+ conversation.** Not implied by "help me remember", not implied by an earlier
41
+ session.
42
+ 2. **Run `--dry-run` first, always.** It builds nothing and prints the exact
43
+ sentences that would be stored. Show that to the user before proceeding.
44
+ 3. **Do not pass `--yes` on the user's behalf.** It skips the confirmation
45
+ prompt. Use it only when the user has already seen the `--dry-run` output
46
+ and told you to go ahead.
47
+
48
+ `link` refuses files whose CONTENT looks like a credential, and reports them
49
+ by path. When it does, relay the refusal — do not try to get the file indexed
50
+ some other way.
51
+
52
+ ## Commands
53
+
54
+ ### `kordis memory link <path> [--dry-run] [--yes] [--json]`
55
+
56
+ Audit a notes folder's exclusion rules, then index it.
57
+
58
+ | flag | meaning |
59
+ | ----------- | ----------------------------------------------------------- |
60
+ | `--dry-run` | print what WOULD be stored, then stop. Nothing is written. |
61
+ | `--yes` | skip the confirmation prompt (only after the user approved) |
62
+
63
+ ### `kordis memory reindex [--vault <path>] [--json]`
64
+
65
+ Discard the derived index and rebuild it from the notes. Use after the user
66
+ changes their exclusion rules (`.memgdeny` / `.memgallow`), or when they want
67
+ to see the audit summary again.
68
+
69
+ ### `kordis memory recall <query> [--vault <path>] [--scope <s>] [--limit <n>] [--json]`
70
+
71
+ Recall memory cards for a question. `--limit` defaults to 8.
72
+
73
+ Re-validates the index before answering, every time — a note edited a second
74
+ ago is reflected. Unchanged files are confirmed by revision rather than
75
+ re-read, so this is cheap enough to call whenever a question sounds like it
76
+ has a prior answer.
77
+
78
+ ### `kordis memory card <handle> [--vault <path>] [--json]`
79
+
80
+ Expand one `detail_handle` from `recall` into its claims and provenance. Pass
81
+ the handle back verbatim — it looks like `recall://launch-date`.
82
+
83
+ ### `kordis memory propose --input <json> [--vault <path>] [--json]`
84
+
85
+ Draft a change to a note. **Writes nothing.** See "Changing a note" below.
86
+
87
+ `--input` takes a file path, `-` for stdin, or inline JSON with two keys:
88
+ `candidate` (what to search with) and `commit` (the decision).
89
+
90
+ ### `kordis memory proposals [--state <state>] [--vault <path>] [--json]`
91
+
92
+ List proposals waiting for the user's decision. Reading the list is also what
93
+ applies expiry, since nothing runs in the background.
94
+
95
+ ### `kordis memory approve <id> [--yes] [--vault <path>] [--json]`
96
+
97
+ Show the diff and, if the user agrees, write it. **This is the only command in
98
+ the product that changes a user's note.**
99
+
100
+ ### `kordis memory reject <id> [--vault <path>] [--json]`
101
+
102
+ Decline a proposal. The record is kept, not deleted.
103
+
104
+ ### `kordis memory retire <id> --reason <text> [--yes] [--vault <path>]`
105
+
106
+ Mark a memory as no longer current. `<id>` is what a recall card prints after
107
+ `recall://`. Writes ONE line into the note's `memgineering:` block —
108
+ `status: retired` — and touches nothing else.
109
+
110
+ **Retiring is not deleting, and the difference is the thing to relay.** The
111
+ note keeps its prose and its conclusion, and recall keeps returning the card,
112
+ ranked last and marked `[retired]`. A user who says "delete it" and then sees
113
+ it in the next recall will think the command failed. Say which one they are
114
+ getting.
115
+
116
+ `kordis memory unretire <id> --reason <text>` reverses it.
117
+
118
+ ### `kordis memory exclude <path> [--yes] [--vault <path>]`
119
+
120
+ Stop reading a note entirely. This is the one that makes a memory **disappear**
121
+ from recall. It adds one line to the vault's `.memgignore` and does not change
122
+ the note — not a byte.
123
+
124
+ Takes a vault-relative file path, and refuses one that matches no file rather
125
+ than writing a rule that silently protects nothing. To exclude a whole folder,
126
+ the user edits `.memgignore` themselves.
127
+
128
+ `kordis memory unexclude <path>` removes the line and the note is read again.
129
+
130
+ ### `kordis memory sync-rules [--yes] [--vault <path>]`
131
+
132
+ Copies this vault's `.memgdeny` and `.memgignore` into `memgineering.md`, a
133
+ visible file at the top of the notes folder. Deletes neither — both sources are
134
+ merged.
135
+
136
+ **Why it exists**: hidden files do not sync between machines. Obsidian Sync
137
+ states it outright, and most vault sync behaves the same way. So the same
138
+ folder opened on a second machine has all the notes and none of the
139
+ protections, and nothing reports it: with no deny file there are no entries to
140
+ report as unmatched.
141
+
142
+ If `link` prints the "protect this machine only" warning, relay it and offer
143
+ this command. Do not run it unprompted — it writes a file into their notes
144
+ folder.
145
+
146
+ ### What travels between machines, and what does not
147
+
148
+ | Travels (it is in the vault) | Does not (it is machine-local) |
149
+ | ------------------------------------ | ------------------------------------------------------- |
150
+ | the notes themselves | the derived index — rebuild with `link` on each machine |
151
+ | `status: retired` from `retire` | **pending proposals** |
152
+ | `memgineering.md` after `sync-rules` | `.memgdeny` / `.memgignore` (hidden) |
153
+
154
+ **A proposal is approved on the machine that drafted it.** Not seeing it
155
+ elsewhere is not a bug and not something to work around — say so rather than
156
+ re-drafting, which would leave two proposals for one change.
157
+
158
+ ### Which one does the user mean by "delete"?
159
+
160
+ Ask, or offer both — they do different things and neither is undoable by the
161
+ other:
162
+
163
+ | They want | Command | Result |
164
+ | -------------------------------- | --------- | ------------------------------------------- |
165
+ | "that conclusion is out of date" | `retire` | still recalled, ranked last, labelled |
166
+ | "stop reading that note" | `exclude` | gone from recall; the file is untouched |
167
+ | the file itself gone | _neither_ | that is their editor's job, not this tool's |
168
+
169
+ Never run `exclude` when they asked to correct a conclusion — it hides the
170
+ whole note, including everything else in it.
171
+
172
+ ### `kordis memory unlink [--purge] [--vault <path>] [--json]`
173
+
174
+ Forget a vault: removes the registration, the derived index and pending
175
+ proposals, and redacts finished ones. `--purge` deletes those records too.
176
+
177
+ It does NOT remove changes already approved into the user's notes — those are
178
+ their files. The output says so; relay it rather than implying a clean slate.
179
+
180
+ ### Which vault
181
+
182
+ With exactly one linked folder, `--vault` is optional. With two or more,
183
+ omitting it is an ERROR rather than a guess, because answering from the wrong
184
+ folder is indistinguishable from answering wrongly. The error names the linked
185
+ paths; pick with the user, don't guess.
186
+
187
+ ## Two tiers — cards first, detail only when needed
188
+
189
+ `recall` returns CARDS: `id`, `title`, `summary`, `confidence`, `freshness`,
190
+ `detail_handle`. It deliberately withholds the note bodies. That is what makes
191
+ it affordable to call on an ordinary turn.
192
+
193
+ Do NOT call `card` on every handle you got back. Read the summaries, decide
194
+ which one actually answers the question, and expand only that one.
195
+
196
+ `confidence` is `stated` when the note declared a claim in frontmatter and
197
+ `inferred` when Kordis derived the card from the note's prose. Say which you
198
+ are relying on when it matters — "your note says" is a different claim from
199
+ "your note seems to say".
200
+
201
+ ## Contradictions are surfaced, never resolved
202
+
203
+ When two notes disagree, `recall` returns both AND a `conflicts` entry naming
204
+ them. It does not pick a winner, and neither should you.
205
+
206
+ Show both sides with their `freshness` (`current` / `superseded`) and let the
207
+ user decide. Silently preferring the newer note is how a memory system starts
208
+ confidently reciting something its owner already changed their mind about.
209
+
210
+ ## What gets refused, and how the user overrides it
211
+
212
+ Three separate nets, in increasing strength:
213
+
214
+ | net | what it catches | override |
215
+ | ----------- | -------------------------------------- | ------------------------------ |
216
+ | name | filenames containing sensitive words | `.memgallow` |
217
+ | content | file bodies that look like credentials | `.memgallow` (exact path only) |
218
+ | `.memgdeny` | paths the user listed explicitly | none — edit the file |
219
+
220
+ `.memgdeny` takes plain relative paths, one per line, **no glob syntax**. A
221
+ line that matches no file protects nothing, so `link` blocks on an unmatched
222
+ entry and suggests where that filename actually lives — relay the suggestion
223
+ rather than telling the user to delete the line.
224
+
225
+ ## Steps
226
+
227
+ 1. Confirm the flag is on. `kordis memory recall --help` failing as an unknown
228
+ command means the feature is off — report that and stop.
229
+ 2. If nothing is linked yet, the error says so. Ask the user whether to connect
230
+ a folder; if yes, run `link --dry-run <path>` and show them the result.
231
+ 3. Run `kordis memory recall "<the user's question>" --json`.
232
+ 4. Read the cards. If `conflicts` is non-empty, present both sides.
233
+ 5. Expand at most the one or two handles that actually answer the question with
234
+ `kordis memory card <handle> --json`.
235
+ 6. Answer in the user's language, citing `provenance.sourcePath` so they can
236
+ open the note themselves.
237
+ 7. Zero cards is a clean signal. Say nothing matched and suggest broader words
238
+ or dropping `--scope`. Never fill the gap from your own memory of earlier
239
+ turns and present it as their note.
240
+
241
+ ## Examples
242
+
243
+ ### Recall with a contradiction present
244
+
245
+ ```
246
+ $ kordis memory recall "when is the launch"
247
+ Launch date
248
+ Moved the launch to 15 April after the supplier slipped a week.
249
+ stated · recall://launch-date
250
+ Pricing
251
+ Settled on 4,500 KRW for the americano; cost is about 900.
252
+ inferred · recall://decisions/pricing.md
253
+ Launch date (first plan) [superseded]
254
+ Agreed 20 March with the team.
255
+ stated · recall://launch-date-march
256
+
257
+ ⚠ 1 contradiction(s):
258
+ launch-date (active) ↔ launch-date-march (superseded)
259
+ Both sides are shown above — neither was hidden.
260
+ ```
261
+
262
+ The right answer here names 15 April AND mentions that an earlier note said
263
+ 20 March and is marked superseded.
264
+
265
+ ### The same call as an agent should make it
266
+
267
+ ```
268
+ $ kordis memory recall "when is the launch" --json
269
+ {"memory_cards":[{"id":"launch-date","title":"Launch date","summary":"Moved the launch to 15 April after the supplier slipped a week.","confidence":"stated","freshness":"current","detail_handle":"recall://launch-date"}, …],"related":[],"conflicts":[{"between":["launch-date","launch-date-march"],"status":["active","superseded"]}]}
270
+ ```
271
+
272
+ ### Dry-run before connecting anything
273
+
274
+ ```
275
+ $ kordis memory link ~/notes --dry-run
276
+
277
+ Nothing was indexed. This is what WOULD be stored:
278
+
279
+ Launch date
280
+ Moved the launch to 15 April after the supplier slipped a week.
281
+ from decisions/launch-date.md
282
+
283
+ 3 note(s). Run without --dry-run to index them.
284
+ ```
285
+
286
+ ### A refusal, reported rather than worked around
287
+
288
+ ```
289
+ $ kordis memory link ~/notes --yes
290
+ ✓ indexed 3 note(s)
291
+ index: ~/.kordis/memgineering/c04f8eedc9723bcc
292
+ 1 note(s) refused — their CONTENT looks like a credential:
293
+ private/server access.md (labelled_credential)
294
+ Redact the file, or list it in .memgdeny to stop being asked.
295
+ ```
296
+
297
+ ## Changing a note
298
+
299
+ A change to a user's note is a PROPOSAL until a human approves it. You draft;
300
+ they decide. This is not a workflow preference — the notes are theirs, and
301
+ they are the only one who can say whether their file should change.
302
+
303
+ ### The rules, in order of how badly they go wrong
304
+
305
+ 1. **Never run `approve --yes` unless the user said yes in this conversation.**
306
+ Not implied by "keep my notes up to date", not carried over from an earlier
307
+ session, not implied by them approving a different proposal. `--yes` skips
308
+ the prompt, and the CLI cannot tell your keystroke from theirs — this rule
309
+ is the only thing standing there.
310
+ 2. **Show the diff before asking.** Run `approve` without `--yes` so the user
311
+ sees the exact lines, or print the `before`/`after` from `propose --json`.
312
+ "I'll update your note" is not a description of a diff.
313
+ 3. **Do not re-approve after a conflict without showing the new diff.** If
314
+ `approve` reports the note changed, their earlier yes applied to a version
315
+ that no longer exists.
316
+ 4. **Propose freely; it writes nothing.** Drafting is cheap and reversible, so
317
+ when a conversation settles something the notes contradict, propose. Just
318
+ never confuse having drafted with having changed.
319
+
320
+ ### Drafting one
321
+
322
+ ```json
323
+ {
324
+ "candidate": {
325
+ "subject": "launch date",
326
+ "scope": "decisions",
327
+ "candidate_claim": "The launch moved to 15 April."
328
+ },
329
+ "commit": {
330
+ "action": "supersede",
331
+ "memory_id": "launch-date",
332
+ "claim": {
333
+ "text": "The launch is on 15 April.",
334
+ "summary": "The launch is on 15 April.",
335
+ "confidence": "stated"
336
+ },
337
+ "reason": "The supplier slipped a week.",
338
+ "source_excerpt": "Agreed 20 March with the team.",
339
+ "target_path": "decisions/launch-date.md"
340
+ }
341
+ }
342
+ ```
343
+
344
+ `action` is one of:
345
+
346
+ | action | when |
347
+ | ----------- | ------------------------------------------------------- |
348
+ | `create` | nothing on this subject exists yet |
349
+ | `reinforce` | same conclusion, more support |
350
+ | `supersede` | the conclusion changed — the old one is over |
351
+ | `conflict` | two things disagree and you are NOT deciding which wins |
352
+ | `ignore` | not worth remembering; nothing is stored |
353
+
354
+ Notes on the fields, each of which is enforced:
355
+
356
+ - **`summary` is required for `create` and `supersede`.** Recall searches and
357
+ displays the summary, never the claim text, so a change without one is
358
+ applied and then invisible.
359
+ - **`source_excerpt` is required.** It is what the person approving reads to
360
+ judge whether you understood their note. Quote them, don't paraphrase.
361
+ - **`memory_id` must come from what `propose` found.** You cannot change a
362
+ memory the search never surfaced.
363
+ - **You do not name the version being changed.** It comes from what the search
364
+ read, so there is no field for it and nothing to invent — a write based on a
365
+ version nobody read is exactly what this prevents.
366
+
367
+ ### What a patch may touch
368
+
369
+ Only the `memgineering:` block in the note's frontmatter, and within it only
370
+ the conclusion (`claim`, `summary`, `contradicts`, `valid_from`,
371
+ `updated_at`). The prose body is never edited, and the author's other
372
+ frontmatter keys — including their comments — are carried through.
373
+
374
+ Two things about the block are normalised rather than preserved byte for byte,
375
+ and it is better to say so than to promise more than holds: a comment's
376
+ alignment padding collapses to one space, and a block written as a one-line
377
+ flow mapping is re-emitted as indented keys. Both keep every key and value.
378
+
379
+ If a user asks you to rewrite the body of a note, do it with your normal file
380
+ tools and say so plainly — that is a different act from recording a memory,
381
+ and it does not go through this approval flow.
382
+
383
+ ## How a note states a claim
384
+
385
+ Cards are `inferred` from prose unless the note declares one. Everything Kordis
386
+ reads lives under a `memgineering:` key, so a note's own top-level `type:` or
387
+ `status:` is never mistaken for an instruction to this tool:
388
+
389
+ ```yaml
390
+ ---
391
+ memgineering:
392
+ id: launch-date
393
+ claim: The launch is on 15 April.
394
+ contradicts: [launch-date-march]
395
+ ---
396
+ ```
397
+
398
+ `contradicts` is what makes two notes surface as a conflict.
399
+
400
+ ## Over MCP
401
+
402
+ The same surface is available as MCP tools when the Kordis MCP server runs
403
+ over **stdio** with the experimental flag set — `memory_recall`, `memory_card`
404
+ and `memory_propose`. Tool names and arguments mirror the commands above.
405
+
406
+ Two things are deliberately different there:
407
+
408
+ - **There is no approve tool.** MCP has no interactive prompt, so one could
409
+ only behave like `--yes`. The user opens a terminal to decide; the tool
410
+ result tells them the exact command.
411
+ - **The hosted endpoint never has these tools.** A hosted MCP server is one
412
+ process serving many people, and the notes it could reach belong to whoever
413
+ runs it. If you are talking to Kordis over a remote MCP endpoint and no
414
+ memory tools appear, that is correct and not a misconfiguration.
415
+
416
+ ## Limitations
417
+
418
+ - **Writes only through an approval.** `approve` is the one command that
419
+ changes a note, and only after a human answers the prompt. Everything else
420
+ here reads. See "Changing a note" for what you may and may not do.
421
+ - **Titles, aliases and summaries are searched** — not full note bodies. A
422
+ question whose words appear only deep inside a note will not match.
423
+ - **The content scanner is shape-based.** It catches labelled credentials
424
+ (`password: …`, `비번은 …`) and known key formats, but a passphrase written
425
+ as ordinary prose reads as ordinary prose. It is a safety net, not a
426
+ guarantee — never tell the user their secrets are certainly excluded.
427
+ - **Personal information generally is not a credential** and no net catches it.
428
+ Diagnoses, salaries, relationships in a note WILL be summarised into the
429
+ index. That is what the `--dry-run` screen is for.
430
+ - Local folders only. No hosted, Git, or Notion backend yet.
@@ -2,12 +2,13 @@
2
2
  name: kordis-recommend
3
3
  description: Use whenever the user wants ideas — phrases like "any new stuff?", "recommend", "추천", "what should I try", "anything fresh?", "discover", "show me good skills", "what's popular for <stack>". Surfaces a personalized menu of Kordis packages the user may want to install or peek at. Read eagerly when the user signals discovery intent without a specific slug in mind.
4
4
  type: skill
5
- allowed-tools: ["Bash(kordis:*)"]
5
+ allowed-tools: ['Bash(kordis:*)']
6
6
  ---
7
7
 
8
8
  # Kordis: Personalized Recommendations
9
9
 
10
10
  ## Goal
11
+
11
12
  Surface a ranked, agent-friendly menu of packages the user may want next.
12
13
  Algorithm is hybrid: content-based (your local stack) + item-CF (other
13
14
  users with similar installs) + popularity decay (stars + recency).
@@ -28,14 +29,18 @@ the user names a slug (use `kordis info` / `kordis read`).
28
29
 
29
30
  ## Command
30
31
 
31
- ### `kordis recommend [--limit N] [--verbose] [--no-context] [--json]`
32
+ ### `kordis recommend [--limit N] [--no-context] [--json]`
32
33
 
33
- | flag | meaning |
34
- | -------------- | ---------------------------------------------------------------- |
35
- | `--limit <n>` | max items (default 10, max 50) |
36
- | `--verbose` | include `score_breakdown` per item (content / cf / popularity) |
34
+ | flag | meaning |
35
+ | -------------- | ------------------------------------------------------------------- |
36
+ | `--limit <n>` | max items (default 10, max 50) |
37
37
  | `--no-context` | skip the `X-Kordis-Context` header for this call (one-shot opt-out) |
38
- | `--json` | global flag — JSON envelope, no human render |
38
+ | `--json` | global flag — JSON envelope, no human render |
39
+
40
+ `--verbose` still parses but does nothing. It used to print each item's
41
+ score components; the ranking blend is server-private, so there is no
42
+ per-item score to surface. Rank order is the signal — do not tell the user
43
+ a package's numeric score, because you no longer receive one.
39
44
 
40
45
  Anonymous: works without login. Personalized signal rides on an opt-in
41
46
  `X-Kordis-Context` HTTP header (installed agent tools, top 3 CWD file
@@ -56,6 +61,7 @@ The user is asked ONCE. Three paths persist the answer to
56
61
  Use this when the user has confirmed consent in chat — see Step 2.5.
57
62
 
58
63
  Override at runtime (does not change the persisted answer):
64
+
59
65
  - `KORDIS_NO_CONTEXT=1` env — no header for the entire shell session
60
66
  - `--no-context` flag — no header for this single call
61
67
 
@@ -85,27 +91,31 @@ Surface this AND ask the user before flipping the flag (Step 2.5).
85
91
 
86
92
  `confidence` interpretation when surfacing to the user:
87
93
 
88
- | value | how to surface |
89
- | -------- | ------------------------------------------------------------------------- |
90
- | `high` | "You should try X" — both content match and co-install signal align |
91
- | `medium` | "You might like X" — one signal strong |
92
- | `low` | "Worth a look — popular right now" — cold-start / no personalization |
94
+ | value | how to surface |
95
+ | -------- | -------------------------------------------------------------------- |
96
+ | `high` | "You should try X" — both content match and co-install signal align |
97
+ | `medium` | "You might like X" — one signal strong |
98
+ | `low` | "Worth a look — popular right now" — cold-start / no personalization |
93
99
 
94
100
  ## Steps
95
101
 
96
102
  ### 1. Fetch menu
103
+
97
104
  `kordis recommend --limit 10 --json`. (Add `--no-context` ONLY if the user
98
105
  explicitly asked you not to share their stack.)
99
106
 
100
107
  ### 2. Triage
108
+
101
109
  Filter mentally by `confidence`. If everything is `low` and `personalized`
102
110
  is `false`, follow Step 2.5 to offer the user a one-click consent path.
103
111
 
104
- If the response carries `capped_by_author > 0`, ALSO surface a one-line
105
- note to the user — e.g. "N more candidates from the same publisher were
106
- skipped by the diversity cap" — so they understand why the list is
107
- shorter than the requested `--limit`. Without this hint users frequently
108
- mistake a short menu for a sparse or broken registry.
112
+ If the menu is shorter than the requested `--limit`, that now means the
113
+ registry genuinely has fewer candidates — compare against `total_candidates`
114
+ and say so plainly rather than implying the tool is broken. The per-author
115
+ diversity cap no longer shortens the menu: it reorders, pushing a publisher's
116
+ surplus down instead of discarding it, so `capped_by_author` is always `0`.
117
+ (It used to drop them, which capped every menu at 3 items regardless of
118
+ `--limit`.)
109
119
 
110
120
  ### 2.5. Offer personalization consent (when `personalized: false`)
111
121
 
@@ -132,13 +142,16 @@ optionally via `kordis setup --personalization off` so the question
132
142
  isn't asked again.
133
143
 
134
144
  ### 3. Present
145
+
135
146
  Surface 3-5 picks max. For each, show: full_slug, one-line `why_recommended`,
136
147
  confidence wording per the table above, and the next-step
137
148
  `install_command` / `jit_read_command`. Do NOT dump the whole menu — agent
138
149
  should curate.
139
150
 
140
151
  ### 4. Act
152
+
141
153
  If the user picks one:
154
+
142
155
  - "install it" → run `install_command` from the menu (already a complete
143
156
  shell line)
144
157
  - "peek first" → run `jit_read_command`, summarize, then offer install
@@ -70,8 +70,9 @@ array.
70
70
  ### 3. Triage the results
71
71
 
72
72
  - Prefer `review_status: passed`, higher `stars`, then `tier: A`.
73
- - For trending results, the `trending_score` field is populated — surface it
74
- when comparing.
73
+ - For trending results, **rank order is the signal** — the server returns no
74
+ numeric score, so never quote or compare one. Say "3rd on the 7-day
75
+ trending board", not a number.
75
76
  - IMPORTANT: if results are 0, RELAX one filter at a time before giving up.
76
77
  **Drop `--tool` first** — many published packages have an empty
77
78
  `tools_compatible` field, so `--tool <name>` undercounts hard. Then drop
@@ -125,7 +126,7 @@ User: "any Cursor-compatible package for code review?"
125
126
  <example>
126
127
  User: "what's trending on Kordis this week?"
127
128
  - kordis search --trending --window 7d --json
128
- - show top 5–10 with their `trending_score`; do NOT fetch bodies unless asked
129
+ - show top 5–10 in the order returned; do NOT fetch bodies unless asked
129
130
  </example>
130
131
 
131
132
  <example>
@@ -66,13 +66,30 @@ response shape is the same whether the call newly starred or no-op'd; if the
66
66
  user wants to verify a NEW star landed, compare `stars` from `kordis info
67
67
  <full_slug> --json` before and after.
68
68
 
69
+ **You cannot star a package the user authored.** The server refuses it with
70
+ `not_authorized` (exit 11) — a star is a reader's endorsement and feeds
71
+ ranking, so authors starring their own work would just be voting for
72
+ themselves. This is a rule, not a glitch: do not retry, do not try another
73
+ route, and do not present it as a failure the user should work around. Say
74
+ what it is in one line and move on.
75
+
76
+ <example>
77
+ User: "star my rss-reader package"
78
+ - kordis star rayul/rss-reader --json -> exit 11, not_authorized
79
+ - "본인이 올린 패키지엔 별점을 누를 수 없어요 — 별점은 읽은 사람의 추천이라
80
+ 순위에 들어가서요. 슬러그를 공유하시면 다른 분이 누를 수 있습니다."
81
+ - do NOT retry, do NOT suggest a second account
82
+ </example>
83
+
69
84
  ### `kordis unstar <full_slug> [--json]`
70
85
 
71
86
  Remove your star. Idempotent — silent no-op when not previously starred.
72
87
  Auth required.
73
88
 
74
- Stars feed `--sort stars` ordering on `kordis search` and contribute to the
75
- `trending_score`.
89
+ Stars feed `--sort stars` ordering on `kordis search` and the trending board.
90
+ Only stars from someone **other than the author** count toward ranking. The
91
+ numeric trending score is server-private and is no longer returned — rank
92
+ order is the signal, so never quote a package's score to the user.
76
93
 
77
94
  ## Steps
78
95
 
@@ -130,6 +147,8 @@ User: "report that the v0.3.0 release of foo/bar is broken on macOS"
130
147
  User: "star foo/bar"
131
148
  - kordis star foo/bar --json
132
149
  - show new star count
150
+ - if it returns `not_authorized` (exit 11), the user authored it — say so
151
+ in one line, do not retry
133
152
  </example>
134
153
 
135
154
  <example>
@@ -255,6 +255,12 @@ starring needs a one-click browser login; if they decline, drop it. NEVER run
255
255
  initiative. Caps: at most ONE offer per session, never twice for the same
256
256
  package, and a "no" suppresses all further offers that session.
257
257
 
258
+ Do NOT offer for a package the USER themselves authored — the server refuses
259
+ it with `not_authorized` (exit 11), because a star is a reader's endorsement
260
+ and feeds ranking. Check the owner segment of the slug against `kordis
261
+ whoami` before offering. If you offer anyway and it fails, say so once and
262
+ drop it; do not retry.
263
+
258
264
  <example>
259
265
  User: "thanks, that fixed the parsing!"
260
266
  - reply ends: "following alice/pdf-extract@1.2.0 — want me to star it on Kordis?"