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 +21 -0
- package/README.md +10 -0
- package/dist/index.js +27 -15
- package/package.json +3 -2
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 = [
|
|
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("
|
|
524
|
-
if (!input.chosen?.trim()) throw new Error("
|
|
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(
|
|
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(
|
|
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}.
|
|
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(
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|