liteagents 2.15.2 → 2.17.0
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 +196 -0
- package/README.md +8 -7
- package/package.json +2 -2
- package/packages/ampcode/AGENT.md +2 -2
- package/packages/ampcode/agents/quality-assurance.md +1 -1
- package/packages/ampcode/commands/docs-builder/docs-builder.cjs +2382 -0
- package/packages/ampcode/commands/docs-builder.md +787 -223
- package/packages/ampcode/commands/remember/AGENT_RULES.md +13 -43
- package/packages/ampcode/commands/remember/{friction.js → friction.cjs} +211 -99
- package/packages/ampcode/commands/remember.md +208 -29
- package/packages/claude/CLAUDE.md +3 -3
- package/packages/claude/commands/docs-builder/docs-builder.cjs +2382 -0
- package/packages/claude/commands/docs-builder.md +873 -0
- package/packages/claude/commands/remember/AGENT_RULES.md +10 -40
- package/packages/claude/commands/remember/{friction.js → friction.cjs} +208 -96
- package/packages/claude/commands/remember.md +208 -29
- package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package-lock.json +9 -9
- package/packages/droid/AGENTS.md +2 -2
- package/packages/droid/commands/docs-builder/docs-builder.cjs +2382 -0
- package/packages/droid/commands/docs-builder.md +787 -223
- package/packages/droid/commands/remember/AGENT_RULES.md +13 -43
- package/packages/droid/commands/remember/{friction.js → friction.cjs} +211 -99
- package/packages/droid/commands/remember.md +208 -29
- package/packages/droid/droids/quality-assurance.md +1 -1
- package/packages/opencode/AGENTS.md +2 -2
- package/packages/opencode/agent/quality-assurance.md +1 -1
- package/packages/opencode/command/docs-builder/docs-builder.cjs +2382 -0
- package/packages/opencode/command/docs-builder.md +787 -223
- package/packages/opencode/command/remember/AGENT_RULES.md +13 -43
- package/packages/opencode/command/remember/{friction.js → friction.cjs} +211 -99
- package/packages/opencode/command/remember.md +208 -29
- package/packages/opencode/opencode.jsonc +2 -6
- package/packages/subagentic-manual.md +31 -32
- package/packages/ampcode/commands/docs-builder/templates.md +0 -601
- package/packages/claude/skills/docs-builder/SKILL.md +0 -309
- package/packages/claude/skills/docs-builder/references/templates.md +0 -601
- package/packages/droid/commands/docs-builder/templates.md +0 -601
- package/packages/opencode/command/docs-builder/templates.md +0 -601
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
3. [Development Standards](#development-standards)
|
|
7
7
|
4. [Testing Standards](#testing-standards)
|
|
8
8
|
5. [Security & Robustness Invariants](#security--robustness-invariants)
|
|
9
|
-
6. [
|
|
10
|
-
7. [
|
|
11
|
-
8. [
|
|
12
|
-
9. [
|
|
13
|
-
10. [
|
|
14
|
-
11. [AI Agent Instructions](#ai-agent-instructions)
|
|
9
|
+
6. [Environment](#environment)
|
|
10
|
+
7. [Development Workflow](#development-workflow)
|
|
11
|
+
8. [Twelve-Factor Checklist](#twelve-factor-checklist)
|
|
12
|
+
9. [AGENT.md Stub](#claudemd-stub)
|
|
13
|
+
10. [AI Agent Instructions](#ai-agent-instructions)
|
|
15
14
|
|
|
16
15
|
---
|
|
17
16
|
|
|
@@ -21,7 +20,7 @@ Every task runs through three layers. Do not skip ahead to code.
|
|
|
21
20
|
|
|
22
21
|
1. **Spec — agree on intent before touching anything.** Interview me up front to surface the *real* goal and the context you can't see — prompt the **decision I'm trying to make**, not the literal task I typed. Break the scope into small buckets with checkpoints. **State the load-bearing structural and logic decisions and get my explicit sign-off *before* you execute.** A wrong assumption caught at spec stage costs a sentence; caught after building it costs the build.
|
|
23
22
|
2. **Verify — define "good" up front, then prove it.** Write down what success looks like *before* changing code. Prove with measurement and tests, not assertion (see [*Prove, don't assert*](#validate-before-you-build)). Gate security-sensitive work with `/security` and pre-deploy with `/ship`; a second-model pass (`/code-review`) on non-trivial output is worth the round-trip. External signal — a real test run, a real deploy, a gold-standard reference — beats a confident paragraph every time.
|
|
24
|
-
3. **Environment — the
|
|
23
|
+
3. **Environment — the standing context.** This file primes every session. Critical-path protections (secrets, auth, schema, CI) are stated as **Always / Ask / Never** below and bind you as written. Where your tool offers a permission allow/ask/deny list, mirror them there so they are enforced and not merely requested.
|
|
25
24
|
|
|
26
25
|
> The model is brilliant at execution and blind to intent. You can outsource the typing; you cannot outsource the understanding. Surface assumptions — don't bury them.
|
|
27
26
|
|
|
@@ -34,7 +33,7 @@ Every task runs through three layers. Do not skip ahead to code.
|
|
|
34
33
|
- **Checkpoint before executing**: State the load-bearing structural and logic decisions and get my explicit sign-off *before* you write code. Never run ahead on an unverified assumption — flag it and stop
|
|
35
34
|
- **Fact-Based**: Base all recommendations on verified, current information. Prefer external signal (a real run, a real source) over a confident guess
|
|
36
35
|
- **Simplicity Advocate**: Call out overcomplications and suggest simpler alternatives
|
|
37
|
-
- **Safety First**: Never modify critical systems without explicit understanding and approval
|
|
36
|
+
- **Safety First**: Never modify critical systems without explicit understanding and approval
|
|
38
37
|
|
|
39
38
|
### User Profile
|
|
40
39
|
- **Technical Level**: Non-coder but technically savvy
|
|
@@ -45,10 +44,10 @@ Every task runs through three layers. Do not skip ahead to code.
|
|
|
45
44
|
|
|
46
45
|
### Required Safeguards (Always / Ask / Never)
|
|
47
46
|
|
|
48
|
-
Not courtesies
|
|
47
|
+
Not courtesies. These bind you as written, whether or not your tool enforces them.
|
|
49
48
|
|
|
50
49
|
- **Always** identify affected files before making changes, and explain what will change and why
|
|
51
|
-
- **Ask first** — stop and get explicit sign-off — before modifying authentication systems, database schema or migrations, CI workflows, or `.
|
|
50
|
+
- **Ask first** — stop and get explicit sign-off — before modifying authentication systems, database schema or migrations, CI workflows, or `.amp/settings.json`
|
|
52
51
|
- **Never** write secrets into the tree (`.env`/`*.env`, keys, credentials). They load from the environment at runtime; only a value-less `.env.example` is committed
|
|
53
52
|
|
|
54
53
|
---
|
|
@@ -220,35 +219,6 @@ Also hold the line on: input validation at every trust boundary (untrusted uploa
|
|
|
220
219
|
|
|
221
220
|
---
|
|
222
221
|
|
|
223
|
-
## Guardrails (Enforced, Not Requested)
|
|
224
|
-
|
|
225
|
-
A prompt rule is a request the model can rationalise past. For anything that actually matters — secrets, auth, schema — don't rely on soft instruction. Enforce it with a **pre-tool hook** that intercepts the call *before* it runs and decides on an **Always / Ask / Never** basis:
|
|
226
|
-
|
|
227
|
-
- **Never** — writing `.env`/`*.env`, keys, or credential files is blocked outright (secrets load from the environment, never the tree). Destructive shell (`rm -rf` of a root-ish target, redirecting into a secret) is blocked too.
|
|
228
|
-
- **Ask** — touching auth, DB schema/migrations, CI workflows, or `.claude/settings.json` forces a human confirmation. Same for force-push / push to a default branch.
|
|
229
|
-
- **Always / allow** — everything else proceeds through the normal permission flow; the hook stays out of the way.
|
|
230
|
-
|
|
231
|
-
The reference implementation ships in this repo at [`.claude/hooks/guardrails.py`](.claude/hooks/guardrails.py) — stdlib only, no deps, fails open on a malformed event so it can never wedge the agent. The Never/Ask lists are constants at the top; **tune them per project**. To wire it up, add to the project's `.claude/settings.json`:
|
|
232
|
-
|
|
233
|
-
```json
|
|
234
|
-
{
|
|
235
|
-
"hooks": {
|
|
236
|
-
"PreToolUse": [
|
|
237
|
-
{
|
|
238
|
-
"matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
239
|
-
"hooks": [
|
|
240
|
-
{ "type": "command", "command": "python3 .claude/hooks/guardrails.py" }
|
|
241
|
-
]
|
|
242
|
-
}
|
|
243
|
-
]
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
The hook is the hard line; the prose rules above are why it exists. Keep them in sync — when you tighten one, tighten the other.
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
222
|
## Environment
|
|
253
223
|
|
|
254
224
|
- **OS**: Fedora Linux (use `dnf` for packages, `systemctl` for services)
|
|
@@ -292,9 +262,9 @@ The [Twelve-Factor App](https://12factor.net) methodology for modern, scalable a
|
|
|
292
262
|
|
|
293
263
|
---
|
|
294
264
|
|
|
295
|
-
##
|
|
265
|
+
## AGENT.md Stub
|
|
296
266
|
|
|
297
|
-
Copy this to any project's
|
|
267
|
+
Copy this to any project's AGENT.md. These are mandatory rules, not suggestions.
|
|
298
268
|
|
|
299
269
|
```markdown
|
|
300
270
|
## Dev Rules
|
|
@@ -311,7 +281,7 @@ Copy this to any project's CLAUDE.md. These are mandatory rules, not suggestions
|
|
|
311
281
|
|
|
312
282
|
**Responsive web UI is mandatory.** Any web UI must work on mobile by default — fluid layouts, viewport meta, breakpoints, no horizontal scroll. Verify in DevTools device emulation before claiming a UI task is done. POCs exempt; real projects are not.
|
|
313
283
|
|
|
314
|
-
For full development and testing standards, see `.
|
|
284
|
+
For full development and testing standards, see `.amp/remember/AGENT_RULES.md`.
|
|
315
285
|
```
|
|
316
286
|
|
|
317
287
|
---
|
|
@@ -325,5 +295,5 @@ When working with this user:
|
|
|
325
295
|
4. **Explain the "why"** behind technical recommendations
|
|
326
296
|
5. **Flag potential issues** before they become problems — name the assumption, don't bury it
|
|
327
297
|
6. **Suggest simpler alternatives** when appropriate
|
|
328
|
-
7. **Ask first** before touching auth, DB schema/migrations, CI, or settings; **never** commit secrets
|
|
298
|
+
7. **Ask first** before touching auth, DB schema/migrations, CI, or settings; **never** commit secrets
|
|
329
299
|
8. **Always identify** which files will be affected by changes
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* Friction analysis pipeline - analyze sessions and extract antigens.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* node friction.
|
|
7
|
-
* node friction.
|
|
6
|
+
* node friction.cjs <sessions-directory>
|
|
7
|
+
* node friction.cjs ~/.config/amp/projects/<encoded-project-dir>/
|
|
8
|
+
*
|
|
9
|
+
* Extension is `.cjs`, not `.js`, ON PURPOSE. Installed project-locally into a repo whose
|
|
10
|
+
* package.json declares "type": "module", a `.js` file loads as an ES module and every
|
|
11
|
+
* `require` below throws before the first line of work. `.cjs` pins CommonJS regardless of
|
|
12
|
+
* the host project. Found via docs-builder.cjs hitting the identical bug on a real repo.
|
|
8
13
|
*
|
|
9
14
|
* Outputs (all in .amp/remember/friction/):
|
|
10
15
|
* friction_analysis.json - Per-session analysis
|
|
@@ -19,6 +24,7 @@
|
|
|
19
24
|
|
|
20
25
|
const fs = require('fs');
|
|
21
26
|
const path = require('path');
|
|
27
|
+
const os = require('os');
|
|
22
28
|
|
|
23
29
|
// =============================================================================
|
|
24
30
|
// EMBEDDED CONFIG (from friction_config.json)
|
|
@@ -73,6 +79,13 @@ const CONFIG = {
|
|
|
73
79
|
},
|
|
74
80
|
};
|
|
75
81
|
|
|
82
|
+
// Genuine USER REACTIONS that a friction antigen may anchor on. tool_loop /
|
|
83
|
+
// repeated_question are agent-behavior signals (no user text to cluster, and
|
|
84
|
+
// repeated_question over-fires on tool output) — they corroborate severity,
|
|
85
|
+
// never seed. Shared by analyzeBadSession (which candidate to build) and
|
|
86
|
+
// extractMain (which sessions are even worth reading for the dedup pass).
|
|
87
|
+
const ANCHOR_SIGNALS = ['user_correction', 'user_curse', 'interrupt_cascade'];
|
|
88
|
+
|
|
76
89
|
// =============================================================================
|
|
77
90
|
// UTILITY FUNCTIONS
|
|
78
91
|
// =============================================================================
|
|
@@ -99,15 +112,6 @@ function parseJsonl(raw, source) {
|
|
|
99
112
|
return records;
|
|
100
113
|
}
|
|
101
114
|
|
|
102
|
-
function parseISODate(s) {
|
|
103
|
-
if (!s) return null;
|
|
104
|
-
try {
|
|
105
|
-
return new Date(s.replace('Z', '+00:00'));
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
115
|
function formatDuration(minutes) {
|
|
112
116
|
if (minutes < 60) return `${minutes}m`;
|
|
113
117
|
const hours = Math.floor(minutes / 60);
|
|
@@ -152,11 +156,14 @@ function deriveSessionName(sessionFile, metadata) {
|
|
|
152
156
|
|
|
153
157
|
let project;
|
|
154
158
|
if (parent.startsWith('-')) {
|
|
159
|
+
// Detected at runtime — never hardcode a username or a personal folder.
|
|
160
|
+
// Claude Code encodes a project dir by replacing path separators with '-',
|
|
161
|
+
// so the current user's home encodes the same way (/home/ana -> -home-ana).
|
|
162
|
+
const homeEnc = os.homedir().replace(/[\\/]/g, '-');
|
|
155
163
|
const prefixes = [
|
|
156
|
-
'-
|
|
157
|
-
'-home-hamr-Documents-PycharmProjects-',
|
|
158
|
-
'-home-hamr-',
|
|
164
|
+
homeEnc + '-',
|
|
159
165
|
'-home-',
|
|
166
|
+
'-Users-',
|
|
160
167
|
'-',
|
|
161
168
|
];
|
|
162
169
|
let found = false;
|
|
@@ -249,7 +256,7 @@ function looksLikeTerminalPaste(text) {
|
|
|
249
256
|
function extractSignals(sessionFile) {
|
|
250
257
|
const signals = [];
|
|
251
258
|
let llmClaimedSuccess = false;
|
|
252
|
-
const
|
|
259
|
+
const toolHistoryCounts = new Map();
|
|
253
260
|
const metadata = {};
|
|
254
261
|
|
|
255
262
|
const raw = fs.readFileSync(sessionFile, 'utf-8');
|
|
@@ -445,6 +452,15 @@ function extractSignals(sessionFile) {
|
|
|
445
452
|
|
|
446
453
|
// User messages (GOLD)
|
|
447
454
|
if (typeof content === 'string') {
|
|
455
|
+
// Harness-injected notifications ride in as user-role turns but are
|
|
456
|
+
// machine text, not user text — skip signal detection entirely so a
|
|
457
|
+
// notification's boilerplate prose can't be mistaken for a curse or
|
|
458
|
+
// correction aimed at the agent.
|
|
459
|
+
const trimmedContent = content.trim();
|
|
460
|
+
if (trimmedContent.startsWith('<task-notification>') || trimmedContent.startsWith('[SYSTEM NOTIFICATION')) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
448
464
|
if (content.toLowerCase().includes('/stash')) {
|
|
449
465
|
signals.push({
|
|
450
466
|
ts,
|
|
@@ -516,12 +532,8 @@ function extractSignals(sessionFile) {
|
|
|
516
532
|
if (block.type === 'tool_use') {
|
|
517
533
|
const toolName = block.name;
|
|
518
534
|
const sig = JSON.stringify([toolName, JSON.stringify(block.input || {})]);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
let count = 0;
|
|
522
|
-
for (const h of toolHistory) {
|
|
523
|
-
if (h === sig) count++;
|
|
524
|
-
}
|
|
535
|
+
toolHistoryCounts.set(sig, (toolHistoryCounts.get(sig) || 0) + 1);
|
|
536
|
+
const count = toolHistoryCounts.get(sig);
|
|
525
537
|
if (count >= 3) {
|
|
526
538
|
signals.push({
|
|
527
539
|
ts,
|
|
@@ -935,7 +947,7 @@ function aggregateSessions(analyses, config) {
|
|
|
935
947
|
const totalObjective = ((aggregateBySource.tool || {}).total_friction || 0) +
|
|
936
948
|
((aggregateBySource.user || {}).total_friction || 0);
|
|
937
949
|
const totalLlm = (aggregateBySource.llm || {}).total_friction || 1;
|
|
938
|
-
const snr =
|
|
950
|
+
const snr = Math.abs(totalObjective / totalLlm);
|
|
939
951
|
|
|
940
952
|
// Verdict
|
|
941
953
|
const thresholds = config.thresholds;
|
|
@@ -970,8 +982,7 @@ function aggregateSessions(analyses, config) {
|
|
|
970
982
|
for (const [source, data] of Object.entries(aggregateBySource)) {
|
|
971
983
|
const sessionsCount = data.sessions_with_signals;
|
|
972
984
|
// Sort top_signals by count descending (like Python's Counter.most_common)
|
|
973
|
-
const sortedSignals =
|
|
974
|
-
.sort((a, b) => b[1] - a[1]);
|
|
985
|
+
const sortedSignals = sortedEntries(data.top_signals);
|
|
975
986
|
const topSignals = {};
|
|
976
987
|
for (const [k, v] of sortedSignals) topSignals[k] = v;
|
|
977
988
|
|
|
@@ -1111,52 +1122,6 @@ function aggregateSessions(analyses, config) {
|
|
|
1111
1122
|
};
|
|
1112
1123
|
}
|
|
1113
1124
|
|
|
1114
|
-
// =============================================================================
|
|
1115
|
-
// FRICTION ANALYZE - print helpers
|
|
1116
|
-
// =============================================================================
|
|
1117
|
-
|
|
1118
|
-
function printBox(title, lines, width) {
|
|
1119
|
-
width = width || 60;
|
|
1120
|
-
const hr = '\u2500'.repeat(width - 2);
|
|
1121
|
-
console.log(`\u250C${hr}\u2510`);
|
|
1122
|
-
console.log(`\u2502 ${title.toUpperCase().padEnd(width - 4)} \u2502`);
|
|
1123
|
-
console.log(`\u251C${hr}\u2524`);
|
|
1124
|
-
for (let line of lines) {
|
|
1125
|
-
if (line.length > width - 4) line = line.slice(0, width - 7) + '...';
|
|
1126
|
-
console.log(`\u2502 ${line.padEnd(width - 4)} \u2502`);
|
|
1127
|
-
}
|
|
1128
|
-
console.log(`\u2514${hr}\u2518`);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function printTable(headers, rows, colWidths) {
|
|
1132
|
-
if (!colWidths) {
|
|
1133
|
-
colWidths = headers.map((h, i) => {
|
|
1134
|
-
let max = String(h).length;
|
|
1135
|
-
for (const row of rows) {
|
|
1136
|
-
const len = String(row[i]).length;
|
|
1137
|
-
if (len > max) max = len;
|
|
1138
|
-
}
|
|
1139
|
-
return max + 2;
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
const topBorder = '\u250C' + colWidths.map(w => '\u2500'.repeat(w)).join('\u252C') + '\u2510';
|
|
1144
|
-
const headerLine = '\u2502' + headers.map((h, i) => ` ${String(h).padEnd(colWidths[i] - 2)} `).join('\u2502') + '\u2502';
|
|
1145
|
-
const sep = '\u251C' + colWidths.map(w => '\u2500'.repeat(w)).join('\u253C') + '\u2524';
|
|
1146
|
-
|
|
1147
|
-
console.log(topBorder);
|
|
1148
|
-
console.log(headerLine);
|
|
1149
|
-
console.log(sep);
|
|
1150
|
-
|
|
1151
|
-
for (const row of rows) {
|
|
1152
|
-
const rowLine = '\u2502' + row.map((v, i) => ` ${String(v).padEnd(colWidths[i] - 2)} `).join('\u2502') + '\u2502';
|
|
1153
|
-
console.log(rowLine);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
const bottomBorder = '\u2514' + colWidths.map(w => '\u2500'.repeat(w)).join('\u2534') + '\u2518';
|
|
1157
|
-
console.log(bottomBorder);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
1125
|
// =============================================================================
|
|
1161
1126
|
// FRICTION ANALYZE - generate_detailed_report
|
|
1162
1127
|
// =============================================================================
|
|
@@ -1383,13 +1348,13 @@ function generateDetailedReport(outputDir, analyses, summary, config, signalCoun
|
|
|
1383
1348
|
const recommendations = [];
|
|
1384
1349
|
|
|
1385
1350
|
if (falseSuccessCount > 10) {
|
|
1386
|
-
recommendations.push('**High Priority:** Add
|
|
1351
|
+
recommendations.push('**High Priority:** Add AGENT.md rule to verify exit codes before claiming success');
|
|
1387
1352
|
}
|
|
1388
1353
|
if (interruptCount > 20) {
|
|
1389
1354
|
recommendations.push('**High Priority:** Commands timing out or stuck - review for heavy operations that need optimization');
|
|
1390
1355
|
}
|
|
1391
1356
|
if ((signalCounts.tool_loop || 0) > 3) {
|
|
1392
|
-
recommendations.push('**Medium Priority:** Add
|
|
1357
|
+
recommendations.push('**Medium Priority:** Add AGENT.md rule to detect and break out of tool loops');
|
|
1393
1358
|
}
|
|
1394
1359
|
if (interventionCountLocal / summary.overall.interactive_sessions > 0.4) {
|
|
1395
1360
|
recommendations.push('**Critical:** >40% abandonment rate - major UX issues, review antigens for patterns');
|
|
@@ -1707,9 +1672,13 @@ function findSessionFile(sessionsDir, sessionId) {
|
|
|
1707
1672
|
// ANTIGEN EXTRACT - extract helpers
|
|
1708
1673
|
// =============================================================================
|
|
1709
1674
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1675
|
+
/**
|
|
1676
|
+
* Read and parse a session file once into its user/assistant turns.
|
|
1677
|
+
* Callers that need multiple context windows from the same session (one per
|
|
1678
|
+
* anchor signal) should build this once and reuse it, instead of re-reading
|
|
1679
|
+
* and re-parsing the whole file per anchor.
|
|
1680
|
+
*/
|
|
1681
|
+
function buildTurns(sessionFile) {
|
|
1713
1682
|
const raw = fs.readFileSync(sessionFile, 'utf-8');
|
|
1714
1683
|
const events = parseJsonl(raw, sessionFile);
|
|
1715
1684
|
|
|
@@ -1720,6 +1689,11 @@ function extractContextWindow(sessionFile, anchorTs, windowSize) {
|
|
|
1720
1689
|
turns.push({ ts, type: event.type, event });
|
|
1721
1690
|
}
|
|
1722
1691
|
}
|
|
1692
|
+
return turns;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function extractContextWindow(turns, anchorTs, windowSize) {
|
|
1696
|
+
windowSize = windowSize || 5;
|
|
1723
1697
|
|
|
1724
1698
|
// Find anchor position
|
|
1725
1699
|
let anchorIdx = null;
|
|
@@ -1852,6 +1826,8 @@ function extractUserMessage(event) {
|
|
|
1852
1826
|
if (trimmed.startsWith('<command-name>')) return '';
|
|
1853
1827
|
if (trimmed.startsWith('<system-reminder>')) return '';
|
|
1854
1828
|
if (trimmed.startsWith('<local-command-stdout>')) return '';
|
|
1829
|
+
if (trimmed.startsWith('<task-notification>')) return '';
|
|
1830
|
+
if (trimmed.startsWith('[SYSTEM NOTIFICATION')) return '';
|
|
1855
1831
|
|
|
1856
1832
|
return text.slice(0, 500);
|
|
1857
1833
|
}
|
|
@@ -1866,25 +1842,21 @@ function analyzeBadSession(sessionFile, analysis, signals) {
|
|
|
1866
1842
|
// NEW: anchor antigens only on OBSERVED user-reaction signals. Inferred
|
|
1867
1843
|
// proxies (false_success/session_abandoned/user_intervention) never seed —
|
|
1868
1844
|
// they only color severity. No fallback: a session with no observed reaction
|
|
1869
|
-
// produces no candidate (silence is not an antigen).
|
|
1870
|
-
|
|
1871
|
-
// agent-behavior signals (no user text to cluster, and repeated_question
|
|
1872
|
-
// over-fires on tool output) — they corroborate severity, never seed.
|
|
1873
|
-
const anchorSignals = [
|
|
1874
|
-
'user_correction',
|
|
1875
|
-
'user_curse',
|
|
1876
|
-
'interrupt_cascade',
|
|
1877
|
-
];
|
|
1878
|
-
|
|
1879
|
-
const anchors = signals.filter(s => s.session === sessionId && anchorSignals.includes(s.signal));
|
|
1845
|
+
// produces no candidate (silence is not an antigen). See ANCHOR_SIGNALS.
|
|
1846
|
+
const anchors = signals.filter(s => s.session === sessionId && ANCHOR_SIGNALS.includes(s.signal));
|
|
1880
1847
|
|
|
1881
1848
|
const candidates = [];
|
|
1849
|
+
if (anchors.length === 0) return candidates;
|
|
1850
|
+
|
|
1851
|
+
// Parse the session file once and reuse it for every anchor's context
|
|
1852
|
+
// window, instead of re-reading and re-parsing the whole file per anchor.
|
|
1853
|
+
const turns = buildTurns(sessionFile);
|
|
1882
1854
|
|
|
1883
1855
|
for (const anchor of anchors) {
|
|
1884
1856
|
const anchorTs = anchor.ts || '';
|
|
1885
1857
|
const anchorSignal = anchor.signal || 'unknown';
|
|
1886
1858
|
|
|
1887
|
-
const window = extractContextWindow(
|
|
1859
|
+
const window = extractContextWindow(turns, anchorTs, 5);
|
|
1888
1860
|
if (window.length === 0) continue;
|
|
1889
1861
|
|
|
1890
1862
|
const allFiles = new Set();
|
|
@@ -1975,11 +1947,93 @@ function analyzeBadSession(sessionFile, analysis, signals) {
|
|
|
1975
1947
|
return candidates;
|
|
1976
1948
|
}
|
|
1977
1949
|
|
|
1950
|
+
// =============================================================================
|
|
1951
|
+
// ANTIGEN EXTRACT - session dedup (forks/resumes of the same conversation)
|
|
1952
|
+
// =============================================================================
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* One conversation can exist as several session files (forks/resumes), each
|
|
1956
|
+
* with its own filename/uuid — friction identifies a session by the first 8
|
|
1957
|
+
* chars of the filename, so without this, one user reaction gets counted as
|
|
1958
|
+
* N distinct sessions, which can falsely trip the recurrence gate that
|
|
1959
|
+
* promotes a cluster to an antigen. Sessions that share >= 1 message uuid
|
|
1960
|
+
* are the SAME conversation; collapse them to one canonical session id
|
|
1961
|
+
* (the lexicographically smallest id in the group) before clustering.
|
|
1962
|
+
*
|
|
1963
|
+
* Returns { canonicalOf, groups }: canonicalOf maps every input id to its
|
|
1964
|
+
* group's canonical id; groups maps that canonical id to EVERY member id in
|
|
1965
|
+
* its group (used to emit the full session_ids set on a cluster, since the
|
|
1966
|
+
* canonical pick itself is only a stable grouping key, not guaranteed to be
|
|
1967
|
+
* the same file across runs when its date prefix falls back to mtime).
|
|
1968
|
+
*/
|
|
1969
|
+
function computeCanonicalSessionIds(sessionIds, fileFor) {
|
|
1970
|
+
const parent = new Map();
|
|
1971
|
+
for (const id of sessionIds) parent.set(id, id);
|
|
1972
|
+
const find = x => {
|
|
1973
|
+
while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); }
|
|
1974
|
+
return x;
|
|
1975
|
+
};
|
|
1976
|
+
const union = (a, b) => {
|
|
1977
|
+
const ra = find(a), rb = find(b);
|
|
1978
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
1979
|
+
};
|
|
1980
|
+
|
|
1981
|
+
// A real uuid is a long, per-message-unique value. Guard against a
|
|
1982
|
+
// degenerate/constant emitter (e.g. a tool whose logs always write "" or a
|
|
1983
|
+
// fixed placeholder) unioning the whole corpus into one session: too-short
|
|
1984
|
+
// uuids never seed a union, and a uuid shared across an implausible number
|
|
1985
|
+
// of distinct sessions (a genuine fork/resume chain is a handful of files,
|
|
1986
|
+
// not a dozen+) is treated as noise rather than real shared history.
|
|
1987
|
+
const MIN_UUID_LEN = 8;
|
|
1988
|
+
const MAX_SESSIONS_PER_UUID = 12;
|
|
1989
|
+
|
|
1990
|
+
const uuidToSessions = new Map();
|
|
1991
|
+
for (const id of sessionIds) {
|
|
1992
|
+
const file = fileFor.get(id);
|
|
1993
|
+
if (!file) continue;
|
|
1994
|
+
let events;
|
|
1995
|
+
try {
|
|
1996
|
+
events = parseJsonl(fs.readFileSync(file, 'utf-8'), file);
|
|
1997
|
+
} catch {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
for (const e of events) {
|
|
2001
|
+
if (typeof e.uuid !== 'string' || e.uuid.length < MIN_UUID_LEN) continue;
|
|
2002
|
+
if (!uuidToSessions.has(e.uuid)) uuidToSessions.set(e.uuid, []);
|
|
2003
|
+
uuidToSessions.get(e.uuid).push(id);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
for (const ids of uuidToSessions.values()) {
|
|
2007
|
+
const distinctIds = [...new Set(ids)];
|
|
2008
|
+
if (distinctIds.length > MAX_SESSIONS_PER_UUID) {
|
|
2009
|
+
console.error(`warn: uuid shared by ${distinctIds.length} sessions (> ${MAX_SESSIONS_PER_UUID}) — treated as noise, not unioned`);
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
for (let i = 1; i < distinctIds.length; i++) union(distinctIds[0], distinctIds[i]);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
const groups = new Map(); // root -> [ids]
|
|
2016
|
+
for (const id of sessionIds) {
|
|
2017
|
+
const root = find(id);
|
|
2018
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
2019
|
+
groups.get(root).push(id);
|
|
2020
|
+
}
|
|
2021
|
+
const canonicalOf = new Map();
|
|
2022
|
+
const groupsByCanonical = new Map();
|
|
2023
|
+
for (const ids of groups.values()) {
|
|
2024
|
+
const canonical = ids.slice().sort()[0];
|
|
2025
|
+
for (const id of ids) canonicalOf.set(id, canonical);
|
|
2026
|
+
groupsByCanonical.set(canonical, ids);
|
|
2027
|
+
}
|
|
2028
|
+
return { canonicalOf, groups: groupsByCanonical };
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1978
2031
|
// =============================================================================
|
|
1979
2032
|
// ANTIGEN EXTRACT - clusterCandidates
|
|
1980
2033
|
// =============================================================================
|
|
1981
2034
|
|
|
1982
|
-
function clusterCandidates(allCandidates) {
|
|
2035
|
+
function clusterCandidates(allCandidates, canonicalGroups) {
|
|
2036
|
+
canonicalGroups = canonicalGroups || new Map();
|
|
1983
2037
|
// NEW: cluster by CONTENT (keyword overlap of what the user actually said),
|
|
1984
2038
|
// not by (anchor_signal, tool_pattern). Inferred signals were already barred
|
|
1985
2039
|
// from seeding upstream; here they survive only as corroborating "errors"
|
|
@@ -1989,7 +2043,10 @@ function clusterCandidates(allCandidates) {
|
|
|
1989
2043
|
// Ubiquitous path/file tokens that carry no topical meaning — if we cluster on
|
|
1990
2044
|
// these we re-create OLD's over-merge (everything touches README/package.json).
|
|
1991
2045
|
const PATH_STOP = new Set([
|
|
1992
|
-
|
|
2046
|
+
// the current user's login name is a path token everywhere and carries no
|
|
2047
|
+
// topical meaning — detected at runtime, never hardcoded
|
|
2048
|
+
(os.userInfo().username || '').toLowerCase(),
|
|
2049
|
+
'home', 'users', 'documents', 'pycharmprojects', 'projects', 'claude', 'stash',
|
|
1993
2050
|
'memory', 'commands', 'command', 'skills', 'skill', 'src', 'lib', 'app', 'dist',
|
|
1994
2051
|
'build', 'node_modules', 'public', 'assets', 'utils', 'util', 'config', 'scripts',
|
|
1995
2052
|
'readme', 'package', 'index', 'main', 'test', 'tests', 'spec', 'lock',
|
|
@@ -2127,11 +2184,18 @@ function clusterCandidates(allCandidates) {
|
|
|
2127
2184
|
const peaks = cl.peaks.slice().sort((a, b) => a - b);
|
|
2128
2185
|
const topSh = [...cl.shCount.entries()]
|
|
2129
2186
|
.sort((a, b) => b[1] - a[1] || b[0].length - a[0].length).map(([k]) => k);
|
|
2130
|
-
|
|
2187
|
+
// Canonical ids are the dedup/grouping key (a session can be pinned to a
|
|
2188
|
+
// different-but-equivalent group member across runs if its date prefix
|
|
2189
|
+
// falls back to file mtime) — emit every raw member id per canonical id
|
|
2190
|
+
// so a downstream matcher (e.g. /remember's ledger) can hit on ANY of
|
|
2191
|
+
// them, while `sessions`/`nSessions` below stays the deduped conversation
|
|
2192
|
+
// count (canonical ids), not the expanded file count.
|
|
2193
|
+
const canonicalIds = Object.keys(cl.sessions);
|
|
2194
|
+
const sessionIds = canonicalIds.flatMap(id => canonicalGroups.get(id) || [id]);
|
|
2131
2195
|
const projects = [...new Set(
|
|
2132
2196
|
sessionIds.map(s => s.includes('/') ? s.split('/')[0] : 'unknown')
|
|
2133
2197
|
)].sort();
|
|
2134
|
-
const nSessions =
|
|
2198
|
+
const nSessions = canonicalIds.length;
|
|
2135
2199
|
const signalNames = Object.keys(cl.signals);
|
|
2136
2200
|
const dominant = sortedEntries(cl.signals)[0] ? sortedEntries(cl.signals)[0][0] : 'unknown';
|
|
2137
2201
|
|
|
@@ -2140,10 +2204,17 @@ function clusterCandidates(allCandidates) {
|
|
|
2140
2204
|
// corroboration (errors) also escalates. #3: judge self-correction from the
|
|
2141
2205
|
// MATCHED quotes — a cluster whose grouping phrase is "wrong project" etc. is
|
|
2142
2206
|
// the user redirecting themselves, not an antigen → not severe.
|
|
2207
|
+
// A 0-context cluster (no real user text survived, e.g. a terse-text
|
|
2208
|
+
// session whose sig came only from the file-referent fallback below) has
|
|
2209
|
+
// nothing to test against SELF_RE, so it must NOT auto-qualify as severe
|
|
2210
|
+
// via user_correction alone — that would bypass the self-suspect filter
|
|
2211
|
+
// by having nothing to filter. curse/interrupt_cascade (observed reaction
|
|
2212
|
+
// signals) and machine-corroborating errors still count without context.
|
|
2143
2213
|
const SELF_RE = /\b(wrong (project|window|repo|directory|folder)|never ?mind|nvm|scratch that|ignore (that|this)|disregard|my bad|oops)\b/i;
|
|
2144
|
-
const
|
|
2214
|
+
const hasContext = cl.contexts.length > 0;
|
|
2215
|
+
const allSelf = hasContext && cl.contexts.every(q => SELF_RE.test(q || ''));
|
|
2145
2216
|
const severe = signalNames.some(s => s === 'user_curse' || s === 'interrupt_cascade')
|
|
2146
|
-
|| (signalNames.includes('user_correction') && !allSelf)
|
|
2217
|
+
|| (hasContext && signalNames.includes('user_correction') && !allSelf)
|
|
2147
2218
|
|| cl.errors.length > 0;
|
|
2148
2219
|
const recurring = nSessions >= 3; // recurrence × severity → artifact (the 2×2)
|
|
2149
2220
|
let artifact;
|
|
@@ -2182,6 +2253,14 @@ function clusterCandidates(allCandidates) {
|
|
|
2182
2253
|
// Final tiebreak on median peak friction — graded intensity discriminates
|
|
2183
2254
|
// among clusters that tie on tier and recurrence. Ranking only: it reorders
|
|
2184
2255
|
// within what recurrence already gated, never promotes across the 2x2.
|
|
2256
|
+
//
|
|
2257
|
+
// A cluster with no real user text is handled above by the `hasContext`
|
|
2258
|
+
// gate on severity (never auto-severe via user_correction alone), not by a
|
|
2259
|
+
// hard drop here — a 0-context cluster with genuine machine corroboration
|
|
2260
|
+
// (errors) or an observed curse/interrupt can still surface, and a
|
|
2261
|
+
// recurring 0-context cluster (file-referent fallback matched across
|
|
2262
|
+
// sessions) can still be surfaced as a 'fact'. A one-off, mild, 0-context
|
|
2263
|
+
// cluster still lands on 'drop' via the severity/recurrence grid itself.
|
|
2185
2264
|
const kept = out.filter(c => c.suggested_artifact !== 'drop');
|
|
2186
2265
|
kept.sort((a, b) =>
|
|
2187
2266
|
b.score - a.score
|
|
@@ -2237,9 +2316,36 @@ function extractMain(sessionsDir) {
|
|
|
2237
2316
|
((b.friction_summary || {}).peak || 0) - ((a.friction_summary || {}).peak || 0)
|
|
2238
2317
|
);
|
|
2239
2318
|
|
|
2319
|
+
// Locate each session's physical file once, then dedup sessions that are
|
|
2320
|
+
// really the same conversation (shared message uuids) before clustering.
|
|
2321
|
+
const fileFor = new Map();
|
|
2322
|
+
for (const analysis of sortedBad) {
|
|
2323
|
+
const f = findSessionFile(sessionsDir, analysis.session_id);
|
|
2324
|
+
if (f) fileFor.set(analysis.session_id, f);
|
|
2325
|
+
}
|
|
2326
|
+
// The dedup pass (computeCanonicalSessionIds) re-reads and re-parses every
|
|
2327
|
+
// candidate file's full JSONL to build its uuid graph — the expensive part
|
|
2328
|
+
// of a run. A session with no anchor signal never produces a candidate
|
|
2329
|
+
// anyway (see analyzeBadSession/ANCHOR_SIGNALS), so restricting the dedup
|
|
2330
|
+
// pass to only anchor-bearing sessions is behaviour-identical while
|
|
2331
|
+
// skipping every file that could never contribute to a cluster.
|
|
2332
|
+
const anchorSessionIds = new Set(
|
|
2333
|
+
signals.filter(s => ANCHOR_SIGNALS.includes(s.signal)).map(s => s.session)
|
|
2334
|
+
);
|
|
2335
|
+
const { canonicalOf, groups } = computeCanonicalSessionIds(
|
|
2336
|
+
sortedBad.map(a => a.session_id).filter(id => anchorSessionIds.has(id)),
|
|
2337
|
+
fileFor
|
|
2338
|
+
);
|
|
2339
|
+
|
|
2340
|
+
// Two files of the same conversation (a fork/resume) share identical
|
|
2341
|
+
// message uuids and therefore identical event timestamps for any reaction
|
|
2342
|
+
// that predates the fork — after remapping to the canonical session id,
|
|
2343
|
+
// dedupe on (session, anchor timestamp, anchor signal) so that shared
|
|
2344
|
+
// reaction contributes to `signals` only once, not once per file.
|
|
2345
|
+
const seenCandidateKeys = new Set();
|
|
2240
2346
|
for (const analysis of sortedBad) {
|
|
2241
2347
|
const sessionId = analysis.session_id;
|
|
2242
|
-
const sessionFile =
|
|
2348
|
+
const sessionFile = fileFor.get(sessionId);
|
|
2243
2349
|
|
|
2244
2350
|
if (!sessionFile) {
|
|
2245
2351
|
failed.push(sessionId);
|
|
@@ -2247,11 +2353,17 @@ function extractMain(sessionsDir) {
|
|
|
2247
2353
|
}
|
|
2248
2354
|
|
|
2249
2355
|
const candidates = analyzeBadSession(sessionFile, analysis, signals);
|
|
2250
|
-
|
|
2356
|
+
for (const c of candidates) {
|
|
2357
|
+
c.session_id = canonicalOf.get(c.session_id) || c.session_id;
|
|
2358
|
+
const dedupeKey = `${c.session_id}|${c.anchor_ts}|${c.anchor_signal}`;
|
|
2359
|
+
if (seenCandidateKeys.has(dedupeKey)) continue;
|
|
2360
|
+
seenCandidateKeys.add(dedupeKey);
|
|
2361
|
+
allCandidates.push(c);
|
|
2362
|
+
}
|
|
2251
2363
|
}
|
|
2252
2364
|
|
|
2253
2365
|
// Cluster candidates by (anchor_signal, tool_pattern)
|
|
2254
|
-
const clusters = clusterCandidates(allCandidates);
|
|
2366
|
+
const clusters = clusterCandidates(allCandidates, groups);
|
|
2255
2367
|
|
|
2256
2368
|
// Terminal output
|
|
2257
2369
|
console.log(`\u2713 ${allCandidates.length} raw candidates \u2192 ${clusters.length} clusters`);
|
|
@@ -2355,8 +2467,8 @@ function main() {
|
|
|
2355
2467
|
Friction analysis pipeline - analyze sessions and extract antigens.
|
|
2356
2468
|
|
|
2357
2469
|
Usage:
|
|
2358
|
-
node friction.
|
|
2359
|
-
node friction.
|
|
2470
|
+
node friction.cjs <sessions-directory>
|
|
2471
|
+
node friction.cjs ~/.config/amp/projects/<encoded-project-dir>/
|
|
2360
2472
|
|
|
2361
2473
|
Outputs (all in .amp/remember/friction/):
|
|
2362
2474
|
friction_analysis.json - Per-session analysis
|
|
@@ -2406,7 +2518,7 @@ Outputs (all in .amp/remember/friction/):
|
|
|
2406
2518
|
console.log('\nReview your antigens:');
|
|
2407
2519
|
console.log(` cat ${reviewFile}`);
|
|
2408
2520
|
console.log('\nOr feed to LLM:');
|
|
2409
|
-
console.log(` cat ${reviewFile} | claude "write
|
|
2521
|
+
console.log(` cat ${reviewFile} | claude "write AGENT.md rules to prevent these patterns"`);
|
|
2410
2522
|
}
|
|
2411
2523
|
|
|
2412
2524
|
return 0;
|