rcf-lite 0.28.0 → 0.28.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.28.1] - 2026-09-17
10
+
11
+ Feedback defects surfaced by the first WSD-lens round-trip on 0.28.0. Fixes both a destination bug that silently misrouted library-owned findings to `Stravica/rcf-lite`, and a redactor over-fold that mangled the docs URL and any prose using the canonical noun `agent`. Includes a package-rename disclosure banner in the README so a colleague hitting `npm i -g @stravica-ai/rcf-build-lite` on the retired scoped name knows what to do.
12
+
13
+ ### Fixed
14
+
15
+ - **Feedback destination for bare-slug library-owned blueprints (AC-15801-4).** A `--kind blueprint` entry whose manifest record has no `libraryPrefix` now cross-checks the blueprint-library registry BEFORE the shelf-constant fall-through: if exactly one registered library owns a blueprint with the applied slug AND that library resolves to a destination (`issuesRepo`, or a derivable `git+https://github.com/...` sourceRef), the resolver routes to that library's destination with `source: 'library-slug-inference'`. Two or more owners return `unresolved` with `reason: 'ambiguous-library-slug'` and a `candidates[]` array of library prefixes. Zero owners keep the shelf-constant fall-through unchanged. This never overrides an existing `libraryPrefix` resolution (AC-15801-2's path is untouched). Before 0.28.1, a WSD colleague who ran `rcf define blueprint add application-spa` from a project with a registered WSD library that also owned `application-spa` saw their feedback routed to `Stravica/rcf-lite` instead of the WSD blueprints repo the library declared. Chain: US-15801 AC-15801-4; design amendment R5a; TS-189 gains three test cases; TAC-4106 responsibility extended.
16
+
17
+ - **Operator-identity redactor no longer folds the canonical noun `agent` (AC-15601-5).** The operator-identity substitution now consults a `RESERVED_IDENTITY_TOKENS` stopword set (agent, user, admin, api, code, system, and the rest of the guarded common nouns) before folding each whitespace-separated token of the identity `## Name`. Before 0.28.1, an identity such as `The Agent Alice` caused the redactor to fold the literal `agent` throughout the body: the docs URL `how-the-agent-files-feedback` became `how-the-<operator>-files-feedback` (no longer resolvable), the canon phrase `when the agent stops` mangled, and every RULE 17 example was affected. Real given names (Alice, Smith) still fold; only common-noun tokens are guarded. When the identity string is ENTIRELY stopwords (a placeholder in disguise), the full-literal substitution is also skipped so the docs and canon phrases survive. Chain: US-15601 AC-15601-5; design amendment R5c; TS-188 gains one test case; TAC-4103 responsibility extended.
18
+
19
+ - **`rcf feedback add --evidence` help text now names its requiredness explicitly.** Header line `'rcf feedback add' options (all required unless noted)` was preserved and truthful, but `--evidence <pointer>` did not itemise its required-and-repeatable behaviour, so a reader could take the line-item shape alone as "optional." Help text now reads `Required, repeatable: a command, a path:line inside the project, or an id. At least one --evidence is required (AC-15501-1); absence exits 2.` No behaviour change; AC-15501-1 already required at least one `--evidence`.
20
+
21
+ ### Changed
22
+
23
+ - **Package rename disclosure surfaced in the README and CHANGELOG.** The scoped name `@stravica-ai/rcf-build-lite` was frozen at 0.7.0 and the suite consolidated into the unscoped `rcf-lite` at 0.7.1; the 0.28.0 line ships ONLY on `rcf-lite`. The README already carried the rename note under "Where this came from" and pointed at the packaging runbook, but there was no in-CLI hint: a colleague running `npm i -g @stravica-ai/rcf-build-lite@0.28.0` saw a bare "no matching version" from npm with no rcf-lite context. 0.28.1 adds a callout box at the top of the README naming the rename explicitly and the CHANGELOG carries this note so a search for the retired name lands somewhere useful. The npm-side deprecate notice on the retired scoped name is a Dave operator step (needs npm auth) tracked separately from this release. No behaviour change.
24
+
9
25
  ## [0.28.0] - 2026-09-17
10
26
 
11
27
  rcf-lite 0.28.0 lands two referee-facing trains together. The `rcf feedback` verb family lets an agent record what a blueprint or rcf-lite did that contradicts its own docs, fails, or forces a workaround, redact it to the eight rules of the design plus a whole-body residual scan, and, after one operator yes at a natural pause, file each finding as a GitHub issue under the operator's own `gh` login (or a pastable outbox bundle on the fallback path). The referee-guarantees train tightens the three sentences the docs page makes about `rcf define validate`, `rcf audit coverage` and `rcf build finalise`: validate now walks ADR `supersededBy`, coverage is strict-by-default with `--mode shallow-any` as the deliberate opt-out, and finalise refuses on the three per-AC verdicts (UI-BASELINE-UNMET, BROWSER-VERIFICATION-MISSING, SCOPE-MISMATCH), gates the manual `verified` override behind `--acknowledge-verified-override`, and adds a merge-state precondition with `--allow-pre-merge` as the deliberate pre-merge dry-run opt-out. `rcf init` now merges three hook entries into `.claude/settings.json` and `.codex/hooks.json` so the feedback ask fires once per session at idle; RULE 17 lands in the managed instructions block as the belt-and-braces path. Coverage-strict-by-default is the single migration surface: downstream projects that ran plain `rcf audit coverage` and treated its exit code as passing will see the same tree exit 4; the fix is either to close the coverage gap or to run `rcf audit coverage --mode shallow-any` when the intent is a printed gap report.
package/README.md CHANGED
@@ -7,6 +7,15 @@
7
7
 
8
8
  Build software with an AI coding agent without losing the plot.
9
9
 
10
+ > **Package name note (0.7.1+).** This tool ships as `rcf-lite` on npm. The retired scoped name `@stravica-ai/rcf-build-lite` was frozen at 0.7.0 in August 2026 and every release from 0.7.1 onwards lives here. If `npm install -g @stravica-ai/rcf-build-lite@<version>` returns "no matching version" for any version above 0.7.0, uninstall the scoped name and install `rcf-lite` at the same version instead:
11
+ >
12
+ > ```sh
13
+ > npm uninstall -g @stravica-ai/rcf-build-lite
14
+ > npm install -g rcf-lite
15
+ > ```
16
+ >
17
+ > The `rcf` and `rcf-verify` bin names are unchanged.
18
+
10
19
  Anyone who has shipped with a coding agent knows the failure mode: the code arrives fast, but what the product is supposed to do lives in prompt history, and prompt history is not a spec. Three weeks later nobody can say what is covered, what is tested, or what breaks when something changes.
11
20
 
12
21
  RCF Lite keeps those answers machine-checkable. It maintains a live chain from what you asked for, through requirements, user stories, acceptance criteria and tests, into the code itself, as plain JSON files in your own repository. Your agent works the chain instead of improvising; `rcf define validate` catches drift the moment it happens; and when your app is deployed, `rcf verify` sends an independent verifier at it before anything gets called done.
@@ -2,8 +2,8 @@
2
2
  "registerCanary": [
3
3
  {
4
4
  "id": "rc-2026-09-17-001",
5
- "createdAt": "2026-09-17T10:17:51.151Z",
6
- "buildVersion": "0.28.0-mockdriver",
5
+ "createdAt": "2026-09-17T14:42:00.660Z",
6
+ "buildVersion": "0.28.1-mockdriver",
7
7
  "fixturePromptId": "canary-prompt-01",
8
8
  "responseWordCount": 55,
9
9
  "grades": {
@@ -35,8 +35,8 @@
35
35
  },
36
36
  {
37
37
  "id": "rc-2026-09-17-002",
38
- "createdAt": "2026-09-17T10:17:51.152Z",
39
- "buildVersion": "0.28.0-mockdriver",
38
+ "createdAt": "2026-09-17T14:42:00.661Z",
39
+ "buildVersion": "0.28.1-mockdriver",
40
40
  "fixturePromptId": "canary-prompt-02",
41
41
  "responseWordCount": 55,
42
42
  "grades": {
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "id": "rc-2026-09-17-003",
71
- "createdAt": "2026-09-17T10:17:51.152Z",
72
- "buildVersion": "0.28.0-mockdriver",
71
+ "createdAt": "2026-09-17T14:42:00.661Z",
72
+ "buildVersion": "0.28.1-mockdriver",
73
73
  "fixturePromptId": "canary-prompt-03",
74
74
  "responseWordCount": 55,
75
75
  "grades": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rcf-lite",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "type": "module",
5
5
  "description": "One-install tooling for the Requirements Confidence Framework (RCF): the unified `rcf` CLI grouped into the five RCF tool groups (discover, define, build, verify, audit) plus a small core set (init, doctor, guidance, mcp), an MCP server, the live tree viewer and the fresh-context adversarial ship-gate verifier. Consumes @stravica-ai/rcf-schemas.",
6
6
  "license": "Apache-2.0",
@@ -17,6 +17,7 @@
17
17
  "AC-15601-4",
18
18
  "AC-15701-2",
19
19
  "AC-15701-3",
20
+ "AC-15801-4",
20
21
  "AC-15901-1",
21
22
  "AC-15901-2",
22
23
  "AC-15902-1",
@@ -41,5 +42,5 @@
41
42
  "version": "0.1.0",
42
43
  "status": "draft",
43
44
  "createdAt": "2026-09-16T14:58:28Z",
44
- "updatedAt": "2026-09-17T03:30:00Z"
45
+ "updatedAt": "2026-09-17T14:14:49Z"
45
46
  }
@@ -5,11 +5,12 @@
5
5
  "AC-15601-1",
6
6
  "AC-15601-2",
7
7
  "AC-15601-3",
8
- "AC-15601-4"
8
+ "AC-15601-4",
9
+ "AC-15601-5"
9
10
  ],
10
11
  "dependencies": [],
11
12
  "version": "0.1.0",
12
13
  "status": "draft",
13
14
  "createdAt": "2026-09-16T15:27:35Z",
14
- "updatedAt": "2026-09-16T20:28:13Z"
15
+ "updatedAt": "2026-09-17T14:14:49Z"
15
16
  }
@@ -4,6 +4,7 @@
4
4
  "implementsAcIds": [
5
5
  "AC-15801-2",
6
6
  "AC-15801-3",
7
+ "AC-15801-4",
7
8
  "AC-15802-1",
8
9
  "AC-15802-2"
9
10
  ],
@@ -13,5 +14,5 @@
13
14
  "version": "0.1.0",
14
15
  "status": "draft",
15
16
  "createdAt": "2026-09-16T16:20:01Z",
16
- "updatedAt": "2026-09-16T16:20:01Z"
17
+ "updatedAt": "2026-09-17T14:14:49Z"
17
18
  }
@@ -12,6 +12,7 @@
12
12
  "AC-15601-2",
13
13
  "AC-15601-3",
14
14
  "AC-15601-4",
15
+ "AC-15601-5",
15
16
  "AC-15701-1"
16
17
  ],
17
18
  "dependsOnFbsIds": [
@@ -41,5 +42,5 @@
41
42
  "estimatedHours": 16,
42
43
  "notes": "Fingerprint decision confined to this slice via ADR-4102; the labels half of that ADR pays off in slice 4.",
43
44
  "createdAt": "2026-09-16T13:45:23.000Z",
44
- "updatedAt": "2026-09-16T13:45:23.000Z"
45
+ "updatedAt": "2026-09-17T14:14:56Z"
45
46
  }
@@ -11,6 +11,7 @@
11
11
  "AC-15801-1",
12
12
  "AC-15801-2",
13
13
  "AC-15801-3",
14
+ "AC-15801-4",
14
15
  "AC-15802-1",
15
16
  "AC-15802-2"
16
17
  ],
@@ -38,5 +39,5 @@
38
39
  "estimatedHours": 8,
39
40
  "notes": "Extends the library manifest surface; downstream library owners can adopt at their own cadence.",
40
41
  "createdAt": "2026-09-16T13:45:23.000Z",
41
- "updatedAt": "2026-09-16T16:20:01Z"
42
+ "updatedAt": "2026-09-17T14:14:56Z"
42
43
  }
@@ -10,7 +10,8 @@
10
10
  "Apply the eight redaction rules from design section 5 to title, body and evidence strings",
11
11
  "Return a ledger of { rule, before, after, count } entries for the operator disclosure",
12
12
  "Refuse on residual-secret matches (second pass) with a line pointer so the offending body is not sent",
13
- "Ship a bundled allowlist of vendor hosts and honour the extension array on rcf/feedback-settings.json"
13
+ "Ship a bundled allowlist of vendor hosts and honour the extension array on rcf/feedback-settings.json",
14
+ "Guard operator-identity substitution with a RESERVED_IDENTITY_TOKENS stopword list (agent, user, admin, api, code, system, etc.) so a common noun in the operator's identity name never folds; substitution still fires for real given names in the identity"
14
15
  ],
15
16
  "internalStructure": "src/feedback/redact.js is a pure module: redact(text, context) -> { text, ledger[] } plus small per-rule helpers. src/feedback/redact-allowlist.json ships the vendor host set; the extension array is merged at call time.",
16
17
  "interfaces": [
@@ -35,5 +36,5 @@
35
36
  "tradeoffs": "Rule-based redaction is testable with a fixture corpus and deterministic under review; the cost is that it will occasionally over-redact (a benign name that matches an entropy rule), which is what the disclosure ledger exists to catch.",
36
37
  "notes": "The stored entry is never mutated by redaction; the rendered output is what leaves.",
37
38
  "createdAt": "2026-09-16T13:45:23.000Z",
38
- "updatedAt": "2026-09-16T13:45:23.000Z"
39
+ "updatedAt": "2026-09-17T13:47:09Z"
39
40
  }
@@ -10,7 +10,8 @@
10
10
  "Walk entry.target -> manifest.blueprints[] -> registry entry -> issuesRepo, with sourceRef derivation as fallback",
11
11
  "Return a { repo, visibility, derived, reason } shape; unresolved carries a reason for the doctor row",
12
12
  "Route --kind core and shelf blueprints to the constant sourced from package.json:bugs.url (a test pins the constant)",
13
- "Report the doctor feedback-destinations warn row for every registered library without a resolvable destination"
13
+ "Report the doctor feedback-destinations warn row for every registered library without a resolvable destination",
14
+ "Cross-check the registry BEFORE the shelf-constant fall-through for --kind blueprint entries with no libraryPrefix on the manifest record: one library owns the slug and resolves -> library destination (source: library-slug-inference); two-or-more libraries own the slug and resolve -> unresolved with reason ambiguous-library-slug and a candidates[] array; zero libraries own the slug -> shelf constant"
14
15
  ],
15
16
  "internalStructure": "src/feedback/destination.js is a pure resolver; the constant lives at the top of the module and is exported for the test to compare against package.json.",
16
17
  "interfaces": [
@@ -35,5 +36,5 @@
35
36
  "tradeoffs": "A declared destination on library.json requires library owners to author one field; the cost is a one-time PR per library, in exchange for correctness on every consuming project.",
36
37
  "notes": "A library mirrored from a fork would have derivation pointing at the fork; the declared issues field is why derivation is only a fallback.",
37
38
  "createdAt": "2026-09-16T13:45:23.000Z",
38
- "updatedAt": "2026-09-16T13:45:23.000Z"
39
+ "updatedAt": "2026-09-17T13:46:24.000Z"
39
40
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "createdAt": "2026-09-16T15:27:35Z",
3
- "updatedAt": "2026-09-17T01:30:00Z",
3
+ "updatedAt": "2026-09-17T13:47:28Z",
4
4
  "purpose": "Cover AC-15601-1 (rcf feedback preview renders each pending entry as the title and body that would be filed, lists the destination, and prints the redaction ledger over the full eight-rule vocabulary), AC-15601-2 (preview marks residual-secret findings so the operator sees before consenting; the submit half of AC-15601-2 lands with slice 4), AC-15601-3 (preview never mutates the stored entries.jsonl), AC-15601-4 (a final whole-text residual-secret pass on the fully rendered and normalised issue body and each bundle refuses submit on any hit) and AC-15701-1 (a stable 12-hex fingerprint is attached per entry, folds across blueprint versions when the anchor+class match, and falls back to normalised-title tokens with a warning when the anchor is absent).",
5
5
  "testLevel": "integration",
6
6
  "acIds": [
@@ -8,7 +8,8 @@
8
8
  "AC-15601-2",
9
9
  "AC-15601-3",
10
10
  "AC-15601-4",
11
- "AC-15701-1"
11
+ "AC-15701-1",
12
+ "AC-15601-5"
12
13
  ],
13
14
  "id": "TS-188",
14
15
  "usId": "US-15601",
@@ -160,6 +161,13 @@
160
161
  "description": "Cover AC-15601-1 (fix round 5, R4 negative case) via the unit test: ordinary prose that carries a vocabulary word (`the password field is required`, `the password is required`, `see commit abc1234 at HEAD`, `keyword=abcdef`, `authors=joe,jane`) does NOT fold because the value-shape gate rejects single-class values and values under 8 characters.",
161
162
  "status": "pending",
162
163
  "testPointer": "test/cli/feedback-fix-round-5.test.js::F-05 (round 5, R4 guard): a vocabulary word in ordinary prose (single-class value, or short value) does NOT fold"
164
+ },
165
+ {
166
+ "id": "TC-188-redact-operator-identity-stopword-guard",
167
+ "acId": "AC-15601-5",
168
+ "description": "Cover AC-15601-5 via the unit test: redact runs over a body containing the docs URL 'https://stravica.ai/docs/rcf/how-the-agent-files-feedback', the prose 'when the agent stops the queue' and the personal name 'Alice reviewed this', with operatorName seeded as 'The Agent Alice'; the docs URL is emitted verbatim; the prose 'when the agent stops the queue' is emitted verbatim; 'Alice' folds to <operator>; no operator-identity ledger row names 'agent' or 'the'.",
169
+ "status": "pending",
170
+ "testPointer": "test/feedback/redact.test.js::AC-15601-5 operator-identity stopword guard: common nouns in the identity string are never folded; real given names still fold"
163
171
  }
164
172
  ],
165
173
  "status": "draft"
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "createdAt": "2026-09-16T16:20:01Z",
3
- "updatedAt": "2026-09-16T16:20:01Z",
3
+ "updatedAt": "2026-09-17T13:46:16Z",
4
4
  "purpose": "Cover AC-15801-1 (library.json:issues object snapshotted onto rcf/blueprint-libraries.json:entries[] as issuesRepo and issuesVisibility on rcf library add; the review-on-add card prints the Feedback destination line naming repo and visibility), AC-15801-2 (destination.resolve walks entry -> manifest -> registry issuesRepo, falls back to sourceRef derivation for github URLs with derived:true, returns unresolved when neither is available; shelf and core route to Stravica/rcf-lite) and AC-15801-3 (rcf doctor --check feedback-destinations lists every registered library with no resolvable destination as a warn-only row with refusedByFix:true; exit stays 0).",
5
5
  "testLevel": "integration",
6
6
  "acIds": [
7
7
  "AC-15801-1",
8
8
  "AC-15801-2",
9
- "AC-15801-3"
9
+ "AC-15801-3",
10
+ "AC-15801-4"
10
11
  ],
11
12
  "id": "TS-189",
12
13
  "usId": "US-15801",
@@ -32,6 +33,27 @@
32
33
  "description": "Cover AC-15801-3 via the CLI test: rcf doctor --check feedback-destinations --json against a project with two registered libraries (one with issues, one without) returns exactly one drift row of item unresolved-library naming the second library, whose message includes the publisher.contact from the registry entry and whose refusedByFix flag is true; the overall doctor exit code stays 0 because the check is warn-only.",
33
34
  "status": "pending",
34
35
  "testPointer": "test/cli/feedback-destinations.test.js::AC-15801-3: rcf doctor --check feedback-destinations warns on a library without a resolvable destination and exits 0"
36
+ },
37
+ {
38
+ "id": "TC-189-resolver-library-slug-inference-single",
39
+ "acId": "AC-15801-4",
40
+ "description": "Cover AC-15801-4 (single-library slug match) via the unit test: a bare-slug manifest record (no libraryPrefix) for 'application-spa' plus exactly one registered library ('wsd') that owns 'application-spa' and declares issuesRepo resolves to that library's declared destination with source 'library-slug-inference', derived false, kind library.",
41
+ "status": "pending",
42
+ "testPointer": "test/feedback/destination.test.js::AC-15801-4 library-slug-inference: bare-slug apply routes to the single library owning the slug when it declares issuesRepo"
43
+ },
44
+ {
45
+ "id": "TC-189-resolver-library-slug-inference-ambiguous",
46
+ "acId": "AC-15801-4",
47
+ "description": "Cover AC-15801-4 (multi-library slug ambiguity) via the unit test: a bare-slug manifest record for 'application-spa' plus two registered libraries ('wsd' and 'ally') that both own 'application-spa' and both declare issuesRepo resolves to unresolved, reason 'ambiguous-library-slug', with candidates ['ally','wsd'] (sorted).",
48
+ "status": "pending",
49
+ "testPointer": "test/feedback/destination.test.js::AC-15801-4 library-slug-inference: two libraries owning the same slug return unresolved with reason ambiguous-library-slug"
50
+ },
51
+ {
52
+ "id": "TC-189-resolver-library-slug-inference-none",
53
+ "acId": "AC-15801-4",
54
+ "description": "Cover AC-15801-4 (no-library fall-through) via the unit test: a bare-slug manifest record for 'security-auth-magic-link' plus registered libraries that do NOT own the slug falls through to the shelf constant unchanged from AC-15801-2.",
55
+ "status": "pending",
56
+ "testPointer": "test/feedback/destination.test.js::AC-15801-4 library-slug-inference: no library owns the slug -> shelf constant fall-through"
35
57
  }
36
58
  ],
37
59
  "status": "draft"
@@ -44,8 +44,17 @@
44
44
  "then": "preview surfaces the residual line and the tainted entry stays pending; submit exits 3 without a create/comment for that entry and without writing a bundle whose text carries the residual shape",
45
45
  "testable": true,
46
46
  "scope": "runtime"
47
+ },
48
+ {
49
+ "id": "AC-15601-5",
50
+ "description": "The operator-identity substitution (design section 5 rule 6) applies word-boundary and stopword guards so a common noun that happens to appear in the operator's identity name is not folded. A RESERVED_IDENTITY_TOKENS constant, exported from src/feedback/redact.js and defence-in-depth applied at every operator-name token loop, blocks the fold when the whitespace-separated token (case-insensitive) is one of: agent, agents, user, users, operator, operators, admin, admins, root, client, clients, server, servers, service, services, system, systems, name, main, test, code, api, apis, anon, anonymous, human, humans, dev, ops, sre, docs, developer, developers, engineer, engineers, team, teams. Substitution still fires for a real given name (Alice, Smith) even when the full identity string contains a stopword. The URL and prose canon phrase 'the agent files feedback' survives unmodified when the identity contains 'agent'. Rationale: the rcf-lite ecosystem uses 'agent' as a canonical noun (RULE 17, docs paths); operator-identity folds must be driven by real identity values, never by a bare common noun.",
51
+ "given": "a fixture project whose rcf/.identity/profile.md ## Name reads 'The Agent Alice', and a body containing the docs URL 'https://stravica.ai/docs/rcf/how-the-agent-files-feedback', the prose 'when the agent stops the queue', and the personal name 'Alice reviewed this'",
52
+ "when": "the redactor runs over the body with the seeded operator identity",
53
+ "then": "the docs URL survives verbatim; the prose 'when the agent stops the queue' survives verbatim; the personal name 'Alice' folds to <operator>; the ledger records an operator-identity row for 'Alice' but no row for 'agent' or 'the'",
54
+ "testable": true,
55
+ "scope": "runtime"
47
56
  }
48
57
  ],
49
58
  "createdAt": "2026-09-16T13:45:23.000Z",
50
- "updatedAt": "2026-09-17T00:36:18Z"
59
+ "updatedAt": "2026-09-17T13:47:03Z"
51
60
  }
@@ -35,8 +35,17 @@
35
35
  "then": "doctor output carries one feedback-destinations warn row naming the second library, --fix is not offered on that row, exit code stays 0",
36
36
  "testable": true,
37
37
  "scope": "runtime"
38
+ },
39
+ {
40
+ "id": "AC-15801-4",
41
+ "description": "The destination resolver cross-checks the registry BEFORE falling through to the shelf constant for `--kind blueprint` entries whose manifest record has no `libraryPrefix`. If exactly one registered library owns a blueprint with the applied slug AND that library resolves to a destination (issuesRepo, or a derivable git+https://github.com/... sourceRef), the resolver returns that library's destination with source: 'library-slug-inference' (derived: false for the declared case, derived: true for the sourceRef case). Two-or-more matches return unresolved with reason: 'ambiguous-library-slug' and a candidates[] array of library prefixes. Zero matches keep the current shelf-constant fall-through. This never overrides an existing libraryPrefix resolution: it only fires where the resolver would have picked the shelf constant.",
42
+ "given": "a fixture with three registered libraries and a bare-slug manifest record for 'application-spa'; case A: only library 'wsd' owns application-spa and declares issuesRepo 'wsd-team-dev/rcf-lite-blueprints'; case B: libraries 'wsd' and 'ally' both own application-spa and both declare issuesRepo; case C: no registered library owns application-spa",
43
+ "when": "destination.resolve() is called for each case",
44
+ "then": "case A resolves to 'wsd-team-dev/rcf-lite-blueprints' with source 'library-slug-inference' and derived false; case B resolves to unresolved with reason 'ambiguous-library-slug' and candidates ['ally','wsd']; case C resolves to Stravica/rcf-lite (shelf-constant fall-through), unchanged from AC-15801-2 shelf behaviour",
45
+ "testable": true,
46
+ "scope": "library"
38
47
  }
39
48
  ],
40
49
  "createdAt": "2026-09-16T13:45:23.000Z",
41
- "updatedAt": "2026-09-16T13:45:23.000Z"
50
+ "updatedAt": "2026-09-17T13:46:02.000Z"
42
51
  }
@@ -40,8 +40,18 @@
40
40
  # `npm install rcf-lite`.
41
41
 
42
42
  feedVersion: 1
43
- latest: "0.28.0"
43
+ latest: "0.28.1"
44
44
  releases:
45
+ - version: "0.28.1"
46
+ date: "2026-09-17"
47
+ breaking: false
48
+ headlines:
49
+ - "rcf-lite 0.28.1 fixes three defects the first WSD-lens round-trip surfaced on 0.28.0. Feedback on a library-owned blueprint now reaches the library owner even when the blueprint was applied by its bare slug, so a colleague running rcf define blueprint add application-spa against a project with a registered WSD library that also owns application-spa no longer sees the finding silently routed to Stravica/rcf-lite."
50
+ - "The operator-identity redactor no longer folds the canonical noun agent when it appears in an operator's ## Name line. The docs URL how-the-agent-files-feedback and prose like when the agent stops survive; real given names still fold. A common-noun stopword set (agent, user, admin, api, code, system, and the rest) is exported as RESERVED_IDENTITY_TOKENS."
51
+ - "The rcf feedback add help text now names --evidence as required and repeatable, matching the shipped behaviour and AC-15501-1. A README callout box discloses the rename from the retired @stravica-ai/rcf-build-lite name to rcf-lite so a colleague hitting no matching version on the old name has a paste-ready recovery command."
52
+ minAgentAction: null
53
+ notesUrl: "https://stravica.ai/docs/rcf/changelog/"
54
+
45
55
  - version: "0.28.0"
46
56
  date: "2026-09-17"
47
57
  breaking: false
@@ -136,8 +136,10 @@ Common options:
136
136
  --body <text> | --body-file <path>
137
137
  Body markdown, <= ${BODY_CAP_BYTES / 1024} KB
138
138
  after UTF-8 encoding.
139
- --evidence <pointer> Repeatable: a command, a path:line
140
- inside the project, or an id.
139
+ --evidence <pointer> Required, repeatable: a command, a
140
+ path:line inside the project, or an id.
141
+ At least one --evidence is required
142
+ (AC-15501-1); absence exits 2.
141
143
  --harness claude-code|codex|other
142
144
  Optional; inferred from env when absent.
143
145
  --ask-now Blocker shortcut; flags the batch so the
@@ -954,6 +956,13 @@ async function handlePreview(argv, ctx) {
954
956
  const reason = p.destination.reason ?? 'unresolved';
955
957
  const contact = p.destination.publisherContact ? ` (library contact: ${p.destination.publisherContact})` : '';
956
958
  stdout.write(` no issue destination declared${p.destination.reason ? ` (${reason})` : ''}; bundle will be written${contact}\n`);
959
+ // AC-15801-4 / design amendment R5b: for ambiguous-library-slug
960
+ // surface the candidate library prefixes so the operator sees
961
+ // exactly which qualified refs would resolve it.
962
+ if (Array.isArray(p.destination.candidates) && p.destination.candidates.length > 0) {
963
+ const qualified = p.destination.candidates.map((prefix) => `${prefix}:<slug>`).join(', ');
964
+ stdout.write(` candidate libraries owning this slug: ${p.destination.candidates.join(', ')} (re-apply with a qualified ref, e.g. ${qualified})\n`);
965
+ }
957
966
  }
958
967
  stdout.write(`fingerprint: ${p.fingerprint}\n`);
959
968
  if (p.fingerprintFallback) {
@@ -7,7 +7,14 @@
7
7
  //
8
8
  // Resolution order (design section 3.3):
9
9
  // 1. --kind core -> CORE_REPO (public)
10
- // 2. --kind blueprint, no libraryPrefix -> CORE_REPO (shelf; public)
10
+ // 2. --kind blueprint, no libraryPrefix -> library-slug inference
11
+ // (AC-15801-4 / design amendment R5a): if the applied slug is
12
+ // owned by exactly one registered library that resolves to a
13
+ // destination (issuesRepo or derivable sourceRef), route there
14
+ // with source: 'library-slug-inference'; if two or more libraries
15
+ // own the slug and resolve, return unresolved with reason
16
+ // 'ambiguous-library-slug' and a candidates[] array of prefixes;
17
+ // otherwise fall through to CORE_REPO (shelf; public).
11
18
  // 3. --kind blueprint, libraryPrefix -> registry entry issuesRepo
12
19
  // (declared destination wins;
13
20
  // visibility from the entry)
@@ -210,9 +217,10 @@ function recordEffectiveSlug(record) {
210
217
  * @property {'public' | 'private' | 'unresolved'} visibility
211
218
  * @property {'core' | 'shelf' | 'library'} kind
212
219
  * @property {boolean} derived true when repo came from sourceRef derivation
213
- * @property {string} [reason] filled on unresolved: 'no-issues-field' | 'no-github-source' | 'no-libraryprefix-record' | 'unknown-target'
220
+ * @property {string} [reason] filled on unresolved: 'no-issues-field' | 'no-github-source' | 'no-libraryprefix-record' | 'unknown-target' | 'ambiguous-library-slug'
221
+ * @property {string[]} [candidates] filled on unresolved with reason 'ambiguous-library-slug': sorted library prefixes owning the slug
214
222
  * @property {string} [publisherContact] registry entry's publisher.contact (bundle uses it)
215
- * @property {string} [source] 'library-manifest' | 'sourceRef-derivation' | 'package-bugs-url' | 'shelf-constant'
223
+ * @property {string} [source] 'library-manifest' | 'sourceRef-derivation' | 'package-bugs-url' | 'shelf-constant' | 'library-slug-inference'
216
224
  */
217
225
 
218
226
  /**
@@ -263,8 +271,27 @@ export async function resolve(entry, context = {}) {
263
271
 
264
272
  const libraryPrefix = record.libraryPrefix ?? entry?.target?.libraryPrefix ?? null;
265
273
 
266
- // Shelf blueprint (no libraryPrefix) -> Stravica/rcf-lite.
274
+ // Shelf blueprint (no libraryPrefix on the manifest record). Before
275
+ // falling through to the shelf constant, cross-check the registry:
276
+ // if exactly one registered library owns a blueprint with the applied
277
+ // slug AND that library resolves to a destination, route to that
278
+ // library so a bare-slug apply of a library-shared name still lands
279
+ // on the library owner's repo (design amendment R5, AC-15801-4).
267
280
  if (!libraryPrefix) {
281
+ const inferred = inferLibraryFromSlug(record, registry);
282
+ if (inferred.kind === 'single') {
283
+ return inferred.destination;
284
+ }
285
+ if (inferred.kind === 'ambiguous') {
286
+ return {
287
+ repo: null,
288
+ visibility: 'unresolved',
289
+ kind: 'library',
290
+ derived: false,
291
+ reason: 'ambiguous-library-slug',
292
+ candidates: inferred.candidates,
293
+ };
294
+ }
268
295
  return {
269
296
  repo: CORE_REPO,
270
297
  visibility: 'public',
@@ -359,3 +386,70 @@ function findLibraryEntry(registry, libraryPrefix) {
359
386
  const libs = Array.isArray(registry?.libraries) ? registry.libraries : [];
360
387
  return libs.find((l) => l?.libraryPrefix === libraryPrefix) ?? null;
361
388
  }
389
+
390
+ /**
391
+ * AC-15801-4 / design amendment R5a: given a manifest record with no
392
+ * libraryPrefix, cross-check the registry to see whether the applied
393
+ * slug is owned by exactly one registered library that resolves to a
394
+ * destination. Returns:
395
+ * { kind: 'none' } no library owns the slug (or none resolve)
396
+ * { kind: 'single', destination } exactly one library owns the slug and resolves
397
+ * { kind: 'ambiguous', candidates: string[] } two or more libraries own the slug and resolve
398
+ *
399
+ * The resolver only calls this on the shelf-constant fall-through
400
+ * branch, so it never overrides an existing libraryPrefix resolution.
401
+ *
402
+ * @param {object} record manifest.blueprints[] record
403
+ * @param {object | null | undefined} registry parsed rcf/blueprint-libraries.json
404
+ */
405
+ function inferLibraryFromSlug(record, registry) {
406
+ const slug = typeof record?.slug === 'string' ? record.slug : null;
407
+ if (!slug) return { kind: 'none' };
408
+ const libs = Array.isArray(registry?.libraries) ? registry.libraries : [];
409
+ const owners = [];
410
+ for (const lib of libs) {
411
+ if (!lib || typeof lib !== 'object') continue;
412
+ if (typeof lib.libraryPrefix !== 'string' || lib.libraryPrefix.length === 0) continue;
413
+ const bps = Array.isArray(lib.blueprints) ? lib.blueprints : [];
414
+ const owns = bps.some((b) => b && typeof b.slug === 'string' && b.slug === slug);
415
+ if (!owns) continue;
416
+ const declared = typeof lib.issuesRepo === 'string' && lib.issuesRepo.length > 0 ? lib.issuesRepo : null;
417
+ const derivedRepo = declared ? null : parseGithubSource(lib.sourceRef ?? '');
418
+ if (!declared && !derivedRepo) continue;
419
+ const contact = typeof lib?.publisher?.contact === 'string' ? lib.publisher.contact : undefined;
420
+ owners.push({ lib, declared, derivedRepo, contact });
421
+ }
422
+ if (owners.length === 0) return { kind: 'none' };
423
+ if (owners.length === 1) {
424
+ const [o] = owners;
425
+ if (o.declared) {
426
+ const declaredVisibility = o.lib.issuesVisibility === 'private' ? 'private'
427
+ : o.lib.issuesVisibility === 'public' ? 'public'
428
+ : 'public';
429
+ return {
430
+ kind: 'single',
431
+ destination: {
432
+ repo: o.declared,
433
+ visibility: declaredVisibility,
434
+ kind: 'library',
435
+ derived: false,
436
+ source: 'library-slug-inference',
437
+ ...(o.contact ? { publisherContact: o.contact } : {}),
438
+ },
439
+ };
440
+ }
441
+ return {
442
+ kind: 'single',
443
+ destination: {
444
+ repo: o.derivedRepo,
445
+ visibility: 'public',
446
+ kind: 'library',
447
+ derived: true,
448
+ source: 'library-slug-inference',
449
+ ...(o.contact ? { publisherContact: o.contact } : {}),
450
+ },
451
+ };
452
+ }
453
+ const candidates = owners.map((o) => o.lib.libraryPrefix).sort();
454
+ return { kind: 'ambiguous', candidates };
455
+ }
@@ -32,6 +32,26 @@ const BUNDLED_ALLOWLIST = JSON.parse(
32
32
  /** Body size cap (bytes). Mirrors src/cli/feedback.js:BODY_CAP_BYTES. */
33
33
  export const BODY_CAP_BYTES = 8 * 1024;
34
34
 
35
+ /**
36
+ * Common-noun stopwords the operator-identity substitution never folds
37
+ * (AC-15601-5 / design amendment R5c). The rcf-lite ecosystem uses
38
+ * `agent` as a canonical noun (RULE 17, docs paths such as
39
+ * `how-the-agent-files-feedback`); if the operator's identity name
40
+ * happens to contain one of these words, the token loop skips it so
41
+ * the docs URL and prose like `when the agent stops` survive. Real
42
+ * given names in the identity still fold. The list is intentionally
43
+ * conservative: anything a normal profile.md ## Name line might carry
44
+ * as a common English or engineering noun.
45
+ */
46
+ export const RESERVED_IDENTITY_TOKENS = Object.freeze(new Set([
47
+ 'agent', 'agents', 'user', 'users', 'operator', 'operators',
48
+ 'admin', 'admins', 'root', 'client', 'clients', 'server', 'servers',
49
+ 'service', 'services', 'system', 'systems', 'name', 'main', 'test',
50
+ 'code', 'api', 'apis', 'anon', 'anonymous', 'human', 'humans',
51
+ 'dev', 'ops', 'sre', 'docs', 'developer', 'developers', 'engineer',
52
+ 'engineers', 'team', 'teams',
53
+ ]));
54
+
35
55
  /**
36
56
  * Open secret-key vocabulary (design amendment R3a). A key/value pair
37
57
  * is treated as a secret when the key, right-bounded at a word edge so
@@ -389,9 +409,20 @@ export function redact(input, context = {}) {
389
409
  if (context.operatorName) {
390
410
  const opName = context.operatorName.trim();
391
411
  if (opName.length > 0) {
392
- text = replaceAndLog(text, escapedLiteral(opName), '<operator>', 'operator-identity', ledger);
393
- for (const tok of opName.split(/\s+/)) {
394
- if (tok.length > 3) {
412
+ // AC-15601-5 / design amendment R5c: never fold a common noun
413
+ // that happens to appear in the identity string. The rcf-lite
414
+ // ecosystem uses 'agent' as a canonical noun (RULE 17, docs
415
+ // paths); folding it as operator-identity mangles the docs URL
416
+ // `how-the-agent-files-feedback` and prose like `when the
417
+ // agent stops`. Substitution still fires for a real given name
418
+ // (Alice, Smith) even when the full identity string carries a
419
+ // stopword. When the identity contains ONLY stopwords (a
420
+ // placeholder-in-disguise), skip identity folds entirely.
421
+ const rawTokens = opName.split(/\s+/);
422
+ const foldable = rawTokens.filter((t) => t.length > 3 && !RESERVED_IDENTITY_TOKENS.has(t.toLowerCase()));
423
+ if (foldable.length > 0) {
424
+ text = replaceAndLog(text, escapedLiteral(opName), '<operator>', 'operator-identity', ledger);
425
+ for (const tok of foldable) {
395
426
  text = replaceAndLog(text, `\\b${escapeRe(tok)}\\b`, '<operator>', 'operator-identity', ledger);
396
427
  }
397
428
  }
@@ -399,7 +430,7 @@ export function redact(input, context = {}) {
399
430
  }
400
431
  if (context.projectName) {
401
432
  const pn = context.projectName.trim();
402
- if (pn.length > 0) {
433
+ if (pn.length > 0 && !RESERVED_IDENTITY_TOKENS.has(pn.toLowerCase())) {
403
434
  text = replaceAndLog(text, `\\b${escapeRe(pn)}\\b`, '<project>', 'operator-identity', ledger);
404
435
  }
405
436
  }
@@ -846,6 +877,15 @@ function looksHighEntropyPath(value) {
846
877
  if (typeof value !== 'string') return false;
847
878
  if (value.length < 16) return false;
848
879
  if (!/^[A-Za-z0-9._~+/=-]+$/.test(value)) return false;
880
+ // AC-15601-5 canon-phrase guard: a lowercase kebab-case segment with
881
+ // three or more hyphen-separated word parts is a documentation slug,
882
+ // not a credential. The rcf-lite docs URL
883
+ // `how-the-agent-files-feedback` is 28 chars, 2 classes (lowercase
884
+ // and separator), and would otherwise trip the 16-char, 2-class rule
885
+ // and produce `<url-credential>` inside the docs URL. Real credential
886
+ // segments in URL paths (webhook tokens, session ids, presigned
887
+ // credentials) are mixed-case-alphanumeric, not kebab-case English.
888
+ if (/^[a-z]+(-[a-z]+){2,}$/.test(value)) return false;
849
889
  let classes = 0;
850
890
  if (/[A-Z]/.test(value)) classes += 1;
851
891
  if (/[a-z]/.test(value)) classes += 1;