@minhpnq1807/contextos 0.1.9 → 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.
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { readStdinJson, writeJson, failOpen, logDebug, pluginRuntimeFile } from "../lib/hook-io.js";
2
+ import { readStdinJson, writeJson, failOpen, logDebug, pluginRuntimeFile, resolveHookCwd } from "../lib/hook-io.js";
3
3
  import { handleStopPayload } from "../lib/stop-hook.js";
4
4
  import { appendTelemetry } from "../lib/telemetry.js";
5
5
 
6
6
  try {
7
7
  const payload = await readStdinJson();
8
- const cwd = payload.cwd || payload.working_directory;
9
- logDebug("Stop", payload);
10
- appendTelemetry({ telemetryPath: pluginRuntimeFile("telemetry.jsonl", cwd), event: "Stop", payload });
11
- writeJson(handleStopPayload(payload, {
8
+ const cwd = resolveHookCwd(payload);
9
+ const normalized = { ...payload, cwd };
10
+ logDebug("Stop", normalized);
11
+ appendTelemetry({ telemetryPath: pluginRuntimeFile("telemetry.jsonl", cwd), event: "Stop", payload: normalized });
12
+ writeJson(handleStopPayload(normalized, {
12
13
  contextPath: pluginRuntimeFile("last-prompt-context.json", cwd),
13
14
  reportPath: pluginRuntimeFile("last-report.json", cwd),
14
15
  historyPath: pluginRuntimeFile("report-history.jsonl", cwd),
@@ -43,7 +43,10 @@ const SEMANTIC_ALIASES = {
43
43
  thong: ["notification", "notify", "message"],
44
44
  bao: ["notification", "notify", "message"],
45
45
  "thong-bao": ["notification", "notify", "message"],
46
- thongbao: ["notification", "notify", "message"]
46
+ thongbao: ["notification", "notify", "message"],
47
+ authen: ["auth", "authentication", "login"],
48
+ authentication: ["auth", "authen", "login"],
49
+ recheck: ["check", "verify", "review"]
47
50
  };
48
51
 
49
52
  const MODERATION_TOKENS = new Set(["moderation", "moderate", "content-moderation", "approval", "approved", "reject", "rejected", "needs_review"]);
@@ -81,6 +84,11 @@ const TOOL_REFERENCE_TOKENS = new Set([
81
84
  "list_communities"
82
85
  ]);
83
86
 
87
+ const ACTION_TOKENS = new Set([
88
+ "add", "avoid", "call", "check", "derive", "ensure", "filter", "follow", "prefer", "run",
89
+ "use", "validate", "verify", "write", "never", "always", "must", "should", "do"
90
+ ]);
91
+
84
92
  export function tokenize(value) {
85
93
  const normalized = String(value || "")
86
94
  .toLowerCase()
@@ -190,6 +198,7 @@ export function isDocumentationOnlyRule(rule) {
190
198
  if (/^<!--.*-->$/.test(normalized)) return true;
191
199
  if (DOCUMENTATION_HEADING_PATTERNS.some((pattern) => pattern.test(normalized))) return true;
192
200
  if (isMarkdownTableRule(normalized)) return true;
201
+ if (isGenericHeading(normalized)) return true;
193
202
  return false;
194
203
  }
195
204
 
@@ -209,6 +218,13 @@ function isMarkdownTableRule(content) {
209
218
  return /\btool\b/.test(lower) && /\buse\s+when\b/.test(lower) && toolReferenceCount >= 2;
210
219
  }
211
220
 
221
+ function isGenericHeading(content) {
222
+ if (content.length > 80 || /[`.:;]/.test(content)) return false;
223
+ const tokens = tokenize(content);
224
+ if (tokens.length > 4) return false;
225
+ return !tokens.some((token) => ACTION_TOKENS.has(token));
226
+ }
227
+
212
228
  function dedupeRules(rules) {
213
229
  const seen = new Set();
214
230
  return rules.filter((rule) => {
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import { resolveHookCwd } from "./hook-io.js";
3
+
4
+ export function antigravityCwd(payload) {
5
+ return payload.cwd
6
+ || payload.working_directory
7
+ || payload.workspacePath
8
+ || payload.workspacePaths?.[0]
9
+ || resolveHookCwd(payload);
10
+ }
11
+
12
+ function textFromValue(value) {
13
+ if (!value) return "";
14
+ if (typeof value === "string") return value;
15
+ if (Array.isArray(value)) return value.map(textFromValue).filter(Boolean).join("\n");
16
+ if (typeof value !== "object") return "";
17
+ if (typeof value.text === "string") return value.text;
18
+ if (typeof value.content === "string") return value.content;
19
+ if (typeof value.message === "string") return value.message;
20
+ if (typeof value.userMessage === "string") return value.userMessage;
21
+ if (Array.isArray(value.parts)) return textFromValue(value.parts);
22
+ if (Array.isArray(value.content)) return textFromValue(value.content);
23
+ return "";
24
+ }
25
+
26
+ function looksUserAuthored(record) {
27
+ const role = String(record.role || record.author || record.type || record.sender || "").toLowerCase();
28
+ return !role || role.includes("user") || role.includes("human");
29
+ }
30
+
31
+ export function extractPromptFromAntigravityPayload(payload) {
32
+ const direct = payload.prompt || payload.userPrompt || payload.userMessage || payload.message;
33
+ if (direct) return textFromValue(direct);
34
+
35
+ const transcriptPath = payload.transcriptPath || payload.transcript_path;
36
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return "";
37
+
38
+ try {
39
+ const lines = fs.readFileSync(transcriptPath, "utf8").trim().split(/\r?\n/).filter(Boolean).slice(-200);
40
+ for (const line of lines.reverse()) {
41
+ let record;
42
+ try {
43
+ record = JSON.parse(line);
44
+ } catch {
45
+ continue;
46
+ }
47
+ if (!looksUserAuthored(record)) continue;
48
+ const text = textFromValue(record.prompt || record.userPrompt || record.userMessage || record.message || record.content || record.parts);
49
+ if (text.trim()) return text.trim();
50
+ }
51
+ } catch {
52
+ return "";
53
+ }
54
+
55
+ return "";
56
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function shellQuote(value) {
5
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
6
+ }
7
+
8
+ function readJsonFile(filePath, fallback) {
9
+ if (!fs.existsSync(filePath)) return fallback;
10
+ const raw = fs.readFileSync(filePath, "utf8").trim();
11
+ if (!raw) return fallback;
12
+ return JSON.parse(raw);
13
+ }
14
+
15
+ function commandFor(installRoot, scriptName, { injectPromptContext = true } = {}) {
16
+ const envPrefix = scriptName === "on-antigravity-preinvocation.js" && !injectPromptContext ? "CONTEXTOS_INJECT=0 " : "";
17
+ return `${envPrefix}node ${shellQuote(path.join(installRoot, "plugins", "ctx", "bin", scriptName))}`;
18
+ }
19
+
20
+ export function antigravityHooksPath() {
21
+ return process.env.ANTIGRAVITY_HOOKS_PATH
22
+ || path.join(process.env.HOME || process.cwd(), ".gemini", "config", "hooks.json");
23
+ }
24
+
25
+ export function buildAntigravityHooksConfig(existingConfig, { installRoot, injectPromptContext = true } = {}) {
26
+ const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
27
+ config.contextos = {
28
+ enabled: true,
29
+ PreInvocation: [
30
+ {
31
+ type: "command",
32
+ command: commandFor(installRoot, "on-antigravity-preinvocation.js", { injectPromptContext }),
33
+ timeout: 10
34
+ }
35
+ ],
36
+ Stop: [
37
+ {
38
+ type: "command",
39
+ command: commandFor(installRoot, "on-antigravity-stop.js"),
40
+ timeout: 10
41
+ }
42
+ ]
43
+ };
44
+ return config;
45
+ }
46
+
47
+ export function installAntigravityHooks({ hooksPath = antigravityHooksPath(), installRoot, injectPromptContext = true } = {}) {
48
+ const existing = readJsonFile(hooksPath, {});
49
+ const next = buildAntigravityHooksConfig(existing, { installRoot, injectPromptContext });
50
+ fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
51
+ fs.writeFileSync(hooksPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
52
+ return hooksPath;
53
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function readJsonFile(filePath, fallback) {
5
+ if (!fs.existsSync(filePath)) return fallback;
6
+ const raw = fs.readFileSync(filePath, "utf8").trim();
7
+ if (!raw) return fallback;
8
+ return JSON.parse(raw);
9
+ }
10
+
11
+ export function antigravityMcpConfigPaths() {
12
+ if (process.env.ANTIGRAVITY_MCP_CONFIG_PATH) {
13
+ return [process.env.ANTIGRAVITY_MCP_CONFIG_PATH];
14
+ }
15
+ const home = process.env.HOME || process.cwd();
16
+ return [
17
+ path.join(home, ".gemini", "antigravity", "mcp_config.json"),
18
+ path.join(home, ".gemini", "antigravity-cli", "mcp_config.json"),
19
+ path.join(home, ".gemini", "config", "mcp_config.json")
20
+ ];
21
+ }
22
+
23
+ export function buildAntigravityMcpConfig(existingConfig, { installRoot } = {}) {
24
+ const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
25
+ if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
26
+ config.mcpServers["ctx-mcp"] = {
27
+ command: "node",
28
+ args: [path.join(installRoot, "plugins", "ctx", "mcp", "server.js")]
29
+ };
30
+ return config;
31
+ }
32
+
33
+ export function installAntigravityMcp({ configPaths = antigravityMcpConfigPaths(), installRoot } = {}) {
34
+ const written = [];
35
+ for (const configPath of configPaths) {
36
+ const existing = readJsonFile(configPath, {});
37
+ const next = buildAntigravityMcpConfig(existing, { installRoot });
38
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
39
+ fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
40
+ written.push(configPath);
41
+ }
42
+ return written;
43
+ }
@@ -0,0 +1,72 @@
1
+ import { parseRules, filterActionableRules, scoreRules } from "./analyzer.js";
2
+ import { readAgentsChain } from "./reader.js";
3
+ import { scheduleContext } from "./scheduler.js";
4
+
5
+ export function benchmarkContext({ markdown, sources = [], task = "", openFiles = [], topK = 8 } = {}) {
6
+ const parsedRules = parseRules(markdown);
7
+ const actionableRules = filterActionableRules(parsedRules);
8
+ const scoredRules = scoreRules(actionableRules, task, openFiles);
9
+ const relevantRules = scoredRules.filter((rule) => Number(rule.score || 0) >= 0.1);
10
+ const scheduled = scheduleContext({ rules: scoredRules, relevantFiles: [] });
11
+
12
+ const originalPositions = new Map(actionableRules.map((rule, index) => [rule.content, index]));
13
+ const middleStart = Math.floor(actionableRules.length * 0.25);
14
+ const middleEnd = Math.ceil(actionableRules.length * 0.75);
15
+ const lostMiddle = relevantRules.filter((rule) => {
16
+ const index = originalPositions.get(rule.content);
17
+ return typeof index === "number" && index >= middleStart && index <= middleEnd;
18
+ });
19
+
20
+ return {
21
+ task,
22
+ sources,
23
+ rulesParsed: parsedRules.length,
24
+ actionableRules: actionableRules.length,
25
+ filteredRules: parsedRules.length - actionableRules.length,
26
+ relevantRules: relevantRules.length,
27
+ baseline: {
28
+ relevantRulesInMiddle: lostMiddle.length,
29
+ middleRiskPercent: relevantRules.length ? Math.round((lostMiddle.length / relevantRules.length) * 100) : 0
30
+ },
31
+ contextOS: {
32
+ highRules: scheduled.highRules.length,
33
+ midRules: scheduled.midRules.length,
34
+ topRules: scoredRules.slice(0, topK).map((rule) => ({
35
+ score: rule.score,
36
+ content: rule.content,
37
+ reasons: rule.reasons || []
38
+ })),
39
+ repeatsHighRulesAtEnd: scheduled.highRules.length > 0
40
+ }
41
+ };
42
+ }
43
+
44
+ export function benchmarkWorkspace({ cwd = process.cwd(), task = "", openFiles = [], topK = 8 } = {}) {
45
+ const merged = readAgentsChain({ cwd });
46
+ return benchmarkContext({
47
+ markdown: merged.content,
48
+ sources: merged.sources,
49
+ task,
50
+ openFiles,
51
+ topK
52
+ });
53
+ }
54
+
55
+ export function formatBenchmark(result) {
56
+ const lines = [];
57
+ lines.push("ContextOS benchmark");
58
+ lines.push(`Task: ${result.task || "(empty)"}`);
59
+ lines.push(`Rules: ${result.rulesParsed} parsed, ${result.actionableRules} actionable, ${result.filteredRules} filtered`);
60
+ lines.push(`Relevant rules: ${result.relevantRules}`);
61
+ lines.push(`Baseline middle-risk: ${result.baseline.relevantRulesInMiddle}/${result.relevantRules} relevant rules (${result.baseline.middleRiskPercent}%)`);
62
+ lines.push(`ContextOS scheduled: ${result.contextOS.highRules} high, ${result.contextOS.midRules} mid`);
63
+ lines.push(`Recency reminder: ${result.contextOS.repeatsHighRulesAtEnd ? "enabled" : "not needed"}`);
64
+ if (result.contextOS.topRules.length) {
65
+ lines.push("Top rules:");
66
+ for (const rule of result.contextOS.topRules) {
67
+ const reasons = rule.reasons?.length ? ` reasons:${rule.reasons.join(",")}` : "";
68
+ lines.push(`- ${Number(rule.score || 0).toFixed(2)} ${rule.content}${reasons}`);
69
+ }
70
+ }
71
+ return lines.join("\n");
72
+ }
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { buildGlobalHooksConfig } from "./global-hooks.js";
5
+
6
+ function readJsonFile(filePath, fallback) {
7
+ if (!fs.existsSync(filePath)) return fallback;
8
+ const raw = fs.readFileSync(filePath, "utf8").trim();
9
+ if (!raw) return fallback;
10
+ return JSON.parse(raw);
11
+ }
12
+
13
+ export function claudeHome() {
14
+ return process.env.CLAUDE_HOME || path.join(process.env.HOME || process.cwd(), ".claude");
15
+ }
16
+
17
+ export function installClaudeHooks({ claudeHome: home = claudeHome(), installRoot, injectPromptContext = true } = {}) {
18
+ const settingsPath = path.join(home, "settings.json");
19
+ const existing = readJsonFile(settingsPath, {});
20
+ const next = buildGlobalHooksConfig(existing, {
21
+ marketplaceRoot: installRoot,
22
+ injectPromptContext
23
+ });
24
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
25
+ fs.writeFileSync(settingsPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
26
+ return settingsPath;
27
+ }
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function readJsonFile(filePath, fallback) {
5
+ if (!fs.existsSync(filePath)) return fallback;
6
+ const raw = fs.readFileSync(filePath, "utf8").trim();
7
+ if (!raw) return fallback;
8
+ return JSON.parse(raw);
9
+ }
10
+
11
+ export function claudeConfigPath() {
12
+ return process.env.CLAUDE_CONFIG_PATH || path.join(process.env.HOME || process.cwd(), ".claude.json");
13
+ }
14
+
15
+ export function buildClaudeMcpConfig(existingConfig, { installRoot } = {}) {
16
+ const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
17
+ if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
18
+ config.mcpServers["ctx-mcp"] = {
19
+ type: "stdio",
20
+ command: "node",
21
+ args: [path.join(installRoot, "plugins", "ctx", "mcp", "server.js")],
22
+ env: {}
23
+ };
24
+ return config;
25
+ }
26
+
27
+ export function installClaudeMcp({ configPath = claudeConfigPath(), installRoot } = {}) {
28
+ const existing = readJsonFile(configPath, {});
29
+ const next = buildClaudeMcpConfig(existing, { installRoot });
30
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
31
+ fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
32
+ return configPath;
33
+ }
@@ -57,6 +57,9 @@ export async function warmRuleEmbeddings({
57
57
  sources = [],
58
58
  allowRemote = true
59
59
  } = {}) {
60
+ if (!allowRemote && !isModelCacheReady(dataDir)) {
61
+ return { count: 0, cachePath: path.join(dataDir, "embeddings.db"), status: "missing-model" };
62
+ }
60
63
  const texts = [...new Set([
61
64
  task,
62
65
  ...rules.map((rule) => rule.content || "")
@@ -131,6 +134,16 @@ export function modelCacheDir(dataDir = defaultDataRoot()) {
131
134
  return path.join(dataDir, "models");
132
135
  }
133
136
 
137
+ export function isModelCacheReady(dataDir = defaultDataRoot()) {
138
+ const modelDir = path.join(modelCacheDir(dataDir), ...DEFAULT_MODEL.split("/"));
139
+ return [
140
+ "config.json",
141
+ "tokenizer.json",
142
+ "tokenizer_config.json",
143
+ path.join("onnx", "model_quantized.onnx")
144
+ ].every((relativePath) => fs.existsSync(path.join(modelDir, relativePath)));
145
+ }
146
+
134
147
  async function getCachedEmbedding({ cache, embedder, text, sources }) {
135
148
  const key = cacheKey(text, sources);
136
149
  const existing = cache.get(key);
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { enhanceRuleScoresWithEmbeddings, warmRuleEmbeddings } from "./embedding-scorer.js";
3
+ import { enhanceRuleScoresWithEmbeddings, isModelCacheReady, warmRuleEmbeddings } from "./embedding-scorer.js";
4
4
 
5
5
  const SOURCE_EXTENSIONS = new Set([
6
6
  ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".sql", ".md", ".json"
@@ -65,6 +65,7 @@ export async function warmFileEmbeddings({
65
65
  maxFiles = Number(process.env.CONTEXTOS_FILE_EMBEDDING_MAX_FILES || DEFAULT_MAX_FILES)
66
66
  } = {}) {
67
67
  if (!dataDir) return { count: 0, cachePath: null };
68
+ if (!allowRemote && !isModelCacheReady(dataDir)) return { count: 0, cachePath: null, status: "missing-model" };
68
69
  const files = listSourceFiles(cwd, { maxFiles });
69
70
  const rules = files.map((filePath) => ({ content: fileSearchText(filePath) }));
70
71
  return warmRuleEmbeddings({
@@ -15,6 +15,22 @@ export function writeJson(value) {
15
15
  process.stdout.write(`${JSON.stringify(value)}\n`);
16
16
  }
17
17
 
18
+ export function resolveHookCwd(payload = {}) {
19
+ return payload.cwd
20
+ || payload.working_directory
21
+ || payload.workspacePath
22
+ || payload.workspace_path
23
+ || payload.workspaceRoot
24
+ || payload.workspace_root
25
+ || payload.projectDir
26
+ || payload.project_dir
27
+ || payload.workspacePaths?.[0]
28
+ || payload.workspace_paths?.[0]
29
+ || process.env.CLAUDE_PROJECT_DIR
30
+ || process.env.PWD
31
+ || process.cwd();
32
+ }
33
+
18
34
  export function pluginDataDir(fileName = "", cwd = process.cwd()) {
19
35
  let root;
20
36
  try {
@@ -183,23 +183,31 @@ export function checkCompliance({ rules = [], addedLines = [], runtimeEvidence =
183
183
  if (!keywords.length || !addedLines.length) {
184
184
  results.push({
185
185
  rule,
186
- status: "unknown",
186
+ status: isRuntimeOnly || !addedLines.length || !keywords.length ? "unmeasurable" : "unknown",
187
187
  kind: isRuntimeOnly ? "runtime" : kind,
188
188
  keywords,
189
189
  evidence: isRuntimeOnly
190
- ? "requires runtime/tool-call telemetry; no matching runtime signal observed"
190
+ ? "requires runtime/tool-call telemetry; no runtime telemetry source observed"
191
191
  : (!addedLines.length ? "no added lines in git diff" : "no concrete compliance keywords found")
192
192
  });
193
193
  continue;
194
194
  }
195
195
 
196
196
  if (isRuntimeOnly) {
197
+ const hasRuntimeSource = Boolean(
198
+ runtimeEvidence.sources?.length ||
199
+ runtimeEvidence.signals?.length ||
200
+ runtimeEvidence.toolSignals?.length ||
201
+ runtimeEvidence.commandSignals?.length
202
+ );
197
203
  results.push({
198
204
  rule,
199
- status: "unknown",
205
+ status: hasRuntimeSource ? "unknown" : "unmeasurable",
200
206
  kind: "runtime",
201
207
  keywords,
202
- evidence: "requires runtime/tool-call telemetry; no matching runtime signal observed"
208
+ evidence: hasRuntimeSource
209
+ ? "requires runtime/tool-call telemetry; no matching runtime signal observed"
210
+ : "requires runtime/tool-call telemetry; no runtime telemetry source observed"
203
211
  });
204
212
  continue;
205
213
  }
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function copyDir(src, dest) {
5
+ fs.mkdirSync(dest, { recursive: true });
6
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
7
+ const srcPath = path.join(src, entry.name);
8
+ const destPath = path.join(dest, entry.name);
9
+ if (entry.isDirectory()) {
10
+ copyDir(srcPath, destPath);
11
+ } else if (entry.isFile()) {
12
+ fs.copyFileSync(srcPath, destPath);
13
+ fs.chmodSync(destPath, fs.statSync(srcPath).mode);
14
+ }
15
+ }
16
+ }
17
+
18
+ export function copyPath(src, dest) {
19
+ const stat = fs.statSync(src);
20
+ if (stat.isDirectory()) {
21
+ copyDir(src, dest);
22
+ } else {
23
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
24
+ fs.copyFileSync(src, dest);
25
+ fs.chmodSync(dest, stat.mode);
26
+ }
27
+ }
28
+
29
+ export function copyPackageRoot({ rootDir, targetRoot }) {
30
+ fs.rmSync(targetRoot, { recursive: true, force: true });
31
+ for (const entry of [".agents", "bin", "plugins", "package.json", "package-lock.json", "README.md", "LICENSE", "node_modules"]) {
32
+ const src = path.join(rootDir, entry);
33
+ if (fs.existsSync(src)) copyPath(src, path.join(targetRoot, entry));
34
+ }
35
+ return targetRoot;
36
+ }
@@ -1,6 +1,7 @@
1
1
  import { scheduleContext } from "./scheduler.js";
2
2
  import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
3
3
  import { callCtxScoreContext } from "./ctx-mcp-client.js";
4
+ import { resolveHookCwd } from "./hook-io.js";
4
5
  import path from "node:path";
5
6
 
6
7
  export async function handlePromptPayload(
@@ -16,7 +17,7 @@ export async function handlePromptPayload(
16
17
  } = {}
17
18
  ) {
18
19
  const prompt = payload.prompt || payload.message || payload.user_prompt || "";
19
- const cwd = payload.cwd || payload.working_directory || process.cwd();
20
+ const cwd = resolveHookCwd(payload);
20
21
  const openFiles = payload.openFiles || payload.open_files || payload.files || [];
21
22
  const dataDir = dataPath ? path.dirname(dataPath) : undefined;
22
23
 
@@ -5,6 +5,7 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
5
5
  const followed = actionableCompliance.filter((item) => item.status === "followed");
6
6
  const ignored = actionableCompliance.filter((item) => item.status === "ignored");
7
7
  const unknown = actionableCompliance.filter((item) => item.status === "unknown");
8
+ const unmeasurable = actionableCompliance.filter((item) => item.status === "unmeasurable");
8
9
  const measured = followed.length + ignored.length;
9
10
  const efficiencyScore = measured ? Math.round((followed.length / measured) * 100) : null;
10
11
 
@@ -20,8 +21,10 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
20
21
  followed,
21
22
  ignored,
22
23
  unknown,
24
+ unmeasurable,
23
25
  measuredRuleCount: measured,
24
26
  unknownRuleCount: unknown.length,
27
+ unmeasurableRuleCount: unmeasurable.length,
25
28
  efficiencyScore
26
29
  };
27
30
  }
@@ -32,7 +35,7 @@ export function formatReport(report) {
32
35
  lines.push("ContextOS report");
33
36
  lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
34
37
  lines.push(`Injected rules: ${report.injectedRuleCount || 0}`);
35
- lines.push(`Rule outcomes: ${report.followed?.length || 0} followed, ${report.ignored?.length || 0} ignored, ${report.unknown?.length || 0} unknown`);
38
+ lines.push(`Rule outcomes: ${report.followed?.length || 0} followed, ${report.ignored?.length || 0} ignored, ${report.unknown?.length || 0} unknown, ${report.unmeasurable?.length || 0} unmeasurable`);
36
39
  lines.push(`Measured rules: ${report.measuredRuleCount ?? ((report.followed?.length || 0) + (report.ignored?.length || 0))}`);
37
40
  lines.push(`Changed files: ${report.changedFiles?.length ? report.changedFiles.join(", ") : "none detected"}`);
38
41
 
@@ -48,6 +51,7 @@ export function formatReport(report) {
48
51
  appendBucket(lines, "Followed", report.followed);
49
52
  appendBucket(lines, "Ignored", report.ignored);
50
53
  appendBucket(lines, "Unknown", report.unknown);
54
+ appendBucket(lines, "Unmeasurable", report.unmeasurable);
51
55
 
52
56
  if (report.ignored?.length) {
53
57
  lines.push(`Suggestion: fix ignored rule evidence first: ${truncate(report.ignored[0].rule?.content || "", 70)}`);
@@ -71,7 +75,8 @@ export function formatEvidence(report) {
71
75
  const items = [
72
76
  ...(report.followed || []).map((item) => ({ ...item, status: "followed" })),
73
77
  ...(report.ignored || []).map((item) => ({ ...item, status: "ignored" })),
74
- ...(report.unknown || []).map((item) => ({ ...item, status: "unknown" }))
78
+ ...(report.unknown || []).map((item) => ({ ...item, status: "unknown" })),
79
+ ...(report.unmeasurable || []).map((item) => ({ ...item, status: "unmeasurable" }))
75
80
  ];
76
81
 
77
82
  if (!items.length) {
@@ -129,15 +134,18 @@ function sanitizeReport(report = {}) {
129
134
  const followed = (report.followed || []).filter((item) => !isSystemUserRule(item.rule));
130
135
  const ignored = (report.ignored || []).filter((item) => !isSystemUserRule(item.rule));
131
136
  const unknown = (report.unknown || []).filter((item) => !isSystemUserRule(item.rule));
137
+ const unmeasurable = (report.unmeasurable || []).filter((item) => !isSystemUserRule(item.rule));
132
138
  const measured = followed.length + ignored.length;
133
139
  return {
134
140
  ...report,
135
- injectedRuleCount: followed.length + ignored.length + unknown.length,
141
+ injectedRuleCount: followed.length + ignored.length + unknown.length + unmeasurable.length,
136
142
  followed,
137
143
  ignored,
138
144
  unknown,
145
+ unmeasurable,
139
146
  measuredRuleCount: measured,
140
147
  unknownRuleCount: unknown.length,
148
+ unmeasurableRuleCount: unmeasurable.length,
141
149
  efficiencyScore: measured ? Math.round((followed.length / measured) * 100) : null
142
150
  };
143
151
  }