@groundtrack/hooks 0.1.3 → 0.1.5

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
@@ -16,7 +16,7 @@ repository root:
16
16
 
17
17
  ```bash
18
18
  groundtrack doctor
19
- groundtrack doctor --fix
19
+ groundtrack doctor --fix --client codex
20
20
  ```
21
21
 
22
22
  `doctor` checks the installed package/runtime version, discovers the actual
@@ -24,6 +24,11 @@ Groundtrack MCP server alias, validates Claude/Codex/Cursor hook configuration,
24
24
  checks the managed `AGENTS.md` and `CLAUDE.md` instructions, and simulates a
25
25
  hook invocation. It exits non-zero when repair is needed.
26
26
 
27
+ Pass `--client claude`, `--client codex`, or `--client cursor` when repairing a
28
+ repository. This limits MCP alias discovery and configuration changes to that
29
+ client. If more than one client is detected, `doctor --fix` refuses to guess
30
+ which one to modify.
31
+
27
32
  The Doctor verifies local configuration and enrichment behavior, but it cannot
28
33
  observe whether a coding-agent host loaded or trusted a project hook. A
29
34
  `search_experiences` MCP response is the authoritative live check: Groundtrack
@@ -1,10 +1,47 @@
1
1
  import { readFileSync } from "node:fs";
2
- const VERSION_PATTERN = /^<!-- groundtrack:start version=(\d+) -->/;
2
+ import { join } from "node:path";
3
+ const VERSION_PATTERN = /<!-- groundtrack:start version=(\d+) -->/;
3
4
  /** Canonical managed Groundtrack policy, packaged with the CLI. */
4
5
  export const AGENT_INSTRUCTION_BODY = readFileSync(new URL("../templates/AGENTS.md", import.meta.url), "utf8").trim();
5
6
  const versionMatch = VERSION_PATTERN.exec(AGENT_INSTRUCTION_BODY);
6
7
  if (!versionMatch) {
7
8
  throw new Error("Canonical Groundtrack AGENTS.md template is missing its version marker.");
8
9
  }
10
+ const AGENT_INSTRUCTION_START_MARKER = versionMatch[0];
9
11
  /** Version is derived from the canonical template marker; never define it separately. */
10
12
  export const AGENT_INSTRUCTION_TEMPLATE_VERSION = Number(versionMatch[1]);
13
+ function readInstructions(path) {
14
+ try {
15
+ return readFileSync(path, "utf8");
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ function instructionVersion(content) {
22
+ const match = content ? VERSION_PATTERN.exec(content) : null;
23
+ return match ? Number(match[1]) : null;
24
+ }
25
+ /** Detect the managed policy version available to a client in one repository. */
26
+ export function detectAgentInstructionsVersion(repoDir, client) {
27
+ const agents = readInstructions(join(repoDir, "AGENTS.md"));
28
+ const agentsVersion = instructionVersion(agents);
29
+ if (client !== "claude")
30
+ return agentsVersion;
31
+ const claude = readInstructions(join(repoDir, "CLAUDE.md"));
32
+ if (!claude)
33
+ return null;
34
+ if (/^@AGENTS\.md\s*$/m.test(claude)) {
35
+ const bridgeVersion = instructionVersion(claude);
36
+ if (agentsVersion == null)
37
+ return null;
38
+ return bridgeVersion == null ? agentsVersion : Math.min(bridgeVersion, agentsVersion);
39
+ }
40
+ const inlineVersion = instructionVersion(claude);
41
+ if (inlineVersion != null)
42
+ return inlineVersion;
43
+ const unmarkedCurrentInstructions = AGENT_INSTRUCTION_BODY.replace(AGENT_INSTRUCTION_START_MARKER, "")
44
+ .replace("<!-- groundtrack:end -->", "")
45
+ .trim();
46
+ return claude.includes(unmarkedCurrentInstructions) ? AGENT_INSTRUCTION_TEMPLATE_VERSION : null;
47
+ }
package/dist/doctor.js CHANGED
@@ -53,9 +53,13 @@ function matcherForAliases(aliases) {
53
53
  const server = escaped.length === 1 ? escaped[0] : `(?:${escaped.join("|")})`;
54
54
  return `^mcp__${server}__(store_event|search_experiences)$`;
55
55
  }
56
- function groundtrackAliases(repoDir) {
56
+ function groundtrackAliases(repoDir, clients) {
57
57
  const aliases = new Set();
58
- for (const relative of [".mcp.json", ".cursor/mcp.json"]) {
58
+ const jsonConfigs = [
59
+ ...(clients.includes("claude") ? [".mcp.json"] : []),
60
+ ...(clients.includes("cursor") ? [".cursor/mcp.json"] : []),
61
+ ];
62
+ for (const relative of jsonConfigs) {
59
63
  const path = join(repoDir, relative);
60
64
  try {
61
65
  const config = readJson(path);
@@ -73,11 +77,13 @@ function groundtrackAliases(repoDir) {
73
77
  // The owning adapter reports malformed config; alias discovery stays fail-open.
74
78
  }
75
79
  }
76
- const codex = readText(join(repoDir, ".codex/config.toml")) ?? "";
77
- const table = /^\[mcp_servers\.([^\]]+)\]([\s\S]*?)(?=^\[|(?![\s\S]))/gm;
78
- for (const match of codex.matchAll(table)) {
79
- if (match[1]?.toLowerCase().includes("groundtrack") || match[2]?.includes("groundtrack")) {
80
- aliases.add(match[1]);
80
+ if (clients.includes("codex")) {
81
+ const codex = readText(join(repoDir, ".codex/config.toml")) ?? "";
82
+ const table = /^\[mcp_servers\.([^\]]+)\]([\s\S]*?)(?=^\[|(?![\s\S]))/gm;
83
+ for (const match of codex.matchAll(table)) {
84
+ if (match[1]?.toLowerCase().includes("groundtrack") || match[2]?.includes("groundtrack")) {
85
+ aliases.add(match[1]);
86
+ }
81
87
  }
82
88
  }
83
89
  if (aliases.size === 0)
@@ -360,7 +366,7 @@ function addFileCheck(checks, changedFiles, options, input) {
360
366
  export function runDoctor(options) {
361
367
  const version = packageVersion();
362
368
  const clients = options.clients ?? detectClients(options.repoDir);
363
- const aliases = groundtrackAliases(options.repoDir);
369
+ const aliases = groundtrackAliases(options.repoDir, clients);
364
370
  const matcher = matcherForAliases(aliases);
365
371
  const checks = [];
366
372
  const changedFiles = [];
@@ -474,7 +480,8 @@ export function runDoctor(options) {
474
480
  }
475
481
  }
476
482
  const instructionFiles = new Map([["AGENTS.md", AGENT_INSTRUCTION_BODY]]);
477
- if (clients.includes("claude") || existsSync(join(options.repoDir, "CLAUDE.md"))) {
483
+ if (clients.includes("claude") ||
484
+ (options.clients === undefined && existsSync(join(options.repoDir, "CLAUDE.md")))) {
478
485
  instructionFiles.set("CLAUDE.md", CLAUDE_INSTRUCTION_BODY);
479
486
  }
480
487
  for (const [name, desired] of instructionFiles) {
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { closeSync, fstatSync, openSync, readSync } from "node:fs";
3
+ import { detectAgentInstructionsVersion, } from "./agent-instructions.js";
3
4
  const PROVIDERS = [
4
5
  [/^claude(?:[-_.]|$)/, "anthropic"],
5
6
  [/^(?:gpt|o\d|codex)(?:[-_.]|$)/, "openai"],
@@ -120,6 +121,11 @@ function gitContext(cwd) {
120
121
  isDirty: status == null ? null : status.length > 0,
121
122
  };
122
123
  }
124
+ function repositoryRoot(cwd) {
125
+ if (!cwd)
126
+ return null;
127
+ return git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd;
128
+ }
123
129
  export function enrichInput(hookInput, clientOverride, tool = "store_event") {
124
130
  const toolInput = hookInput.tool_input &&
125
131
  typeof hookInput.tool_input === "object" &&
@@ -133,7 +139,8 @@ export function enrichInput(hookInput, clientOverride, tool = "store_event") {
133
139
  ? (hookInput.model_id ?? hookInput.model)
134
140
  : (hookInput.model ?? hookInput.model_id ?? transcriptModel(hookInput.transcript_path)));
135
141
  const invocationId = nonEmptyString(isCursor ? hookInput.generation_id : hookInput.tool_use_id);
136
- const context = gitContext(workspacePath(hookInput));
142
+ const cwd = workspacePath(hookInput);
143
+ const context = gitContext(cwd);
137
144
  if (hostSessionId)
138
145
  toolInput.sessionId = `${host}:${hostSessionId}`;
139
146
  if (hostModel)
@@ -141,6 +148,11 @@ export function enrichInput(hookInput, clientOverride, tool = "store_event") {
141
148
  if (tool === "store_event" && invocationId) {
142
149
  toolInput.clientEventId = [host, hostSessionId, invocationId].filter(Boolean).join(":");
143
150
  }
151
+ if (tool === "search_experiences") {
152
+ const client = host === "claude" || host === "cursor" ? host : "codex";
153
+ const root = repositoryRoot(cwd);
154
+ toolInput.agentInstructionsVersion = root ? detectAgentInstructionsVersion(root, client) : null;
155
+ }
144
156
  if (context)
145
157
  Object.assign(toolInput, context);
146
158
  return toolInput;
@@ -10,13 +10,16 @@ function usage() {
10
10
  return `Groundtrack CLI
11
11
 
12
12
  Usage:
13
- groundtrack doctor [--fix] [--yes] [--json] [--repo PATH]
13
+ groundtrack doctor [--fix] [--yes] [--json] [--client CLIENT] [--repo PATH]
14
14
  groundtrack version [--json]
15
15
 
16
16
  Commands:
17
17
  doctor Diagnose Groundtrack hooks and managed agent instructions
18
18
  doctor --fix Repair deterministic configuration drift
19
19
  version Print the installed CLI/runtime version
20
+
21
+ Options:
22
+ --client Limit diagnosis and repair to claude, codex, or cursor
20
23
  `;
21
24
  }
22
25
  function parseArguments(argv) {
@@ -28,22 +31,37 @@ function parseArguments(argv) {
28
31
  const command = argv[0] === "version" || argv[0] === "--version" ? "version" : "doctor";
29
32
  let repoDir = process.cwd();
30
33
  let homeDir = homedir();
34
+ let client;
31
35
  for (let index = 0; index < argv.length; index += 1) {
32
36
  if (argv[index] === "--repo" && argv[index + 1])
33
37
  repoDir = resolve(argv[++index]);
34
38
  if (argv[index] === "--home" && argv[index + 1])
35
39
  homeDir = resolve(argv[++index]);
40
+ if (argv[index] === "--client") {
41
+ const value = argv[++index];
42
+ if (value !== "claude" && value !== "codex" && value !== "cursor") {
43
+ throw new Error("--client must be one of: claude, codex, cursor");
44
+ }
45
+ client = value;
46
+ }
36
47
  }
37
48
  return {
38
49
  command,
39
50
  fix: argv.includes("--fix"),
40
51
  yes: argv.includes("--yes") || argv.includes("-y"),
41
52
  json: argv.includes("--json"),
53
+ client,
42
54
  repoDir,
43
55
  homeDir,
44
56
  };
45
57
  }
46
58
  function printResult(result, options = {}) {
59
+ const selectedClient = options.client ?? (result.clients.length === 1 ? result.clients[0] : null);
60
+ const fixHint = !options.showFixHint
61
+ ? ""
62
+ : selectedClient
63
+ ? ` Run: groundtrack doctor --fix --client ${selectedClient}`
64
+ : " Re-run with --client claude, --client codex, or --client cursor.";
47
65
  stdout.write(`Groundtrack doctor ${result.version}\n`);
48
66
  stdout.write(`Clients: ${result.clients.join(", ")}\n`);
49
67
  stdout.write(`MCP aliases: ${result.aliases.join(", ")}\n\n`);
@@ -64,7 +82,7 @@ function printResult(result, options = {}) {
64
82
  }
65
83
  stdout.write(result.healthy
66
84
  ? "\nGroundtrack local configuration is healthy. Live hook activation is verified by Groundtrack MCP calls.\n"
67
- : `\nGroundtrack local configuration needs attention.${options.showFixHint ? " Run: groundtrack doctor --fix" : ""}\n`);
85
+ : `\nGroundtrack local configuration needs attention.${fixHint}\n`);
68
86
  }
69
87
  async function confirmFix() {
70
88
  const prompt = createInterface({ input: stdin, output: stdout });
@@ -132,6 +150,7 @@ async function main() {
132
150
  const base = {
133
151
  repoDir: args.repoDir,
134
152
  homeDir: args.homeDir,
153
+ clients: args.client ? [args.client] : undefined,
135
154
  skipPackageInstall: process.env.GROUNDTRACK_DOCTOR_SKIP_INSTALL === "1",
136
155
  latestVersion: await latestPackageVersion(),
137
156
  };
@@ -140,10 +159,13 @@ async function main() {
140
159
  if (args.json)
141
160
  stdout.write(`${JSON.stringify(diagnosis, null, 2)}\n`);
142
161
  else
143
- printResult(diagnosis, { showFixHint: true });
162
+ printResult(diagnosis, { showFixHint: true, client: args.client });
144
163
  process.exitCode = diagnosis.healthy ? 0 : 1;
145
164
  return;
146
165
  }
166
+ if (!args.client && diagnosis.clients.length > 1) {
167
+ throw new Error(`Multiple coding-agent clients detected: ${diagnosis.clients.join(", ")}. Re-run with --client claude, --client codex, or --client cursor so Doctor does not modify unrelated client configuration.`);
168
+ }
147
169
  if (diagnosis.healthy) {
148
170
  if (args.json)
149
171
  stdout.write(`${JSON.stringify(diagnosis, null, 2)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundtrack/hooks",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Lifecycle hook utilities for Groundtrack MCP clients.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,4 +1,4 @@
1
- <!-- groundtrack:start version=1 -->
1
+ <!-- groundtrack:start version=3 -->
2
2
 
3
3
  ## Groundtrack
4
4
 
@@ -8,9 +8,9 @@ Use Groundtrack as the shared engineering memory for this repository.
8
8
  - When a relevant experience needs more context or verification, call `get_experience_events` with its id to inspect the underlying raw evidence and provenance.
9
9
  - Apply relevant current experiences and heed any caveats that an experience was contradicted, superseded, or expired.
10
10
  - Once the effect of a search is known, call `report_experience_use` with its retrieval id. Report when a result changed the approach or avoided a failure, and report `not_helpful` when none applied.
11
+ - After a task reaches a concrete result, call `record_outcome` with a short title, a concise summary, and the retrieval ids for searches that contributed. Include measurements only for sourced, quantified impact on time, cost, reliability, quality, performance, or throughput; tests, typechecks, lint, and builds are validation rather than impact. Include avoided work only when it was explicitly estimated, and never infer unknown values.
11
12
  - After a non-obvious discovery, decision, workaround, correction, or failed approach that would help a future agent, call `store_event` promptly with a concise title and an explanation of what happened and why it matters.
12
13
  - Do not store routine implementation details, obvious facts, unrestricted transcripts, or secrets.
13
14
  - Keep the raw-event ids returned by `store_event`. When the related work lands in a commit or pull request, call `link_outcomes` with those ids and the explicit commit SHA and/or pull-request number. Never infer an outcome from worktree HEAD.
14
- - When recurring engineering friction may support or oppose an existing initiative, call `search_initiatives` before storing the event and link it to the matching initiative with the appropriate direction. Propose a new initiative only when explicitly recommending organizational change and no existing initiative matches.
15
15
 
16
16
  <!-- groundtrack:end -->