@astudioplus/compressor 0.1.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 +52 -0
- package/LICENSE +20 -0
- package/README.md +167 -0
- package/dist/adapters/agents-md.d.ts +2 -0
- package/dist/adapters/agents-md.js +91 -0
- package/dist/adapters/apply.d.ts +3 -0
- package/dist/adapters/apply.js +83 -0
- package/dist/adapters/claude-code.d.ts +2 -0
- package/dist/adapters/claude-code.js +403 -0
- package/dist/adapters/copilot.d.ts +2 -0
- package/dist/adapters/copilot.js +418 -0
- package/dist/adapters/cursor.d.ts +2 -0
- package/dist/adapters/cursor.js +149 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +19 -0
- package/dist/adapters/markers.d.ts +7 -0
- package/dist/adapters/markers.js +129 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +1 -0
- package/dist/bench/ablate.d.ts +35 -0
- package/dist/bench/ablate.js +163 -0
- package/dist/bench/cell.d.ts +33 -0
- package/dist/bench/cell.js +437 -0
- package/dist/bench/results.d.ts +37 -0
- package/dist/bench/results.js +157 -0
- package/dist/bench/runner.d.ts +24 -0
- package/dist/bench/runner.js +121 -0
- package/dist/bench/tasks.d.ts +4 -0
- package/dist/bench/tasks.js +147 -0
- package/dist/bench/types.d.ts +109 -0
- package/dist/bench/types.js +1 -0
- package/dist/claude/transcripts.d.ts +30 -0
- package/dist/claude/transcripts.js +154 -0
- package/dist/cli/commands/benchmark.d.ts +33 -0
- package/dist/cli/commands/benchmark.js +203 -0
- package/dist/cli/commands/compress.d.ts +8 -0
- package/dist/cli/commands/compress.js +45 -0
- package/dist/cli/commands/count.d.ts +5 -0
- package/dist/cli/commands/count.js +25 -0
- package/dist/cli/commands/hook.d.ts +6 -0
- package/dist/cli/commands/hook.js +30 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +76 -0
- package/dist/cli/commands/report.d.ts +90 -0
- package/dist/cli/commands/report.js +464 -0
- package/dist/cli/commands/savings.d.ts +38 -0
- package/dist/cli/commands/savings.js +196 -0
- package/dist/cli/commands/set-mode.d.ts +5 -0
- package/dist/cli/commands/set-mode.js +13 -0
- package/dist/cli/commands/stats.d.ts +5 -0
- package/dist/cli/commands/stats.js +51 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +11 -0
- package/dist/cli/commands/uninstall.d.ts +7 -0
- package/dist/cli/commands/uninstall.js +22 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +146 -0
- package/dist/copilot-hook-entry.d.ts +1 -0
- package/dist/copilot-hook-entry.js +36 -0
- package/dist/copilot-hook.js +1000 -0
- package/dist/engine/detect.d.ts +2 -0
- package/dist/engine/detect.js +47 -0
- package/dist/engine/index.d.ts +4 -0
- package/dist/engine/index.js +90 -0
- package/dist/engine/policy.d.ts +2 -0
- package/dist/engine/policy.js +48 -0
- package/dist/engine/tiers/code.d.ts +7 -0
- package/dist/engine/tiers/code.js +206 -0
- package/dist/engine/tiers/logs.d.ts +4 -0
- package/dist/engine/tiers/logs.js +139 -0
- package/dist/engine/tiers/structural.d.ts +28 -0
- package/dist/engine/tiers/structural.js +199 -0
- package/dist/engine/types.d.ts +71 -0
- package/dist/engine/types.js +5 -0
- package/dist/hook/copilot.d.ts +5 -0
- package/dist/hook/copilot.js +136 -0
- package/dist/hook/core.d.ts +36 -0
- package/dist/hook/core.js +138 -0
- package/dist/hook/exit.d.ts +22 -0
- package/dist/hook/exit.js +56 -0
- package/dist/hook/post-tool-use.d.ts +5 -0
- package/dist/hook/post-tool-use.js +57 -0
- package/dist/hook-entry.d.ts +1 -0
- package/dist/hook-entry.js +35 -0
- package/dist/hook.js +946 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/ledger/read.d.ts +9 -0
- package/dist/ledger/read.js +91 -0
- package/dist/ledger/write.d.ts +29 -0
- package/dist/ledger/write.js +61 -0
- package/dist/packs/atoms.d.ts +3 -0
- package/dist/packs/atoms.js +108 -0
- package/dist/packs/modes.d.ts +3 -0
- package/dist/packs/modes.js +34 -0
- package/dist/packs/render.d.ts +24 -0
- package/dist/packs/render.js +115 -0
- package/dist/packs/types.d.ts +32 -0
- package/dist/packs/types.js +1 -0
- package/dist/paths.d.ts +29 -0
- package/dist/paths.js +87 -0
- package/dist/tokens/estimate.d.ts +12 -0
- package/dist/tokens/estimate.js +23 -0
- package/dist/tokens/exact.d.ts +5 -0
- package/dist/tokens/exact.js +16 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +2 -0
- package/package.json +77 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { compress, policyFor, OMISSION_MARKER } from './engine/index.ts';
|
|
2
|
+
export type { AppliedTransform, CompressMeta, CompressResult, CompressStats, ContentKind, Estimator, Mode, Policy, ToolKind, } from './engine/index.ts';
|
|
3
|
+
export { cheapEstimator, estimateTokens, tiktokenEstimator } from './tokens/estimate.ts';
|
|
4
|
+
export { countTokensExact } from './tokens/exact.ts';
|
|
5
|
+
export { addUsage, aggregateUsage, encodeProjectDir, findTranscripts, readSessionUsage, } from './claude/transcripts.ts';
|
|
6
|
+
export type { SessionUsage, UsageTotals } from './claude/transcripts.ts';
|
|
7
|
+
export { ATOMS, getAtom } from './packs/atoms.ts';
|
|
8
|
+
export { atomsForMode, MODE_DESCRIPTIONS } from './packs/modes.ts';
|
|
9
|
+
export { atomManifest, markerBegin, MARKER_BEGIN_PREFIX, MARKER_END, parseAtomManifest, renderCursorRules, renderMarkedSection, renderOutputStyle, } from './packs/render.ts';
|
|
10
|
+
export type { AgentName, Atom, AtomCategory, PackMode, RenderedArtifact, } from './packs/types.ts';
|
|
11
|
+
export { adapters, getAdapter, applyChanges, renderChanges, claudeCodeAdapter, copilotAdapter, cursorAdapter, agentsMdAdapter, } from './adapters/index.ts';
|
|
12
|
+
export type { Adapter, AdapterContext, AdapterStatus, FileChange, ModeArg, } from './adapters/index.ts';
|
|
13
|
+
export { handlePostToolUse } from './hook/post-tool-use.ts';
|
|
14
|
+
export type { HookResult } from './hook/post-tool-use.ts';
|
|
15
|
+
export { resolveHookCommand } from './paths.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// engine
|
|
2
|
+
export { compress, policyFor, OMISSION_MARKER } from "./engine/index.js";
|
|
3
|
+
// tokens
|
|
4
|
+
export { cheapEstimator, estimateTokens, tiktokenEstimator } from "./tokens/estimate.js";
|
|
5
|
+
export { countTokensExact } from "./tokens/exact.js";
|
|
6
|
+
// transcripts
|
|
7
|
+
export { addUsage, aggregateUsage, encodeProjectDir, findTranscripts, readSessionUsage, } from "./claude/transcripts.js";
|
|
8
|
+
// packs
|
|
9
|
+
export { ATOMS, getAtom } from "./packs/atoms.js";
|
|
10
|
+
export { atomsForMode, MODE_DESCRIPTIONS } from "./packs/modes.js";
|
|
11
|
+
export { atomManifest, markerBegin, MARKER_BEGIN_PREFIX, MARKER_END, parseAtomManifest, renderCursorRules, renderMarkedSection, renderOutputStyle, } from "./packs/render.js";
|
|
12
|
+
// adapters
|
|
13
|
+
export { adapters, getAdapter, applyChanges, renderChanges, claudeCodeAdapter, copilotAdapter, cursorAdapter, agentsMdAdapter, } from "./adapters/index.js";
|
|
14
|
+
// hook
|
|
15
|
+
export { handlePostToolUse } from "./hook/post-tool-use.js";
|
|
16
|
+
export { resolveHookCommand } from "./paths.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LedgerEvent } from './write.ts';
|
|
2
|
+
export interface ReadLedgerOptions {
|
|
3
|
+
/** ledger directory (default: COMPRESSOR_LEDGER_DIR or ~/.compressor/ledger) */
|
|
4
|
+
dir?: string;
|
|
5
|
+
/** only events at or after this instant */
|
|
6
|
+
since?: Date;
|
|
7
|
+
}
|
|
8
|
+
/** Read every monthly file, tolerant of garbage lines; sorted by timestamp. */
|
|
9
|
+
export declare function readLedger(opts?: ReadLedgerOptions): Promise<LedgerEvent[]>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolveLedgerDir } from "./write.js";
|
|
4
|
+
const AGENTS = new Set(['claude-code', 'copilot']);
|
|
5
|
+
const TOOLS = new Set(['read', 'bash', 'search', 'other']);
|
|
6
|
+
const MODES = new Set(['full', 'optimized', 'slim']);
|
|
7
|
+
function isFiniteNumber(value) {
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
9
|
+
}
|
|
10
|
+
function parseEvent(line) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = JSON.parse(line);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const record = raw;
|
|
22
|
+
const ts = record['ts'];
|
|
23
|
+
const agent = record['agent'];
|
|
24
|
+
const tool = record['tool'];
|
|
25
|
+
const mode = record['mode'];
|
|
26
|
+
const transforms = record['transforms'];
|
|
27
|
+
if (typeof ts !== 'string' ||
|
|
28
|
+
typeof agent !== 'string' ||
|
|
29
|
+
!AGENTS.has(agent) ||
|
|
30
|
+
typeof tool !== 'string' ||
|
|
31
|
+
!TOOLS.has(tool) ||
|
|
32
|
+
typeof mode !== 'string' ||
|
|
33
|
+
!MODES.has(mode) ||
|
|
34
|
+
!isFiniteNumber(record['charsIn']) ||
|
|
35
|
+
!isFiniteNumber(record['charsOut']) ||
|
|
36
|
+
!isFiniteNumber(record['estTokensIn']) ||
|
|
37
|
+
!isFiniteNumber(record['estTokensOut']) ||
|
|
38
|
+
!Array.isArray(transforms) ||
|
|
39
|
+
!transforms.every((t) => typeof t === 'string')) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ts,
|
|
44
|
+
agent: agent,
|
|
45
|
+
tool: tool,
|
|
46
|
+
mode: mode,
|
|
47
|
+
charsIn: record['charsIn'],
|
|
48
|
+
charsOut: record['charsOut'],
|
|
49
|
+
estTokensIn: record['estTokensIn'],
|
|
50
|
+
estTokensOut: record['estTokensOut'],
|
|
51
|
+
transforms,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Read every monthly file, tolerant of garbage lines; sorted by timestamp. */
|
|
55
|
+
export async function readLedger(opts = {}) {
|
|
56
|
+
const dir = opts.dir ?? resolveLedgerDir();
|
|
57
|
+
let names;
|
|
58
|
+
try {
|
|
59
|
+
names = await readdir(dir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const events = [];
|
|
65
|
+
for (const name of names.filter((n) => n.endsWith('.jsonl')).sort()) {
|
|
66
|
+
let body;
|
|
67
|
+
try {
|
|
68
|
+
body = await readFile(path.join(dir, name), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const line of body.split('\n')) {
|
|
74
|
+
if (line.trim() === '') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const event = parseEvent(line);
|
|
78
|
+
if (event === null) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (opts.since !== undefined) {
|
|
82
|
+
const when = Date.parse(event.ts);
|
|
83
|
+
if (Number.isNaN(when) || when < opts.since.getTime()) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
events.push(event);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return events.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
91
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Mode, ToolKind } from '../engine/types.ts';
|
|
2
|
+
export interface LedgerEvent {
|
|
3
|
+
/** ISO timestamp of the compression event */
|
|
4
|
+
ts: string;
|
|
5
|
+
agent: 'claude-code' | 'copilot';
|
|
6
|
+
tool: ToolKind;
|
|
7
|
+
mode: Mode;
|
|
8
|
+
charsIn: number;
|
|
9
|
+
charsOut: number;
|
|
10
|
+
estTokensIn: number;
|
|
11
|
+
estTokensOut: number;
|
|
12
|
+
/** AppliedTransform ids, e.g. ['dedupe-lines', 'truncate'] */
|
|
13
|
+
transforms: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Resolved at call time (not module load) so tests can swap the env var. */
|
|
16
|
+
export declare function resolveLedgerDir(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Append one event to the monthly ledger file. Never rejects; every error is
|
|
19
|
+
* swallowed (fail-open). When COMPRESSOR_NO_LEDGER=1 this returns before any
|
|
20
|
+
* filesystem work.
|
|
21
|
+
*/
|
|
22
|
+
export declare function appendLedger(event: LedgerEvent): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves when all appends in flight at call time have finished (each one
|
|
25
|
+
* already swallows its own errors, so this never rejects). Hook entries race
|
|
26
|
+
* this against a hard 250ms timer before exiting so writes flush without
|
|
27
|
+
* ever delaying the agent meaningfully.
|
|
28
|
+
*/
|
|
29
|
+
export declare function settleLedger(): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
/** Resolved at call time (not module load) so tests can swap the env var. */
|
|
6
|
+
export function resolveLedgerDir() {
|
|
7
|
+
return (process.env['COMPRESSOR_LEDGER_DIR'] ?? path.join(os.homedir(), '.compressor', 'ledger'));
|
|
8
|
+
}
|
|
9
|
+
/** Monthly file name from the event timestamp; falls back to 'unknown'. */
|
|
10
|
+
function monthOf(ts) {
|
|
11
|
+
const month = ts.slice(0, 7);
|
|
12
|
+
return /^\d{4}-\d{2}$/.test(month) ? month : 'unknown';
|
|
13
|
+
}
|
|
14
|
+
// mkdir -p once per directory (keyed by dir: tests point COMPRESSOR_LEDGER_DIR
|
|
15
|
+
// at fresh temp dirs within one process). Errors are swallowed here AND
|
|
16
|
+
// surfaced again by appendFile, which is also swallowed.
|
|
17
|
+
const mkdirCache = new Map();
|
|
18
|
+
const pending = new Set();
|
|
19
|
+
/**
|
|
20
|
+
* Append one event to the monthly ledger file. Never rejects; every error is
|
|
21
|
+
* swallowed (fail-open). When COMPRESSOR_NO_LEDGER=1 this returns before any
|
|
22
|
+
* filesystem work.
|
|
23
|
+
*/
|
|
24
|
+
export async function appendLedger(event) {
|
|
25
|
+
if (process.env['COMPRESSOR_NO_LEDGER'] === '1') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const task = (async () => {
|
|
29
|
+
try {
|
|
30
|
+
const dir = resolveLedgerDir();
|
|
31
|
+
let made = mkdirCache.get(dir);
|
|
32
|
+
if (made === undefined) {
|
|
33
|
+
made = mkdir(dir, { recursive: true }).then(() => undefined, () => undefined);
|
|
34
|
+
mkdirCache.set(dir, made);
|
|
35
|
+
}
|
|
36
|
+
await made;
|
|
37
|
+
const file = path.join(dir, `${monthOf(event.ts)}.jsonl`);
|
|
38
|
+
await appendFile(file, `${JSON.stringify(event)}\n`, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// FAIL-OPEN: ledger problems are never the agent's problem.
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
pending.add(task);
|
|
45
|
+
void task.finally(() => pending.delete(task));
|
|
46
|
+
return task;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves when all appends in flight at call time have finished (each one
|
|
50
|
+
* already swallows its own errors, so this never rejects). Hook entries race
|
|
51
|
+
* this against a hard 250ms timer before exiting so writes flush without
|
|
52
|
+
* ever delaying the agent meaningfully.
|
|
53
|
+
*/
|
|
54
|
+
export async function settleLedger() {
|
|
55
|
+
try {
|
|
56
|
+
await Promise.all([...pending]);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// unreachable (tasks never reject), kept for fail-open symmetry
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const ATOMS = [
|
|
2
|
+
{
|
|
3
|
+
id: 'out.no-preamble',
|
|
4
|
+
category: 'output',
|
|
5
|
+
text: `Start every response with the answer or the action. No preamble ("I'll help you...", "Let me...", "Great question"), no plan narration for single-step work.`,
|
|
6
|
+
modes: ['optimized', 'slim'],
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'out.no-postamble',
|
|
10
|
+
category: 'output',
|
|
11
|
+
text: `No filler, hedging, or politeness padding. No "feel free to ask", no recap closings.`,
|
|
12
|
+
modes: ['optimized', 'slim'],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'out.answer-first',
|
|
16
|
+
category: 'output',
|
|
17
|
+
text: 'Lead with the conclusion. Supporting detail comes after, only if it changes what the reader does next.',
|
|
18
|
+
modes: ['optimized', 'slim'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'out.no-recap',
|
|
22
|
+
category: 'output',
|
|
23
|
+
text: `Do not summarize what you just did unless asked. After edits, state the result in one line at most ("Fixed the null check in parser.ts:142").`,
|
|
24
|
+
modes: ['optimized', 'slim'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'out.no-code-echo',
|
|
28
|
+
category: 'output',
|
|
29
|
+
text: `Never restate code in prose. The diff or the file is the explanation. Don't quote code you just wrote back to the user.`,
|
|
30
|
+
modes: ['optimized', 'slim'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'out.minimal-formatting',
|
|
34
|
+
category: 'output',
|
|
35
|
+
text: 'Use plain sentences for short answers. Reserve headers and bullets for genuinely multi-part responses.',
|
|
36
|
+
modes: ['optimized'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'out.explanation-budget',
|
|
40
|
+
category: 'output',
|
|
41
|
+
text: 'Hard budget: at most ~10% of your response may be explanation. For a 40-line diff, that is 2-4 short lines. When in doubt, omit the explanation.',
|
|
42
|
+
modes: ['slim'],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'out.code-only-default',
|
|
46
|
+
category: 'output',
|
|
47
|
+
text: `Respond with code, diffs, commands, and file paths. Prose is for what code cannot express: a decision between alternatives, a risk, a required manual step. Acceptable response shapes: a bare code block; a path followed by a diff; a one-line answer. "Done." is a complete response to a completed task. Never explain what standard code does — only flag the non-obvious: side effects, breaking changes, things the user must do themselves. If the task is ambiguous enough that proceeding risks wasted work, ask one terse question instead of writing a hedged essay.`,
|
|
48
|
+
modes: ['slim'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'beh.targeted-reads',
|
|
52
|
+
category: 'behavior',
|
|
53
|
+
text: 'Read only what you need: prefer Grep/Glob to locate, then Read with offset/limit for the relevant range. Read a whole file only when it is small or you must edit broadly across it.',
|
|
54
|
+
modes: ['optimized', 'slim'],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'beh.no-reread',
|
|
58
|
+
category: 'behavior',
|
|
59
|
+
text: 'Never re-read a file you already have in context, unless it changed or earlier output was compressed — a [compressor: ...] marker means lines were omitted and tells you the exact offset/limit to Read if you need them.',
|
|
60
|
+
modes: ['optimized', 'slim'],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'beh.no-tool-echo',
|
|
64
|
+
category: 'behavior',
|
|
65
|
+
text: `Do not quote tool output back in your response; reference it ("tests pass", "3 matches in src/").`,
|
|
66
|
+
modes: ['optimized', 'slim'],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'beh.surgical-edits',
|
|
70
|
+
category: 'behavior',
|
|
71
|
+
text: 'Prefer surgical edits to full-file rewrites. Batch related edits to the same file into one operation.',
|
|
72
|
+
modes: ['optimized', 'slim'],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'beh.bounded-commands',
|
|
76
|
+
category: 'behavior',
|
|
77
|
+
text: 'Bound command output: use flags like --quiet, head -50, or targeted test selection rather than dumping full logs.',
|
|
78
|
+
modes: ['optimized', 'slim'],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'tokens.drop-articles',
|
|
82
|
+
category: 'output',
|
|
83
|
+
text: 'Omit articles (a, an, the) and filler words from responses.',
|
|
84
|
+
modes: ['optimized', 'slim'],
|
|
85
|
+
rejected: {
|
|
86
|
+
reason: '~1 token saved per article, output-only; degrades grammar and pushes the model off its training distribution for single-digit savings. Empirically refuted: bench-20260610-124626 (sonnet, 9 tasks ×2 trials) measured optimized-plus-tokens-drop-articles at −2.2% output vs optimized — noise, no benefit on top of a concise baseline. Kept for --ablate-add reproduction.',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'tokens.no-politeness-words',
|
|
91
|
+
category: 'output',
|
|
92
|
+
text: 'Never use the words please or thank you.',
|
|
93
|
+
modes: ['optimized', 'slim'],
|
|
94
|
+
rejected: {
|
|
95
|
+
reason: 'Micro-optimization already subsumed by out.no-postamble; word-level bans distract the model more than they save.',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
const byId = new Map();
|
|
100
|
+
for (const atom of ATOMS) {
|
|
101
|
+
if (byId.has(atom.id)) {
|
|
102
|
+
throw new Error(`duplicate atom id: ${atom.id}`);
|
|
103
|
+
}
|
|
104
|
+
byId.set(atom.id, atom);
|
|
105
|
+
}
|
|
106
|
+
export function getAtom(id) {
|
|
107
|
+
return byId.get(id);
|
|
108
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getAtom } from "./atoms.js";
|
|
2
|
+
/** Curated render order — explicit so object-key order can never reorder output. */
|
|
3
|
+
const MODE_ORDER = [
|
|
4
|
+
'out.no-preamble',
|
|
5
|
+
'out.answer-first',
|
|
6
|
+
'out.no-recap',
|
|
7
|
+
'out.no-code-echo',
|
|
8
|
+
'out.no-postamble',
|
|
9
|
+
'out.minimal-formatting',
|
|
10
|
+
'out.explanation-budget',
|
|
11
|
+
'out.code-only-default',
|
|
12
|
+
'beh.targeted-reads',
|
|
13
|
+
'beh.no-reread',
|
|
14
|
+
'beh.no-tool-echo',
|
|
15
|
+
'beh.surgical-edits',
|
|
16
|
+
'beh.bounded-commands',
|
|
17
|
+
];
|
|
18
|
+
const ORDERED_ATOMS = MODE_ORDER.map((id) => {
|
|
19
|
+
const atom = getAtom(id);
|
|
20
|
+
if (atom === undefined) {
|
|
21
|
+
throw new Error(`unknown atom id in mode order: ${id}`);
|
|
22
|
+
}
|
|
23
|
+
return atom;
|
|
24
|
+
});
|
|
25
|
+
export function atomsForMode(mode, agent) {
|
|
26
|
+
return ORDERED_ATOMS.filter((atom) => atom.rejected === undefined &&
|
|
27
|
+
atom.modes.includes(mode) &&
|
|
28
|
+
(atom.agents === undefined ||
|
|
29
|
+
(agent !== undefined && atom.agents.includes(agent))));
|
|
30
|
+
}
|
|
31
|
+
export const MODE_DESCRIPTIONS = {
|
|
32
|
+
optimized: 'Concise answer-first responses with disciplined context use',
|
|
33
|
+
slim: 'Code-first responses under a hard explanation budget',
|
|
34
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentName, Atom, PackMode, RenderedArtifact } from './types.ts';
|
|
2
|
+
export declare const MARKER_BEGIN_PREFIX = "<!-- compressor:begin";
|
|
3
|
+
export declare const MARKER_END = "<!-- compressor:end -->";
|
|
4
|
+
/**
|
|
5
|
+
* Full grammar for a begin-marker LINE (test against the trimmed line).
|
|
6
|
+
* Prefix-only matching is unsafe: user prose that merely starts with the
|
|
7
|
+
* prefix must never be mistaken for a section boundary.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MARKER_BEGIN_LINE_RE: RegExp;
|
|
10
|
+
export declare function markerBegin(mode: PackMode): string;
|
|
11
|
+
export declare function atomManifest(atomIds: string[]): string;
|
|
12
|
+
export declare function parseAtomManifest(text: string): {
|
|
13
|
+
mode: PackMode;
|
|
14
|
+
atomIds: string[];
|
|
15
|
+
} | null;
|
|
16
|
+
/**
|
|
17
|
+
* Output style over an explicit atom list (benchmark ablation variants).
|
|
18
|
+
* styleName is validated only: Claude Code resolves styles by file name
|
|
19
|
+
* (`<styleName>.md`), and a `name:` frontmatter field is deliberately omitted.
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderOutputStyleFromAtoms(atoms: Atom[], styleName: string, description: string): RenderedArtifact;
|
|
22
|
+
export declare function renderOutputStyle(mode: PackMode): RenderedArtifact;
|
|
23
|
+
export declare function renderMarkedSection(mode: PackMode, agent: AgentName): RenderedArtifact;
|
|
24
|
+
export declare function renderCursorRules(mode: PackMode): RenderedArtifact;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { atomsForMode, MODE_DESCRIPTIONS } from "./modes.js";
|
|
2
|
+
export const MARKER_BEGIN_PREFIX = '<!-- compressor:begin';
|
|
3
|
+
export const MARKER_END = '<!-- compressor:end -->';
|
|
4
|
+
/**
|
|
5
|
+
* Full grammar for a begin-marker LINE (test against the trimmed line).
|
|
6
|
+
* Prefix-only matching is unsafe: user prose that merely starts with the
|
|
7
|
+
* prefix must never be mistaken for a section boundary.
|
|
8
|
+
*/
|
|
9
|
+
export const MARKER_BEGIN_LINE_RE = /^<!-- compressor:begin mode=\S+ v=\d+ -->$/;
|
|
10
|
+
export function markerBegin(mode) {
|
|
11
|
+
return `${MARKER_BEGIN_PREFIX} mode=${mode} v=1 -->`;
|
|
12
|
+
}
|
|
13
|
+
export function atomManifest(atomIds) {
|
|
14
|
+
return `<!-- atoms: ${atomIds.join(',')} -->`;
|
|
15
|
+
}
|
|
16
|
+
const BEGIN_RE = /<!-- compressor:begin mode=(\S+) v=\d+ -->/;
|
|
17
|
+
const MANIFEST_RE = /<!-- atoms: ([^>]*) -->/;
|
|
18
|
+
const DESCRIPTION_RE = /^description: (.+)$/m;
|
|
19
|
+
function isPackMode(value) {
|
|
20
|
+
return value === 'optimized' || value === 'slim';
|
|
21
|
+
}
|
|
22
|
+
function parseMode(text) {
|
|
23
|
+
const fromMarker = BEGIN_RE.exec(text)?.[1];
|
|
24
|
+
if (fromMarker !== undefined && isPackMode(fromMarker)) {
|
|
25
|
+
return fromMarker;
|
|
26
|
+
}
|
|
27
|
+
const description = DESCRIPTION_RE.exec(text)?.[1];
|
|
28
|
+
if (description !== undefined) {
|
|
29
|
+
const modes = ['optimized', 'slim'];
|
|
30
|
+
return modes.find((m) => MODE_DESCRIPTIONS[m] === description) ?? null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function parseAtomManifest(text) {
|
|
35
|
+
const idsRaw = MANIFEST_RE.exec(text)?.[1];
|
|
36
|
+
if (idsRaw === undefined) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const mode = parseMode(text);
|
|
40
|
+
if (mode === null) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const atomIds = idsRaw
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((id) => id.trim())
|
|
46
|
+
.filter((id) => id.length > 0);
|
|
47
|
+
return { mode, atomIds };
|
|
48
|
+
}
|
|
49
|
+
function bullet(atom) {
|
|
50
|
+
return `- ${atom.text}`;
|
|
51
|
+
}
|
|
52
|
+
/** Atoms slim pulls out into a leading '## Code-first responses' section. */
|
|
53
|
+
const CODE_FIRST_IDS = new Set([
|
|
54
|
+
'out.explanation-budget',
|
|
55
|
+
'out.code-only-default',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Output style over an explicit atom list (benchmark ablation variants).
|
|
59
|
+
* styleName is validated only: Claude Code resolves styles by file name
|
|
60
|
+
* (`<styleName>.md`), and a `name:` frontmatter field is deliberately omitted.
|
|
61
|
+
*/
|
|
62
|
+
export function renderOutputStyleFromAtoms(atoms, styleName, description) {
|
|
63
|
+
if (styleName === '' || /[/\\]/.test(styleName)) {
|
|
64
|
+
throw new Error(`invalid output-style name ${JSON.stringify(styleName)} — becomes the file name <styleName>.md`);
|
|
65
|
+
}
|
|
66
|
+
const codeFirst = atoms.filter((a) => CODE_FIRST_IDS.has(a.id));
|
|
67
|
+
const output = atoms.filter((a) => a.category === 'output' && !CODE_FIRST_IDS.has(a.id));
|
|
68
|
+
const behavior = atoms.filter((a) => a.category === 'behavior');
|
|
69
|
+
const atomIds = [...codeFirst, ...output, ...behavior].map((a) => a.id);
|
|
70
|
+
const lines = [
|
|
71
|
+
'---',
|
|
72
|
+
`description: ${description}`,
|
|
73
|
+
'keep-coding-instructions: true',
|
|
74
|
+
'---',
|
|
75
|
+
'',
|
|
76
|
+
atomManifest(atomIds),
|
|
77
|
+
'',
|
|
78
|
+
];
|
|
79
|
+
if (codeFirst.length > 0) {
|
|
80
|
+
lines.push('## Code-first responses', ...codeFirst.map(bullet), '');
|
|
81
|
+
}
|
|
82
|
+
lines.push('## Output discipline', ...output.map(bullet), '');
|
|
83
|
+
lines.push('## Context discipline', ...behavior.map(bullet));
|
|
84
|
+
return { body: `${lines.join('\n')}\n`, atomIds };
|
|
85
|
+
}
|
|
86
|
+
export function renderOutputStyle(mode) {
|
|
87
|
+
return renderOutputStyleFromAtoms(atomsForMode(mode, 'claude-code'), `compressor-${mode}`, MODE_DESCRIPTIONS[mode]);
|
|
88
|
+
}
|
|
89
|
+
export function renderMarkedSection(mode, agent) {
|
|
90
|
+
const atoms = atomsForMode(mode, agent);
|
|
91
|
+
const atomIds = atoms.map((a) => a.id);
|
|
92
|
+
const lines = [
|
|
93
|
+
markerBegin(mode),
|
|
94
|
+
atomManifest(atomIds),
|
|
95
|
+
'## Response & context discipline (compressor)',
|
|
96
|
+
...atoms.map(bullet),
|
|
97
|
+
MARKER_END,
|
|
98
|
+
];
|
|
99
|
+
return { body: lines.join('\n'), atomIds };
|
|
100
|
+
}
|
|
101
|
+
export function renderCursorRules(mode) {
|
|
102
|
+
const atoms = atomsForMode(mode, 'cursor');
|
|
103
|
+
const atomIds = atoms.map((a) => a.id);
|
|
104
|
+
const lines = [
|
|
105
|
+
'---',
|
|
106
|
+
`description: ${MODE_DESCRIPTIONS[mode]}`,
|
|
107
|
+
'alwaysApply: true',
|
|
108
|
+
'---',
|
|
109
|
+
'',
|
|
110
|
+
atomManifest(atomIds),
|
|
111
|
+
'',
|
|
112
|
+
...atoms.map(bullet),
|
|
113
|
+
];
|
|
114
|
+
return { body: `${lines.join('\n')}\n`, atomIds };
|
|
115
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Modes that render instructions. 'full' renders nothing — it is the absence of artifacts. */
|
|
2
|
+
export type PackMode = 'optimized' | 'slim';
|
|
3
|
+
export type AtomCategory = 'output' | 'behavior';
|
|
4
|
+
export type AgentName = 'claude-code' | 'copilot' | 'cursor' | 'agents-md';
|
|
5
|
+
/**
|
|
6
|
+
* One independently ablatable instruction. Rendered artifacts embed the atom
|
|
7
|
+
* IDs as a manifest comment so `status` and the benchmark can read what is
|
|
8
|
+
* installed.
|
|
9
|
+
*/
|
|
10
|
+
export interface Atom {
|
|
11
|
+
/** namespaced id, e.g. 'out.no-preamble', 'beh.targeted-reads' */
|
|
12
|
+
id: string;
|
|
13
|
+
category: AtomCategory;
|
|
14
|
+
/** markdown bullet line(s), no trailing newline, no timestamps (cache-stable) */
|
|
15
|
+
text: string;
|
|
16
|
+
modes: PackMode[];
|
|
17
|
+
/** undefined = applies to all agents */
|
|
18
|
+
agents?: AgentName[];
|
|
19
|
+
/**
|
|
20
|
+
* Rejected atoms are never rendered; they exist so nobody re-adds them and
|
|
21
|
+
* so the benchmark can demonstrate the rejection with data (--ablate-add).
|
|
22
|
+
*/
|
|
23
|
+
rejected?: {
|
|
24
|
+
reason: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface RenderedArtifact {
|
|
28
|
+
/** complete file body or marked section, byte-deterministic */
|
|
29
|
+
body: string;
|
|
30
|
+
/** atoms included, in render order */
|
|
31
|
+
atomIds: string[];
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PackMode } from './packs/types.ts';
|
|
2
|
+
export declare function packageRoot(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Hook command for ownership matching in status/uninstall — must work even
|
|
5
|
+
* when the bundle is missing (e.g. uninstalling a broken install).
|
|
6
|
+
*/
|
|
7
|
+
export declare function describeHookCommand(mode: PackMode, root?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Command line installed for the PostToolUse hook. Refuses to install a
|
|
10
|
+
* command that would fail on every tool call (fail-open hook = silent no-op).
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveHookCommand(mode: PackMode, root?: string): string;
|
|
13
|
+
/** Copilot hook command for display/matching — works without the bundle. */
|
|
14
|
+
export declare function describeCopilotHookCommand(mode: PackMode, root?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Command line installed for the Copilot postToolUse hook. Like
|
|
17
|
+
* resolveHookCommand, refuses to install a command that would fail on every
|
|
18
|
+
* tool call (Copilot postToolUse is fail-open: a dead command = silent no-op).
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveCopilotHookCommand(mode: PackMode, root?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Copilot hook command derived from the Claude Code hook command carried in
|
|
23
|
+
* AdapterContext (the only resolved-path carrier adapters receive; the
|
|
24
|
+
* adapters/types.ts contract is frozen). Both commands are generated in this
|
|
25
|
+
* module from the same root, so swapping the sibling bundle name and --mode
|
|
26
|
+
* flag reproduces copilotHookCommandFor exactly. Mode omitted → base form
|
|
27
|
+
* for ownership matching (mode-agnostic, like claude-code's predicate).
|
|
28
|
+
*/
|
|
29
|
+
export declare function copilotHookCommandFrom(hookCommand: string, mode?: PackMode): string;
|