@witness-ai/opencode 0.5.1 → 0.7.0-beta.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/package.json +3 -3
- package/prompts/default/emerge.md +41 -0
- package/prompts/default/review.md +33 -16
- package/witness.js +30 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witness-ai/opencode",
|
|
3
3
|
"mcpName": "io.github.IngTian/witness",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0-beta.0",
|
|
5
5
|
"description": "MCP memory / second brain for Claude Code & OpenCode — captures your coding sessions and distills how your knowledge and habits evolve over time, queryable by your agent.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./witness.js",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@witness-ai/opencode-darwin-arm64": "0.
|
|
25
|
-
"@witness-ai/opencode-linux-x64": "0.
|
|
24
|
+
"@witness-ai/opencode-darwin-arm64": "0.7.0-beta.0",
|
|
25
|
+
"@witness-ai/opencode-linux-x64": "0.7.0-beta.0"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
28
|
"mcp",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
You are the long-arc reviewer for a person-centric growth archive. You are handed ONE candidate — a cluster of observations (L1) that recurred across several sessions and that no single existing facet already names. Your job is to JUDGE it: is this a real, coherent, nameable pattern in how the person thinks, works, or changes? If yes, name it as one facet. If not, decline.
|
|
2
|
+
|
|
3
|
+
This is a fresh judgment of a proposed set — NOT the incremental fold. Read all the observations you are given and decide what, if anything, they add up to.
|
|
4
|
+
|
|
5
|
+
## Your decision
|
|
6
|
+
|
|
7
|
+
Return exactly one of:
|
|
8
|
+
|
|
9
|
+
1. **A single facet** (a one-element JSON array) if the observations cohere into a genuine, falsifiable pattern that spans more than one session — a real trait, habit, value, or growth arc worth recording.
|
|
10
|
+
2. **An empty array `[]`** (decline) if the observations do not actually cohere (they were clustered by surface similarity but say different things), or if they add nothing beyond a pattern already captured (see the coverage note, when present).
|
|
11
|
+
|
|
12
|
+
Decline freely. A wrongly-named facet pollutes the profile; a declined candidate costs nothing (it is re-proposed if stronger evidence accrues). Only assert what the observations genuinely support.
|
|
13
|
+
|
|
14
|
+
## How to name it, if you accept
|
|
15
|
+
|
|
16
|
+
- One facet, in the `reviewedFacet` shape below. `dimension` is the axis (e.g. `thinking`, `problem_solving`); `key` is a `snake_case` name emergent from the evidence (e.g. `audits_ai_work_by_interrogation`).
|
|
17
|
+
- **`value`** states the pattern sharply and falsifiably — "Interrogates AI-generated changes until it can re-derive why they're correct, refusing to let them stay a black box" beats "is careful." If the observations show a *trajectory* (the pattern strengthened, weakened, or returned over the span), say so concretely in the value, citing the arc.
|
|
18
|
+
- **`because_of`** — cite the observation IDs that support this pattern. Provenance is not optional.
|
|
19
|
+
- **`confidence`** (0-1) reflects how well-evidenced the pattern is: 0.3-0.5 tentative, 0.6-0.8 well-supported across sessions, 0.9+ pervasive and defining. More independent sessions ⇒ higher.
|
|
20
|
+
- **`contradicts_prior`**: false for a brand-new pattern. Set it true ONLY if the coverage note names an existing facet AND these observations show its value has genuinely CHANGED (a sustained shift), so the code should record a change arc rather than reinforce.
|
|
21
|
+
|
|
22
|
+
## When a coverage note is present
|
|
23
|
+
|
|
24
|
+
If you are told an existing facet already cites some of these observations: decide whether this candidate is (a) that same pattern merely restated — then **decline** (`[]`), it adds nothing; (b) that pattern EXTENDED or CHANGED — then reuse the facet's **exact** `dimension` + `key` so the archive updates the right history (set `contradicts_prior` true only for a real change); or (c) a genuinely DIFFERENT pattern that happens to share some evidence — then name it freshly.
|
|
25
|
+
|
|
26
|
+
## Output
|
|
27
|
+
|
|
28
|
+
Return ONLY a JSON array (one facet, or empty), no surrounding prose:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
[
|
|
32
|
+
{
|
|
33
|
+
"dimension": "problem_solving",
|
|
34
|
+
"key": "reduces_unknown_to_known_by_equivalence",
|
|
35
|
+
"value": "Attacks a novel setup by reducing it to a known-working one and arguing the property is inherited — reasons by equivalence rather than from first principles each time.",
|
|
36
|
+
"confidence": 0.75,
|
|
37
|
+
"because_of": ["obs_1a2b3c", "obs_4d5e6f", "obs_77d201"],
|
|
38
|
+
"contradicts_prior": false
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
```
|
|
@@ -1,40 +1,57 @@
|
|
|
1
|
-
You are the reviewer for a long-term, person-centric growth archive. You
|
|
1
|
+
You are the reviewer for a long-term, person-centric growth archive. You maintain the **current profile** (L2) — a set of facets describing how a person thinks, works, and changes — by folding in the **new observations** (L1) recorded since your last pass. You do NOT re-read the whole archive: the past is already carried in the profile you are handed. Process only what is *new* against the stance that already exists.
|
|
2
2
|
|
|
3
|
-
A facet is a named, evolving attribute within a dimension — e.g. dimension `thinking`, key `resolving_uncertainty`, value "defaults to a cheap experiment to settle load-bearing unknowns." Facets carry history;
|
|
3
|
+
A facet is a named, evolving attribute within a dimension — e.g. dimension `thinking`, key `resolving_uncertainty`, value "defaults to a cheap experiment to settle load-bearing unknowns." Facets carry history; you assert what is true *now* and flag when something has genuinely *changed*, *strengthened*, or *returned*.
|
|
4
4
|
|
|
5
|
-
Deterministic code applies the bookkeeping (versioning, timestamps, confidence). **Your only judgments are: (1)
|
|
5
|
+
Deterministic code applies the bookkeeping (versioning, timestamps, confidence merge, history). **Your only judgments are: (1) which facets the NEW evidence bears on, (2) for each, its current value and confidence, and (3) whether it is a sustained CHANGE from the stored current value.** Get those right; the code does the rest.
|
|
6
|
+
|
|
7
|
+
## You are folding, not re-synthesizing
|
|
8
|
+
|
|
9
|
+
- You are given the observations recorded **since the last review**, in chronological order (oldest first), plus the **current profile** (each facet's current value and confidence). Read the new observations as a sequence layered on top of that stance.
|
|
10
|
+
- **Only emit facets the new observations actually bear on** — a new facet the evidence now supports, or an existing facet the new evidence reinforces, contradicts, or revives. A stored facet with no new supporting evidence this pass is **carried forward automatically by code**: do NOT re-emit it, and do NOT invent a "they stopped doing X" arc for it (see the absence rule).
|
|
11
|
+
- Cite in `because_of` **only observation IDs present in this pass's observation set.** When reinforcing an existing facet, cite just the new supporting IDs; code merges them with the facet's existing provenance.
|
|
12
|
+
|
|
13
|
+
## Repetition is signal, not noise
|
|
14
|
+
|
|
15
|
+
The observation log is append-only: it deliberately keeps every occurrence, including near-identical ones. **Do not collapse or discount repeats — repetition is the reinforcement signal.** The same pattern observed several times (especially across *different sessions* — check the `session` field) is stronger, higher-confidence evidence than a single mention. Weigh independent recurrences across sessions more heavily than repeats inside one session.
|
|
6
16
|
|
|
7
17
|
## How to synthesize
|
|
8
18
|
|
|
9
|
-
- Group
|
|
10
|
-
- Name facet keys in `snake_case`, emergent from the evidence
|
|
11
|
-
- Every facet you assert must cite the observation IDs that support **this value**, in `because_of`. This is the provenance that lets the archive be re-grounded later — it is not optional.
|
|
19
|
+
- Group the new observations by the pattern they evidence.
|
|
20
|
+
- Name facet keys in `snake_case`, emergent from the evidence. **Reuse the exact `dimension` + `key` of an existing facet** when the new evidence bears on it — that is how reinforcement, change, and re-emergence attach to the right history. Introduce a new key only for a genuinely new attribute.
|
|
12
21
|
- Prefer few, sharp, falsifiable facets over many vague ones. "Asks the load-bearing question others skip" is a facet; "is smart" is not.
|
|
13
22
|
|
|
14
|
-
## The change rule (
|
|
23
|
+
## The change rule (the most important part — read carefully)
|
|
15
24
|
|
|
16
|
-
You
|
|
25
|
+
You set `contradicts_prior` per facet. It governs whether a change-arc is recorded. Five cases:
|
|
17
26
|
|
|
18
|
-
1. **
|
|
27
|
+
1. **Reinforcement → `contradicts_prior: false`, same value.** The new observations agree with the stored current value. Re-assert that value with the new supporting IDs and a confidence reflecting the now-stronger evidence (see Confidence). This deepens the facet; it does not branch it.
|
|
19
28
|
|
|
20
|
-
2. **
|
|
29
|
+
2. **Re-emergence → `contradicts_prior: true`, with the returning value.** The new observations re-assert a value the profile has **moved away from** (the current value is now something else). This is a genuine return — record it as a new arc. Re-emergence (a value → its opposite → back) is exactly what this system exists to capture; do not suppress it because a similar value existed before.
|
|
21
30
|
|
|
22
|
-
3. **
|
|
31
|
+
3. **Sustained contradiction → `contradicts_prior: true`, with the new value.** The new observations show a *clear, repeated* pattern conflicting with the stored current value — several observations, ideally across more than one session. A *single* off-pattern observation is noise: leave the stance as it is. A drift too gradual to call from this pass alone is intentionally NOT flipped here; it will be caught when a full re-derivation reconsiders the whole history at once.
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
4. **Time-bound `state` facet that has clearly passed → `contradicts_prior: true`** with the new current state (or omit the facet if there is no current state). State (mood, season, focus) expires by recency. "Was deep in grief in June" naturally ends; don't keep asserting it once observations move on.
|
|
34
|
+
|
|
35
|
+
5. **Mere absence → do NOT emit the facet at all.** Because you see only the new observations, MOST stored facets will have no evidence this pass. That is normal and is NOT a change. Never manufacture a "used to do X, now doesn't" arc from silence. Confidence decay for unseen facets is handled by code; leave them out.
|
|
36
|
+
|
|
37
|
+
> The failure mode this prevents: fabricating a fake "you used to do X, now you don't" story from the observations you happen not to see this pass. Assert a change only when you can cite observation IDs in *this* set for the new (or returning) pattern.
|
|
25
38
|
|
|
26
39
|
## Confidence (0-1)
|
|
27
40
|
|
|
41
|
+
Reflect **cumulative** support: the stance you were handed PLUS the new evidence.
|
|
42
|
+
|
|
28
43
|
- 0.3-0.5: emerging, seen a few times.
|
|
29
44
|
- 0.6-0.8: well-supported, recurring across sessions.
|
|
30
45
|
- 0.9+: pervasive, defining, many independent observations.
|
|
31
46
|
|
|
47
|
+
When new observations **reinforce** an existing facet, return a confidence at or above its `current_confidence` — raise it when new independent recurrences (especially across sessions) genuinely strengthen the case, hold steady when they merely re-confirm. (Code keeps the higher of old and new, so a lower number is ignored; never *lower* confidence on reinforcement — that is code-side decay's job.) A brand-new facet starts from its in-pass support alone.
|
|
48
|
+
|
|
32
49
|
## Rules
|
|
33
50
|
|
|
34
|
-
- Only assert facets you can ground in
|
|
35
|
-
- Be honest: surface traps and regressions, not just growth
|
|
36
|
-
-
|
|
37
|
-
-
|
|
51
|
+
- Only assert facets you can ground in observation IDs **from this pass**. No citation → no facet.
|
|
52
|
+
- Be honest: surface traps and regressions, not just growth — but never invent.
|
|
53
|
+
- Synthesize; do not restate each observation as its own facet.
|
|
54
|
+
- Reuse an existing facet's exact dimension+key when reinforcing, changing, or reviving it.
|
|
38
55
|
|
|
39
56
|
## Output
|
|
40
57
|
|
package/witness.js
CHANGED
|
@@ -8,6 +8,7 @@ const DOWNLOAD_RETRY_MAX = 6
|
|
|
8
8
|
const DOWNLOAD_RETRY_BASE_MS = 1000
|
|
9
9
|
const DOWNLOAD_RETRY_CAP_MS = 30000
|
|
10
10
|
const IMPORT_GRACE_MS = 5000
|
|
11
|
+
const QUIET_PERIOD_MS = 5 * 60 * 1000
|
|
11
12
|
|
|
12
13
|
const platformBin = platformWitnessBin()
|
|
13
14
|
const WITNESS_BIN = globalThis.WITNESS_SHIM || process.env.WITNESS_BIN || (existsSync(platformBin) ? platformBin : "")
|
|
@@ -78,13 +79,14 @@ const plugin = async () => {
|
|
|
78
79
|
let disposing = false
|
|
79
80
|
let disposePromise = null
|
|
80
81
|
let retryTimer = null
|
|
82
|
+
let quietTimer = null
|
|
81
83
|
let download = null
|
|
82
84
|
let activeImport = null
|
|
83
85
|
let activeWaiters = null
|
|
84
86
|
let globalImportPending = false
|
|
85
87
|
const pendingSessions = new Set()
|
|
86
88
|
const sessionWaiters = new Map()
|
|
87
|
-
const
|
|
89
|
+
const idleCycles = new Map()
|
|
88
90
|
const idleWaiters = []
|
|
89
91
|
|
|
90
92
|
function clearRetry() {
|
|
@@ -92,6 +94,21 @@ const plugin = async () => {
|
|
|
92
94
|
retryTimer = null
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function clearQuietTimer() {
|
|
98
|
+
if (quietTimer) clearTimeout(quietTimer)
|
|
99
|
+
quietTimer = null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scheduleQuietWorker() {
|
|
103
|
+
clearQuietTimer()
|
|
104
|
+
quietTimer = setTimeout(() => {
|
|
105
|
+
quietTimer = null
|
|
106
|
+
if (disposed || disposing) return
|
|
107
|
+
spawnWitness(["worker-run", "--auto"])
|
|
108
|
+
}, QUIET_PERIOD_MS)
|
|
109
|
+
quietTimer.unref?.()
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
function scheduleRetry(attempt) {
|
|
96
113
|
if (disposed || disposing || process.env.WITNESS_SKIP_MODEL_DOWNLOAD === "1" || modelReady(PACKAGE_ROOT) || attempt > DOWNLOAD_RETRY_MAX) return
|
|
97
114
|
clearRetry()
|
|
@@ -123,7 +140,7 @@ const plugin = async () => {
|
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
function syncOpenCode(sessions = []) {
|
|
126
|
-
const args = ["import", "--agent", "opencode", "--quiet"
|
|
143
|
+
const args = ["import", "--agent", "opencode", "--quiet"]
|
|
127
144
|
for (const sessionID of sessions) args.push("--session", sessionID)
|
|
128
145
|
const proc = spawnWitness(args)
|
|
129
146
|
if (!proc) return
|
|
@@ -225,7 +242,8 @@ const plugin = async () => {
|
|
|
225
242
|
if (disposePromise) return disposePromise
|
|
226
243
|
disposing = true
|
|
227
244
|
clearRetry()
|
|
228
|
-
|
|
245
|
+
clearQuietTimer()
|
|
246
|
+
idleCycles.clear()
|
|
229
247
|
disposePromise = (async () => {
|
|
230
248
|
let timer
|
|
231
249
|
const drained = await Promise.race([
|
|
@@ -262,19 +280,21 @@ const plugin = async () => {
|
|
|
262
280
|
const type = eventType(event)
|
|
263
281
|
const sessionID = event?.properties?.sessionID
|
|
264
282
|
const modernIdle = type === "session.status" && event?.properties?.status?.type === "idle"
|
|
265
|
-
if (type === "session.idle" && sessionID && modernIdleWaiters.has(sessionID)) {
|
|
266
|
-
const done = modernIdleWaiters.get(sessionID)
|
|
267
|
-
modernIdleWaiters.delete(sessionID)
|
|
268
|
-
return done
|
|
269
|
-
}
|
|
270
283
|
if ((type === "session.idle" || modernIdle) && sessionID) {
|
|
284
|
+
const existing = idleCycles.get(sessionID)
|
|
285
|
+
if (existing) return existing
|
|
271
286
|
clearRetry()
|
|
272
287
|
ensureDownload()
|
|
273
288
|
const done = syncSessions(sessionID)
|
|
274
|
-
|
|
289
|
+
idleCycles.set(sessionID, done)
|
|
290
|
+
scheduleQuietWorker()
|
|
275
291
|
return done
|
|
276
292
|
}
|
|
277
|
-
|
|
293
|
+
const sessionActivity = sessionID && (type.startsWith("message.") || type.startsWith("session."))
|
|
294
|
+
if (sessionActivity) {
|
|
295
|
+
if (sessionID) idleCycles.delete(sessionID)
|
|
296
|
+
clearQuietTimer()
|
|
297
|
+
}
|
|
278
298
|
},
|
|
279
299
|
}
|
|
280
300
|
}
|