getprismo 0.1.29 → 0.1.31
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 +71 -3
- package/lib/prismo-dev/boundaries.js +86 -0
- package/lib/prismo-dev/instructions.js +371 -0
- package/lib/prismo-dev/mcp.js +75 -2
- package/lib/prismo-dev/receipt.js +291 -0
- package/lib/prismo-dev/replay.js +191 -0
- package/lib/prismo-dev/timeline.js +185 -0
- package/lib/prismo-dev/usage-watch.js +1 -0
- package/lib/prismo-dev-scan.js +251 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,13 +32,15 @@ prismodev covers the full AI coding session:
|
|
|
32
32
|
before you code npx getprismo doctor
|
|
33
33
|
while you code npx getprismo watch
|
|
34
34
|
noisy commands npx getprismo shield -- npm test
|
|
35
|
-
after you code npx getprismo
|
|
35
|
+
after you code npx getprismo receipt
|
|
36
|
+
postmortem npx getprismo replay
|
|
36
37
|
agent-native npx getprismo mcp
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
**doctor** diagnoses the repo, applies safe fixes, and shows the before/after score.
|
|
40
41
|
**watch** monitors context pressure live and warns when things go wrong.
|
|
41
|
-
**
|
|
42
|
+
**receipt** explains what repeated, what output dominated, what artifacts leaked, and what likely influenced the run.
|
|
43
|
+
**replay** reconstructs why a session went sideways and prints a recovery prompt.
|
|
42
44
|
**shield** runs noisy commands without dumping full output back into the agent context.
|
|
43
45
|
**mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
|
|
44
46
|
|
|
@@ -589,6 +591,51 @@ the `prismo_cursor_sessions` mcp tool exposes all of this to compatible agents.
|
|
|
589
591
|
|
|
590
592
|
---
|
|
591
593
|
|
|
594
|
+
## run receipts and incident replay
|
|
595
|
+
|
|
596
|
+
`receipt` turns recent local sessions into a plain-English run receipt:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
npx getprismo receipt
|
|
600
|
+
npx getprismo receipt codex --json
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
it summarizes repeated reads, generated artifacts, tool-output floods, repeated commands, likely influence, and the next scoped action to take.
|
|
604
|
+
|
|
605
|
+
`replay` is the postmortem view:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
npx getprismo replay
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
it classifies the incident pattern, explains what happened, and prints a recovery prompt for the next agent run.
|
|
612
|
+
|
|
613
|
+
`timeline` looks across many sessions instead of one:
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
npx getprismo timeline --last 20
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
it surfaces recurring waste patterns such as the same lockfile leaking into many sessions, the same source file being repeatedly reread, or several sessions crossing high context pressure.
|
|
620
|
+
|
|
621
|
+
`instructions audit` looks at persistent rules:
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
npx getprismo instructions audit
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
it scores rules in `CLAUDE.md`, `AGENTS.md`, `.codex/AGENTS.md`, `.codex/instructions.md`, and `.openai/instructions.md`, then flags duplicated rules, low-signal rules, trim candidates, and rules that appear ineffective based on recent session evidence.
|
|
628
|
+
|
|
629
|
+
`boundaries` checks parallel-agent isolation:
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
npx getprismo boundaries
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
it reports whether visible local agents are overlapping on the same files, leaking the same artifacts, or running noisy sessions that should move into shield or separate worktrees.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
592
639
|
## how watch catches waste live
|
|
593
640
|
|
|
594
641
|
watch reads local session logs from codex, claude code, and cursor. it detects:
|
|
@@ -645,7 +692,8 @@ npx getprismo scan --simple
|
|
|
645
692
|
# the full workflow
|
|
646
693
|
npx getprismo doctor
|
|
647
694
|
npx getprismo watch --once
|
|
648
|
-
npx getprismo
|
|
695
|
+
npx getprismo receipt
|
|
696
|
+
npx getprismo replay
|
|
649
697
|
```
|
|
650
698
|
|
|
651
699
|
if you don't have node installed, get it from [nodejs.org](https://nodejs.org) (LTS). then:
|
|
@@ -668,6 +716,11 @@ no install needed. npx runs it directly.
|
|
|
668
716
|
| `cc` | claude code cost breakdown |
|
|
669
717
|
| `cc timeline` | session reconstruction with events |
|
|
670
718
|
| `cursor` | cursor session tracking and ai authorship |
|
|
719
|
+
| `receipt` | run receipt for reads, repeats, output, artifacts, likely influence, and next-run scope |
|
|
720
|
+
| `replay` | incident replay with root cause and recovery prompt |
|
|
721
|
+
| `timeline` | recurring context-waste patterns across recent sessions |
|
|
722
|
+
| `instructions audit` | instruction ROI audit for CLAUDE.md / AGENTS.md violations, partial compliance, duplicates, and influence-unknown rules |
|
|
723
|
+
| `boundaries` | multi-agent boundary check for shared files/artifacts and worktree overlap |
|
|
671
724
|
| `scan --usage` | full repo scan with local usage data |
|
|
672
725
|
| `scan --optimizer-fit` | recommend which token-optimization path fits your repo/session |
|
|
673
726
|
| `scan --report-card` | shortest decision-layer summary |
|
|
@@ -755,6 +808,11 @@ npx getprismo mcp /path/to/repo
|
|
|
755
808
|
- `prismo_firewall`
|
|
756
809
|
- `prismo_cc_timeline`
|
|
757
810
|
- `prismo_cursor_sessions`
|
|
811
|
+
- `prismo_receipt`
|
|
812
|
+
- `prismo_instructions_audit`
|
|
813
|
+
- `prismo_timeline`
|
|
814
|
+
- `prismo_replay`
|
|
815
|
+
- `prismo_boundaries`
|
|
758
816
|
|
|
759
817
|
This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs, inspect token-waste signals, or coordinate multiple local agents without pasting giant logs into the conversation.
|
|
760
818
|
|
|
@@ -929,13 +987,18 @@ then your team can run `npm run ai:doctor` without remembering the full command.
|
|
|
929
987
|
lib/prismo-dev-scan.js cli entry and command dispatch
|
|
930
988
|
lib/prismo-dev/constants.js shared defaults, pricing, patterns
|
|
931
989
|
lib/prismo-dev/context-optimize.js context packs, scoped prompts
|
|
990
|
+
lib/prismo-dev/boundaries.js multi-agent boundary and worktree overlap checks
|
|
932
991
|
lib/prismo-dev/doctor.js doctor/dev/init orchestration
|
|
933
992
|
lib/prismo-dev/fixes.js safe ignore/template generation
|
|
993
|
+
lib/prismo-dev/instructions.js instruction ROI and dead-rule analysis
|
|
934
994
|
lib/prismo-dev/mcp.js local MCP server and Prismo tool bindings
|
|
995
|
+
lib/prismo-dev/receipt.js run receipts for reads, output, artifacts, and next scope
|
|
935
996
|
lib/prismo-dev/report.js terminal, markdown, ci reports
|
|
997
|
+
lib/prismo-dev/replay.js incident replay and recovery prompts
|
|
936
998
|
lib/prismo-dev/scan.js repo scanning, scoring, readiness
|
|
937
999
|
lib/prismo-dev/scan-path-utils.js scan ignore/path helper logic
|
|
938
1000
|
lib/prismo-dev/shield.js local command shield and searchable output index
|
|
1001
|
+
lib/prismo-dev/timeline.js recurring multi-session waste patterns
|
|
939
1002
|
lib/prismo-dev/usage-cost.js Claude Code cost and timeline analysis
|
|
940
1003
|
lib/prismo-dev/usage-log-utils.js local session log parsing helpers
|
|
941
1004
|
lib/prismo-dev/cursor-sessions.js Cursor SQLite session and authorship tracking
|
|
@@ -960,6 +1023,11 @@ npx getprismo mcp --help
|
|
|
960
1023
|
npx getprismo mcp doctor
|
|
961
1024
|
npx getprismo cc --help
|
|
962
1025
|
npx getprismo cursor --help
|
|
1026
|
+
npx getprismo receipt --help
|
|
1027
|
+
npx getprismo replay --help
|
|
1028
|
+
npx getprismo timeline --help
|
|
1029
|
+
npx getprismo instructions --help
|
|
1030
|
+
npx getprismo boundaries --help
|
|
963
1031
|
npx getprismo scan --help
|
|
964
1032
|
```
|
|
965
1033
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module.exports = function createBoundaries(deps) {
|
|
2
|
+
const {
|
|
3
|
+
NPX_COMMAND,
|
|
4
|
+
buildMultiAgentView,
|
|
5
|
+
getUsageSummary,
|
|
6
|
+
} = deps;
|
|
7
|
+
|
|
8
|
+
function buildBoundaryCheck(options = {}) {
|
|
9
|
+
const cwd = options.cwd || process.cwd();
|
|
10
|
+
const limit = options.limit || 10;
|
|
11
|
+
const tool = options.tool || "all";
|
|
12
|
+
const usage = getUsageSummary({ cwd, limit, tool });
|
|
13
|
+
const multiAgent = buildMultiAgentView(usage);
|
|
14
|
+
const sameWorktree = (usage.sessions || []).length >= 2;
|
|
15
|
+
const sharedFiles = multiAgent.sharedFiles || [];
|
|
16
|
+
const sharedArtifacts = multiAgent.sharedArtifacts || [];
|
|
17
|
+
const noisyAgents = (multiAgent.agents || []).filter((agent) => ["tool-output-flood", "possible-loop"].includes(agent.liveAction?.cause));
|
|
18
|
+
const highPressureAgents = (multiAgent.agents || []).filter((agent) => agent.contextPressure === "High");
|
|
19
|
+
const score = Math.max(0, 100
|
|
20
|
+
- Math.min(35, sharedFiles.length * 12)
|
|
21
|
+
- Math.min(30, sharedArtifacts.length * 10)
|
|
22
|
+
- Math.min(25, noisyAgents.length * 10)
|
|
23
|
+
- Math.min(25, highPressureAgents.length * 10)
|
|
24
|
+
- (sameWorktree && multiAgent.agentCount >= 3 ? 10 : 0));
|
|
25
|
+
const risk =
|
|
26
|
+
score < 50 ? "High" :
|
|
27
|
+
score < 75 ? "Medium" :
|
|
28
|
+
"Low";
|
|
29
|
+
const recommendations = [];
|
|
30
|
+
if (sameWorktree && multiAgent.agentCount >= 2) recommendations.push("Run parallel agents in separate worktrees or strict task scopes when possible.");
|
|
31
|
+
if (sharedFiles.length) recommendations.push(`Assign file ownership before continuing: ${sharedFiles.slice(0, 3).map((item) => item.path).join(", ")}.`);
|
|
32
|
+
if (sharedArtifacts.length) recommendations.push(`Add/verify ignore coverage for shared artifacts: ${sharedArtifacts.slice(0, 3).map((item) => item.type).join(", ")}.`);
|
|
33
|
+
if (noisyAgents.length) recommendations.push(`Route noisy commands through ${NPX_COMMAND} shield -- <command>.`);
|
|
34
|
+
if (highPressureAgents.length) recommendations.push("Ask high-pressure agents for handoff summaries, then restart scoped sessions.");
|
|
35
|
+
if (!recommendations.length) recommendations.push("Boundaries look acceptable; keep agents scoped to separate tasks/files.");
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
command: "boundaries",
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
scannedPath: cwd,
|
|
42
|
+
tool,
|
|
43
|
+
sessionsAnalyzed: usage.sessions.length,
|
|
44
|
+
agentCount: multiAgent.agentCount,
|
|
45
|
+
boundaryScore: score,
|
|
46
|
+
risk,
|
|
47
|
+
sameWorktree,
|
|
48
|
+
agents: multiAgent.agents,
|
|
49
|
+
sharedFiles,
|
|
50
|
+
sharedArtifacts,
|
|
51
|
+
coordinationWarnings: multiAgent.coordinationWarnings,
|
|
52
|
+
recommendations,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderBoundaryTerminal(check) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("Prismo Agent Boundary Check");
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push(`Agents visible: ${check.agentCount}`);
|
|
62
|
+
lines.push(`Boundary score: ${check.boundaryScore}/100 (${check.risk} risk)`);
|
|
63
|
+
lines.push(`Same worktree signal: ${check.sameWorktree ? "yes" : "no"}`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("Coordination Warnings");
|
|
66
|
+
if (check.coordinationWarnings.length) check.coordinationWarnings.forEach((warning) => lines.push(`- ${warning}`));
|
|
67
|
+
else lines.push("- None detected.");
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push("Shared Files");
|
|
70
|
+
if (check.sharedFiles.length) check.sharedFiles.slice(0, 5).forEach((item) => lines.push(`- ${item.path} (${item.owners.length} agents)`));
|
|
71
|
+
else lines.push("- None detected.");
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push("Shared Artifacts");
|
|
74
|
+
if (check.sharedArtifacts.length) check.sharedArtifacts.slice(0, 5).forEach((item) => lines.push(`- ${item.type} (${item.owners.length} agents)`));
|
|
75
|
+
else lines.push("- None detected.");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("Next");
|
|
78
|
+
check.recommendations.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
buildBoundaryCheck,
|
|
84
|
+
renderBoundaryTerminal,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
module.exports = function createInstructionsAudit(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
INSTRUCTION_FILES,
|
|
6
|
+
NPX_COMMAND,
|
|
7
|
+
estimateTokens,
|
|
8
|
+
formatTokenCount,
|
|
9
|
+
getUsageSummary,
|
|
10
|
+
readIfText,
|
|
11
|
+
} = deps;
|
|
12
|
+
|
|
13
|
+
const RULE_KEYWORDS = [
|
|
14
|
+
"test", "tests", "pytest", "npm", "shield", "log", "logs", "dist", "build", "coverage",
|
|
15
|
+
"package-lock", "lockfile", "lockfiles", "node_modules", "generated", "artifact", "cache",
|
|
16
|
+
"read", "grep", "search", "edit", "diff", "commit", "auth", "frontend", "backend", "api",
|
|
17
|
+
"database", "migration", "security", "lint", "format", "typescript", "python",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function normalizeRule(text) {
|
|
21
|
+
return String(text || "")
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[`*_#>\-[\]().,:;]/g, " ")
|
|
24
|
+
.replace(/\s+/g, " ")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ruleKeywords(rule) {
|
|
29
|
+
const normalized = normalizeRule(rule);
|
|
30
|
+
return RULE_KEYWORDS.filter((keyword) => normalized.includes(keyword));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractRules(file) {
|
|
34
|
+
const lines = file.text.split(/\r?\n/);
|
|
35
|
+
const rules = [];
|
|
36
|
+
lines.forEach((line, index) => {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed) return;
|
|
39
|
+
if (/^#{1,6}\s+/.test(trimmed)) return;
|
|
40
|
+
if (/^[-*]\s*$/.test(trimmed)) return;
|
|
41
|
+
if (/^```/.test(trimmed)) return;
|
|
42
|
+
if (trimmed.length < 8) return;
|
|
43
|
+
const cleaned = trimmed
|
|
44
|
+
.replace(/^[-*]\s+/, "")
|
|
45
|
+
.replace(/^\d+[.)]\s+/, "")
|
|
46
|
+
.trim();
|
|
47
|
+
if (cleaned.length < 8) return;
|
|
48
|
+
rules.push({
|
|
49
|
+
id: `${file.path}:${index + 1}`,
|
|
50
|
+
file: file.path,
|
|
51
|
+
line: index + 1,
|
|
52
|
+
text: cleaned,
|
|
53
|
+
tokens: estimateTokens(cleaned),
|
|
54
|
+
keywords: ruleKeywords(cleaned),
|
|
55
|
+
normalized: normalizeRule(cleaned),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return rules;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readInstructionFiles(root) {
|
|
62
|
+
const files = [];
|
|
63
|
+
for (const rel of INSTRUCTION_FILES.filter((item) => !["README.md", "README"].includes(item))) {
|
|
64
|
+
const fullPath = path.join(root, rel);
|
|
65
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
66
|
+
const stat = fs.statSync(fullPath);
|
|
67
|
+
if (!stat.isFile()) continue;
|
|
68
|
+
const text = readIfText(fullPath) || "";
|
|
69
|
+
files.push({
|
|
70
|
+
path: rel,
|
|
71
|
+
size: stat.size,
|
|
72
|
+
tokens: estimateTokens(text || stat.size),
|
|
73
|
+
text,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sessionEvidence(usage) {
|
|
80
|
+
const sessions = usage?.sessions || [];
|
|
81
|
+
const evidenceText = [];
|
|
82
|
+
let toolOutputTokens = 0;
|
|
83
|
+
let generatedArtifactMentions = 0;
|
|
84
|
+
let repeatedReadMentions = 0;
|
|
85
|
+
let repeatedCommandMentions = 0;
|
|
86
|
+
let failureMentions = 0;
|
|
87
|
+
let loopSessions = 0;
|
|
88
|
+
for (const session of sessions) {
|
|
89
|
+
toolOutputTokens += Number(session.estimatedToolTokens || 0);
|
|
90
|
+
failureMentions += Number(session.failureMentions || 0);
|
|
91
|
+
if (session.loopSuspicion) loopSessions += 1;
|
|
92
|
+
for (const item of session.repeatedPathMentions || []) {
|
|
93
|
+
evidenceText.push(item.value);
|
|
94
|
+
repeatedReadMentions += Number(item.count || 0);
|
|
95
|
+
}
|
|
96
|
+
for (const item of session.generatedArtifacts || []) {
|
|
97
|
+
evidenceText.push(item.value);
|
|
98
|
+
generatedArtifactMentions += Number(item.count || 0);
|
|
99
|
+
}
|
|
100
|
+
for (const item of session.repeatedCommands || []) {
|
|
101
|
+
evidenceText.push(item.value);
|
|
102
|
+
repeatedCommandMentions += Number(item.count || 0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
sessions,
|
|
107
|
+
text: normalizeRule(evidenceText.join(" ")),
|
|
108
|
+
toolOutputTokens,
|
|
109
|
+
generatedArtifactMentions,
|
|
110
|
+
repeatedReadMentions,
|
|
111
|
+
repeatedCommandMentions,
|
|
112
|
+
failureMentions,
|
|
113
|
+
loopSessions,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeEvidenceTerm(value) {
|
|
118
|
+
return normalizeRule(String(value || "").replace(/['"`]/g, ""));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractRuleTerms(ruleText) {
|
|
122
|
+
const source = String(ruleText || "");
|
|
123
|
+
const terms = [];
|
|
124
|
+
const pathPattern = /(?:[\w.@-]+\/)+[\w.@+-]+\.[A-Za-z0-9]{1,12}|[\w.@+-]+\.[A-Za-z0-9]{1,12}/g;
|
|
125
|
+
for (const match of source.matchAll(pathPattern)) terms.push(match[0]);
|
|
126
|
+
|
|
127
|
+
const beforeEditMatch = source.match(/\b(?:edit|editing|change|changing|modify|modifying|touch|touching)\s+([A-Za-z0-9_./@-]+(?:\s+[A-Za-z0-9_./@-]+){0,2})/i);
|
|
128
|
+
if (beforeEditMatch) terms.push(beforeEditMatch[1]);
|
|
129
|
+
|
|
130
|
+
return Array.from(new Set(terms.map(normalizeEvidenceTerm).filter((term) => term.length >= 3)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getRitualComplianceSignal(rule, evidence) {
|
|
134
|
+
const text = rule.normalized;
|
|
135
|
+
const asksReadBeforeEdit =
|
|
136
|
+
/\b(?:always|must|first|before|prior to)\b/.test(text) &&
|
|
137
|
+
/\b(?:read|review|check|inspect|open)\b/.test(text) &&
|
|
138
|
+
/\b(?:before|prior to)\b/.test(text) &&
|
|
139
|
+
/\b(?:edit|editing|change|changing|modify|modifying|touch|touching)\b/.test(text);
|
|
140
|
+
if (!asksReadBeforeEdit) return null;
|
|
141
|
+
|
|
142
|
+
const terms = extractRuleTerms(rule.text);
|
|
143
|
+
if (terms.length < 2) return null;
|
|
144
|
+
|
|
145
|
+
const matchedTerms = terms.filter((term) => evidence.text.includes(term));
|
|
146
|
+
const enoughRitualEvidence = matchedTerms.length >= 2;
|
|
147
|
+
const outcomeStillNoisy =
|
|
148
|
+
evidence.failureMentions > 0 ||
|
|
149
|
+
evidence.repeatedCommandMentions >= 3 ||
|
|
150
|
+
evidence.toolOutputTokens >= 25000 ||
|
|
151
|
+
evidence.loopSessions > 0;
|
|
152
|
+
|
|
153
|
+
if (!enoughRitualEvidence || !outcomeStillNoisy) return null;
|
|
154
|
+
return {
|
|
155
|
+
terms,
|
|
156
|
+
matchedTerms,
|
|
157
|
+
reason: "read-before-edit ritual appeared in session evidence, but noisy/failing session evidence suggests it may not have constrained the final work",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function classifyRule(rule, evidence, duplicateCount) {
|
|
162
|
+
const text = rule.normalized;
|
|
163
|
+
const evidenceHits = rule.keywords.filter((keyword) => evidence.text.includes(keyword));
|
|
164
|
+
const concerns = [];
|
|
165
|
+
const signals = [];
|
|
166
|
+
|
|
167
|
+
if (duplicateCount > 1) concerns.push("duplicate");
|
|
168
|
+
if (rule.tokens > 80) concerns.push("verbose");
|
|
169
|
+
if (rule.keywords.length === 0) concerns.push("low-signal");
|
|
170
|
+
if (/(always|never|must|do not|don't|avoid)/i.test(rule.text)) signals.push("directive");
|
|
171
|
+
|
|
172
|
+
const asksNoArtifacts = /(do not|don't|avoid|never|ignore|exclude).*(generated|artifact|dist|build|coverage|node_modules|lockfile|package-lock|logs?)/i.test(rule.text);
|
|
173
|
+
const asksNoOutputFlood = /(shield|summarize|avoid|do not|don't|never).*(output|logs?|stdout|stderr|command)/i.test(rule.text);
|
|
174
|
+
const asksNarrowReads = /(small|narrow|scoped|focused|avoid broad|do not read|don't read).*(read|search|grep|context|file)/i.test(rule.text);
|
|
175
|
+
|
|
176
|
+
if (asksNoArtifacts && evidence.generatedArtifactMentions > 0) concerns.push("violated-by-session-evidence");
|
|
177
|
+
if (asksNoOutputFlood && evidence.toolOutputTokens >= 25000) concerns.push("violated-by-session-evidence");
|
|
178
|
+
if (asksNarrowReads && evidence.repeatedReadMentions >= 10) concerns.push("violated-by-session-evidence");
|
|
179
|
+
|
|
180
|
+
if (evidenceHits.length) signals.push("session-correlated");
|
|
181
|
+
if (asksNoArtifacts || asksNoOutputFlood || asksNarrowReads) signals.push("guardrail");
|
|
182
|
+
const ritualCompliance = getRitualComplianceSignal(rule, evidence);
|
|
183
|
+
if (ritualCompliance) {
|
|
184
|
+
concerns.push("paraphrased-not-applied");
|
|
185
|
+
signals.push("ritual-compliance");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let status = "unclear";
|
|
189
|
+
let recommendation = "Keep for now, but review after more sessions.";
|
|
190
|
+
let score = 35;
|
|
191
|
+
|
|
192
|
+
if (concerns.includes("violated-by-session-evidence")) {
|
|
193
|
+
status = "observably-violated";
|
|
194
|
+
score = 15;
|
|
195
|
+
recommendation = `Keep the intent, but move it into a live guardrail/firewall or make it more enforceable; session evidence shows a measurable violation.`;
|
|
196
|
+
} else if (concerns.includes("paraphrased-not-applied")) {
|
|
197
|
+
status = "partial-compliance";
|
|
198
|
+
score = 40;
|
|
199
|
+
recommendation = "Rewrite this as an observable acceptance check; the session appears to perform the read/check ritual but still shows failure or noisy follow-through.";
|
|
200
|
+
} else if (signals.includes("session-correlated") && signals.includes("directive")) {
|
|
201
|
+
status = "pulling-weight";
|
|
202
|
+
score = 80;
|
|
203
|
+
recommendation = "Keep this rule; it maps to observed session behavior.";
|
|
204
|
+
} else if (signals.includes("guardrail")) {
|
|
205
|
+
status = "guardrail-no-recent-violation";
|
|
206
|
+
score = 65;
|
|
207
|
+
recommendation = "Keep, but consider moving task-specific parts into a firewall/context pack.";
|
|
208
|
+
} else if (concerns.includes("duplicate")) {
|
|
209
|
+
status = "duplicate";
|
|
210
|
+
score = 25;
|
|
211
|
+
recommendation = "Deduplicate this rule so it is paid for once, not in every instruction file.";
|
|
212
|
+
} else if (concerns.includes("low-signal")) {
|
|
213
|
+
status = "influence-unknown";
|
|
214
|
+
score = 45;
|
|
215
|
+
recommendation = "Do not prune automatically. This rule has no grep-able behavioral signal; use A/B ablation or rewrite it into observable instructions.";
|
|
216
|
+
} else if (rule.tokens > 80) {
|
|
217
|
+
status = "trim-candidate";
|
|
218
|
+
score = 30;
|
|
219
|
+
recommendation = "Shorten this rule or move details into a task-specific context pack.";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
...rule,
|
|
224
|
+
score,
|
|
225
|
+
status,
|
|
226
|
+
signals,
|
|
227
|
+
concerns,
|
|
228
|
+
evidenceHits,
|
|
229
|
+
partialCompliance: ritualCompliance || null,
|
|
230
|
+
recommendation,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildInstructionsAudit(options = {}) {
|
|
235
|
+
const root = options.cwd || process.cwd();
|
|
236
|
+
const limit = options.limit || 20;
|
|
237
|
+
const files = readInstructionFiles(root);
|
|
238
|
+
const rules = files.flatMap(extractRules);
|
|
239
|
+
const duplicates = new Map();
|
|
240
|
+
for (const rule of rules) {
|
|
241
|
+
duplicates.set(rule.normalized, (duplicates.get(rule.normalized) || 0) + 1);
|
|
242
|
+
}
|
|
243
|
+
const usage = getUsageSummary({ cwd: root, tool: options.tool || "all", limit });
|
|
244
|
+
const evidence = sessionEvidence(usage);
|
|
245
|
+
const auditedRules = rules.map((rule) => classifyRule(rule, evidence, duplicates.get(rule.normalized) || 0));
|
|
246
|
+
|
|
247
|
+
const byStatus = {};
|
|
248
|
+
for (const rule of auditedRules) byStatus[rule.status] = (byStatus[rule.status] || 0) + 1;
|
|
249
|
+
const prunable = auditedRules.filter((rule) => ["observably-violated", "duplicate", "trim-candidate"].includes(rule.status));
|
|
250
|
+
const partialCompliance = auditedRules.filter((rule) => rule.status === "partial-compliance");
|
|
251
|
+
const influenceUnknown = auditedRules.filter((rule) => rule.status === "influence-unknown");
|
|
252
|
+
const pullingWeight = auditedRules.filter((rule) => rule.status === "pulling-weight" || rule.status === "guardrail-no-recent-violation");
|
|
253
|
+
const duplicatedRules = auditedRules.filter((rule) => (duplicates.get(rule.normalized) || 0) > 1);
|
|
254
|
+
const prunableTokensPerLoad = prunable.reduce((sum, rule) => sum + Number(rule.tokens || 0), 0);
|
|
255
|
+
const sortedPrunable = prunable.sort((a, b) => a.score - b.score || b.tokens - a.tokens);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
schemaVersion: 1,
|
|
259
|
+
command: "instructions audit",
|
|
260
|
+
generatedAt: new Date().toISOString(),
|
|
261
|
+
scannedPath: root,
|
|
262
|
+
confidence: usage.confidence,
|
|
263
|
+
files: files.map((file) => ({ path: file.path, tokens: file.tokens, size: file.size })),
|
|
264
|
+
sessionsAnalyzed: evidence.sessions.length,
|
|
265
|
+
evidence: {
|
|
266
|
+
sources: usage.sources || [],
|
|
267
|
+
toolOutputTokens: evidence.toolOutputTokens,
|
|
268
|
+
generatedArtifactMentions: evidence.generatedArtifactMentions,
|
|
269
|
+
repeatedReadMentions: evidence.repeatedReadMentions,
|
|
270
|
+
repeatedCommandMentions: evidence.repeatedCommandMentions,
|
|
271
|
+
failureMentions: evidence.failureMentions,
|
|
272
|
+
loopSessions: evidence.loopSessions,
|
|
273
|
+
},
|
|
274
|
+
summary: {
|
|
275
|
+
totalRules: auditedRules.length,
|
|
276
|
+
byStatus,
|
|
277
|
+
prunableCandidates: prunable.length,
|
|
278
|
+
partialCompliance: partialCompliance.length,
|
|
279
|
+
influenceUnknown: influenceUnknown.length,
|
|
280
|
+
duplicatedRules: duplicatedRules.length,
|
|
281
|
+
prunableTokensPerLoad,
|
|
282
|
+
deadWeightCandidates: prunable.length,
|
|
283
|
+
wastedTokensPerLoad: prunableTokensPerLoad,
|
|
284
|
+
},
|
|
285
|
+
pullingWeight: pullingWeight.sort((a, b) => b.score - a.score).slice(0, 10),
|
|
286
|
+
prunable: sortedPrunable.slice(0, 20),
|
|
287
|
+
partialCompliance: partialCompliance.sort((a, b) => a.score - b.score || b.tokens - a.tokens).slice(0, 20),
|
|
288
|
+
influenceUnknown: influenceUnknown.sort((a, b) => b.tokens - a.tokens).slice(0, 20),
|
|
289
|
+
deadWeight: sortedPrunable.slice(0, 20),
|
|
290
|
+
rules: auditedRules,
|
|
291
|
+
next: [
|
|
292
|
+
prunable.length ? "Fix observably violated, duplicate, or trim-safe rules first." : "No safely prunable rules detected yet.",
|
|
293
|
+
partialCompliance.length ? "Rewrite partial-compliance rules as observable acceptance checks." : null,
|
|
294
|
+
influenceUnknown.length ? "Treat influence-unknown rules as A/B ablation candidates, not safe deletes." : null,
|
|
295
|
+
duplicatedRules.length ? "Deduplicate repeated rules across CLAUDE.md / AGENTS.md / Codex instruction files." : null,
|
|
296
|
+
evidence.sessions.length < 5 ? "Run this again after more sessions; multi-session evidence makes instruction ROI stronger." : null,
|
|
297
|
+
`${NPX_COMMAND} firewall <task> for rules that only matter in one workflow.`,
|
|
298
|
+
].filter(Boolean),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderInstructionsAuditTerminal(audit) {
|
|
303
|
+
const lines = [];
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("Prismo Instruction ROI Audit");
|
|
306
|
+
lines.push("");
|
|
307
|
+
if (!audit.files.length) {
|
|
308
|
+
lines.push("No instruction files found.");
|
|
309
|
+
lines.push("Checked: CLAUDE.md, AGENTS.md, .codex/*, .openai/instructions.md");
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
lines.push(`Files: ${audit.files.map((file) => `${file.path} (${formatTokenCount(file.tokens)} tokens)`).join(", ")}`);
|
|
313
|
+
lines.push(`Rules: ${audit.summary.totalRules}`);
|
|
314
|
+
lines.push(`Sessions analyzed: ${audit.sessionsAnalyzed}`);
|
|
315
|
+
lines.push(`Safely prunable candidates: ${audit.summary.prunableCandidates} (~${formatTokenCount(audit.summary.prunableTokensPerLoad)} tokens/load)`);
|
|
316
|
+
lines.push(`Partial compliance: ${audit.summary.partialCompliance}`);
|
|
317
|
+
lines.push(`Influence unknown: ${audit.summary.influenceUnknown}`);
|
|
318
|
+
lines.push(`Duplicates: ${audit.summary.duplicatedRules}`);
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("Pulling Weight");
|
|
321
|
+
if (audit.pullingWeight.length) {
|
|
322
|
+
audit.pullingWeight.slice(0, 5).forEach((rule) => {
|
|
323
|
+
lines.push(`- ${rule.file}:${rule.line} [${rule.status}] ${rule.text}`);
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
lines.push("- No high-confidence useful rules detected yet.");
|
|
327
|
+
}
|
|
328
|
+
lines.push("");
|
|
329
|
+
lines.push("Safely Prunable / Rewrite Candidates");
|
|
330
|
+
if (audit.prunable.length) {
|
|
331
|
+
audit.prunable.slice(0, 8).forEach((rule) => {
|
|
332
|
+
lines.push(`- ${rule.file}:${rule.line} [${rule.status}] ${rule.text}`);
|
|
333
|
+
lines.push(` ${rule.recommendation}`);
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
lines.push("- No obvious candidates.");
|
|
337
|
+
}
|
|
338
|
+
lines.push("");
|
|
339
|
+
lines.push("Partial Compliance / Ritual Without Proof");
|
|
340
|
+
if (audit.partialCompliance.length) {
|
|
341
|
+
audit.partialCompliance.slice(0, 6).forEach((rule) => {
|
|
342
|
+
lines.push(`- ${rule.file}:${rule.line} [${rule.status}] ${rule.text}`);
|
|
343
|
+
if (rule.partialCompliance?.matchedTerms?.length) {
|
|
344
|
+
lines.push(` Matched: ${rule.partialCompliance.matchedTerms.join(", ")}`);
|
|
345
|
+
}
|
|
346
|
+
lines.push(` ${rule.recommendation}`);
|
|
347
|
+
});
|
|
348
|
+
} else {
|
|
349
|
+
lines.push("- None detected.");
|
|
350
|
+
}
|
|
351
|
+
lines.push("");
|
|
352
|
+
lines.push("Influence Unknown / A-B Candidates");
|
|
353
|
+
if (audit.influenceUnknown.length) {
|
|
354
|
+
audit.influenceUnknown.slice(0, 6).forEach((rule) => {
|
|
355
|
+
lines.push(`- ${rule.file}:${rule.line} [${rule.status}] ${rule.text}`);
|
|
356
|
+
lines.push(` ${rule.recommendation}`);
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
lines.push("- None detected.");
|
|
360
|
+
}
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push("Next");
|
|
363
|
+
audit.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
|
|
364
|
+
return lines.join("\n");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
buildInstructionsAudit,
|
|
369
|
+
renderInstructionsAuditTerminal,
|
|
370
|
+
};
|
|
371
|
+
};
|