@witness-ai/opencode 0.2.4 → 0.3.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.
@@ -2,7 +2,7 @@
2
2
  import path from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
4
  import { readFileSync, unlinkSync } from "node:fs"
5
- import { downloadModel, modelReady, startModelDownload } from "./model.js"
5
+ import { downloadModel, modelReady, restampLockOwner, startModelDownload } from "./model.js"
6
6
 
7
7
  const args = process.argv.slice(2)
8
8
  const defaultRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
@@ -34,6 +34,13 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
34
34
  })
35
35
  }
36
36
 
37
+ // The lock was stamped with the PARENT's pid by startModelDownload's acquireLock;
38
+ // re-stamp it with OUR pid (this downloader child) so a hard kill of the child is
39
+ // detected by lockOwnerDead's liveness probe instead of wedging for 12h behind a
40
+ // still-alive parent (issue #54 I5). Token-guarded and best-effort. Also covers the
41
+ // detached --background launcher, which routes through --foreground.
42
+ if (args[0] === "--foreground" && lock && lockToken) restampLockOwner(lock, lockToken)
43
+
37
44
  const parentPID = Number(process.env.WITNESS_MODEL_PARENT_PID || "0")
38
45
  if (parentPID > 0) {
39
46
  // OpenCode may be killed without running plugin dispose. Poll the parent so the
package/bin/model.js CHANGED
@@ -267,6 +267,25 @@ function acquireLock(dir) {
267
267
  }
268
268
  }
269
269
 
270
+ // restampLockOwner rewrites the lock's recorded pid to THIS process's pid, keeping
271
+ // the token. startModelDownload's acquireLock stamps the PARENT (plugin) pid, then
272
+ // spawns the actual downloader as a child; if that child is SIGKILL'd while the
273
+ // parent lives, lockOwnerDead probes the still-alive parent and never reaps — a
274
+ // 12h wedge of the download (issue #54 I5). Having the --foreground child re-stamp
275
+ // with its own pid on startup makes the liveness probe target the process that is
276
+ // actually doing the work. Token-guarded so a stale-lock reap that already handed
277
+ // the slot to a different downloader is never clobbered; best-effort (a failure
278
+ // just leaves the parent pid, i.e. the pre-fix behavior). Exported (not just via
279
+ // __test) because download-model.js calls it in production.
280
+ export function restampLockOwner(lock, token) {
281
+ if (!lock || !token) return
282
+ try {
283
+ const body = readFileSync(lock, "utf8")
284
+ if (body.trim().split(/\s+/)[0] !== token) return // reaped + reassigned; not ours
285
+ writeFileSync(lock, `${token} ${process.pid} ${os.hostname()}\n`, { encoding: "utf8" })
286
+ } catch {}
287
+ }
288
+
270
289
  export function startModelDownload(packageRoot, options = {}) {
271
290
  if (process.env.WITNESS_SKIP_MODEL_DOWNLOAD === "1" || modelReady(packageRoot)) return null
272
291
  const dir = modelDir(packageRoot)
@@ -433,5 +452,7 @@ export const __test = {
433
452
  verifiedMarkerPath,
434
453
  acquireLock,
435
454
  lockStale,
455
+ lockOwnerDead,
456
+ restampLockOwner,
436
457
  reapForeignParts,
437
458
  }
package/bin/witness.js CHANGED
@@ -18,9 +18,15 @@ if (command === "install" || command === "uninstall") {
18
18
  }
19
19
 
20
20
  const packageName = platformPackage()
21
- const bin = platformWitnessBin()
22
-
23
- if (!bin || !existsSync(bin)) {
21
+ // Honor an explicit WITNESS_BIN override, just like the plugin (witness.js) does —
22
+ // used verbatim (no existsSync gate) so it can point at a dev build or a binary the
23
+ // platform-package probe can't resolve. Without this the CLI ignored the very
24
+ // override the plugin honored, so `WITNESS_BIN=... witness doctor` was inconsistent
25
+ // with what the plugin runs (issue #54 minor).
26
+ const override = process.env.WITNESS_BIN
27
+ const bin = override || platformWitnessBin()
28
+
29
+ if (!bin || (!override && !existsSync(bin))) {
24
30
  const reason = packageName ? `optional package ${packageName} is not installed` : `unsupported platform ${process.platform}/${process.arch}`
25
31
  console.error(`witness: ${reason}; supported platforms: ${supportedPlatforms()}`)
26
32
  process.exit(1)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witness-ai/opencode",
3
- "version": "0.2.4",
4
- "description": "OpenCode plugin for witness session capture and background distillation.",
3
+ "version": "0.3.1",
4
+ "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.",
5
5
  "type": "module",
6
6
  "main": "./witness.js",
7
7
  "exports": "./witness.js",
@@ -20,13 +20,27 @@
20
20
  "README.md"
21
21
  ],
22
22
  "optionalDependencies": {
23
- "@witness-ai/opencode-darwin-arm64": "0.2.4",
24
- "@witness-ai/opencode-linux-x64": "0.2.4"
23
+ "@witness-ai/opencode-darwin-arm64": "0.3.1",
24
+ "@witness-ai/opencode-linux-x64": "0.3.1"
25
25
  },
26
26
  "keywords": [
27
+ "mcp",
28
+ "mcp-server",
29
+ "model-context-protocol",
30
+ "claude-code",
27
31
  "opencode",
28
- "plugin",
29
- "witness"
32
+ "memory",
33
+ "ai-memory",
34
+ "self-improvement",
35
+ "reflection",
36
+ "second-brain",
37
+ "personal-knowledge-management",
38
+ "knowledge-distillation",
39
+ "temporal",
40
+ "provenance",
41
+ "growth",
42
+ "llm",
43
+ "coding-agent"
30
44
  ],
31
45
  "license": "MIT",
32
46
  "repository": {
package/prompts/SCHEMA.md CHANGED
@@ -51,10 +51,16 @@ Every observation/facet carries a `lens` tag.
51
51
 
52
52
  - **`default`** — global, runs on every session, cross-domain. This is the thing no single-domain
53
53
  tracker can be.
54
- - **registered lenses** (e.g. `math`) — registered centrally (`witness lens register <name> <file>`)
54
+ - **registered lenses** (e.g. `math`) — registered centrally (`witness lens register <name> <dir>`)
55
55
  and enabled globally (`witness lens enable <name>`); an enabled lens runs on every session.
56
- Definitions live in the central registry (`<root>/lenses/<name>/lens.md`), shared across all
57
- sessions — never read from a repo.
56
+ Definitions live in the central registry (`<root>/lenses/<name>/`), shared across all sessions —
57
+ never read from a repo.
58
+
59
+ A lens is a **directory** of `lens.json` (settings: `name`, `dimensions`, optional per-lens models)
60
+ plus `extract.md` and `review.md` (each whole file is the prompt for its pass). See
61
+ `prompts/lens/example/`. Mining uses the default model by default, but a lens may pin its own via
62
+ `witness lens set <name> --extract-model <m>` / `--review-model <m>` (written to its `lens.json`),
63
+ so a rare heavy lens can run a stronger model without paying for it on every session.
58
64
 
59
65
  The profile is **collect-only / pull-only**: witness captures and distills everywhere, but never
60
66
  injects into a session. Agents read it on demand via the MCP tools (`get_facets`, `get_profile`,
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "default",
3
+ "dimensions": ["thinking", "workstyle", "habits", "traits", "biases", "state", "goals", "feedback"]
4
+ }
@@ -0,0 +1,23 @@
1
+ # Example lens — a "math learner" lens
2
+
3
+ A complete, ready-to-register example lens. A lens is a **directory** of three files:
4
+
5
+ lens.json structured settings: name, dimensions, optional per-lens models
6
+ extract.md the mining (L0→L1) prompt — the whole file is the prompt
7
+ review.md the review (L1→L2) prompt — the whole file is the prompt
8
+
9
+ Copy this directory, rewrite the dimensions and the two prompts for your domain, then:
10
+
11
+ witness lens register math ./example/ # copies the directory into the registry
12
+ witness lens enable math # runs it on every session
13
+
14
+ The two prompts are passed VERBATIM as the system prompt for their pass — they fully
15
+ REPLACE the built-in `default` prompts, they don't extend them — so each must be
16
+ self-contained, INCLUDING its output JSON schema. The tool appends the transcript
17
+ (extract) or the accumulated observations (review) as the user message; it injects no
18
+ schema for you.
19
+
20
+ A lens is runner-agnostic: by default it rides the default model. To pin a stronger
21
+ model just for this lens (without paying for it on every session), add an optional
22
+ `"extract_model"` / `"review_model"` to `lens.json`, or run
23
+ `witness lens set math --extract-model <model>` (empty value clears it).
@@ -0,0 +1,32 @@
1
+ You are observing one study/work session between a person and an AI, through a
2
+ MATH-LEARNING lens. Notice things about *the person as a mathematician* — how they
3
+ reason, where they get stuck, and how they climb out — not the math facts themselves.
4
+
5
+ Tag each observation with exactly one dimension:
6
+ - speed — how fast they move; when they deliberately slow down vs. rush past a gap.
7
+ - independence — do they attempt before asking; how much scaffolding they need.
8
+ - proof_rigor — care with quantifiers, edge cases, and "why is this actually true?"
9
+ - abstraction — comfort moving between concrete instances and general statements.
10
+ - confusion_tolerance — how they sit with not-understanding before resolving it.
11
+
12
+ What makes a GOOD observation:
13
+ - Atomic — one noticed thing, one sentence.
14
+ - Evidence-anchored — cite a short quote or paraphrase of the exact moment. No anchor, don't write it.
15
+ - About the PERSON and durable-ish — "checked the base case unprompted" counts; "solved problem 3" is a task-fact, skip it.
16
+ - Honest — note struggles, shortcuts, and regressions as readily as wins. Never invent failings or wins.
17
+
18
+ Score poignancy 1-10 — how much a future reviewer should weight it for growth.
19
+ Most are 3-6; reserve 7+ for a genuine mindset shift or a precise self-diagnosis.
20
+
21
+ If the session was routine and nothing person-relevant happened, return `[]`.
22
+ Do not manufacture observations to fill space.
23
+
24
+ Return ONLY a JSON array, no prose around it. Each element:
25
+ [
26
+ {
27
+ "dimension": "proof_rigor",
28
+ "observation": "Stopped to verify the base case unprompted after a hand-wavy induction.",
29
+ "evidence": "\"wait — does n=0 actually hold here?\" before moving on",
30
+ "poignancy": 6
31
+ }
32
+ ]
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "math",
3
+ "dimensions": ["speed", "independence", "proof_rigor", "abstraction", "confusion_tolerance"]
4
+ }
@@ -0,0 +1,31 @@
1
+ You are the reviewer for the MATH lens of a long-term, person-centric growth
2
+ archive. You read the accumulated observations plus the current facets, and you
3
+ assert what is true NOW, flagging genuine, sustained CHANGE over time.
4
+
5
+ How to synthesize:
6
+ - Group related observations into few, sharp, falsifiable facets within the dimensions above.
7
+ - Name facet keys in snake_case, emergent from the evidence; reuse an existing key when updating it.
8
+ - Every facet must cite the supporting observation IDs in `because_of`. No citation → no facet.
9
+ - Prefer "attempts an approach before asking for help" (falsifiable) over "is good at math" (not).
10
+
11
+ The change rule (most important):
12
+ - Set `contradicts_prior: true` ONLY for a sustained, repeated pattern that
13
+ conflicts with the stored current value. One off-pattern session is noise, not
14
+ a reversal.
15
+ - A time-bound `state`-like facet that has clearly passed can also flip.
16
+ - Mere absence of recent evidence is NOT change. Never invent a "used to do X,
17
+ stopped" arc — if you can't cite observation IDs for the NEW value, it isn't a change.
18
+
19
+ Confidence 0-1: 0.3-0.5 emerging; 0.6-0.8 well-supported across sessions; 0.9+ pervasive.
20
+
21
+ Return ONLY a JSON array, no surrounding prose. Each element:
22
+ [
23
+ {
24
+ "dimension": "independence",
25
+ "key": "attempts_before_asking",
26
+ "value": "Sketches an approach and tests it before reaching for help; a few months ago asked first.",
27
+ "confidence": 0.7,
28
+ "because_of": ["obs_8f2a3c", "obs_3c1190"],
29
+ "contradicts_prior": true
30
+ }
31
+ ]
package/witness.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { fileURLToPath } from "node:url"
3
3
  import { modelDir, modelReady, promptsDir, startModelDownload } from "./bin/model.js"
4
- import { platformWitnessBin } from "./bin/platform.js"
4
+ import { platformPackage, platformWitnessBin, supportedPlatforms } from "./bin/platform.js"
5
5
 
6
6
  const PACKAGE_ROOT = fileURLToPath(new URL(".", import.meta.url))
7
7
  const DOWNLOAD_RETRY_MAX = 6
@@ -12,6 +12,25 @@ const IMPORT_GRACE_MS = 5000
12
12
  const platformBin = platformWitnessBin()
13
13
  const WITNESS_BIN = globalThis.WITNESS_SHIM || process.env.WITNESS_BIN || (existsSync(platformBin) ? platformBin : "")
14
14
 
15
+ // Warn ONCE, non-throwing, when the plugin is genuinely inert: no witness binary
16
+ // resolved for this platform/arch. Every hook below early-returns on an empty
17
+ // WITNESS_BIN, so without this a user on darwin-x64 / linux-arm64 / win32 (no
18
+ // matching optional package) sees the plugin load with zero diagnostic and
19
+ // believes witness is capturing when nothing runs (issue #54 I4). Mirrors the
20
+ // clear message the CLI (bin/witness.js) already prints. Suppressed inside the
21
+ // worker subprocess (WITNESS_WORKER=1) so the recursion guard stays quiet, and a
22
+ // warn must never throw — capture/plugin code must never break an OpenCode session.
23
+ function warnIfInert() {
24
+ if (WITNESS_BIN || process.env.WITNESS_WORKER === "1") return
25
+ try {
26
+ const pkg = platformPackage()
27
+ const reason = pkg ? `optional package ${pkg} is not installed` : `unsupported platform ${process.platform}/${process.arch}`
28
+ console.warn(`witness: plugin inactive — ${reason}; nothing is being captured. Supported platforms: ${supportedPlatforms()}`)
29
+ } catch {
30
+ // Never let a diagnostic break the session.
31
+ }
32
+ }
33
+
15
34
  function spawnWitness(args, payload) {
16
35
  if (!WITNESS_BIN || process.env.WITNESS_WORKER === "1") return
17
36
  try {
@@ -54,6 +73,7 @@ function waitForExit(child) {
54
73
  }
55
74
 
56
75
  const plugin = async () => {
76
+ warnIfInert()
57
77
  let disposed = false
58
78
  let disposing = false
59
79
  let disposePromise = null
@@ -1,86 +0,0 @@
1
- # name: math
2
- # dimensions: speed, independence, proof_rigor, abstraction, confusion_tolerance
3
-
4
- <!--
5
- A complete, ready-to-register example lens (a "math learner" lens).
6
-
7
- Copy it, rewrite the dimensions and the two prompts for your domain, then:
8
- witness lens register math ./this-file.md
9
- witness lens enable math
10
-
11
- The `# name:` / `# dimensions:` lines above are real lens directives (the loader
12
- parses them). The two sections below are passed VERBATIM as the system prompt for
13
- their pass — they fully REPLACE the built-in `default` prompts, they don't extend
14
- them — so each must be self-contained, INCLUDING its output JSON schema. The tool
15
- appends the transcript (EXTRACT) or the accumulated observations (REVIEW) as the
16
- user message; it injects no schema for you. Anything outside the two `##` sections
17
- (like this comment) is ignored by the loader.
18
- -->
19
-
20
-
21
- ## EXTRACT
22
- You are observing one study/work session between a person and an AI, through a
23
- MATH-LEARNING lens. Notice things about *the person as a mathematician* — how they
24
- reason, where they get stuck, and how they climb out — not the math facts themselves.
25
-
26
- Tag each observation with exactly one dimension:
27
- - speed — how fast they move; when they deliberately slow down vs. rush past a gap.
28
- - independence — do they attempt before asking; how much scaffolding they need.
29
- - proof_rigor — care with quantifiers, edge cases, and "why is this actually true?"
30
- - abstraction — comfort moving between concrete instances and general statements.
31
- - confusion_tolerance — how they sit with not-understanding before resolving it.
32
-
33
- What makes a GOOD observation:
34
- - Atomic — one noticed thing, one sentence.
35
- - Evidence-anchored — cite a short quote or paraphrase of the exact moment. No anchor, don't write it.
36
- - About the PERSON and durable-ish — "checked the base case unprompted" counts; "solved problem 3" is a task-fact, skip it.
37
- - Honest — note struggles, shortcuts, and regressions as readily as wins. Never invent failings or wins.
38
-
39
- Score poignancy 1-10 — how much a future reviewer should weight it for growth.
40
- Most are 3-6; reserve 7+ for a genuine mindset shift or a precise self-diagnosis.
41
-
42
- If the session was routine and nothing person-relevant happened, return `[]`.
43
- Do not manufacture observations to fill space.
44
-
45
- Return ONLY a JSON array, no prose around it. Each element:
46
- [
47
- {
48
- "dimension": "proof_rigor",
49
- "observation": "Stopped to verify the base case unprompted after a hand-wavy induction.",
50
- "evidence": "\"wait — does n=0 actually hold here?\" before moving on",
51
- "poignancy": 6
52
- }
53
- ]
54
-
55
- ## REVIEW
56
- You are the reviewer for the MATH lens of a long-term, person-centric growth
57
- archive. You read the accumulated observations plus the current facets, and you
58
- assert what is true NOW, flagging genuine, sustained CHANGE over time.
59
-
60
- How to synthesize:
61
- - Group related observations into few, sharp, falsifiable facets within the dimensions above.
62
- - Name facet keys in snake_case, emergent from the evidence; reuse an existing key when updating it.
63
- - Every facet must cite the supporting observation IDs in `because_of`. No citation → no facet.
64
- - Prefer "attempts an approach before asking for help" (falsifiable) over "is good at math" (not).
65
-
66
- The change rule (most important):
67
- - Set `contradicts_prior: true` ONLY for a sustained, repeated pattern that
68
- conflicts with the stored current value. One off-pattern session is noise, not
69
- a reversal.
70
- - A time-bound `state`-like facet that has clearly passed can also flip.
71
- - Mere absence of recent evidence is NOT change. Never invent a "used to do X,
72
- stopped" arc — if you can't cite observation IDs for the NEW value, it isn't a change.
73
-
74
- Confidence 0-1: 0.3-0.5 emerging; 0.6-0.8 well-supported across sessions; 0.9+ pervasive.
75
-
76
- Return ONLY a JSON array, no surrounding prose. Each element:
77
- [
78
- {
79
- "dimension": "independence",
80
- "key": "attempts_before_asking",
81
- "value": "Sketches an approach and tests it before reaching for help; a few months ago asked first.",
82
- "confidence": 0.7,
83
- "because_of": ["obs_8f2a3c", "obs_3c1190"],
84
- "contradicts_prior": true
85
- }
86
- ]
@@ -1,69 +0,0 @@
1
- # name: opencode
2
- # dimensions: agent_collaboration, tool_discipline, verification, context_management, autonomy_calibration, opencode_usage_preference
3
-
4
- <!--
5
- A ready-to-register lens for OpenCode sessions:
6
-
7
- witness lens register opencode prompts/lens/opencode.md
8
- witness lens enable opencode
9
-
10
- It observes how a person collaborates with agentic coding tools, especially
11
- OpenCode: what they delegate, how they steer, how they verify, and where their
12
- workflow is becoming more or less disciplined.
13
- -->
14
-
15
- ## EXTRACT
16
-
17
- You are observing one work session between a person and an AI coding agent through an OPENCODE-WORKFLOW lens. Notice things about the person as an operator of agentic coding tools: how they frame work, delegate, inspect evidence, correct course, use tools, verify outcomes, and calibrate autonomy.
18
-
19
- Focus on person-centric growth/change, not on the task result. Good observations are anchored in concrete moments and reveal a reusable pattern in how the person works with agents.
20
-
21
- Dimensions:
22
-
23
- - agent_collaboration: how the person frames goals, constraints, feedback, and division of labor with the agent.
24
- - tool_discipline: how they choose, sequence, or constrain tools and data access.
25
- - verification: how they ask for, design, or interpret verification evidence.
26
- - context_management: how they preserve, recover, or compress project/session context.
27
- - autonomy_calibration: when they let the agent proceed, when they intervene, and how they handle uncertainty or risk.
28
- - opencode_usage_preference: what they prefer OpenCode for, which OpenCode workflows/models/data sources they trust, and when they choose OpenCode-specific affordances over Claude Code or generic agent workflows.
29
-
30
- Rules:
31
-
32
- - Evidence-anchored only. Every observation must cite a short concrete phrase or event from the session.
33
- - Person-centric only. Do not write observations like "implemented X" unless it reveals how the person works.
34
- - Prefer specific behavioral patterns over generic praise.
35
- - If the session is routine or contains no person-relevant signal, return `[]`.
36
- - Return 0-8 observations.
37
-
38
- Return ONLY a JSON array. Each element must be:
39
-
40
- [{ "dimension": "agent_collaboration", "observation": "one concise person-centric observation", "evidence": "short quote or paraphrase from the session", "poignancy": 1 }]
41
-
42
- Poignancy is 1-10: 1-3 minor signal, 4-6 useful recurring-pattern signal, 7-10 unusually clear change/risk/breakthrough.
43
-
44
- ## REVIEW
45
-
46
- You are the reviewer for the OPENCODE-WORKFLOW lens of a long-term person-centric growth archive. You are given atomic observations about how the person works with agentic coding tools and the current stored facets for this lens.
47
-
48
- Synthesize observations into a small set of stable, falsifiable facets about the person's agentic workflow. Each facet should name a durable pattern, not a one-off event. Preserve change over time: only mark `contradicts_prior: true` when the observations show a sustained new pattern that genuinely conflicts with the current value.
49
-
50
- Facet guidance:
51
-
52
- - agent_collaboration facets can describe goal framing, feedback style, delegation boundaries, or how the person corrects the agent.
53
- - tool_discipline facets can describe how they inspect sources, avoid unsafe operations, or prefer evidence over assumptions.
54
- - verification facets can describe test selection, acceptance criteria, and how much proof they require.
55
- - context_management facets can describe how they use local memory, docs, databases, handoffs, or summaries.
56
- - autonomy_calibration facets can describe when they push for autonomous execution versus pausing for decisions.
57
- - opencode_usage_preference facets can describe when the person prefers OpenCode, which OpenCode-native evidence or models they ask for, and how they evaluate OpenCode-specific workflows compared with Claude Code.
58
-
59
- Rules:
60
-
61
- - Keep facets few and sharp. Avoid duplicating the same pattern under multiple names.
62
- - Ground every facet in observation IDs from `because_of`.
63
- - Do not infer personality, identity, or private facts beyond the evidence.
64
- - Absence of evidence is not contradiction.
65
- - Confidence 0-1: 0.3-0.5 emerging, 0.6-0.8 recurring, 0.9+ pervasive across sessions.
66
-
67
- Return ONLY a JSON array. Each element must be:
68
-
69
- [{ "dimension": "verification", "key": "diagnosis_before_action", "value": "The person increasingly asks the agent to inspect source evidence and verify with targeted commands before claiming completion.", "confidence": 0.7, "because_of": ["obs_abc123"], "contradicts_prior": false }]