truthtrail 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dhairya Modh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -74,3 +74,13 @@ The plugin (in `plugin/` of the TruthTrail repo) adds:
74
74
  | `decisionsDir` | `.decisions` | where records live |
75
75
  | `jiraSite` | `null` | used by the TruthTrail server |
76
76
  | `failOnUnconfirmed` | `false` | used by the TruthTrail server's PR check |
77
+
78
+ ## Troubleshooting: help improve answer capture
79
+
80
+ The Claude Code hook that captures your answers to "A or B?" questions is built from documentation that doesn't describe the exact payload. If answered questions never show up in `npx truthtrail pending list`, run Claude Code with:
81
+
82
+ ```bash
83
+ TRUTHTRAIL_DEBUG_HOOKS=1 claude
84
+ ```
85
+
86
+ Answer one question, then open `.truthtrail/hook-payloads.jsonl` and share it in an issue (remove anything private first). It is git-ignored and stays on your machine.
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ function validateRecord(obj) {
48
48
  }
49
49
  var FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
50
50
  function parseRecord(text) {
51
- const m = FRONT_MATTER.exec(text);
51
+ const m = FRONT_MATTER.exec(text.replace(/^\uFEFF/, ""));
52
52
  if (!m) throw new RecordError(["missing --- front matter"]);
53
53
  let data;
54
54
  try {
@@ -226,7 +226,7 @@ async function walk(dir) {
226
226
  for (const e of entries) {
227
227
  const p = join2(dir, e.name);
228
228
  if (e.isDirectory()) out.push(...await walk(p));
229
- else if (e.name.endsWith(".md")) out.push(p);
229
+ else if (e.name.endsWith(".md") && e.name.toLowerCase() !== "readme.md") out.push(p);
230
230
  }
231
231
  return out;
232
232
  }
@@ -238,7 +238,7 @@ async function listRecords(root, cfg, ticket) {
238
238
  const rel = abs.slice(root.length + 1).split("\\").join("/");
239
239
  try {
240
240
  const r = parseRecord(await readFile2(abs, "utf8"));
241
- if (!ticket || r.ticket === ticket) records.push({ ...r, path: rel });
241
+ if (!ticket || r.ticket.toUpperCase() === ticket.toUpperCase()) records.push({ ...r, path: rel });
242
242
  } catch (e) {
243
243
  errors.push({ path: rel, message: e instanceof RecordError ? e.problems.join("; ") : e.message });
244
244
  }
@@ -495,7 +495,10 @@ async function sessionStartContext(root, cfg, branch) {
495
495
  return "TruthTrail: no ticket key in this branch name. Before recording decisions, ask the user for the ticket key once.";
496
496
  }
497
497
  const { records } = await listRecords(root, cfg, ticket);
498
- const lines = [`TruthTrail: working on ticket ${ticket}. Follow the TruthTrail rules in AGENTS.md.`];
498
+ const lines = [
499
+ `TruthTrail: working on ticket ${ticket}. Follow the TruthTrail rules in AGENTS.md.`,
500
+ "If TruthTrail MCP tools are available, use record_decision to save decisions and list_pending / resolve_pending for answered questions; otherwise use the npx truthtrail commands."
501
+ ];
499
502
  if (records.length === 0) lines.push("No decisions recorded for this ticket yet.");
500
503
  else {
501
504
  lines.push(`Recorded decisions for ${ticket} (follow them):`);
@@ -520,10 +523,10 @@ Record each one that changes behaviour a user or QA can see; dismiss the rest. I
520
523
  import { realpath } from "fs/promises";
521
524
  var clean = (xs) => (xs ?? []).map((x) => x.trim()).filter(Boolean);
522
525
  async function recordDecision(root, cwd, cfg, input, now) {
523
- if (!input.question?.trim()) throw new Error("--question is required");
524
- if (!input.chosen?.trim()) throw new Error("--chosen is required");
526
+ if (!input.question?.trim()) throw new Error("question is required");
527
+ if (!input.chosen?.trim()) throw new Error("chosen is required");
525
528
  const type = input.type ?? "decision";
526
- if (!DECISION_TYPES.includes(type)) throw new Error(`--type must be one of ${DECISION_TYPES.join(", ")}`);
529
+ if (!DECISION_TYPES.includes(type)) throw new Error(`type must be one of ${DECISION_TYPES.join(", ")}`);
527
530
  let ticket = input.ticket?.trim().toUpperCase() || null;
528
531
  if (ticket && !new RegExp(`^(?:${cfg.ticketPattern})$`, "i").test(ticket)) {
529
532
  throw new Error(`'${input.ticket}' is not a valid ticket key (expected pattern ${cfg.ticketPattern})`);
@@ -531,12 +534,12 @@ async function recordDecision(root, cwd, cfg, input, now) {
531
534
  if (!ticket) {
532
535
  const branch = await currentBranch(root);
533
536
  ticket = detectTicket({ branch }, cfg.ticketPattern);
534
- if (!ticket) throw new Error(`No ticket key found in branch '${branch ?? "(detached)"}'. Pass --ticket <KEY>.`);
537
+ if (!ticket) throw new Error(`No ticket key found in branch '${branch ?? "(detached)"}'. Pass a ticket key (ticket, or --ticket <KEY> on the CLI).`);
535
538
  }
536
539
  const base = await realpath(cwd).catch(() => cwd);
537
540
  const files = clean(input.files).map((f) => {
538
541
  const rel = toRepoPath(root, base, f);
539
- if (!rel) throw new Error(`--files: ${f} is outside the repository`);
542
+ if (!rel) throw new Error(`files: ${f} is outside the repository`);
540
543
  return rel;
541
544
  });
542
545
  return writeRecord(
@@ -562,7 +565,7 @@ async function recordDecision(root, cwd, cfg, input, now) {
562
565
  }
563
566
  async function resolvePending(root, cwd, cfg, index, overrides, now) {
564
567
  const entry = (await listPending(root)).find((p) => p.index === index);
565
- if (!entry) throw new Error(`No pending question #${index}. Run \`npx truthtrail pending list\`.`);
568
+ if (!entry) throw new Error(`No pending question #${index}. List them with list_pending or \`npx truthtrail pending list\`.`);
566
569
  const given = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
567
570
  const record = await recordDecision(
568
571
  root,
@@ -587,7 +590,9 @@ var MCP_TARGETS = [
587
590
  { file: ".vscode/mcp.json", key: "servers", entry: { type: "stdio", command: "npx", args: ARGS } }
588
591
  ];
589
592
  var FORMAT = {};
590
- function upsertMcpEntry(existing, target) {
593
+ function upsertMcpEntry(raw, target) {
594
+ const bom = raw?.startsWith("\uFEFF") ? "\uFEFF" : "";
595
+ const existing = raw === null ? null : raw.slice(bom.length);
591
596
  if (existing === null || existing.trim() === "") {
592
597
  return { status: "created", text: `${JSON.stringify({ [target.key]: { truthtrail: target.entry } }, null, 2)}
593
598
  ` };
@@ -598,10 +603,10 @@ function upsertMcpEntry(existing, target) {
598
603
  if (tree.type !== "object") return { status: "skipped", reason: "top level is not a JSON object" };
599
604
  const parent = findNodeAtLocation(tree, [target.key]);
600
605
  if (parent && parent.type !== "object") return { status: "skipped", reason: `"${target.key}" is not an object` };
601
- if (findNodeAtLocation(tree, [target.key, "truthtrail"])) return { status: "unchanged", text: existing };
606
+ if (findNodeAtLocation(tree, [target.key, "truthtrail"])) return { status: "unchanged", text: raw };
602
607
  try {
603
608
  const text = applyEdits(existing, modify(existing, [target.key, "truthtrail"], target.entry, FORMAT));
604
- return { status: "added", text };
609
+ return { status: "added", text: bom + text };
605
610
  } catch (e) {
606
611
  return { status: "skipped", reason: e.message };
607
612
  }
@@ -815,6 +820,8 @@ async function runPendingDismiss(cwd, index) {
815
820
  }
816
821
 
817
822
  // src/commands/hook.ts
823
+ import { appendFile as appendFile2, mkdir as mkdir4 } from "fs/promises";
824
+ import { join as join6 } from "path";
818
825
  async function runHook(event, stdin, cwd) {
819
826
  try {
820
827
  let payload = null;
@@ -829,6 +836,11 @@ async function runHook(event, stdin, cwd) {
829
836
  return await sessionStartContext(root, await loadConfig(root), await currentBranch(root));
830
837
  }
831
838
  if (event === "capture") {
839
+ if (process.env.TRUTHTRAIL_DEBUG_HOOKS === "1") {
840
+ await mkdir4(join6(root, ".truthtrail"), { recursive: true });
841
+ await appendFile2(join6(root, ".truthtrail", "hook-payloads.jsonl"), `${JSON.stringify(payload)}
842
+ `);
843
+ }
832
844
  for (const a of extractAskAnswers(payload)) {
833
845
  await appendPending(root, { ...a, at: (/* @__PURE__ */ new Date()).toISOString() });
834
846
  }
@@ -884,7 +896,7 @@ var decisionFields = {
884
896
  };
885
897
  function createServer(opts) {
886
898
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
887
- const server = new McpServer({ name: "truthtrail", version: "0.2.0" });
899
+ const server = new McpServer({ name: "truthtrail", version: "0.2.1" });
888
900
  async function workspace() {
889
901
  const candidates = [];
890
902
  if (server.server.getClientCapabilities()?.roots) {
@@ -989,7 +1001,7 @@ async function runMcp() {
989
1001
  // src/index.ts
990
1002
  function buildProgram() {
991
1003
  const program = new Command();
992
- program.name("truthtrail").description("Record the decisions your AI coding agent makes, right next to the code.").version("0.2.0");
1004
+ program.name("truthtrail").description("Record the decisions your AI coding agent makes, right next to the code.").version("0.2.1");
993
1005
  program.command("init").description("Set up TruthTrail in this repo (.decisions/, AGENTS.md rules, MCP config, config)").option("--no-mcp", "don't write MCP config for Claude Code, Cursor and Copilot").action(async (o) => {
994
1006
  await runInit(process.cwd(), (l) => console.log(l), { mcp: o.mcp });
995
1007
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truthtrail",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Record the decisions your AI coding agent makes, right next to the code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "README.md"
11
+ "README.md",
12
+ "LICENSE"
12
13
  ],
13
14
  "engines": {
14
15
  "node": ">=20"