threadnote 0.3.4 → 0.3.6

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/README.md CHANGED
@@ -29,6 +29,28 @@ Run `threadnote seed-skills` to make local `SKILL.md` guidance discoverable thro
29
29
  Agents are instructed to compact them into one concise replacement memory and forget only the clearly redundant originals,
30
30
  keeping future recall sharper without losing useful detail.
31
31
 
32
+ **Still working on the same issue?**
33
+ Use `threadnote remember --replace <uri>` or `threadnote handoff --replace <uri>` to keep one current-state memory fresh
34
+ instead of accumulating near-duplicate progress notes.
35
+
36
+ ## Why Not Just CLAUDE.md Or AGENTS.md?
37
+
38
+ Use them. Threadnote is not a replacement for checked-in instructions.
39
+
40
+ `CLAUDE.md`, `AGENTS.md`, Cursor rules, and repo docs are the right place for stable, canonical guidance: coding
41
+ standards, test commands, review rules, architecture notes, and anything the whole team should version and discuss.
42
+
43
+ They are less ideal for living context: what happened in the last agent session, which branch was halfway through a
44
+ refactor, what an on-call investigation concluded, which workaround was verified on one machine, or which duplicate
45
+ memories should be compacted. Putting that history into instruction files makes them noisy, stale, and expensive to load
46
+ into every context window.
47
+
48
+ Threadnote keeps that moving layer local, searchable, and shared across agents. Agents recall only the relevant memories,
49
+ handoffs, and skill/resource pointers when they need them, while the source files stay authoritative for project rules.
50
+
51
+ The split is simple: put durable repo policy in `CLAUDE.md`/`AGENTS.md`; put task history, handoffs, personal workflow
52
+ facts, and local cross-agent memory in Threadnote.
53
+
32
54
  ## Safety Model
33
55
 
34
56
  - Machine writes stay **locally** under `THREADNOTE_HOME`, which defaults to `~/.openviking`.
@@ -50,13 +72,13 @@ Install with one command:
50
72
  curl -fsSL https://raw.githubusercontent.com/Kashkovsky/threadnote/main/scripts/install.sh | sh
51
73
  ```
52
74
 
53
- This installs the published package from npmjs and runs `threadnote install`. It does not use npm `postinstall`,
54
- because setup writes local machine config and should be an explicit action.
75
+ This installs the published package from npmjs and runs `threadnote install`. The install command sets up local config,
76
+ repairs the OpenViking Python environment, and starts the local server so health problems surface immediately. It does
77
+ not use npm `postinstall`, because setup writes local machine config and should be an explicit action.
55
78
 
56
- Start the local server and confirm it is healthy:
79
+ Confirm it is healthy:
57
80
 
58
81
  ```bash
59
- threadnote start
60
82
  threadnote doctor --dry-run
61
83
  ```
62
84
 
@@ -74,7 +96,6 @@ Or install manually:
74
96
  ```bash
75
97
  npm install --global threadnote
76
98
  threadnote install
77
- threadnote start
78
99
  threadnote doctor --dry-run
79
100
  ```
80
101
 
@@ -194,7 +215,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
194
215
 
195
216
  - `doctor`: checks prerequisites, the generated command shim, manifest shape, templates, and local OpenViking health.
196
217
  - `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
197
- writes the command shim, and upserts user-level agent instructions.
218
+ writes the command shim, upserts user-level agent instructions, and starts/checks OpenViking health by default. Use
219
+ `--no-start` to skip the health check.
198
220
  - `update`: updates the published Threadnote package, then runs `repair` so shims and MCP config point at the new
199
221
  version.
200
222
  - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor MCP configs from the
@@ -208,14 +230,16 @@ This is it! Start working with your agents as usual. The agent will automaticall
208
230
  - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog so agents can discover
209
231
  reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
210
232
  - `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
211
- - `remember`: stores a durable memory.
233
+ - `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded one
234
+ after the new memory succeeds.
212
235
  - `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
213
236
  `migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
214
237
  - `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
215
238
  `skills for api service`; use `--uri` or `--no-infer-scope` to override.
216
239
  - `read`: reads a `viking://` URI returned by `recall` or `list`.
217
240
  - `list` / `ls`: lists a `viking://` directory.
218
- - `handoff`: stores current git state and next-step notes as a durable handoff.
241
+ - `handoff`: stores current git state and next-step notes as a durable handoff. Use `--replace <uri>` to update the
242
+ current handoff for the same active issue.
219
243
  - `forget`: removes a `viking://` URI.
220
244
  - `export-pack` / `import-pack`: moves local context through `.ovpack` files.
221
245
 
@@ -240,7 +264,6 @@ any repo or working directory:
240
264
  ```bash
241
265
  threadnote doctor --dry-run
242
266
  threadnote init-manifest --repo ~/src/my-service --repo ~/work/mobile-app
243
- threadnote start
244
267
  threadnote seed --dry-run
245
268
  threadnote seed-skills --dry-run
246
269
  ```
@@ -31095,6 +31095,7 @@ async function main() {
31095
31095
  "Prefer `recall_context` to find candidate viking:// URIs, then `read_context` files or `list_context` directories.",
31096
31096
  'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff"}).',
31097
31097
  "Older clients may use the compatibility aliases `search`, `read`, and `list`.",
31098
+ "When updating the same active issue, pass replaceUri to remember_context so the superseded memory is forgotten after the replacement is stored.",
31098
31099
  "Do not store secrets, customer data, raw production logs, or credentials."
31099
31100
  ].join("\n")
31100
31101
  }
@@ -31379,32 +31380,36 @@ function registerStoreTool(server, config2, name, description) {
31379
31380
  server.registerTool(
31380
31381
  name,
31381
31382
  {
31382
- annotations: { readOnlyHint: false, destructiveHint: false },
31383
+ annotations: { readOnlyHint: false, destructiveHint: true },
31383
31384
  description: `${description} Never store secrets, credentials, customer data, or raw logs.`,
31384
31385
  inputSchema: {
31386
+ replaceUri: external_exports.string().optional().describe("Optional viking:// memory URI to forget after the new memory is safely stored"),
31385
31387
  text: external_exports.string().optional().describe("Required memory text to store"),
31386
31388
  sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, codex, or claude")
31387
31389
  }
31388
31390
  },
31389
- async ({ sourceAgentClient, text }) => {
31391
+ async ({ replaceUri, sourceAgentClient, text }) => {
31390
31392
  const checkedText = requiredText(text, name, "text", { text: "Durable engineering note..." });
31391
31393
  if (!checkedText.ok) {
31392
31394
  return checkedText.error;
31393
31395
  }
31394
- return writeDurableMemory(
31395
- config2,
31396
- [
31397
- "MEMORY",
31398
- `source_agent_client: ${sourceAgentClient ?? "mcp"}`,
31399
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
31400
- "",
31401
- checkedText.value
31402
- ].join("\n")
31403
- );
31396
+ const checkedReplaceUri = optionalVikingUri(replaceUri, name);
31397
+ if (!checkedReplaceUri.ok) {
31398
+ return checkedReplaceUri.error;
31399
+ }
31400
+ const header = [
31401
+ "MEMORY",
31402
+ `source_agent_client: ${sourceAgentClient ?? "mcp"}`,
31403
+ `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
31404
+ ];
31405
+ if (checkedReplaceUri.value) {
31406
+ header.push(`supersedes: ${checkedReplaceUri.value}`);
31407
+ }
31408
+ return writeDurableMemory(config2, [...header, "", checkedText.value].join("\n"), checkedReplaceUri.value);
31404
31409
  }
31405
31410
  );
31406
31411
  }
31407
- async function writeDurableMemory(config2, memory) {
31412
+ async function writeDurableMemory(config2, memory, replaceUri) {
31408
31413
  try {
31409
31414
  const ov = await requiredOpenVikingCli();
31410
31415
  const directoryUri = durableMemoryDirectoryUri(config2);
@@ -31427,8 +31432,13 @@ async function writeDurableMemory(config2, memory) {
31427
31432
  memoryUri,
31428
31433
  withIdentity(config2, ["write", memoryUri, "--content", memory, "--mode", "create", "--wait", "--timeout", "120"])
31429
31434
  );
31435
+ const messages = [`Stored durable memory: ${memoryUri}`];
31436
+ if (replaceUri) {
31437
+ await runCommand(ov, withIdentity(config2, ["rm", replaceUri]));
31438
+ messages.push(`Forgot replaced memory: ${replaceUri}`);
31439
+ }
31430
31440
  const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
31431
- return { content: [{ type: "text", text: [`Stored durable memory: ${memoryUri}`, text].filter(Boolean).join("\n") }] };
31441
+ return { content: [{ type: "text", text: [...messages, text].filter(Boolean).join("\n") }] };
31432
31442
  } catch (err) {
31433
31443
  return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
31434
31444
  }
@@ -7038,14 +7038,16 @@ async function runRemember(config, options) {
7038
7038
  if (!text.trim()) {
7039
7039
  throw new Error("Provide memory text with --text or --stdin.");
7040
7040
  }
7041
- const memory = [
7041
+ const header = [
7042
7042
  "MEMORY",
7043
7043
  `source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
7044
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
7045
- "",
7046
- text.trim()
7047
- ].join("\n");
7048
- await storeMemory(config, memory, options.dryRun === true);
7044
+ `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
7045
+ ];
7046
+ if (options.replace) {
7047
+ header.push(`supersedes: ${options.replace}`);
7048
+ }
7049
+ const memory = [...header, "", text.trim()].join("\n");
7050
+ await storeMemory(config, memory, { dryRun: options.dryRun === true, replaceUri: options.replace });
7049
7051
  }
7050
7052
  async function runMigrateMemories(config, options) {
7051
7053
  const dryRun = options.dryRun === true;
@@ -7160,7 +7162,7 @@ async function runList(config, uri, options) {
7160
7162
  }
7161
7163
  async function runHandoff(config, options) {
7162
7164
  const handoff = await buildHandoff(options);
7163
- await storeMemory(config, handoff, options.dryRun === true);
7165
+ await storeMemory(config, handoff, { dryRun: options.dryRun === true, replaceUri: options.replace });
7164
7166
  }
7165
7167
  async function runForget(config, uri, options) {
7166
7168
  assertVikingUri(uri);
@@ -7231,11 +7233,14 @@ async function printExactMemoryMatches(config, ov, query, dryRun) {
7231
7233
  console.log("\nExact durable memory matches:");
7232
7234
  console.log(outputs.join("\n\n"));
7233
7235
  }
7234
- async function storeMemory(config, memory, dryRun) {
7235
- const ov = await openVikingCliForMode(dryRun);
7236
+ async function storeMemory(config, memory, options) {
7237
+ if (options.replaceUri) {
7238
+ assertVikingUri(options.replaceUri);
7239
+ }
7240
+ const ov = await openVikingCliForMode(options.dryRun);
7236
7241
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
7237
7242
  const memoryUri = durableMemoryUri(config, memory);
7238
- if (dryRun) {
7243
+ if (options.dryRun) {
7239
7244
  console.log(memory);
7240
7245
  console.log("\nWould run:");
7241
7246
  console.log(
@@ -7254,6 +7259,9 @@ async function storeMemory(config, memory, dryRun) {
7254
7259
  ])
7255
7260
  )
7256
7261
  );
7262
+ if (options.replaceUri) {
7263
+ console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
7264
+ }
7257
7265
  return;
7258
7266
  }
7259
7267
  await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
@@ -7261,6 +7269,10 @@ async function storeMemory(config, memory, dryRun) {
7261
7269
  await ensureDurableMemoryDirectory(ov, config);
7262
7270
  await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
7263
7271
  console.log(`Stored durable memory: ${memoryUri}`);
7272
+ if (options.replaceUri) {
7273
+ await maybeRun(false, ov, withIdentity(config, ["rm", options.replaceUri]));
7274
+ console.log(`Forgot replaced memory: ${options.replaceUri}`);
7275
+ }
7264
7276
  }
7265
7277
  async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
7266
7278
  const args = withIdentity(config, [
@@ -7501,13 +7513,19 @@ async function buildHandoff(options) {
7501
7513
  const status = await gitValue(["status", "--short"], repoRoot) ?? "";
7502
7514
  const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
7503
7515
  const touchedFiles = await gitTouchedFiles(repoRoot);
7504
- return [
7516
+ const header = [
7505
7517
  "HANDOFF",
7506
7518
  `repo: ${(0, import_node_path4.basename)(repoRoot)}`,
7507
7519
  `repo_path: ${repoRoot}`,
7508
7520
  `branch: ${branch || "unknown"}`,
7509
7521
  `source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
7510
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
7522
+ `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
7523
+ ];
7524
+ if (options.replace) {
7525
+ header.push(`supersedes: ${options.replace}`);
7526
+ }
7527
+ return [
7528
+ ...header,
7511
7529
  `task: ${options.task ?? "unspecified"}`,
7512
7530
  "",
7513
7531
  "files_touched:",
@@ -7903,12 +7921,13 @@ Summary: ${failureCount} failure(s), ${warningCount} warning(s)`);
7903
7921
  }
7904
7922
  async function runInstall(config, options) {
7905
7923
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
7906
- await ensureDirectory(config.agentContextHome, options.dryRun === true);
7907
- await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "logs"), options.dryRun === true);
7908
- await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "redacted"), options.dryRun === true);
7909
- await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "mcp"), options.dryRun === true);
7910
- await installCommandShim(options.dryRun === true);
7911
- await installUserAgentInstructions(options.dryRun === true);
7924
+ const dryRun = options.dryRun === true;
7925
+ await ensureDirectory(config.agentContextHome, dryRun);
7926
+ await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "logs"), dryRun);
7927
+ await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "redacted"), dryRun);
7928
+ await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "mcp"), dryRun);
7929
+ await installCommandShim(dryRun);
7930
+ await installUserAgentInstructions(dryRun);
7912
7931
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
7913
7932
  if (serverPath) {
7914
7933
  console.log(`OpenViking server already installed: ${serverPath}`);
@@ -7921,37 +7940,49 @@ async function runInstall(config, options) {
7921
7940
  repairReasons.push("Python system certificate bridge is missing");
7922
7941
  }
7923
7942
  console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
7924
- await runInstallCommands(config, options.packageManager, true, options.dryRun === true);
7943
+ await runInstallCommands(config, options.packageManager, true, dryRun);
7925
7944
  } else if (pythonSystemCertificatesMissing) {
7926
7945
  console.log("OpenViking install needs repair: Python system certificate bridge is missing.");
7927
7946
  const installCommand = await getPythonSystemCertificatesInstallCommand(serverPath);
7928
- await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
7947
+ await maybeRun(dryRun, installCommand.executable, installCommand.args);
7929
7948
  }
7930
7949
  } else {
7931
- await runInstallCommands(config, options.packageManager, false, options.dryRun === true);
7950
+ await runInstallCommands(config, options.packageManager, false, dryRun);
7932
7951
  }
7933
7952
  await writeTemplateIfMissing({
7934
7953
  config,
7935
7954
  destinationPath: (0, import_node_path6.join)(config.agentContextHome, "ov.conf"),
7936
- dryRun: options.dryRun === true,
7955
+ dryRun,
7937
7956
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
7938
7957
  templatePath: (0, import_node_path6.join)(toolRoot(), "config", "ov.conf.template.json")
7939
7958
  });
7940
7959
  await writeTemplateIfMissing({
7941
7960
  config,
7942
7961
  destinationPath: (0, import_node_path6.join)(config.agentContextHome, "ovcli.conf"),
7943
- dryRun: options.dryRun === true,
7962
+ dryRun,
7944
7963
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
7945
7964
  templatePath: (0, import_node_path6.join)(toolRoot(), "config", "ovcli.conf.template.json")
7946
7965
  });
7947
- console.log("Install complete. Run start, then doctor:");
7948
- console.log(" threadnote start");
7949
- console.log(" threadnote doctor");
7966
+ if (options.start !== false) {
7967
+ const healthy = await repairServerHealth(config, dryRun);
7968
+ if (!healthy && !dryRun) {
7969
+ throw new Error(`OpenViking did not become healthy. Check logs: ${openVikingLogPath(config)}`);
7970
+ }
7971
+ }
7972
+ if (options.printNextSteps !== false) {
7973
+ printInstallNextSteps({ dryRun, startsServer: options.start !== false });
7974
+ }
7950
7975
  }
7951
7976
  async function runRepair(config, options) {
7952
7977
  const dryRun = options.dryRun === true;
7953
7978
  console.log("Repairing local OpenViking agent context from this checkout.");
7954
- await runInstall(config, { dryRun, packageManager: options.packageManager, repairInvalidConfigs: true });
7979
+ await runInstall(config, {
7980
+ dryRun,
7981
+ packageManager: options.packageManager,
7982
+ printNextSteps: false,
7983
+ repairInvalidConfigs: true,
7984
+ start: false
7985
+ });
7955
7986
  await repairManifest(config, dryRun);
7956
7987
  if (options.start !== false) {
7957
7988
  await repairServerHealth(config, dryRun);
@@ -8047,13 +8078,15 @@ async function repairServerHealth(config, dryRun) {
8047
8078
  const existingHealth = await readOpenVikingHealthIfAvailable(config, 800);
8048
8079
  if (existingHealth) {
8049
8080
  console.log(`OpenViking health OK at http://${config.host}:${config.port}/health`);
8050
- return;
8081
+ return true;
8051
8082
  }
8052
8083
  console.log(`OpenViking health is not responding at http://${config.host}:${config.port}/health; starting server.`);
8053
8084
  try {
8054
8085
  await runStart(config, { dryRun });
8086
+ return true;
8055
8087
  } catch (err) {
8056
8088
  console.log(`WARN could not repair OpenViking health: ${errorMessage(err)}`);
8089
+ return false;
8057
8090
  }
8058
8091
  }
8059
8092
  async function runStart(config, options) {
@@ -8385,14 +8418,28 @@ async function getInstallCommands(config, preferred, force) {
8385
8418
  return [{ executable: "python3", args: pipArgs }];
8386
8419
  }
8387
8420
  async function detectPackageManager() {
8388
- if (await findExecutable(["pipx"])) {
8389
- return "pipx";
8390
- }
8391
8421
  if (await findExecutable(["uv"])) {
8392
8422
  return "uv";
8393
8423
  }
8424
+ if (await findExecutable(["pipx"])) {
8425
+ return "pipx";
8426
+ }
8394
8427
  return "pip";
8395
8428
  }
8429
+ function printInstallNextSteps(options) {
8430
+ if (options.dryRun) {
8431
+ console.log("Dry run complete. Run without --dry-run to install and start OpenViking.");
8432
+ return;
8433
+ }
8434
+ if (options.startsServer) {
8435
+ console.log("Install complete. OpenViking health is ready. Next:");
8436
+ console.log(" threadnote doctor --dry-run");
8437
+ return;
8438
+ }
8439
+ console.log("Install complete. Run start, then doctor:");
8440
+ console.log(" threadnote start");
8441
+ console.log(" threadnote doctor --dry-run");
8442
+ }
8396
8443
  async function writeTemplateIfMissing(options) {
8397
8444
  if (await exists(options.destinationPath)) {
8398
8445
  const currentContent = await (0, import_promises6.readFile)(options.destinationPath, "utf8");
@@ -8946,7 +8993,7 @@ async function main() {
8946
8993
  await runDoctor(config, options);
8947
8994
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8948
8995
  });
8949
- program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
8996
+ program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
8950
8997
  const config = getRuntimeConfig(program2);
8951
8998
  await runInstall(config, options);
8952
8999
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
@@ -8958,7 +9005,7 @@ async function main() {
8958
9005
  "--mcp <clients>",
8959
9006
  "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
8960
9007
  "available"
8961
- ).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
9008
+ ).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
8962
9009
  const config = getRuntimeConfig(program2);
8963
9010
  await runRepair(config, options);
8964
9011
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
@@ -8990,7 +9037,7 @@ async function main() {
8990
9037
  program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, or cursor").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
8991
9038
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
8992
9039
  });
8993
- program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
9040
+ program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
8994
9041
  await runRemember(getRuntimeConfig(program2), options);
8995
9042
  });
8996
9043
  program2.command("migrate-memories").description("Migrate legacy session-only Threadnote memories into durable memory files").option("--all-accounts", "Scan all local OpenViking accounts under THREADNOTE_HOME").option("--dry-run", "Print migration actions without writing memories").option("--limit <count>", "Maximum number of memories to migrate").option(
@@ -9010,7 +9057,7 @@ async function main() {
9010
9057
  program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
9011
9058
  await runList(getRuntimeConfig(program2), uri, options);
9012
9059
  });
9013
- program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").action(async (options) => {
9060
+ program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").action(async (options) => {
9014
9061
  await runHandoff(getRuntimeConfig(program2), options);
9015
9062
  });
9016
9063
  program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
@@ -38,6 +38,11 @@ canonical docs. Prefer updating checked-in docs for canonical repo rules.
38
38
 
39
39
  ## Memory Compaction
40
40
 
41
+ When working on the same active issue, prefer keeping one current-state memory updated instead of creating many small
42
+ progress memories. If an existing memory is clearly the current state for the issue, store the updated version with
43
+ `remember_context({"text":"...","replaceUri":"<uri>"})`, `threadnote remember --replace <uri>`, or
44
+ `threadnote handoff --replace <uri>` so the old memory is removed only after the replacement is stored.
45
+
41
46
  When recall/read surfaces several memories that describe the same durable fact, incident, branch, or handoff, compact
42
47
  them when it is safe:
43
48
 
@@ -72,6 +77,7 @@ threadnote recall --query "last handoff for this branch"
72
77
  threadnote read viking://agent/threadnote/memories/.abstract.md
73
78
  threadnote list viking://agent/threadnote/memories --all --recursive
74
79
  threadnote remember --text "Durable engineering note..."
80
+ threadnote remember --replace viking://user/example/memories/events/current.md --text "Updated durable engineering note..."
75
81
  threadnote forget viking://user/example/memories/events/duplicate.md
76
82
  threadnote handoff --task "short task summary" --tests "checks run" --next-step "what to do next"
77
83
  ```
package/docs/migration.md CHANGED
@@ -90,10 +90,9 @@ paths.
90
90
  `--repo` can be repeated. Paths may be anywhere on the machine. If no `--repo` is provided, the current git repo is
91
91
  used. The manifest is written to `~/.openviking/seed-manifest.yaml` by default and is intentionally not checked in.
92
92
 
93
- 4. Start the local service:
93
+ 4. Verify the local service:
94
94
 
95
95
  ```bash
96
- threadnote start
97
96
  threadnote doctor --dry-run
98
97
  ```
99
98
 
@@ -171,6 +170,8 @@ Preferred agent behavior is automatic after `threadnote install` has updated the
171
170
 
172
171
  - On non-trivial task start, search OpenViking for recent handoffs and relevant repo guidance.
173
172
  - When the user says "remember", store the memory after checking that it contains no secret or customer data.
173
+ - When continuing the same active issue, update the current-state memory with `remember --replace <uri>` or
174
+ `handoff --replace <uri>` instead of creating another near-duplicate progress memory.
174
175
  - When recall surfaces clearly duplicate or stale memories, store one concise replacement memory and forget only the
175
176
  redundant originals.
176
177
  - Before pausing, switching agents, or finishing meaningful code changes, store a concise handoff with status, tests,
@@ -181,6 +182,7 @@ Manual CLI remains available for scripts and emergencies:
181
182
  ```bash
182
183
  threadnote recall --query "last handoff for this branch"
183
184
  threadnote remember --text "Durable engineering note..."
185
+ threadnote remember --replace viking://user/example/memories/events/current.md --text "Updated durable engineering note..."
184
186
  threadnote handoff --task "short task summary" --tests "checks run" --next-step "what the next agent should do"
185
187
  ```
186
188
 
@@ -8,7 +8,7 @@ Run:
8
8
  threadnote install
9
9
  ```
10
10
 
11
- The installer prefers `pipx`, then `uv`, then `python3 -m pip install --user`.
11
+ The installer prefers `uv`, then `pipx`, then `python3 -m pip install --user`.
12
12
 
13
13
  ## `uv` Fails With `UnknownIssuer`
14
14
 
@@ -30,13 +30,13 @@ threadnote install --package-manager pip
30
30
 
31
31
  ## Model Download Fails With `CERTIFICATE_VERIFY_FAILED`
32
32
 
33
- On first start, OpenViking may download the local embedding model from Hugging Face. If `~/.openviking/logs/server.log`
34
- shows `SSLCertVerificationError`, `self-signed certificate in certificate chain`, or `Failed to download local embedding
35
- model`, repair the OpenViking Python environment and start again:
33
+ On install or first start, OpenViking may download the local embedding model from Hugging Face. If
34
+ `~/.openviking/logs/server.log` shows `SSLCertVerificationError`, `self-signed certificate in certificate chain`, or
35
+ `Failed to download local embedding model`, repair the OpenViking Python environment and start again:
36
36
 
37
37
  ```bash
38
38
  threadnote repair --package-manager uv
39
- threadnote start
39
+ threadnote doctor --dry-run
40
40
  ```
41
41
 
42
42
  Threadnote installs `pip-system-certs` into the OpenViking environment so Python `requests` can use certificates trusted
@@ -63,8 +63,8 @@ The installer repairs this by installing `openviking[local-embed]`.
63
63
 
64
64
  ## Server Health Fails
65
65
 
66
- If `doctor` reports `WARN openviking health: connect ECONNREFUSED 127.0.0.1:1933`, the local server is not running.
67
- Start it and recheck:
66
+ Current Threadnote installs start the local server by default. If `doctor` reports
67
+ `WARN openviking health: connect ECONNREFUSED 127.0.0.1:1933`, the local server is not running. Start it and recheck:
68
68
 
69
69
  ```bash
70
70
  threadnote start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",
@@ -126,7 +126,6 @@ fi
126
126
 
127
127
  say ""
128
128
  say "Threadnote is installed. Next:"
129
- say " threadnote start"
130
129
  say " threadnote doctor --dry-run"
131
130
  say " threadnote mcp-install codex --apply # if you use Codex"
132
131
  say " threadnote mcp-install claude --apply # if you use Claude"