@vibecompass/vibecompass 0.14.0 → 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/CHANGELOG.md +183 -0
- package/README.md +111 -8
- package/package.json +1 -1
- package/src/brief-keywords.js +203 -0
- package/src/brief-lifecycle.js +638 -0
- package/src/brief-render.js +341 -0
- package/src/brief-settings.js +160 -0
- package/src/brief.js +1557 -0
- package/src/cli.js +731 -7
- package/src/credential-store.js +856 -0
- package/src/decision-lineage.js +1717 -0
- package/src/docs-review.js +28 -4
- package/src/generators/agent-files/template.js +2 -1
- package/src/index.js +44 -0
- package/src/login.js +257 -0
- package/src/mode-transition.js +8 -2
- package/src/project-memory.js +30 -4
- package/src/read-model.js +92 -2
- package/src/scaffold.js +10 -2
- package/src/secret-prompt.js +64 -0
- package/src/session.js +30 -0
- package/src/status.js +54 -9
- package/src/sync.js +12 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,188 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-09-30
|
|
4
|
+
|
|
5
|
+
The session brief ships as an opt-in experiment (D-368). In a baseline-versus-brief
|
|
6
|
+
evaluation that did not fully follow its own protocol, the brief arm met the numerical
|
|
7
|
+
recall targets and made none of the labeled forbidden authority claims, but lost nine
|
|
8
|
+
facts that agents without a brief found. That does not support turning it on by
|
|
9
|
+
default, so `start-session` and `continue-session` write a lane brief only when
|
|
10
|
+
`project.yaml` sets `brief.enabled: true`. `vibecompass brief` works on request
|
|
11
|
+
either way.
|
|
12
|
+
|
|
13
|
+
- Read model: architecture components now carry `retrieval_guidance` (the
|
|
14
|
+
`## Retrieval guidance` section) and `retrieval_scope` (the "Retrieval
|
|
15
|
+
scope" bullet under `## Review metadata`); both are `null` when absent.
|
|
16
|
+
Existing fields are unchanged.
|
|
17
|
+
- Add typed decision references and declared decision lineage
|
|
18
|
+
(`decision_lineage` on the read model, contract version 1). Architecture
|
|
19
|
+
docs `cites` decisions, session notes `references` them and `made` the ones
|
|
20
|
+
listed at the start of a top-level "Decisions made" bullet, and decision
|
|
21
|
+
entries declare `supersedes` / `amends` / `preserves` only from their own
|
|
22
|
+
wording, with extent (`full` / `partial` / `scoped` / `unqualified`) and
|
|
23
|
+
scope. Other lineage-like wording is `unknown`; mentions are `references`;
|
|
24
|
+
nothing is inferred as "governs" or "currently valid" (D-359). Every
|
|
25
|
+
relation carries source path, section, line, document and section hashes,
|
|
26
|
+
and explicit-or-inferred evidence (range interiors are inferred).
|
|
27
|
+
- Lineage stays conservative: fenced examples, another decision's restated,
|
|
28
|
+
reported, or uncertain claim ("D-020 supersedes D-010", "As D-020
|
|
29
|
+
supersedes …", "Documents how D-020 supersedes …"), contrast objects
|
|
30
|
+
("separate from D-169"), hypothetical wording (may, would, if, unless, …),
|
|
31
|
+
and negated preservation ("cannot preserve") never certify a relation;
|
|
32
|
+
prohibited change ("cannot supersede", "cannot be superseded by <this
|
|
33
|
+
entry>") is preservation, and a requirement ("must retain") still
|
|
34
|
+
certifies preservation. An
|
|
35
|
+
`unknown` is kept beside certified relations unless one shares its object
|
|
36
|
+
(coordinated verbs). Historical retention ("kept for historical reference")
|
|
37
|
+
never overrides partial or scoped wording. Decision IDs must be positive
|
|
38
|
+
safe integers, so malformed ranges cannot loop.
|
|
39
|
+
- Recognize the optional structured lineage fields `**Supersedes:**`,
|
|
40
|
+
`**Partially supersedes:**`, `**Amends:**`, and `**Preserves:**` on new
|
|
41
|
+
decision entries (D-363); they win over prose for the targets they name, and
|
|
42
|
+
malformed items become diagnostics.
|
|
43
|
+
- Add `getDecisionLineage(readModel, id)` (declared successors transitively,
|
|
44
|
+
incoming and outgoing lineage, citations, and the notes that made it) and
|
|
45
|
+
export the pure extractors (`extractDecisionFileRelations`,
|
|
46
|
+
`extractSessionNoteRelations`, `extractArchitectureDocCitations`,
|
|
47
|
+
`scanDecisionReferences`, `buildDecisionLineageModel`) with shared fixtures
|
|
48
|
+
in `src/tests/fixtures/decision-lineage/` for the app port.
|
|
49
|
+
- Add `vibecompass brief` and the read-only session brief engine
|
|
50
|
+
(`buildSessionBrief`, `renderBrief`; plan task A3, D-359). Given a task, and
|
|
51
|
+
optionally files, feature slugs, claims, a lane, and a budget, it selects the
|
|
52
|
+
memory a session should read first — through the file-owner index (repo
|
|
53
|
+
aliases and workspace paths such as `vibecompass-app/src/…` both match),
|
|
54
|
+
lane-declared docs, feature slugs, and deterministic keyword matching on
|
|
55
|
+
titles, Description, and Retrieval guidance (no embeddings, no network, no
|
|
56
|
+
model) — and packs whole units (lane, decision-lineage, doc, note,
|
|
57
|
+
watch-out) into an estimated token budget (Unicode code points ÷ 4 over the
|
|
58
|
+
whole rendered brief; default 6,000, minimum 800). Every brief states
|
|
59
|
+
`complete`, `partial`, `incomplete` (with prioritized required reads, also
|
|
60
|
+
used for retrieval errors), or `no-match`, lists at most 12 follow-up reads
|
|
61
|
+
plus "+N more" (the full list is in `--json`), and never emits a decision
|
|
62
|
+
without every declared superseding or amending successor. Uncertified
|
|
63
|
+
lineage wording becomes a follow-up read, never a status. The command
|
|
64
|
+
writes nothing: no manifest refresh, state write, lane write, or agent-file
|
|
65
|
+
sync, so it is safe against a hand-maintained root.
|
|
66
|
+
- Session brief in the lane lifecycle (plan task A4, D-359, D-364, D-368),
|
|
67
|
+
opt-in: when `project.yaml` sets `brief.enabled: true`, `start-session` and
|
|
68
|
+
`continue-session` write `sessions/active/<lane-id>/brief.md` from the lane's working-on,
|
|
69
|
+
claims, and feature slugs, after every lifecycle write succeeded and inside
|
|
70
|
+
the same lock. `continue-session` keeps a current brief and regenerates a
|
|
71
|
+
missing or stale one, and lists it under "Read next". A brief failure is a
|
|
72
|
+
warning plus, when the file can be written, an `incomplete` brief naming the
|
|
73
|
+
reason; it never fails the command, and existing lifecycle refusals fail as
|
|
74
|
+
before. Without the setting (or with `brief.enabled: false`), lifecycle
|
|
75
|
+
commands print `Brief: off (…)`, write no brief, and leave a brief already
|
|
76
|
+
in the lane untouched; `--no-brief` (start and continue) skips generation
|
|
77
|
+
for one command on a root that opted in. Every other lifecycle effect is
|
|
78
|
+
unchanged. `vibecompass brief --write` writes or regenerates the lane brief
|
|
79
|
+
on request whatever the setting; `vibecompass brief --check [--json]`
|
|
80
|
+
reports whether it is stale, without writing.
|
|
81
|
+
- `brief.md` opens with a YAML header that binds its inputs: source root and
|
|
82
|
+
lane with the lane's session date and number, destination root and lane, a
|
|
83
|
+
digest of the canonical corpus it read, short hashes of the docs, decision
|
|
84
|
+
entries, and notes it drew text from, the lane scratch it read (`handoff.md`
|
|
85
|
+
bytes, the `session.yaml` fields it uses, the other active lanes), and the
|
|
86
|
+
repo alias map, plus ready-to-run check and refresh commands that keep the
|
|
87
|
+
root, lane, and source explicit. The body is exactly what
|
|
88
|
+
`vibecompass brief` prints for the lane. Staleness is computed from the
|
|
89
|
+
bindings, not stored: any hash, the session identity, or the alias map
|
|
90
|
+
differs, or the check cannot read what it needs; resume bookkeeping
|
|
91
|
+
(`resumed_at`, `resume_count`) never makes a brief stale. Memory that
|
|
92
|
+
cannot be read (invalid settings included) is a failed generation
|
|
93
|
+
(`generation: "failed"`; `brief --write` exits 1), while mandatory units
|
|
94
|
+
overflowing the budget is a successful `incomplete` brief.
|
|
95
|
+
- `project.yaml` gains an optional `brief:` mapping (D-364): `enabled`
|
|
96
|
+
(default `false`; `true` opts the root into lifecycle generation, D-368),
|
|
97
|
+
and `exclude`, a list of root-relative globs (`*`, `**`, `?`; case-insensitive)
|
|
98
|
+
naming canonical documents every brief must never read. Excluded files are
|
|
99
|
+
dropped from the scan before they are opened, so they contribute nothing to
|
|
100
|
+
selection, ranking, citation propagation, bindings, or output. Malformed or
|
|
101
|
+
unknown `brief` settings, or an unreadable `project.yaml`, fail closed: the
|
|
102
|
+
brief reads no canonical document and is `incomplete`; scans warn with
|
|
103
|
+
`project-brief-invalid`. `loadProjectReadModel` and other consumers are
|
|
104
|
+
unaffected.
|
|
105
|
+
- Dual-root source for dogfood roots (D-255, D-364): `continue-session`,
|
|
106
|
+
`brief --write`, and `brief --check` accept `--source-root <root>` (and
|
|
107
|
+
`--source-session <lane-id>`, which must equal the lane) to build the brief
|
|
108
|
+
from another memory root's matching lane — same id, session date, and
|
|
109
|
+
session number, or the brief is refused (`incomplete`, with the reason).
|
|
110
|
+
The source root is only read (no lock, manifest, lane, stamp, or agent-file
|
|
111
|
+
write) and is byte-identical afterwards; both roots' exclusions apply;
|
|
112
|
+
destination repo ids translate to the source's by matching remotes. The
|
|
113
|
+
option is never inferred: a brief built from another root is stale for a
|
|
114
|
+
run that does not name it, and that root is not read.
|
|
115
|
+
- Generated `context.md`, the managed agent block, and the scratch listings
|
|
116
|
+
now tell agents to read the lane's `brief.md`, when present, before planning,
|
|
117
|
+
and describe lifecycle generation as opt-in. The lane
|
|
118
|
+
`wip.md` template gains an optional `## Context used` section for the docs
|
|
119
|
+
and decisions the lane actually relied on.
|
|
120
|
+
|
|
121
|
+
## 0.15.0 - 2026-09-25
|
|
122
|
+
|
|
123
|
+
- Add a layered local credential store for hosted sync tokens (D-355). Every
|
|
124
|
+
hosted command now resolves the token in a fixed order: the binding's env
|
|
125
|
+
var when set (per-shell override for CI, automation, and agents), then the
|
|
126
|
+
per-user credential store keyed by hosted URL + project id. The store lives
|
|
127
|
+
outside every project root (`VIBECOMPASS_CONFIG_DIR`, else
|
|
128
|
+
`$XDG_CONFIG_HOME/vibecompass`, else `~/.config/vibecompass`;
|
|
129
|
+
`%APPDATA%\vibecompass` on Windows); its `credentials.json` index is
|
|
130
|
+
written `0600` inside a `0700` directory and a world-readable file is
|
|
131
|
+
refused. Secrets go to the OS keychain when one is available (macOS
|
|
132
|
+
Keychain via `security`, Linux `secret-tool`) and otherwise into the file;
|
|
133
|
+
the index records which backend holds each entry. `project.yaml` is
|
|
134
|
+
unchanged (`credential_source: env` + `credential_env_var` stay required),
|
|
135
|
+
so older CLIs keep working against the env var.
|
|
136
|
+
- `connect-hosted` captures the token after writing the binding: a hidden
|
|
137
|
+
prompt on a TTY, `--token-stdin` for scripts and agents, `--no-store` to
|
|
138
|
+
keep the env-only path, and `--credential-store <auto|keychain|file>`.
|
|
139
|
+
When the env var is already set it says so and points at
|
|
140
|
+
`sync-credential set --from-env` instead of storing silently.
|
|
141
|
+
- Add `vibecompass login [--sync-target <name>] [--api-url <url>]
|
|
142
|
+
[--project-id <id>] [--label <text>] [--no-browser] [--poll-timeout <s>]`:
|
|
143
|
+
a browser device-code sign-in. The CLI prints and opens the dashboard
|
|
144
|
+
approval link with a short code, polls until the project owner approves,
|
|
145
|
+
stores the minted token, and writes the sync binding when the root is not
|
|
146
|
+
bound yet (promoting `local-only` to `local-primary` like `connect-hosted`).
|
|
147
|
+
An unknown `--sync-target` is created only with an explicit `--api-url`;
|
|
148
|
+
a mismatched `--api-url` on a bound root fails closed.
|
|
149
|
+
- Add `vibecompass sync-credential <set|remove|list>`: store a dashboard
|
|
150
|
+
token (hidden prompt, `--token-stdin`, or `--from-env`), forget one, or
|
|
151
|
+
list stored entries (metadata only, never values; `--json`). `--api-url`
|
|
152
|
+
+ `--project-id` address a hosted project without a bound root.
|
|
153
|
+
- `status` prints `Hosted sync:` and `Sync credential:` lines (source only:
|
|
154
|
+
env var, OS keychain, credential store file, or missing) and recommends
|
|
155
|
+
`vibecompass login` when nothing holds a token. Missing-credential errors
|
|
156
|
+
from push, pull, mode transitions, and hosted docs-review now name
|
|
157
|
+
`login` first, then `sync-credential set`, then the env var override.
|
|
158
|
+
- Init and connect-hosted no longer tell users to export the token in
|
|
159
|
+
every terminal or persist it in a shell profile.
|
|
160
|
+
- Credential store hardening (review passes 1–3): every store mutation
|
|
161
|
+
takes a cross-process lock file next to the index that appears atomically
|
|
162
|
+
(owner record hard-linked into place) and names its owner (id, pid, host).
|
|
163
|
+
The lock is never taken from a live process on this host, however long it
|
|
164
|
+
waits on a keychain prompt; writers wait up to 10s, then fail naming the
|
|
165
|
+
owner. A lock whose owner process no longer exists is recovered through a
|
|
166
|
+
second reclaim lock so exactly one reclaimer removes it after re-reading
|
|
167
|
+
the owner; locks from another host, unreadable locks, and a reclaim lock
|
|
168
|
+
left by a crashed reclaimer are never removed automatically (the error
|
|
169
|
+
names them). The index is written through an exclusively created random
|
|
170
|
+
temp file plus atomic rename while the lock is held, so parallel logins,
|
|
171
|
+
`sync-credential set`, and `remove` never drop each other's entries;
|
|
172
|
+
target identity lowercases only scheme and host (paths keep their case);
|
|
173
|
+
keychain labels are sanitized and keys outside the keychain-safe charset
|
|
174
|
+
map to a digest account, so any project name can be stored; under the
|
|
175
|
+
default `auto` policy a failed keychain write (headless session, locked
|
|
176
|
+
keychain, missing Secret Service) falls back to the file store with a
|
|
177
|
+
printed warning instead of losing a one-time claim. `login` bounds every
|
|
178
|
+
network wait (start, polls, body reads) by `--poll-timeout`.
|
|
179
|
+
- `login` is the one recommended connection path (D-356): help lists it
|
|
180
|
+
right after `init`, `init` and missing-token errors point only at it, and
|
|
181
|
+
`connect-hosted` on a terminal starts the browser sign-in when you press
|
|
182
|
+
Enter at its token prompt. Dashboard tokens, `--token-stdin`,
|
|
183
|
+
`sync-credential`, and the env-var override remain for CI, scripts, and
|
|
184
|
+
browserless machines and are labelled advanced.
|
|
185
|
+
|
|
3
186
|
## 0.14.0 - 2026-09-23
|
|
4
187
|
|
|
5
188
|
- Add `continue-session [<lane-id>]` (D-353): resume an already-open lane
|
package/README.md
CHANGED
|
@@ -143,6 +143,17 @@ npx -y @vibecompass/vibecompass@latest close-session --root .compass --session a
|
|
|
143
143
|
# start-session --id <existing-lane> fails closed and points at continue-session.
|
|
144
144
|
npx -y @vibecompass/vibecompass@latest continue-session auth-flow --root .compass --working-on "Auth flow, phase 2"
|
|
145
145
|
|
|
146
|
+
# Session brief in the lane (D-359, D-364), opt-in (D-368): with
|
|
147
|
+
# project.yaml `brief: { enabled: true }`, start-session and continue-session
|
|
148
|
+
# write sessions/active/<lane-id>/brief.md — the memory the lane's task should
|
|
149
|
+
# start from, with a YAML header binding the inputs it read. continue-session
|
|
150
|
+
# keeps a current brief and regenerates a stale one; a brief failure is a
|
|
151
|
+
# warning, never a failed start or resume. Without the setting nothing is
|
|
152
|
+
# written, and --no-brief skips it for one command. brief --write briefs any
|
|
153
|
+
# lane on request; `brief.exclude` lists root-relative globs no brief may read.
|
|
154
|
+
npx -y @vibecompass/vibecompass@latest brief --root .compass --session auth-flow --write # write or regenerate it
|
|
155
|
+
npx -y @vibecompass/vibecompass@latest brief --root .compass --session auth-flow --check # current or stale, and why
|
|
156
|
+
|
|
146
157
|
# Optional git binding for a lane (D-281): create/reuse a branch in each bound
|
|
147
158
|
# repo; --worktree provisions per-repo worktrees under
|
|
148
159
|
# <workspace>/worktrees/<lane-id>/<repo-id> with the lane marker in the
|
|
@@ -186,6 +197,12 @@ npx -y @vibecompass/vibecompass@latest sync-agents --root .compass
|
|
|
186
197
|
npx -y @vibecompass/vibecompass@latest append-decision --root .compass --target cross-cutting.md --entry staged-entry.md
|
|
187
198
|
npx -y @vibecompass/vibecompass@latest refresh-decision-index --root .compass --check
|
|
188
199
|
|
|
200
|
+
# Session brief (read-only): the memory a task should start from, packed into
|
|
201
|
+
# an estimated token budget, with a complete/partial/incomplete/no-match status
|
|
202
|
+
# and follow-up reads. Decisions always appear with their declared successors.
|
|
203
|
+
npx -y @vibecompass/vibecompass@latest brief --root .compass --task "Change the refresh denial message" --files app:src/lib/entitlements.ts
|
|
204
|
+
npx -y @vibecompass/vibecompass@latest brief --root .compass --session auth-flow --budget 3000 --json
|
|
205
|
+
|
|
189
206
|
# Targeted and comprehensive architecture docs maintenance
|
|
190
207
|
npx -y @vibecompass/vibecompass@latest docs-update --root .compass --session auth-flow --changed app:src/auth/login.ts
|
|
191
208
|
npx -y @vibecompass/vibecompass@latest docs-review --root .compass --guided
|
|
@@ -240,10 +257,57 @@ workflow files, updating handoffs, and applying reviewed changes. Use
|
|
|
240
257
|
`docs update` / `vibecompass docs-update` for ordinary session-delta
|
|
241
258
|
maintenance tied to the current lane.
|
|
242
259
|
|
|
260
|
+
## Session Brief (opt-in)
|
|
261
|
+
|
|
262
|
+
`vibecompass brief` picks the architecture docs, decisions, and session notes a
|
|
263
|
+
task should start from, and packs them into a short markdown brief with a
|
|
264
|
+
`complete`, `partial`, `incomplete`, or `no-match` status and a list of
|
|
265
|
+
follow-up reads. It runs locally and deterministically: no network, no model,
|
|
266
|
+
no account. A decision always appears together with any later decision whose
|
|
267
|
+
own text explicitly declares that it replaces or amends it, and the brief never
|
|
268
|
+
claims that a decision governs your task. It cannot spot a replacement that is
|
|
269
|
+
only implied, so read the follow-up list too.
|
|
270
|
+
|
|
271
|
+
The brief in a session lane is an experiment you turn on (D-368):
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
# .compass/project.yaml
|
|
275
|
+
brief:
|
|
276
|
+
enabled: true
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
With it on, `start-session` and `continue-session` write
|
|
280
|
+
`sessions/active/<lane-id>/brief.md`, and your AI tool reads it before
|
|
281
|
+
planning. Without it, nothing changes, and
|
|
282
|
+
`vibecompass brief --session <lane-id> --write` still briefs one lane on
|
|
283
|
+
request. `brief.exclude` lists files no brief may read.
|
|
284
|
+
|
|
285
|
+
The brief is opt-in because of the project's own evaluation, which did not
|
|
286
|
+
fully follow its protocol. Agents with a brief found about as much of the
|
|
287
|
+
relevant memory as agents without one, and made none of the specific false
|
|
288
|
+
authority claims the evaluation checked for. But they missed nine facts the
|
|
289
|
+
others found, in one case after the brief left out the key document. Treat a
|
|
290
|
+
brief as a starting point, not the whole answer.
|
|
291
|
+
|
|
243
292
|
## Hosted Sync
|
|
244
293
|
|
|
245
294
|
Use hosted sync when you want browser review, team workflows, or hosted
|
|
246
|
-
docs-review proposals
|
|
295
|
+
docs-review proposals. The quickest path signs you in from the terminal and
|
|
296
|
+
keeps the token in your OS keychain (or a private per-user file), so it works
|
|
297
|
+
in every new terminal and in agents launched from a desktop app:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npx -y @vibecompass/vibecompass@latest login --root .compass
|
|
301
|
+
# opens vibecompass.dev, you confirm a short code and pick the project,
|
|
302
|
+
# the token is stored locally, and the hosted binding is written to project.yaml
|
|
303
|
+
|
|
304
|
+
npx -y @vibecompass/vibecompass@latest push --root .compass
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Advanced (CI, scripts, a machine without a browser, or a hosted-only demote):
|
|
308
|
+
bind the root with a dashboard-created token, pasted at the hidden prompt or
|
|
309
|
+
piped in. On a terminal, pressing Enter at that prompt starts the browser
|
|
310
|
+
sign-in instead:
|
|
247
311
|
|
|
248
312
|
```bash
|
|
249
313
|
npx -y @vibecompass/vibecompass@latest connect-hosted \
|
|
@@ -251,8 +315,12 @@ npx -y @vibecompass/vibecompass@latest connect-hosted \
|
|
|
251
315
|
--sync-api-url https://vibecompass.dev \
|
|
252
316
|
--sync-project-id vc_proj_example \
|
|
253
317
|
--sync-credential-env-var VIBECOMPASS_SYNC_TOKEN
|
|
318
|
+
# "Paste the sync token from the dashboard (input hidden)"
|
|
254
319
|
|
|
255
|
-
|
|
320
|
+
# non-interactive:
|
|
321
|
+
printf '%s' "$TOKEN" | npx -y @vibecompass/vibecompass@latest connect-hosted --root .compass \
|
|
322
|
+
--sync-api-url https://vibecompass.dev --sync-project-id vc_proj_example \
|
|
323
|
+
--sync-credential-env-var VIBECOMPASS_SYNC_TOKEN --token-stdin
|
|
256
324
|
```
|
|
257
325
|
|
|
258
326
|
After `close-session`, run `push` again when a connected local-primary root
|
|
@@ -260,16 +328,29 @@ changed canonical project-memory files and the hosted dashboard should catch
|
|
|
260
328
|
up. Hosted-only projects do not use a local authoritative push; update or
|
|
261
329
|
refresh them through the hosted dashboard/proposal flow.
|
|
262
330
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
331
|
+
### Where the token lives (D-355)
|
|
332
|
+
|
|
333
|
+
Hosted commands look for the token in this order:
|
|
334
|
+
|
|
335
|
+
1. the environment variable named in `project.yaml` (`VIBECOMPASS_SYNC_TOKEN`
|
|
336
|
+
by default) — a per-shell override for CI and scripts;
|
|
337
|
+
2. the local credential store, keyed by hosted URL + project id: the OS
|
|
338
|
+
keychain when available (macOS Keychain, Linux `secret-tool`), otherwise
|
|
339
|
+
`~/.config/vibecompass/credentials.json` (`0600`; `%APPDATA%\vibecompass`
|
|
340
|
+
on Windows; `VIBECOMPASS_CONFIG_DIR` overrides the location).
|
|
341
|
+
|
|
342
|
+
The token value is never written to `project.yaml`, `state/`, or any file in
|
|
343
|
+
your repo. Manage stored tokens with:
|
|
266
344
|
|
|
267
345
|
```bash
|
|
268
|
-
|
|
346
|
+
npx -y @vibecompass/vibecompass@latest sync-credential list --root .compass
|
|
347
|
+
npx -y @vibecompass/vibecompass@latest sync-credential set --root .compass # hidden prompt, --token-stdin, or --from-env
|
|
348
|
+
npx -y @vibecompass/vibecompass@latest sync-credential remove --root .compass
|
|
269
349
|
```
|
|
270
350
|
|
|
271
|
-
|
|
272
|
-
|
|
351
|
+
`vibecompass status` shows which source is in use (never the value). Lost a
|
|
352
|
+
token? Run `login` again, or create a new one on the dashboard under Setup ->
|
|
353
|
+
Hosted sync and revoke the old one there.
|
|
273
354
|
|
|
274
355
|
### Multiple environments
|
|
275
356
|
|
|
@@ -304,6 +385,9 @@ import {
|
|
|
304
385
|
scanProjectMemory,
|
|
305
386
|
loadProjectReadModel,
|
|
306
387
|
getProjectContext,
|
|
388
|
+
getDecisionLineage,
|
|
389
|
+
buildSessionBrief,
|
|
390
|
+
renderBrief,
|
|
307
391
|
detectChangelogShapedLines,
|
|
308
392
|
ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
|
|
309
393
|
} from "@vibecompass/vibecompass";
|
|
@@ -319,6 +403,25 @@ so external validators (for example the hosted proposal parser) stay in parity.
|
|
|
319
403
|
`ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES` is the byte-valued soft size budget
|
|
320
404
|
(12000) both docs-review apply warnings and the docs-update size advisory read.
|
|
321
405
|
|
|
406
|
+
`loadProjectReadModel(root)` also returns `decision_lineage`: evidence-typed
|
|
407
|
+
relations between documents and decisions. Architecture docs `cites`
|
|
408
|
+
decisions; session notes `references` them and `made` the ones listed under
|
|
409
|
+
"Decisions made". Decision entries declare `supersedes`, `amends`, or
|
|
410
|
+
`preserves` only from their own wording, with an extent and scope; anything
|
|
411
|
+
the parser cannot certify is `unknown`. Every relation carries its source
|
|
412
|
+
path, section, line, and content hashes. `getDecisionLineage(readModel, id)`
|
|
413
|
+
returns one decision's incoming and outgoing lineage and its declared
|
|
414
|
+
successors, followed transitively. Nothing is ever reported as "governs" or
|
|
415
|
+
"currently valid". Architecture components also carry `retrieval_guidance`
|
|
416
|
+
and `retrieval_scope` when their docs define them.
|
|
417
|
+
|
|
418
|
+
`buildSessionBrief({ rootDir, task, files, featureSlugs, claims, laneId, budget })`
|
|
419
|
+
is the read-only engine behind `vibecompass brief`: it returns the selection
|
|
420
|
+
result (status, packed units, omitted units, follow-up reads, gaps, and the
|
|
421
|
+
estimated size), and `renderBrief(result)` turns it into markdown. It never
|
|
422
|
+
writes to the root, and it never reads a path the root's `project.yaml`
|
|
423
|
+
`brief.exclude` names.
|
|
424
|
+
|
|
322
425
|
## More Documentation
|
|
323
426
|
|
|
324
427
|
- Developer guide: https://vibecompass.dev/developers
|
package/package.json
CHANGED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic keyword matching for the session brief (plan task A3; D-359):
|
|
3
|
+
* a tokenizer with light suffix stemming and a BM25F-style scorer over a
|
|
4
|
+
* small fielded index. No embeddings, no network, no model — the same inputs
|
|
5
|
+
* always produce the same terms and scores.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const STOPWORDS = new Set(
|
|
9
|
+
(
|
|
10
|
+
'a about above after again against all also am an and any are as at be because been before being below between both but by ' +
|
|
11
|
+
'can cannot could did do does doing done down during each either etc even ever every few for from further had has have having ' +
|
|
12
|
+
'he her here hers him his how however i if in into is it its itself just me more most much my neither no nor not now of off on ' +
|
|
13
|
+
'once one only onto or other our ours out over own per same she should so some such than that the their theirs them then there ' +
|
|
14
|
+
'these they this those though through thus to too under until up upon us very via was we were what whatever when where whether ' +
|
|
15
|
+
'which while who whom whose why will with within without would yet you your yours'
|
|
16
|
+
).split(' '),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Instruction framing that says what to do, not what the task is about.
|
|
20
|
+
// Dropped from query text only; indexed memory keeps every content word.
|
|
21
|
+
const QUERY_FRAMING = new Set(
|
|
22
|
+
(
|
|
23
|
+
'add answer anything change create edit everything afterward afterwards happen implement include including instead make ' +
|
|
24
|
+
'modify need please rewrite something thing things walk want write'
|
|
25
|
+
).split(' '),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const IRREGULAR = new Map([
|
|
29
|
+
['paid', 'pay'],
|
|
30
|
+
['made', 'make'],
|
|
31
|
+
['built', 'build'],
|
|
32
|
+
['ran', 'run'],
|
|
33
|
+
['written', 'write'],
|
|
34
|
+
['wrote', 'write'],
|
|
35
|
+
['chosen', 'choose'],
|
|
36
|
+
['chose', 'choose'],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Ordered suffix rules: the first rule whose suffix matches (with at least
|
|
40
|
+
// `min` characters left) applies. A final `e` is dropped afterwards so
|
|
41
|
+
// "translate" and "translation" meet at "translat".
|
|
42
|
+
const SUFFIX_RULES = [
|
|
43
|
+
['ational', 'ate', 3],
|
|
44
|
+
['izations', 'ize', 3],
|
|
45
|
+
['ization', 'ize', 3],
|
|
46
|
+
['ations', 'ate', 3],
|
|
47
|
+
['ation', 'ate', 3],
|
|
48
|
+
['ements', '', 4],
|
|
49
|
+
['ement', '', 4],
|
|
50
|
+
['ments', '', 3],
|
|
51
|
+
['ment', '', 3],
|
|
52
|
+
['nesses', '', 3],
|
|
53
|
+
['ness', '', 3],
|
|
54
|
+
['ingly', '', 3],
|
|
55
|
+
['ings', '', 3],
|
|
56
|
+
['ing', '', 3],
|
|
57
|
+
['edly', '', 3],
|
|
58
|
+
['ies', 'y', 2],
|
|
59
|
+
['ied', 'y', 2],
|
|
60
|
+
['sses', 'ss', 2],
|
|
61
|
+
['xes', 'x', 2],
|
|
62
|
+
['ches', 'ch', 2],
|
|
63
|
+
['shes', 'sh', 2],
|
|
64
|
+
['ed', '', 3],
|
|
65
|
+
['ly', '', 4],
|
|
66
|
+
['s', '', 3],
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export function stemWord(word) {
|
|
70
|
+
const lower = word.toLowerCase();
|
|
71
|
+
if (IRREGULAR.has(lower)) return IRREGULAR.get(lower);
|
|
72
|
+
if (lower.length <= 3 || lower.endsWith('ss') || lower.endsWith('us') || lower.endsWith('is')) {
|
|
73
|
+
return lower;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let stem = lower;
|
|
77
|
+
for (const [suffix, replacement, min] of SUFFIX_RULES) {
|
|
78
|
+
if (stem.endsWith(suffix) && stem.length - suffix.length >= min) {
|
|
79
|
+
stem = stem.slice(0, stem.length - suffix.length) + replacement;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// "running" → "runn" → "run"; keep "ll"/"ss"/"zz" doubles ("install", "access").
|
|
85
|
+
if (stem !== lower && /([b-df-hj-kmnp-rtv-y])\1$/.test(stem) && !/(ll|ss|zz)$/.test(stem)) {
|
|
86
|
+
stem = stem.slice(0, -1);
|
|
87
|
+
}
|
|
88
|
+
if (stem.length > 4 && stem.endsWith('e')) {
|
|
89
|
+
stem = stem.slice(0, -1);
|
|
90
|
+
}
|
|
91
|
+
return stem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Raw lowercase word tokens: letters and digits, split on everything else. */
|
|
95
|
+
export function splitWords(text) {
|
|
96
|
+
return String(text ?? '')
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.split(/[^a-z0-9]+/)
|
|
99
|
+
.filter((word) => word.length >= 2 && !/^\d+$/.test(word));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Content terms for indexing: stopwords dropped, stemmed, order kept. */
|
|
103
|
+
export function indexTerms(text) {
|
|
104
|
+
const terms = [];
|
|
105
|
+
for (const word of splitWords(text)) {
|
|
106
|
+
if (STOPWORDS.has(word)) continue;
|
|
107
|
+
terms.push(stemWord(word));
|
|
108
|
+
}
|
|
109
|
+
return terms;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Unique query terms: stopwords and instruction framing dropped, stemmed. */
|
|
113
|
+
export function queryTerms(text) {
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
const terms = [];
|
|
116
|
+
for (const word of splitWords(text)) {
|
|
117
|
+
if (STOPWORDS.has(word) || QUERY_FRAMING.has(word)) continue;
|
|
118
|
+
const stem = stemWord(word);
|
|
119
|
+
if (QUERY_FRAMING.has(stem) || seen.has(stem)) continue;
|
|
120
|
+
seen.add(stem);
|
|
121
|
+
terms.push(stem);
|
|
122
|
+
}
|
|
123
|
+
return terms;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The first raw word behind each query term, for disclosures ("japanes" → "Japanese"). */
|
|
127
|
+
export function queryTermOrigins(text) {
|
|
128
|
+
const origins = new Map();
|
|
129
|
+
for (const word of String(text ?? '').split(/[^A-Za-z0-9]+/)) {
|
|
130
|
+
if (word.length < 2 || /^\d+$/.test(word)) continue;
|
|
131
|
+
const stem = stemWord(word.toLowerCase());
|
|
132
|
+
if (!origins.has(stem)) origins.set(stem, word);
|
|
133
|
+
}
|
|
134
|
+
return origins;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const K1 = 1.2;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A fielded keyword index. `fields` maps a field name to `{ weight, b }`;
|
|
141
|
+
* each document supplies text per field.
|
|
142
|
+
*/
|
|
143
|
+
export function createKeywordIndex(fields, documents) {
|
|
144
|
+
const entries = documents.map((document) => {
|
|
145
|
+
const perField = {};
|
|
146
|
+
for (const name of Object.keys(fields)) {
|
|
147
|
+
const terms = indexTerms(document.fields[name] ?? '');
|
|
148
|
+
const counts = new Map();
|
|
149
|
+
for (const term of terms) counts.set(term, (counts.get(term) ?? 0) + 1);
|
|
150
|
+
perField[name] = { counts, length: terms.length };
|
|
151
|
+
}
|
|
152
|
+
return { id: document.id, perField };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const averageLength = {};
|
|
156
|
+
for (const name of Object.keys(fields)) {
|
|
157
|
+
const total = entries.reduce((sum, entry) => sum + entry.perField[name].length, 0);
|
|
158
|
+
averageLength[name] = entries.length > 0 ? Math.max(1, total / entries.length) : 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const documentFrequency = new Map();
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (const name of Object.keys(fields)) {
|
|
165
|
+
for (const term of entry.perField[name].counts.keys()) seen.add(term);
|
|
166
|
+
}
|
|
167
|
+
for (const term of seen) documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const count = entries.length;
|
|
171
|
+
const idf = (term) => {
|
|
172
|
+
const frequency = documentFrequency.get(term) ?? 0;
|
|
173
|
+
return Math.log(1 + (count - frequency + 0.5) / (frequency + 0.5));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
count,
|
|
178
|
+
documentFrequency: (term) => documentFrequency.get(term) ?? 0,
|
|
179
|
+
idf,
|
|
180
|
+
/** `{ id, score, matched }` per document with any matched term. */
|
|
181
|
+
score(terms) {
|
|
182
|
+
const results = [];
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
let score = 0;
|
|
185
|
+
const matched = [];
|
|
186
|
+
for (const term of terms) {
|
|
187
|
+
let pseudo = 0;
|
|
188
|
+
for (const [name, { weight, b }] of Object.entries(fields)) {
|
|
189
|
+
const field = entry.perField[name];
|
|
190
|
+
const frequency = field.counts.get(term);
|
|
191
|
+
if (!frequency) continue;
|
|
192
|
+
pseudo += (weight * frequency) / (1 - b + (b * field.length) / averageLength[name]);
|
|
193
|
+
}
|
|
194
|
+
if (pseudo === 0) continue;
|
|
195
|
+
matched.push(term);
|
|
196
|
+
score += idf(term) * ((pseudo * (K1 + 1)) / (K1 + pseudo));
|
|
197
|
+
}
|
|
198
|
+
if (matched.length > 0) results.push({ id: entry.id, score, matched });
|
|
199
|
+
}
|
|
200
|
+
return results;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|