@minhpnq1807/contextos 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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ - Sanitizes stale Stop reports at display time so previously recorded system-user rules no longer appear in `ctx report` or `ctx evidence` after upgrading.
6
+ - Filters system-user rules again inside the Stop hook to protect reports created from older prompt contexts.
7
+
8
+ ## 0.1.4
9
+
10
+ - Filters host/session user rules such as `sudo -u user`, `sudo su - user`, and "commands must run as user X" before scoring and injection.
11
+ - Prevents system-user setup instructions from inflating `unknown` outcomes or skewing ContextOS efficiency reports.
12
+
3
13
  ## 0.1.3
4
14
 
5
15
  - Separates runtime prompt/report/stats files per workspace under `~/.ctx/contextos/workspaces/<workspace-id>`.
package/README.md CHANGED
@@ -53,7 +53,7 @@ With ContextOS, each prompt gets a compact block:
53
53
  ```text
54
54
  ## Critical ContextOS rules
55
55
  - Use code-review-graph before reading files.
56
- - All shell commands must run as minh_dev.
56
+ - Check upload moderation flows before editing approval code.
57
57
 
58
58
  ## Suggested files to check
59
59
  - services/content-service/src/infrastructure/services/content-moderation.service.ts
@@ -66,6 +66,7 @@ With ContextOS, each prompt gets a compact block:
66
66
  - Registers a `ctx-mcp` MCP server that owns model loading and semantic scoring.
67
67
  - Reads the active `AGENTS.md` chain for the current workspace.
68
68
  - Scores rules by relevance to the user prompt.
69
+ - Filters host/session setup rules such as "run commands as user X" or `sudo -u user` because they are environment instructions, not project guidance.
69
70
  - Finds likely relevant files with a hybrid retriever:
70
71
  - first, local prompt/file heuristics create seed candidates;
71
72
  - then, if `.code-review-graph/graph.db` exists, ContextOS queries `code-review-graph` semantic search and re-ranks graph-backed matches;
@@ -288,7 +289,7 @@ unknown = the rule was relevant, but the diff does not prove either way
288
289
 
289
290
  For runtime-only rules, ContextOS also checks `telemetry.jsonl` for hook-visible tool names, MCP server names, and command metadata. A rule like "use code-review-graph before reading files" can be marked `followed` when telemetry contains a matching `code-review-graph` signal.
290
291
 
291
- Example `unknown`: a rule says shell commands must run as a specific OS user, but neither git diff nor hook telemetry records that user identity. ContextOS cannot prove the rule was followed from available evidence alone.
292
+ Host/session setup rules such as "run shell commands as user X", `sudo su - user`, `sudo -i -u user`, and `sudo -u user` are filtered before scoring. They are not injected and do not count toward `unknown` outcomes because they describe the agent runtime environment rather than project behavior.
292
293
 
293
294
  ## Development
294
295
 
package/bin/ctx.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { execFileSync } from "node:child_process";
6
6
 
7
7
  import { readAgentsChain } from "../plugins/ctx/lib/reader.js";
8
- import { parseRules, scoreRules } from "../plugins/ctx/lib/analyzer.js";
8
+ import { filterActionableRules, parseRules, scoreRules } from "../plugins/ctx/lib/analyzer.js";
9
9
  import { scheduleContext } from "../plugins/ctx/lib/scheduler.js";
10
10
  import { formatEvidence, formatReport } from "../plugins/ctx/lib/reporter.js";
11
11
  import { installGlobalHooks } from "../plugins/ctx/lib/global-hooks.js";
@@ -222,7 +222,7 @@ async function debug(task) {
222
222
  async function warmEmbeddings(task) {
223
223
  const cwd = process.cwd();
224
224
  const merged = readAgentsChain({ cwd });
225
- const rules = scoreRules(parseRules(merged.content), task, []);
225
+ const rules = scoreRules(filterActionableRules(parseRules(merged.content)), task, []);
226
226
  const result = await warmRuleEmbeddings({
227
227
  rules,
228
228
  task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,6 +48,20 @@ const SEMANTIC_ALIASES = {
48
48
 
49
49
  const MODERATION_TOKENS = new Set(["moderation", "moderate", "content-moderation", "approval", "approved", "reject", "rejected", "needs_review"]);
50
50
 
51
+ const SYSTEM_USER_RULE_PATTERNS = [
52
+ /\ball\s+shell\s+commands?\s+must\s+run\s+as\b/i,
53
+ /\bcommands?\s+must\s+run\s+as\b/i,
54
+ /\bstrictly\s+follow\s+this\s+sequence\b/i,
55
+ /\bswitch\s+the\s+user\s+context\b/i,
56
+ /\bdo\s+not\s+prefix\b.*\bsudo\s+-u\b/i,
57
+ /\bsudo\s+su\s+-\s*[a-z_][a-z0-9_-]*\b/i,
58
+ /\bsudo\s+-i\s+-u\s+[a-z_][a-z0-9_-]*\b/i,
59
+ /\bsudo\s+-u\s+[a-z_][a-z0-9_-]*\b/i,
60
+ /\bsu\s+-\s+[a-z_][a-z0-9_-]*\b/i,
61
+ /[/\\]\.codex[/\\]RTK\.md\b/i,
62
+ /\bminh_dev\b/i
63
+ ];
64
+
51
65
  export function tokenize(value) {
52
66
  const normalized = String(value || "")
53
67
  .toLowerCase()
@@ -138,6 +152,17 @@ export function parseRules(markdown) {
138
152
  return dedupeRules(rules);
139
153
  }
140
154
 
155
+ export function filterActionableRules(rules = []) {
156
+ return rules
157
+ .filter((rule) => !isSystemUserRule(rule))
158
+ .map((rule, index) => ({ ...rule, id: `r${index + 1}`, originalOrder: index }));
159
+ }
160
+
161
+ export function isSystemUserRule(rule) {
162
+ const content = typeof rule === "string" ? rule : rule?.content;
163
+ return SYSTEM_USER_RULE_PATTERNS.some((pattern) => pattern.test(String(content || "")));
164
+ }
165
+
141
166
  function dedupeRules(rules) {
142
167
  const seen = new Set();
143
168
  return rules.filter((rule) => {
@@ -1,7 +1,10 @@
1
+ import { isSystemUserRule } from "./analyzer.js";
2
+
1
3
  export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot, compliance, runtimeEvidence }) {
2
- const followed = compliance.filter((item) => item.status === "followed");
3
- const ignored = compliance.filter((item) => item.status === "ignored");
4
- const unknown = compliance.filter((item) => item.status === "unknown");
4
+ const actionableCompliance = compliance.filter((item) => !isSystemUserRule(item.rule));
5
+ const followed = actionableCompliance.filter((item) => item.status === "followed");
6
+ const ignored = actionableCompliance.filter((item) => item.status === "ignored");
7
+ const unknown = actionableCompliance.filter((item) => item.status === "unknown");
5
8
  const measured = followed.length + ignored.length;
6
9
  const efficiencyScore = measured ? Math.round((followed.length / measured) * 100) : null;
7
10
 
@@ -24,6 +27,7 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
24
27
  }
25
28
 
26
29
  export function formatReport(report) {
30
+ report = sanitizeReport(report);
27
31
  const lines = [];
28
32
  lines.push("ContextOS report");
29
33
  lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
@@ -55,6 +59,7 @@ export function formatReport(report) {
55
59
  }
56
60
 
57
61
  export function formatEvidence(report) {
62
+ report = sanitizeReport(report);
58
63
  const lines = [];
59
64
  lines.push("ContextOS evidence");
60
65
  lines.push(`Prompt: ${report.prompt || "(empty)"}`);
@@ -119,3 +124,20 @@ function summarizeRuntimeEvidence(runtimeEvidence = {}) {
119
124
  sources: (runtimeEvidence.sources || []).slice(0, 10)
120
125
  };
121
126
  }
127
+
128
+ function sanitizeReport(report = {}) {
129
+ const followed = (report.followed || []).filter((item) => !isSystemUserRule(item.rule));
130
+ const ignored = (report.ignored || []).filter((item) => !isSystemUserRule(item.rule));
131
+ const unknown = (report.unknown || []).filter((item) => !isSystemUserRule(item.rule));
132
+ const measured = followed.length + ignored.length;
133
+ return {
134
+ ...report,
135
+ injectedRuleCount: followed.length + ignored.length + unknown.length,
136
+ followed,
137
+ ignored,
138
+ unknown,
139
+ measuredRuleCount: measured,
140
+ unknownRuleCount: unknown.length,
141
+ efficiencyScore: measured ? Math.round((followed.length / measured) * 100) : null
142
+ };
143
+ }
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import { readAgentsChain } from "./reader.js";
4
- import { parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
4
+ import { filterActionableRules, parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
5
5
  import { enhanceRuleScoresWithEmbeddings } from "./embedding-scorer.js";
6
6
 
7
7
  export async function scoreContext({
@@ -15,7 +15,8 @@ export async function scoreContext({
15
15
  } = {}) {
16
16
  const started = Date.now();
17
17
  const merged = readAgentsChain({ cwd });
18
- const parsedRules = parseRules(merged.content);
18
+ const rawRules = parseRules(merged.content);
19
+ const parsedRules = filterActionableRules(rawRules);
19
20
  const baseScoredRules = scoreRules(parsedRules, prompt, openFiles);
20
21
  const embedding = await enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
21
22
  dataDir,
@@ -47,6 +48,7 @@ export async function scoreContext({
47
48
  model: embedding.model,
48
49
  cachePath: embedding.cachePath,
49
50
  rulesParsed: parsedRules.length,
51
+ rulesFiltered: rawRules.length - parsedRules.length,
50
52
  rulesInjected: scoredRules.filter((rule) => Number(rule.score || 0) >= 0.1).length,
51
53
  filesSuggested: suggestedFiles.length,
52
54
  sources: merged.sources.map((source) => path.relative(cwd, source))
@@ -4,14 +4,27 @@ import { appendJsonLine, readJsonFile, writeJsonFile } from "./fs-utils.js";
4
4
  import { readGitSnapshot, checkCompliance } from "./measure.js";
5
5
  import { buildReport, formatReport } from "./reporter.js";
6
6
  import { loadRuntimeEvidence } from "./telemetry.js";
7
+ import { filterActionableRules } from "./analyzer.js";
7
8
 
8
9
  export function handleStopPayload(payload, { contextPath, reportPath, historyPath, telemetryPath } = {}) {
9
10
  const cwd = payload.cwd || payload.working_directory || process.cwd();
10
11
  const promptContext = contextPath && fs.existsSync(contextPath) ? readJsonFile(contextPath) : null;
11
- const scheduledRules = [
12
+ const rawScheduledRules = [
12
13
  ...(promptContext?.scheduled?.highRules || []),
13
14
  ...(promptContext?.scheduled?.midRules || [])
14
15
  ];
16
+ const scheduledRules = filterActionableRules(rawScheduledRules);
17
+ const scheduled = promptContext?.scheduled
18
+ ? {
19
+ ...promptContext.scheduled,
20
+ highRules: filterActionableRules(promptContext.scheduled.highRules || []),
21
+ midRules: filterActionableRules(promptContext.scheduled.midRules || []),
22
+ droppedRules: [
23
+ ...(promptContext.scheduled.droppedRules || []),
24
+ ...rawScheduledRules.filter((rule) => !scheduledRules.some((item) => item.content === rule.content && item.sourcePath === rule.sourcePath))
25
+ ]
26
+ }
27
+ : null;
15
28
  const gitSnapshot = readGitSnapshot({ cwd });
16
29
  const runtimeEvidence = loadRuntimeEvidence({
17
30
  telemetryPath,
@@ -28,7 +41,7 @@ export function handleStopPayload(payload, { contextPath, reportPath, historyPat
28
41
  cwd,
29
42
  prompt: promptContext?.prompt || "",
30
43
  relevantFiles: promptContext?.relevantFiles || [],
31
- scheduled: promptContext?.scheduled || null,
44
+ scheduled,
32
45
  gitSnapshot,
33
46
  compliance,
34
47
  runtimeEvidence