@rayul/kordis 0.15.1 → 0.16.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.
package/NOTICE CHANGED
@@ -31,4 +31,4 @@ package and is not licensed with it.
31
31
  where no keychain is available.
32
32
  * `KORDIS_API_URL` points the CLI at a different host.
33
33
  * Error telemetry is off unless turned on, carries no identity, and is
34
- described in the repository's CLAUDE.md.
34
+ described in the repository's `docs/security.md`, under "Error telemetry".
@@ -0,0 +1,108 @@
1
+ # packages/skills — the agent guidance both CLIs ship
2
+
3
+ Markdown that an agent on a user's machine reads and acts on. It is not a pnpm
4
+ package (no `package.json`, no scripts): the two CLIs copy it into their npm tarballs
5
+ at build time, and their `setup` commands install it into the user's agent tools.
6
+ These files are the product's user documentation; a reader who gets a wrong command
7
+ from them runs it in front of a user.
8
+
9
+ ## Scope
10
+
11
+ - `MEMGINEERING.md` — the memgineering hub. `memgineering setup` strips its
12
+ frontmatter and injects the body between `<!-- memgineering-start -->` and
13
+ `<!-- memgineering-end -->` into `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` and
14
+ `~/.gemini/GEMINI.md` (and writes it as its own file under `~/.grok/rules/`). It is
15
+ in context for every session of every one of those tools.
16
+ - `memgineering-*` — seven sub-skills (`memory` is the map from question to verb;
17
+ `recall`, `writing`, `rules`, `setup`, `registry`, `installing` carry the detail),
18
+ installed into each tool's `skills/` directory and loaded by description match.
19
+ - `KORDIS.md` — the hub of the older `kordis` CLI, injected between
20
+ `<!-- kordis-start -->` and `<!-- kordis-end -->` by `kordis setup`.
21
+ - `kordis-*` — six sub-skills. `kordis setup` installs five; `kordis-memory` stays
22
+ in the repo and is drift-checked but is not installed, because its commands are
23
+ registered only under `KORDIS_EXPERIMENTAL_MEMORY=1`.
24
+
25
+ ## Not in scope
26
+
27
+ - The behaviour these files describe and the code that installs them:
28
+ `apps/memgineering` (`src/brand.ts`, `src/commands/setup.ts`) and `apps/cli`
29
+ (`src/brand.ts`, `src/commands/setup.ts`). Change the code there, the words here,
30
+ in the same change.
31
+ - `apps/memgineering/assets/` and `apps/cli/assets/`: gitignored build output that
32
+ every build deletes and recopies. Never edit them.
33
+ - Contributor documentation; these files are written for users' agents.
34
+
35
+ ## How the files ship
36
+
37
+ - `pnpm -F memgineering build` copies `MEMGINEERING.md` and every `memgineering-*`
38
+ directory into `apps/memgineering/assets/` (`apps/memgineering/scripts/copy-skills.mjs`).
39
+ `pnpm -F @rayul/kordis build` copies this whole directory into `apps/cli/assets/`.
40
+ - In both CLIs the hub file's presence in `assets/` is how the program tells a
41
+ published install from a source checkout; without it, `setup` reads this
42
+ directory directly.
43
+ - A user's installed copy changes only when they upgrade the CLI and `setup` runs
44
+ again. A fix here does not reach anyone until a release.
45
+
46
+ ## Invariants
47
+
48
+ - Every command, subcommand and flag a file names exists in the CLI that ships it.
49
+ `memgineering <verb> [<subcommand>] --flag` is checked against the program
50
+ `apps/memgineering` builds; `kordis <verb> --flag` against `kordis <verb> --help`.
51
+ Every backticked `snake_case` identifier in a kordis file must exist as a key or
52
+ string literal in the source of `packages/contracts`, `packages/harness-schema`,
53
+ `packages/memory-engine`, `apps/mcp` or `apps/cli`.
54
+ - memgineering files spell the registry as `memgineering registry <verb>`. The flat
55
+ top-level aliases still work and are never taught.
56
+ - The set of `memgineering-*` directories equals `BRAND.subSkills` in
57
+ `apps/memgineering/src/brand.ts`, in both directions, and the hub names every one.
58
+ A new sub-skill is a directory, a `BRAND.subSkills` entry and a hub mention.
59
+ - Directory names are permanent. `setup` has no delete path, so a renamed sub-skill
60
+ leaves the old one on users' machines teaching the old surface.
61
+ - The `MEMGINEERING.md` body (after frontmatter) stays under 5,000 characters with no
62
+ fenced code blocks. It sits two characters under the cap, so any addition moves
63
+ text into a sub-skill; phrases already moved into `memgineering-writing` are pinned
64
+ by the drift test and may not disappear.
65
+ - `MEMGINEERING.md`'s `version:` frontmatter equals `apps/memgineering/package.json`
66
+ `version`; bumping the CLI means bumping this line. `KORDIS.md` carries its own
67
+ `version:` (0.2.0) that nothing ties to a package.
68
+ - memgineering files are English only (no Hangul anywhere, triggers included), and
69
+ no shipped file contains an absolute home path (`/Users/...`, `/home/...`) or a
70
+ real person's name or conversation. The kordis files' descriptions carry Korean
71
+ trigger phrases on purpose.
72
+ - The hosted connector URL in `memgineering-setup/SKILL.md` and in the hub equals the
73
+ one documented in `apps/mcp/README.md` and `HOSTED_CONNECTOR_URL` in the setup
74
+ command.
75
+ - The hub and `memgineering-writing` both keep the rule that a stored fact is one
76
+ that was checked, not worked out.
77
+ - A behaviour change in either CLI updates the matching file here in the same
78
+ change, and that release's changelog gets an `### Agent guidance` subsection: root
79
+ `CHANGELOG.md` (both language halves) for `KORDIS.md` and `kordis-*`,
80
+ `apps/memgineering/CHANGELOG.md` for the memgineering files.
81
+
82
+ ## Writing patterns
83
+
84
+ - The hub routes; sub-skills explain. The hub holds only what must be true before
85
+ any skill loads, because it is paid for in every session.
86
+ - A sub-skill keeps its frontmatter (`name`, `description`, `type`,
87
+ `allowed-tools`). The description says when to load it, lists trigger phrases and
88
+ says what it is not for — tools pick skills by that text alone.
89
+ - When guidance has to override what a model assumes by default, state the negation
90
+ outright ("It does NOT wait for them") rather than only the fact.
91
+ - Commands are written for the agent to run on the user's behalf, not as
92
+ instructions for the user to type; the reader may be someone who has never opened
93
+ a terminal.
94
+
95
+ ## Tests
96
+
97
+ There is no test runner here; the suites that check these files live in the CLIs.
98
+
99
+ - `pnpm -F memgineering test`, or `test:unit` (what CI's unit job runs) — both build
100
+ first, which recopies these files into `assets/`; `src/skill-drift.test.ts` covers
101
+ the memgineering files. Vitest run without that build checks whatever `assets/` an
102
+ earlier build left, not these files.
103
+ - `pnpm -F @rayul/kordis test` — `src/__tests__/skill-doc-drift.test.ts` spawns
104
+ `kordis <verb> --help` through tsx for every verb the kordis files mention; it is
105
+ slow by design and has a two-minute per-test ceiling.
106
+ - Read `Test Files N passed` for each package. A drift failure names the file and
107
+ the command or flag that does not exist; fix the guidance or the CLI, never the
108
+ test's verb list alone.
@@ -2,7 +2,7 @@
2
2
  name: memgineering
3
3
  description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
4
4
  type: skill
5
- version: 0.21.0
5
+ version: 0.27.0
6
6
  ---
7
7
 
8
8
  # memgineering
@@ -83,6 +83,8 @@ at six skills, each of which loads on its own when its topic comes up:
83
83
  - `memgineering-registry` — the public layer
84
84
  - `memgineering-installing` — putting a package in their project, and updating it
85
85
 
86
+ Not shell-only: ChatGPT and Claude.ai reach a hosted brain via connector `https://mcp.memgineering.com/mcp`.
87
+
86
88
  A host short on room lists a skill by NAME ONLY, no description — measured on
87
89
  Claude Code. When you cannot see what it covers, READ
88
90
  `<tool-home>/skills/memgineering-<name>/SKILL.md` before deciding it is empty.
@@ -20,19 +20,46 @@ Publish a valid, AI-review-passing version. ALWAYS dry-run first.
20
20
  - `license` — SPDX (e.g. `MIT`, `Apache-2.0`, `CC-BY-4.0`).
21
21
  - Optional: `homepage`, `tags`, `type`.
22
22
 
23
- **`type` auto-inference (0.6.4+):** The CLI infers `type: skill` when
24
- you push a directory bundle, a `.skill` ZIP, OR a single file named
25
- `SKILL.md` or `*.skill.md`. You don't need to write `type:` in the
26
- frontmatter for any of these paths. Use `--type <t>` (or write the
27
- field manually) only when pushing a different package type from a
28
- single `.md` file that doesn't match the SKILL.md filename pattern.
23
+ **`type` and `--type`:** A package is published as the type its
24
+ frontmatter's `type:` says, and nothing else. When the frontmatter has
25
+ no `type:` line, the CLI writes one in: `type: skill` for a directory
26
+ bundle, a `.skill` ZIP, or a single file named `SKILL.md` or
27
+ `*.skill.md` — so none of these needs `type:` written — or `type: <t>`
28
+ when you pass `--type <t>`, on any path. When the frontmatter names a
29
+ type, `--type` must say the same: a different value is refused before
30
+ anything is sent (`usage_error`, exit 2, in `--dry-run` too). Change the
31
+ frontmatter's `type:`, or drop `--type`.
32
+
33
+ Nothing is written over a `type:` line that is already there, or into a
34
+ file that begins with a byte-order mark. These fail instead, in
35
+ `--dry-run` too; write `type: <t>` in by hand:
36
+
37
+ - `type:` with no value (or `null`, a number, a list), and a file with a
38
+ byte-order mark and no `type:` line: `missing_required_frontmatter`
39
+ (exit 30), with or without `--type`.
40
+ - `type: ""`, an empty string: without `--type` it fails as
41
+ `unknown_type` (exit 30); with `--type` it disagrees, so it is refused
42
+ before anything is sent (`usage_error`, exit 2).
43
+
44
+ **Older CLIs (0.15.2 and earlier)** send `--type` only beside the
45
+ content and never write it in. They fill in `type: skill` themselves
46
+ when the frontmatter names no type: for a folder or `.skill` bundle
47
+ from 0.1.12, and for a single `SKILL.md` or `*.skill.md` from 0.6.4.
48
+ The registry refuses a `--type` that differs from the type it reads —
49
+ written or filled in — with `usage_error` (exit 2) and a hint naming
50
+ that type. From 0.2.2 their `--dry-run` checks the file locally and
51
+ sends no publish request (from 0.9.1, `--bump minor` or `major` makes
52
+ it look the package up), so it does not show this refusal; only a real
53
+ push does. 0.2.1 and earlier send the dry run to the registry, which
54
+ refuses it the same way as a real push. Writing `type:` in the
55
+ frontmatter works on every version.
29
56
 
30
57
  **`version` field handling:**
31
58
 
32
59
  - **First publish** — used as-is (else defaults to `0.1.0`).
33
60
  - **Update** — the registry computes the next version from
34
61
  `current_version + --bump <strategy>` (default `patch`). If the file's
35
- `version` differs from that result, the API rejects with `usage`. Either
62
+ `version` differs from that result, the API rejects with `usage_error`. Either
36
63
  drop the `version` field on updates, set `--bump` to match, or align
37
64
  them.
38
65
  - RECOMMENDED: **omit `version:` from frontmatter on update pushes** and
@@ -70,8 +97,11 @@ get rejected. Dry-run first like any push.
70
97
  ## Recording lineage (0.12.0+)
71
98
 
72
99
  When a package descends from another (especially a judgment you adapted), record
73
- the provenance on `push` so it shows up in the "Derived from / Adapted by" graph,
74
- feeds the `community-adapted` trust tier, and gives a small trending boost:
100
+ the provenance on `push`. It shows up in the "Derived from / Adapted by" graph,
101
+ and when the derivative is another author's — public, with its current version
102
+ passed review — it also feeds the source's `community-adapted` trust tier and
103
+ gives the source a small trending boost. A derivative of a package you authored
104
+ yourself does neither. Record it with one of these flags:
75
105
 
76
106
  | flag | edge kind | meaning |
77
107
  | ----------------------- | ------------ | ----------------------------------------------- |
@@ -80,10 +110,13 @@ feeds the `community-adapted` trust tier, and gives a small trending boost:
80
110
  | `--absorbs <slug>` | `absorption` | folds the source in as one part |
81
111
  | `--remixes <slug>` | `remix` | recombines multiple sources |
82
112
 
83
- Unresolved sources (typo'd / unpublished slug) are **skipped fail-open** — the
84
- publish still succeeds and the response's `lineage.skipped` lists them so you
85
- can fix the slug or publish the source first. Declare lineage honestly; it is
86
- the registry's trust signal, not decoration.
113
+ A source that does not resolve — a typo'd slug, or a package that is
114
+ unpublished, deleted or private — **refuses the whole publish** with
115
+ `usage_error` (exit 2), and nothing is written. `--dry-run` does not look
116
+ sources up, so a typo first shows on the real push. Fix the slug, or publish
117
+ the source first, then push again. (Naming your own package as its source is
118
+ skipped instead, and listed in the response's `lineage.skipped`.) Declare
119
+ lineage honestly; it is the registry's trust signal, not decoration.
87
120
 
88
121
  ## Yanking a bad version (0.10.0+)
89
122
 
@@ -160,7 +193,7 @@ operator restores it.
160
193
  | `--dry-run` | validate only; do not publish |
161
194
  | `--bump <strategy>` | `patch` (default) / `minor` / `major` — only on update |
162
195
  | `--slug <slug>` | override slug on first publish |
163
- | `--type <type>` | override type inference |
196
+ | `--type <type>` | fills a missing `type:`; must match an existing one |
164
197
  | `--changelog <text>` | optional changelog entry attached to this version |
165
198
  | `--no-wait` | return immediately without waiting for AI review |
166
199
  | `--adapted-from <slug>` / `--forked-from` / `--absorbs` / `--remixes` | record lineage (see "Recording lineage") |
@@ -171,33 +204,66 @@ operator restores it.
171
204
 
172
205
  ### 1. Dry-run
173
206
 
174
- `kordis push <file> --dry-run --json`. Returns the same validation, conflict,
175
- and version-resolution verdicts a real push would produce.
176
-
177
- CAVEAT: dry-run does NOT exercise the AI reviewer — it short-circuits with
178
- a synthetic `review.result: "passed"` (`prompt_version: "stub-v0"`). Treat
179
- dry-run as syntactic + structural validation only. A real push routes
180
- through the AI review worker, which can land terminal `passed` /
181
- `rejected`, OR leave the publish in `status: "pending"` if the worker
182
- hasn't decided within the wait window.
183
-
184
- **Success criteria**: dry-run returns a `status: "published"`-shaped envelope
185
- with no error flags.
186
-
187
- ### 2. Resolve flags
188
-
189
- Possible verdicts:
190
-
191
- - `validation_failed` (exit 30) — fix every listed flag, re-dry-run.
207
+ `kordis push <file> --dry-run --json` runs on this machine (0.2.2+): it
208
+ sends no publish request and needs no sign-in. It validates the content
209
+ the real push would send — the frontmatter, including any `type:` the CLI
210
+ writes in, the body, and bundled files' paths and sizes — and answers
211
+ `{status, type, tier, capabilities, asset_count, flags, warnings, note}`
212
+ with `status` `dry_run_passed` (exit 0) or `dry_run_failed` (exit 30).
213
+ A pass can still list `flags` that do not block. `warnings` come from a
214
+ local scan for content that looks private — `ipv4_or_internal_host`,
215
+ `credential_like_string`, `absolute_home_path`,
216
+ `first_person_context_hint` — and never block.
217
+
218
+ It decides nothing the registry decides: no name conflict, no version (a
219
+ frontmatter `version` is not checked against the bump), no lineage
220
+ source, no declared-capability check, no rate limit, and no AI review.
221
+ Apart from finishing a pending `kordis login --emit-only`, as every
222
+ command does, its one registry call is a package lookup for
223
+ `--bump minor` or `--bump major` (0.9.1+), made when it can tell which
224
+ package: the frontmatter has a `name`, and you are signed in or `--slug`
225
+ names `<user>/<slug>`. A bump on a package that does not exist yet is
226
+ refused — `usage_error`, exit 2, from 0.9.2 (0.9.1 exits 1) — and if the
227
+ lookup fails any other way, the check is skipped. Only a real push gets
228
+ the registry's verdicts, including the AI review, which can land
229
+ `passed` / `rejected`, or leave the publish `status: "pending"` if the
230
+ worker hasn't decided within the wait window.
231
+
232
+ **Success criteria**: `status: "dry_run_passed"`.
233
+
234
+ ### 2. Resolve what comes back
235
+
236
+ From the dry run:
237
+
238
+ - `dry_run_failed` (exit 30) — fix every listed flag, dry-run again.
239
+ - `usage_error` (exit 2) — a `--type` the frontmatter contradicts, or
240
+ `--bump minor` / `major` on a first publish; the message says which.
241
+ - `generic_failure` (exit 1) with a YAML hint — the frontmatter does not
242
+ parse; fix it as the hint says.
243
+ - `generic_failure` (exit 1) — the path is not a package the CLI can read:
244
+ it does not exist, a folder has no SKILL.md at its top, or a `.skill`
245
+ archive is broken or has no SKILL.md; the message says which.
246
+
247
+ Only from the real push (step 3), however clean the dry run was:
248
+
249
+ - `validation_failed` (exit 30) — the registry's checks: the dry run's,
250
+ plus ones it does not make, such as a `hook` or `mcp-server` that
251
+ declares `executable: false`; fix every listed flag.
252
+ - `usage_error` (exit 2) — for example a lineage source that does not
253
+ resolve, or a frontmatter `version` the bump does not produce; the
254
+ message says which. Nothing is written.
192
255
  - `conflict` (exit 40) — slug taken; suggest a different slug (or pass
193
256
  `--slug` if first publish).
194
- - `ai_review_rejected` (exit 32) — EXPLAIN the rejection to the user. Do NOT
195
- silently retry. Includes deterministic Stage-1 hits like
196
- `curl_pipe_shell`, `aws_access_key`, `private_key_block`, etc.
257
+ - Review rejection — not an error: the push exits 0 and returns
258
+ `status: "rejected"` with the reasons in `review.flags`
259
+ (`ai_review_rejected`, exit 32, is defined but never sent). EXPLAIN the
260
+ rejection to the user. Do NOT silently retry. Includes deterministic
261
+ Stage-1 hits like `curl_pipe_shell`, `aws_access_key`,
262
+ `private_key_block`, etc.
197
263
  - `rate_limited` (exit 31) — wait `retry_after_seconds`.
198
264
  - `not_authenticated` (exit 10) — `kordis login`, then retry.
199
265
 
200
- **Success criteria**: dry-run is clean.
266
+ **Success criteria**: the dry run passed.
201
267
 
202
268
  ### 3. Real push
203
269
 
@@ -212,9 +278,10 @@ yet — re-check with `kordis info <full_slug> --json` (or wait and re-run
212
278
  ## Versioning your push (0.9.0+)
213
279
 
214
280
  - **First publish** derives the version from the frontmatter `version:`
215
- field if present, else defaults to `0.1.0`. Passing `--bump` on a
216
- first publish now returns a clear `usage_error` (pre-0.9 silently
217
- ignored it) — drop `--bump` or set the frontmatter version explicitly.
281
+ field if present, else defaults to `0.1.0`. Passing `--bump minor` or
282
+ `--bump major` on a first publish now returns a clear `usage_error`
283
+ (pre-0.9 silently ignored it); `--bump patch`, the default, is ignored
284
+ there — drop `--bump` or set the frontmatter version explicitly.
218
285
  - **Subsequent pushes** default to `--bump patch`. Explicit
219
286
  `--bump minor` or `--bump major` ships the matching jump.
220
287
  - **Frontmatter `version:` on an update** must match the result of
@@ -242,19 +309,21 @@ silently retry — explain why.
242
309
  User (push-back after adapting a judgment): "that decision rule we adapted from
243
310
  <upstream/slug> worked well — share our version back"
244
311
  1. author the refitted judgment as a judgment-pattern file (structured
245
- frontmatter; see "Publishing a judgment-pattern")
312
+ frontmatter with `type: judgment-pattern`, which the `--type` below
313
+ must match; see "Publishing a judgment-pattern")
246
314
  2. kordis push ./judgment.md --type judgment-pattern --adapted-from <upstream/slug> --dry-run --json
247
315
  3. clean → drop --dry-run for the real push
248
- 4. the response's `lineage.recorded` confirms the edge; if `lineage.skipped`
249
- lists the source, the upstream slug was wrong or unpublished — fix it, or
250
- publish the source first, then re-push. The edge gives the upstream
316
+ 4. the response's `lineage.recorded` confirms the edge. A wrong or unpublished
317
+ upstream slug is refused here, by the real push, with `usage_error` and
318
+ nothing written — the dry run in step 2 does not look sources up. Fix the
319
+ slug, or publish the source first. The edge gives the upstream
251
320
  `community-adapted` credit and shows in its "Adapted by" graph.
252
321
  </example>
253
322
 
254
323
  <example>
255
324
  User: "publish my new skill"
256
325
  1. kordis push ./SKILL.md --dry-run --json
257
- 2. validation_failed → list each flag, fix the file, re-run
326
+ 2. dry_run_failed → list each flag, fix the file, re-run
258
327
  3. clean → kordis push ./SKILL.md --json
259
328
  4. show {full_slug, version, url}
260
329
  </example>
@@ -264,7 +333,8 @@ User: "release v0.2.0 of <full_slug>"
264
333
  - previous version was 0.1.x
265
334
  1. drop or align the frontmatter `version` field — `--bump minor` from
266
335
  0.1.x will yield 0.2.0; if the file says `0.2.0` that matches and is OK,
267
- if it says anything else (e.g. `0.1.0`) the API will reject
336
+ if it says anything else (e.g. `0.1.0`) the real push is refused (the
337
+ dry run does not compare versions)
268
338
  2. kordis push <file> --bump minor --dry-run --json
269
339
  3. clean → kordis push <file> --bump minor --json
270
340
  </example>
@@ -46,7 +46,8 @@ Anonymous: works without login. Personalized signal rides on an opt-in
46
46
  `X-Kordis-Context` HTTP header (installed agent tools, top 3 CWD file
47
47
  extensions, framework hints from package.json/pyproject.toml/Cargo.toml,
48
48
  SHA-256-truncated hashes of installed slugs). Server uses it for in-flight
49
- re-ranking ONLY — never persisted (see CLAUDE.md "Telemetry / privacy").
49
+ re-ranking ONLY — never persisted (see `docs/security.md` in the Kordis
50
+ repository, "Search, context and addresses").
50
51
 
51
52
  ## Personalization opt-in
52
53
 
@@ -51,6 +51,23 @@ Post a top-level comment on a package. Auth required.
51
51
  - `question` — explicit question expecting a reply (typically from the
52
52
  author). Use when the user wants a response, not just to share thought.
53
53
 
54
+ **What the server refuses.** A comment's title and body get the same content
55
+ screen a published package gets: a download piped into a shell, an encoded
56
+ `eval`, API keys, tokens and private keys, and acceptable-use violations
57
+ (markets for stolen data, malware or attacks for hire, drug and weapon
58
+ brokering, abuse material, targeted harassment). A hit refuses the whole
59
+ comment with `usage_error` (exit 2) and nothing is posted, so there is nothing
60
+ to delete. The message names the field that matched (`comment title rejected …`
61
+ or `comment body rejected …`). Rewrite that field as the hint says and run the
62
+ command again; never resend the same text.
63
+
64
+ - **Reports: describe, do not quote.** A `report` that pastes the offending
65
+ line of a dangerous package (a `curl … | bash`, a key) is refused by the
66
+ same screen. Say what the step does and where it is instead.
67
+ - **Marker lines come back `[redacted]`.** A title or body line that imitates
68
+ the registry's untrusted-content markers is stored as `[redacted]`. Show the
69
+ user the `title` the response returns, not the one you sent.
70
+
54
71
  Replies, edits, status flips (open/resolved on a `report`), listing
55
72
  existing comments, and reading back your own comments are NOT yet exposed
56
73
  in the CLI. Direct the user to `https://kordis.store/<full_slug>` for
@@ -140,6 +157,8 @@ User: "leave a tip on foo/bar saying it pairs well with baz/qux"
140
157
  User: "report that the v0.3.0 release of foo/bar is broken on macOS"
141
158
  - kordis comment foo/bar --kind report --title "v0.3.0 broken on macOS" \
142
159
  --body "<reproducible details>" --json
160
+ - if it is refused (`usage_error`, exit 2), describe the problem without
161
+ quoting the offending text and post again
143
162
  - mention that opening/resolving the report later requires the web UI
144
163
  </example>
145
164
 
@@ -56,7 +56,13 @@ Returns the full body (Markdown) of a text-bearing package, plus an
56
56
  `attribution` string. `--pin <semver>` overrides any `@suffix` on the slug.
57
57
  **Context economy**: `--outline` returns just the section map (heading names);
58
58
  then `--section <name>` returns one section's body. Read the outline first
59
- when a body is large and you only need part of it.
59
+ when a body is large and you only need part of it. A section name the package
60
+ does not have exits 2 (`usage_error`): under `--json` the refusal is the error
61
+ envelope on stdout, and its `hint` lists the section names that do exist (or,
62
+ for a package with none, says to read the whole body); without `--json` it goes
63
+ to stderr only, so a body redirected into a file never captures it. Either way,
64
+ the `kordis read` command it prints keeps the version, `--pin` and
65
+ `--allow-pending` you read with, so it reaches the same body.
60
66
 
61
67
  **Untrusted-content fence (all output modes, incl. `--json`)**: the body (and
62
68
  outline description / section slice) arrives wrapped between exactly two lines
@@ -153,6 +153,11 @@ What you must not do is report what came back as everything the brain holds,
153
153
  which is exactly what a short answer looks like to anyone who does not read the
154
154
  line under it.
155
155
 
156
+ When every card with a project came from ONE project and the brain holds
157
+ others, recall says so (`scope_spread`: the project, and up to five others). If
158
+ the user meant one of those, ask again with `--scope <that project>` — the note
159
+ they want may be ranked just below a page full of the one the question named.
160
+
156
161
  ## Whether meaning was searched
157
162
 
158
163
  A hosted brain can match on meaning as well as words — the same idea in other
@@ -177,28 +182,91 @@ anything about what the user knows.
177
182
  If coverage says notes are missing entries, meaning was searched over part of
178
183
  the brain. Do not report that as a complete search.
179
184
 
185
+ When meaning was used, each card also says what reached it — `matched`:
186
+ `"words"`, `"meaning"`, or `"both"`. A `"meaning"` card (printed as _matched by
187
+ meaning only_) shares no word with the question. That is right for a note in
188
+ another language and wrong for one the model merely found nearby, so read its
189
+ summary before you answer from it — never quote it back on the label alone.
190
+
180
191
  ## Two answers that are answers
181
192
 
182
193
  **Nothing matched.** Say so, and do not go read their folder yourself to
183
- compensate. At the default depth this has already searched every note body of
184
- **the brain it searched**, so "nothing" is real information about that brain
185
- rather than a hint to search harder — read `semantic` first, per above. Broader
186
- words are still worth one retry; `--depth exact` is not — it looks at strictly
187
- less.
194
+ compensate. At the default depth this has already searched the body of every
195
+ note recall could read in **the brain it searched**, so "nothing" is real
196
+ information about those notes rather than a hint to search harder — read
197
+ `semantic` first, per above. Broader words are still worth one retry;
198
+ `--depth exact` is not — it looks at strictly less.
199
+
200
+ In a brain in a folder, that is not a promise about every file. Notes it left
201
+ out are counted below, but recall never looks inside a folder the user's own
202
+ rules exclude (`.gitignore`, `.memgignore`, the rules file): notes in one are
203
+ neither searched nor counted, and an answer where other notes were searched does
204
+ not mention them. So an empty answer is not proof that the user never wrote
205
+ something down. Say what was searched, and that folders their rules exclude
206
+ were not, rather than telling them they never wrote it. Do not open such a
207
+ folder yourself.
188
208
 
189
209
  One brain is not the account. On a hosted account with more than one brain, an
190
- empty answer now names the brains that were NOT read and prints a runnable
191
- `--brain` command for each. Run it before you tell them the fact was never
192
- written down — four testers reached that conclusion about a note sitting in the
193
- next brain, and three of them said it out loud. Reading another brain does not
194
- move the machine's pointer.
210
+ empty answer — or one of template cards only — names the brains that were NOT
211
+ read and prints a runnable `--brain` command for each. Run it before you tell
212
+ them the fact was never written down — four testers reached that conclusion
213
+ about a note sitting in the next brain, and three of them said it out loud.
214
+ Reading another brain does not move the machine's pointer.
215
+
216
+ With `--json`, an answer with nothing the user wrote in it carries `hint`: no
217
+ cards, or only template cards (`scaffolding: true`, below) — those are what
218
+ `init` wrote, not the user's notes. `hint` is the one next step, the most
219
+ specific that applies. First, the brain had nothing to search: it has no notes
220
+ yet (the `remember` command that writes the first one into that brain — or,
221
+ after `--brain` named a brain this machine does not point at, the
222
+ `memgineering use --brain "<name>"` that makes it the target, since `remember`
223
+ has no `--brain`), or it is a folder whose every note was left out (how many,
224
+ why, and the `memgineering reindex` command that names each one). Then: its
225
+ notes are in a script the question is not (ask again in that language), another
226
+ brain was not read (the `--brain` or `--vault` command that reads it), meaning
227
+ was not searched (and the server's reason), the hosted server's own advice, or
228
+ fewer and broader words. Read it before you conclude anything from
229
+ `cards: []` or from template cards. A `memgineering use --brain` in a hint
230
+ moves this machine's pointer for every later command, not just the next one:
231
+ offer it to the user and run it only on their yes. `hint` is absent whenever a
232
+ card they wrote came back.
233
+
234
+ **Held-back notes are counted in both modes.** When an answer has nothing the
235
+ user wrote in it, recall counts what a brain in a folder left out, says why,
236
+ and prints the `memgineering reindex` command that names each one: in the
237
+ terminal right under `_Nothing matched._` or under the template cards' header,
238
+ and at the front of `hint`. It opens with "Nothing in this brain could be
239
+ searched: …" when no note could be searched at all, and with "Not every note in
240
+ this brain was searched: …" when other notes were searched and at least one
241
+ note of the first kind below was left out. Recall itself never names or shows a
242
+ left-out note. The counts come in two kinds, and they mean different things:
243
+
244
+ - **Notes**: excluded by the user's rules (`.gitignore`, `.memgignore`, the
245
+ rules file), listed in `.memgdeny`, held back because the name looks private,
246
+ refused because the content looks like a credential, or behind a symlink. A
247
+ note the user wrote exists and was left out on purpose. Do NOT tell them the
248
+ brain is empty or that they never wrote it down: tell them such a note exists,
249
+ and why; to say where it is, run that `reindex` — it lists each path and
250
+ changes no note — and give them the path. Do not open, print or read a
251
+ held-back note into the conversation unless they ask what it says: it was
252
+ held back because it may be private.
253
+ - **Places nobody looked inside**: an excluded folder ("not looked inside"), or
254
+ another symlink, which is never followed. These are not evidence of a note —
255
+ an ignored `images/` or `.venv/` holds none. Say that recall could not look
256
+ inside them and that `reindex` names them; do not say a note was held back.
257
+ When "Nothing in this brain could be searched" lists only these, the brain may
258
+ hold no notes at all. Beside notes that were searched they are not counted at
259
+ all — see **Nothing matched** above.
195
260
 
196
261
  **Cards that are not answers.** A card marked `not filled in yet` is one of the
197
- five `01_BASE/` files, still byte-for-byte what `init` wrote. Nobody has answered
262
+ five `01_BASE/` files, still exactly what `init` wrote (line endings aside). Nobody has answered
198
263
  it, so it is evidence of nothing — do not quote it back as what they think. When
199
264
  every card carries that mark, recall says so above them (`--json`:
200
265
  `scaffolding: true`), and that is the moment to offer to fill them in rather
201
- than answer from them — see `memgineering-writing`.
266
+ than answer from them — see `memgineering-writing`. At most two such cards come
267
+ back per answer, plus any that is one side of a conflict, so both sides of a
268
+ conflict always show; the rest are counted (`templates_withheld`), never
269
+ silently dropped, and nothing anyone wrote is ever held back.
202
270
 
203
271
  The mark disappears once the user writes in the file. A filled-in base note is
204
272
  often the best answer in the brain: _who am I_, _what am I not allowed to do_,
@@ -219,8 +287,11 @@ On a hosted brain the folder counts too, as a name rather than a path: the CLI
219
287
  sends the folder's project — `owner/repo` from the git remote, or whatever
220
288
  `memgineering project` shows — and the server ranks what was asked about in
221
289
  that project first. The response says so itself: `ranked_by` gains `asked about
222
- in this project`, a card's reason reads `recalled 2× in this project`, and the
223
- footer names which project those words mean. Trust the footer over your
290
+ in this project`, a card's reason reads `opened 1× · surfaced 2× in this project`
291
+ (or `surfaced 2× …, never opened`), and the footer names which project those
292
+ words mean. Only an open, or being the FIRST card of a recall, counts toward
293
+ the order; a card merely listed further down is counted in words but earns
294
+ nothing. Trust the footer over your
224
295
  assumption in the other cases as well — it says when this folder names no
225
296
  project (nothing was sent), and when the server does not rank by project yet
226
297
  (it was sent and ignored). A path never leaves the machine either way.