@entelligentsia/forgecli 1.0.2 → 1.0.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/CHANGELOG-forge-plugin.md +24 -0
  3. package/dist/extensions/forgecli/audience-gate.js +1 -1
  4. package/dist/extensions/forgecli/audience-gate.js.map +1 -1
  5. package/dist/extensions/forgecli/fix-bug.d.ts +1 -2
  6. package/dist/extensions/forgecli/fix-bug.js +678 -609
  7. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  8. package/dist/extensions/forgecli/forge-artifact-tool.js +15 -3
  9. package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
  10. package/dist/extensions/forgecli/forge-subagent.d.ts +17 -0
  11. package/dist/extensions/forgecli/forge-subagent.js +31 -12
  12. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  13. package/dist/extensions/forgecli/forge-tools.d.ts +6 -0
  14. package/dist/extensions/forgecli/forge-tools.js +69 -6
  15. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  16. package/dist/extensions/forgecli/run-task.js +461 -391
  17. package/dist/extensions/forgecli/run-task.js.map +1 -1
  18. package/dist/extensions/forgecli/session-registry.d.ts +12 -0
  19. package/dist/extensions/forgecli/session-registry.js +23 -0
  20. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  21. package/dist/extensions/forgecli/subagent/caller-context.d.ts +35 -11
  22. package/dist/extensions/forgecli/subagent/caller-context.js +49 -21
  23. package/dist/extensions/forgecli/subagent/caller-context.js.map +1 -1
  24. package/dist/extensions/forgecli/subagent/orchestrator-transcript.d.ts +66 -0
  25. package/dist/extensions/forgecli/subagent/orchestrator-transcript.js +66 -0
  26. package/dist/extensions/forgecli/subagent/orchestrator-transcript.js.map +1 -0
  27. package/dist/extensions/forgecli/subagent/phase-guard.d.ts +34 -0
  28. package/dist/extensions/forgecli/subagent/phase-guard.js +139 -0
  29. package/dist/extensions/forgecli/subagent/phase-guard.js.map +1 -0
  30. package/dist/extensions/forgecli/subagent/phase-summary-map.d.ts +1 -0
  31. package/dist/extensions/forgecli/subagent/phase-summary-map.js +22 -0
  32. package/dist/extensions/forgecli/subagent/phase-summary-map.js.map +1 -0
  33. package/dist/extensions/forgecli/thread-switcher.js +2 -2
  34. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  35. package/dist/extensions/forgecli/viewport-events.d.ts +4 -0
  36. package/dist/extensions/forgecli/viewport-events.js +18 -1
  37. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  38. package/dist/extensions/forgecli/viewport-renderer.d.ts +12 -2
  39. package/dist/extensions/forgecli/viewport-renderer.js +8 -6
  40. package/dist/extensions/forgecli/viewport-renderer.js.map +1 -1
  41. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +10 -28
  42. package/dist/forge-payload/.base-pack/workflows/triage.md +190 -0
  43. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  44. package/dist/forge-payload/.schemas/enum-catalog.json +1 -1
  45. package/dist/forge-payload/.schemas/migrations.json +9 -0
  46. package/dist/forge-payload/integrity.json +3 -3
  47. package/dist/forge-payload/meta/fragments/tool-discipline.md +21 -2
  48. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +210 -0
  49. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +10 -28
  50. package/dist/forge-payload/schemas/enum-catalog.json +1 -1
  51. package/dist/forge-payload/schemas/structure-manifest.json +20 -1
  52. package/dist/forge-payload/tools/artifact.cjs +34 -5
  53. package/node_modules/@entelligentsia/forge-compress/dist/compressor.d.ts +6 -0
  54. package/node_modules/@entelligentsia/forge-compress/dist/compressor.js +137 -0
  55. package/node_modules/@entelligentsia/forge-compress/dist/entropy.d.ts +3 -0
  56. package/node_modules/@entelligentsia/forge-compress/dist/entropy.js +99 -0
  57. package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.d.ts +8 -0
  58. package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.js +149 -0
  59. package/node_modules/@entelligentsia/forge-compress/dist/forge/index.d.ts +7 -0
  60. package/node_modules/@entelligentsia/forge-compress/dist/forge/index.js +4 -0
  61. package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.d.ts +5 -0
  62. package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.js +92 -0
  63. package/node_modules/@entelligentsia/forge-compress/dist/forge/query.d.ts +7 -0
  64. package/node_modules/@entelligentsia/forge-compress/dist/forge/query.js +60 -0
  65. package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.d.ts +1 -0
  66. package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.js +82 -0
  67. package/node_modules/@entelligentsia/forge-compress/dist/index.d.ts +6 -0
  68. package/node_modules/@entelligentsia/forge-compress/dist/index.js +5 -0
  69. package/node_modules/@entelligentsia/forge-compress/dist/progressive.d.ts +1 -0
  70. package/node_modules/@entelligentsia/forge-compress/dist/progressive.js +108 -0
  71. package/node_modules/@entelligentsia/forge-compress/dist/strip.d.ts +4 -0
  72. package/node_modules/@entelligentsia/forge-compress/dist/strip.js +55 -0
  73. package/node_modules/@entelligentsia/forge-compress/dist/tokens.d.ts +2 -0
  74. package/node_modules/@entelligentsia/forge-compress/dist/tokens.js +17 -0
  75. package/node_modules/@entelligentsia/forge-compress/package.json +45 -0
  76. package/node_modules/@entelligentsia/forge-compress/src/__tests__/compress.test.ts +409 -0
  77. package/node_modules/@entelligentsia/forge-compress/src/compressor.ts +147 -0
  78. package/node_modules/@entelligentsia/forge-compress/src/entropy.ts +105 -0
  79. package/node_modules/@entelligentsia/forge-compress/src/forge/entity.ts +184 -0
  80. package/node_modules/@entelligentsia/forge-compress/src/forge/index.ts +10 -0
  81. package/node_modules/@entelligentsia/forge-compress/src/forge/markdown.ts +122 -0
  82. package/node_modules/@entelligentsia/forge-compress/src/forge/query.ts +105 -0
  83. package/node_modules/@entelligentsia/forge-compress/src/forge/validate.ts +86 -0
  84. package/node_modules/@entelligentsia/forge-compress/src/index.ts +22 -0
  85. package/node_modules/@entelligentsia/forge-compress/src/progressive.ts +123 -0
  86. package/node_modules/@entelligentsia/forge-compress/src/strip.ts +58 -0
  87. package/node_modules/@entelligentsia/forge-compress/src/tokens.ts +19 -0
  88. package/package.json +5 -10
@@ -29,6 +29,9 @@ const ARTIFACT_CATALOG = {
29
29
  'triage': { filename: 'TRIAGE.md', type: 'md' },
30
30
  'bug-report': { filename: 'BUG_REPORT.md', type: 'md' },
31
31
  'index': { filename: 'INDEX.md', type: 'md' },
32
+ 'task-prompt': { filename: 'TASK_PROMPT.md', type: 'md' },
33
+ 'sprint-requirements': { filename: 'SPRINT_REQUIREMENTS.md', type: 'md' },
34
+ 'sprint-completion-review': { filename: 'SPRINT_COMPLETION_REVIEW.md', type: 'md' },
32
35
  'cost-report': { filename: 'COST_REPORT.md', type: 'md' },
33
36
  'timesheet': { filename: 'TIMESHEET.md', type: 'md' },
34
37
  'plan-summary': { filename: 'PLAN-SUMMARY.json', type: 'json' },
@@ -44,6 +47,25 @@ const ARTIFACT_CATALOG = {
44
47
  'collation-summary': { filename: 'COLLATION-SUMMARY.json', type: 'json' },
45
48
  };
46
49
 
50
+ // Per-entity filename overrides. Bug-mode plans and plan-summaries use the
51
+ // BUG_FIX_PLAN prefix to match the long-standing forge convention and the
52
+ // preflight-gate.cjs expectations for review-plan in bug mode. Without this
53
+ // override, plan-fix (routed via plan_task.md post FORGE-BUG-040) writes
54
+ // PLAN.md and review-plan preflight then fails "artifact missing:
55
+ // BUG_FIX_PLAN.md" — see FORGE-BUG-041.
56
+ const ARTIFACT_FILENAME_OVERRIDES = {
57
+ bug: {
58
+ 'plan': 'BUG_FIX_PLAN.md',
59
+ 'plan-summary': 'BUG-FIX-PLAN-SUMMARY.json',
60
+ },
61
+ };
62
+
63
+ function resolveArtifactFilename(entity, artifactName) {
64
+ const override = ARTIFACT_FILENAME_OVERRIDES[entity];
65
+ if (override && override[artifactName]) return override[artifactName];
66
+ return ARTIFACT_CATALOG[artifactName].filename;
67
+ }
68
+
47
69
  const ARTIFACT_NAMES = Object.keys(ARTIFACT_CATALOG).sort();
48
70
 
49
71
  // ── Summary JSON validation ──────────────────────────────────────────────────
@@ -200,7 +222,13 @@ if (subcmd === 'list') {
200
222
  const known = [];
201
223
  const other = [];
202
224
  for (const f of files) {
203
- const catalogEntry = Object.entries(ARTIFACT_CATALOG).find(([, v]) => v.filename === f);
225
+ // Recognise the per-entity overrides as well, so e.g. BUG_FIX_PLAN.md
226
+ // surfaces as the `plan` artifact in bug-mode listings.
227
+ const overrideEntry = ARTIFACT_FILENAME_OVERRIDES[entity]
228
+ ? Object.entries(ARTIFACT_FILENAME_OVERRIDES[entity]).find(([, fn]) => fn === f)
229
+ : undefined;
230
+ const catalogEntry = overrideEntry
231
+ || Object.entries(ARTIFACT_CATALOG).find(([, v]) => v.filename === f);
204
232
  if (catalogEntry) {
205
233
  known.push(` ${catalogEntry[0]} → ${f}`);
206
234
  } else {
@@ -233,13 +261,14 @@ if (!catalogEntry) {
233
261
  process.exit(1);
234
262
  }
235
263
 
236
- const filePath = path.join(absDir, catalogEntry.filename);
264
+ const resolvedFilename = resolveArtifactFilename(entity, artifactName);
265
+ const filePath = path.join(absDir, resolvedFilename);
237
266
 
238
267
  // ── read ─────────────────────────────────────────────────────────────────────
239
268
 
240
269
  if (subcmd === 'read') {
241
270
  if (!fs.existsSync(filePath)) {
242
- process.stderr.write(`Artifact not found: ${path.join(entityDir, catalogEntry.filename)}\n`);
271
+ process.stderr.write(`Artifact not found: ${path.join(entityDir, resolvedFilename)}\n`);
243
272
  process.exit(2);
244
273
  }
245
274
  process.stdout.write(fs.readFileSync(filePath, 'utf8'));
@@ -272,7 +301,7 @@ if (subcmd === 'write') {
272
301
  const validationError = validateSummaryJson(content);
273
302
  if (validationError) {
274
303
  process.stderr.write(
275
- `Summary validation failed for ${catalogEntry.filename}: ${validationError}. ` +
304
+ `Summary validation failed for ${resolvedFilename}: ${validationError}. ` +
276
305
  `Required fields: ${SUMMARY_REQUIRED.join(', ')}.\n`
277
306
  );
278
307
  process.exit(1);
@@ -282,7 +311,7 @@ if (subcmd === 'write') {
282
311
  fs.mkdirSync(absDir, { recursive: true });
283
312
  fs.writeFileSync(filePath, content, 'utf8');
284
313
  process.stdout.write(
285
- `Wrote ${Buffer.byteLength(content, 'utf8')} bytes to ${path.join(entityDir, catalogEntry.filename)}\n`
314
+ `Wrote ${Buffer.byteLength(content, 'utf8')} bytes to ${path.join(entityDir, resolvedFilename)}\n`
286
315
  );
287
316
  process.exit(0);
288
317
  }
@@ -0,0 +1,6 @@
1
+ import { stripAnsi } from "./strip.js";
2
+ export declare function lightweightCleanup(content: string): string;
3
+ export declare function verbatimCompact(text: string): string;
4
+ export declare function aggressiveCompress(content: string, ext?: string): string;
5
+ export declare function safeguardRatio(original: string, compressed: string): string;
6
+ export { stripAnsi };
@@ -0,0 +1,137 @@
1
+ import { countTokens } from "./tokens.js";
2
+ import { stripAnsi, stripTimestampsAndHashes, normalizeWhitespace, isBoilerplate, } from "./strip.js";
3
+ export function lightweightCleanup(content) {
4
+ const lines = content.split("\n");
5
+ const total = lines.length;
6
+ const result = [];
7
+ let blankCount = 0;
8
+ let braceRun = [];
9
+ const flushBraceRun = () => {
10
+ if (total <= 200 || braceRun.length <= 5) {
11
+ result.push(...braceRun);
12
+ }
13
+ else {
14
+ result.push(braceRun[0], braceRun[1]);
15
+ result.push(`[${braceRun.length - 2} brace-only lines collapsed]`);
16
+ }
17
+ braceRun = [];
18
+ };
19
+ for (const line of lines) {
20
+ const trimmed = line.trim();
21
+ if (!trimmed) {
22
+ flushBraceRun();
23
+ blankCount++;
24
+ if (blankCount <= 1)
25
+ result.push("");
26
+ continue;
27
+ }
28
+ blankCount = 0;
29
+ if (/^[}\]);]+$/.test(trimmed)) {
30
+ braceRun.push(trimmed);
31
+ continue;
32
+ }
33
+ flushBraceRun();
34
+ result.push(line);
35
+ }
36
+ flushBraceRun();
37
+ return result.join("\n");
38
+ }
39
+ export function verbatimCompact(text) {
40
+ const lines = [];
41
+ let blankCount = 0;
42
+ let prevLine = null;
43
+ let repeatCount = 0;
44
+ const flushRepeats = () => {
45
+ if (repeatCount > 1 && prevLine !== null) {
46
+ const lastIdx = lines.length - 1;
47
+ if (lastIdx >= 0) {
48
+ lines[lastIdx] = `[${repeatCount}x] ${prevLine}`;
49
+ }
50
+ }
51
+ repeatCount = 0;
52
+ prevLine = null;
53
+ };
54
+ for (const line of text.split("\n")) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed) {
57
+ blankCount++;
58
+ if (blankCount <= 1) {
59
+ flushRepeats();
60
+ lines.push("");
61
+ }
62
+ continue;
63
+ }
64
+ blankCount = 0;
65
+ if (isBoilerplate(trimmed))
66
+ continue;
67
+ const normalized = normalizeWhitespace(trimmed);
68
+ const stripped = stripTimestampsAndHashes(normalized);
69
+ if (prevLine !== null && prevLine === stripped) {
70
+ repeatCount++;
71
+ continue;
72
+ }
73
+ flushRepeats();
74
+ prevLine = stripped;
75
+ repeatCount = 1;
76
+ lines.push(stripped);
77
+ }
78
+ flushRepeats();
79
+ return lines.join("\n");
80
+ }
81
+ export function aggressiveCompress(content, ext) {
82
+ const result = [];
83
+ const isPython = ext === "py";
84
+ const isHtml = ext === "html" || ext === "htm" || ext === "xml" || ext === "svg";
85
+ const isSql = ext === "sql";
86
+ const isShell = ext === "sh" || ext === "bash" || ext === "zsh" || ext === "fish";
87
+ let inBlockComment = false;
88
+ for (const line of content.split("\n")) {
89
+ const trimmed = line.trim();
90
+ if (!trimmed)
91
+ continue;
92
+ if (inBlockComment) {
93
+ if (trimmed.includes("*/") || (isHtml && trimmed.includes("-->"))) {
94
+ inBlockComment = false;
95
+ }
96
+ continue;
97
+ }
98
+ if (trimmed.startsWith("/*") || (isHtml && trimmed.startsWith("<!--"))) {
99
+ if (!(trimmed.includes("*/") || trimmed.includes("-->"))) {
100
+ inBlockComment = true;
101
+ }
102
+ continue;
103
+ }
104
+ if (trimmed.startsWith("//") && !trimmed.startsWith("///"))
105
+ continue;
106
+ if (trimmed.startsWith("*") || trimmed.startsWith("*/"))
107
+ continue;
108
+ if (isPython && trimmed.startsWith("#"))
109
+ continue;
110
+ if (isSql && trimmed.startsWith("--"))
111
+ continue;
112
+ if (isShell && trimmed.startsWith("#") && !trimmed.startsWith("#!"))
113
+ continue;
114
+ if (/^[}\]);]+$/.test(trimmed)) {
115
+ const last = result[result.length - 1];
116
+ if (last && /^[}\]);]+$/.test(last.trim())) {
117
+ result[result.length - 1] = last + trimmed;
118
+ continue;
119
+ }
120
+ }
121
+ result.push(trimmed);
122
+ }
123
+ return result.join("\n");
124
+ }
125
+ export function safeguardRatio(original, compressed) {
126
+ const origTokens = countTokens(original);
127
+ const compTokens = countTokens(compressed);
128
+ if (origTokens === 0)
129
+ return compressed;
130
+ if (compTokens > origTokens)
131
+ return original;
132
+ const ratio = compTokens / origTokens;
133
+ if (ratio < 0.05 && origTokens < 2000)
134
+ return original;
135
+ return compressed;
136
+ }
137
+ export { stripAnsi };
@@ -0,0 +1,3 @@
1
+ export declare function shannonEntropy(text: string): number;
2
+ export declare function normalizedTokenEntropy(text: string): number;
3
+ export declare function compressIb(text: string, targetRatio: number): string;
@@ -0,0 +1,99 @@
1
+ import { countTokens } from "./tokens.js";
2
+ export function shannonEntropy(text) {
3
+ if (!text)
4
+ return 0;
5
+ const freq = new Map();
6
+ for (const ch of text) {
7
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
8
+ }
9
+ const len = text.length;
10
+ let entropy = 0;
11
+ for (const count of freq.values()) {
12
+ const p = count / len;
13
+ if (p > 0)
14
+ entropy -= p * Math.log2(p);
15
+ }
16
+ return entropy;
17
+ }
18
+ export function normalizedTokenEntropy(text) {
19
+ if (!text || text.trim().length === 0)
20
+ return 0;
21
+ const words = text.trim().split(/\s+/);
22
+ if (words.length <= 1) {
23
+ const charEntropy = shannonEntropy(text.trim());
24
+ return Math.min(1, charEntropy / 4.5);
25
+ }
26
+ const freq = new Map();
27
+ for (const w of words) {
28
+ freq.set(w, (freq.get(w) ?? 0) + 1);
29
+ }
30
+ const n = words.length;
31
+ const uniqueRatio = freq.size / n;
32
+ let entropy = 0;
33
+ for (const count of freq.values()) {
34
+ const p = count / n;
35
+ if (p > 0)
36
+ entropy -= p * Math.log2(p);
37
+ }
38
+ const maxEntropy = Math.log2(n);
39
+ const normalized = maxEntropy > 0 ? entropy / maxEntropy : 0;
40
+ return Math.min(1, normalized * 0.7 + uniqueRatio * 0.3);
41
+ }
42
+ function renderIb(lines, scores, threshold) {
43
+ const out = [];
44
+ let omitRun = 0;
45
+ for (let i = 0; i < lines.length; i++) {
46
+ if (scores[i] >= threshold) {
47
+ if (omitRun > 0) {
48
+ out.push(`// ... ${omitRun} low-info lines omitted`);
49
+ omitRun = 0;
50
+ }
51
+ out.push(lines[i]);
52
+ }
53
+ else {
54
+ omitRun++;
55
+ }
56
+ }
57
+ if (omitRun > 0) {
58
+ out.push(`// ... ${omitRun} low-info lines omitted`);
59
+ }
60
+ return out.join("\n");
61
+ }
62
+ export function compressIb(text, targetRatio) {
63
+ if (!text)
64
+ return "";
65
+ const inputTokens = countTokens(text);
66
+ if (inputTokens === 0)
67
+ return text;
68
+ const ratio = Math.max(0.02, Math.min(1.0, targetRatio));
69
+ const lines = text.split("\n");
70
+ const scores = lines.map((ln) => normalizedTokenEntropy(ln));
71
+ let lo = 0;
72
+ let hi = 1;
73
+ let best = renderIb(lines, scores, 0);
74
+ let bestDiff = Infinity;
75
+ const consider = (thr) => {
76
+ const cand = renderIb(lines, scores, thr);
77
+ const r = countTokens(cand) / inputTokens;
78
+ const diff = Math.abs(r - ratio);
79
+ if (diff < bestDiff) {
80
+ bestDiff = diff;
81
+ best = cand;
82
+ }
83
+ return r;
84
+ };
85
+ for (let i = 0; i < 26; i++) {
86
+ const mid = (lo + hi) / 2;
87
+ const r = consider(mid);
88
+ if (r > ratio) {
89
+ lo = mid;
90
+ }
91
+ else {
92
+ hi = mid;
93
+ }
94
+ }
95
+ for (const thr of [0, 1, lo, hi, (lo + hi) / 2]) {
96
+ consider(thr);
97
+ }
98
+ return best;
99
+ }
@@ -0,0 +1,8 @@
1
+ export interface CompressEntityOptions {
2
+ keepSummaries?: boolean | "latest" | "all";
3
+ maxKeyChanges?: number;
4
+ flatFormat?: boolean;
5
+ maxTokens?: number;
6
+ }
7
+ export declare function compressEntity(input: string, opts?: CompressEntityOptions): string;
8
+ export declare function compressEntityList(input: string, opts?: CompressEntityOptions): string;
@@ -0,0 +1,149 @@
1
+ import { countTokens } from "../tokens.js";
2
+ const PHASE_ORDER = [
3
+ "plan",
4
+ "triage",
5
+ "review_plan",
6
+ "implementation",
7
+ "code_review",
8
+ "validation",
9
+ "approve",
10
+ ];
11
+ export function compressEntity(input, opts) {
12
+ let entity;
13
+ try {
14
+ entity = JSON.parse(input);
15
+ }
16
+ catch {
17
+ return input;
18
+ }
19
+ const keepSummaries = opts?.keepSummaries ?? "latest";
20
+ const maxKeyChanges = opts?.maxKeyChanges ?? 3;
21
+ const result = {};
22
+ const idKey = findIdKey(entity);
23
+ if (idKey)
24
+ result[idKey] = entity[idKey];
25
+ if (entity.title)
26
+ result.title = entity.title;
27
+ if (entity.status)
28
+ result.status = entity.status;
29
+ if (entity.sprintId)
30
+ result.sprintId = entity.sprintId;
31
+ if (entity.feature_id)
32
+ result.feature_id = entity.feature_id;
33
+ if (entity.severity)
34
+ result.severity = entity.severity;
35
+ if (entity.estimate)
36
+ result.estimate = entity.estimate;
37
+ if (entity.executionMode)
38
+ result.executionMode = entity.executionMode;
39
+ if (entity.pipeline)
40
+ result.pipeline = entity.pipeline;
41
+ if (Array.isArray(entity.taskIds)) {
42
+ result.taskIds = entity.taskIds;
43
+ result.taskCount = entity.taskIds.length;
44
+ }
45
+ if (Array.isArray(entity.dependencies) && entity.dependencies.length > 0) {
46
+ result.dependencies = entity.dependencies;
47
+ }
48
+ const summaries = entity.summaries;
49
+ if (summaries && typeof summaries === "object") {
50
+ if (keepSummaries === "all") {
51
+ result.summaries = compressSummaries(summaries, maxKeyChanges);
52
+ }
53
+ else if (keepSummaries === "latest") {
54
+ const latest = findLatestPhase(summaries);
55
+ if (latest) {
56
+ result.latestPhase = latest.phase;
57
+ result.latestSummary = compressOneSummary(latest.summary, maxKeyChanges);
58
+ }
59
+ }
60
+ }
61
+ if (opts?.flatFormat) {
62
+ return formatFlat(result);
63
+ }
64
+ let output = JSON.stringify(result, null, 2);
65
+ if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
66
+ output = JSON.stringify(result);
67
+ }
68
+ return output;
69
+ }
70
+ export function compressEntityList(input, opts) {
71
+ let entities;
72
+ try {
73
+ entities = JSON.parse(input);
74
+ }
75
+ catch {
76
+ return input;
77
+ }
78
+ if (!Array.isArray(entities))
79
+ return input;
80
+ const compressed = entities.map((e) => JSON.parse(compressEntity(JSON.stringify(e), { ...opts, flatFormat: false })));
81
+ let output = JSON.stringify(compressed, null, 2);
82
+ if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
83
+ output = JSON.stringify(compressed);
84
+ }
85
+ return output;
86
+ }
87
+ function findIdKey(entity) {
88
+ for (const key of ["taskId", "sprintId", "bugId", "featureId", "id"]) {
89
+ if (entity[key])
90
+ return key;
91
+ }
92
+ return undefined;
93
+ }
94
+ function compressSummaries(summaries, maxKeyChanges) {
95
+ const result = {};
96
+ for (const phase of PHASE_ORDER) {
97
+ if (summaries[phase]) {
98
+ result[phase] = compressOneSummary(summaries[phase], maxKeyChanges);
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ function compressOneSummary(summary, maxKeyChanges) {
104
+ const result = {};
105
+ if (summary.objective)
106
+ result.objective = summary.objective;
107
+ if (summary.verdict)
108
+ result.verdict = summary.verdict;
109
+ if (summary.key_changes && summary.key_changes.length > 0) {
110
+ const shown = summary.key_changes.slice(0, maxKeyChanges);
111
+ const extra = summary.key_changes.length - maxKeyChanges;
112
+ result.key_changes = extra > 0 ? [...shown, `+${extra} more`] : shown;
113
+ }
114
+ if (summary.route)
115
+ result.route = summary.route;
116
+ return result;
117
+ }
118
+ function findLatestPhase(summaries) {
119
+ for (let i = PHASE_ORDER.length - 1; i >= 0; i--) {
120
+ const phase = PHASE_ORDER[i];
121
+ if (summaries[phase]) {
122
+ return { phase, summary: summaries[phase] };
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function formatFlat(obj, prefix = "") {
128
+ const lines = [];
129
+ for (const [key, value] of Object.entries(obj)) {
130
+ const fullKey = prefix ? `${prefix}.${key}` : key;
131
+ if (value === null || value === undefined)
132
+ continue;
133
+ if (Array.isArray(value)) {
134
+ if (value.length <= 5 && value.every((v) => typeof v === "string")) {
135
+ lines.push(`${fullKey}: ${value.join(", ")}`);
136
+ }
137
+ else {
138
+ lines.push(`${fullKey}: [${value.length} items]`);
139
+ }
140
+ }
141
+ else if (typeof value === "object") {
142
+ lines.push(formatFlat(value, fullKey));
143
+ }
144
+ else {
145
+ lines.push(`${fullKey}: ${value}`);
146
+ }
147
+ }
148
+ return lines.filter(Boolean).join("\n");
149
+ }
@@ -0,0 +1,7 @@
1
+ export { compressStoreQuery } from "./query.js";
2
+ export type { CompressQueryOptions } from "./query.js";
3
+ export { compressEntity, compressEntityList } from "./entity.js";
4
+ export type { CompressEntityOptions } from "./entity.js";
5
+ export { compressMarkdown } from "./markdown.js";
6
+ export type { CompressMarkdownOptions } from "./markdown.js";
7
+ export { compressValidateStore } from "./validate.js";
@@ -0,0 +1,4 @@
1
+ export { compressStoreQuery } from "./query.js";
2
+ export { compressEntity, compressEntityList } from "./entity.js";
3
+ export { compressMarkdown } from "./markdown.js";
4
+ export { compressValidateStore } from "./validate.js";
@@ -0,0 +1,5 @@
1
+ export interface CompressMarkdownOptions {
2
+ maxTokens?: number;
3
+ mode?: "map" | "headings" | "truncate";
4
+ }
5
+ export declare function compressMarkdown(input: string, opts?: CompressMarkdownOptions): string;
@@ -0,0 +1,92 @@
1
+ import { countTokens, truncateToTokenBudget } from "../tokens.js";
2
+ export function compressMarkdown(input, opts) {
3
+ if (!input.trim())
4
+ return "";
5
+ const mode = opts?.mode ?? "map";
6
+ const maxTokens = opts?.maxTokens ?? Infinity;
7
+ switch (mode) {
8
+ case "headings":
9
+ return compressHeadings(input, maxTokens);
10
+ case "truncate":
11
+ return truncateToTokenBudget(input, maxTokens);
12
+ case "map":
13
+ default:
14
+ return compressMap(input, maxTokens);
15
+ }
16
+ }
17
+ function compressMap(input, maxTokens) {
18
+ const lines = input.split("\n");
19
+ const picked = [];
20
+ let inFrontmatter = false;
21
+ let frontmatterDone = false;
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const line = lines[i];
24
+ const trimmed = line.trim();
25
+ if (i === 0 && trimmed === "---") {
26
+ inFrontmatter = true;
27
+ continue;
28
+ }
29
+ if (inFrontmatter) {
30
+ if (trimmed === "---") {
31
+ inFrontmatter = false;
32
+ frontmatterDone = true;
33
+ }
34
+ continue;
35
+ }
36
+ if (trimmed.startsWith("#")) {
37
+ picked.push(line);
38
+ continue;
39
+ }
40
+ if (trimmed.startsWith("- **") ||
41
+ trimmed.startsWith("* **") ||
42
+ trimmed.startsWith("| ") ||
43
+ trimmed.startsWith("```")) {
44
+ picked.push(line);
45
+ continue;
46
+ }
47
+ if (trimmed.startsWith("- [ ]") ||
48
+ trimmed.startsWith("- [x]") ||
49
+ trimmed.startsWith("- [X]")) {
50
+ picked.push(line);
51
+ continue;
52
+ }
53
+ if (i === 0 || (frontmatterDone && picked.length === 0)) {
54
+ picked.push(line);
55
+ }
56
+ }
57
+ if (picked.length === 0) {
58
+ return truncateToTokenBudget(input, maxTokens);
59
+ }
60
+ const totalLines = lines.length;
61
+ const keptLines = picked.length;
62
+ const omitted = totalLines - keptLines;
63
+ let result = picked.join("\n");
64
+ if (omitted > 0) {
65
+ result += `\n\n[${omitted} body lines omitted — ${totalLines} total]`;
66
+ }
67
+ if (countTokens(result) > maxTokens) {
68
+ result = truncateToTokenBudget(result, maxTokens);
69
+ }
70
+ return result;
71
+ }
72
+ function compressHeadings(input, maxTokens) {
73
+ const lines = input.split("\n");
74
+ const headings = [];
75
+ let depth = 0;
76
+ for (const line of lines) {
77
+ const match = line.match(/^(#{1,6})\s+(.+)/);
78
+ if (match) {
79
+ const level = match[1].length;
80
+ const indent = " ".repeat(level - 1);
81
+ headings.push(`${indent}${match[2]}`);
82
+ depth = Math.max(depth, level);
83
+ }
84
+ }
85
+ if (headings.length === 0) {
86
+ return truncateToTokenBudget(input, maxTokens);
87
+ }
88
+ const result = `${headings.length} sections (depth ${depth}):\n${headings.join("\n")}`;
89
+ return countTokens(result) > maxTokens
90
+ ? truncateToTokenBudget(result, maxTokens)
91
+ : result;
92
+ }
@@ -0,0 +1,7 @@
1
+ export interface CompressQueryOptions {
2
+ keepExcerpts?: boolean;
3
+ maxExcerptLines?: number;
4
+ maxResults?: number;
5
+ maxTokens?: number;
6
+ }
7
+ export declare function compressStoreQuery(input: string, opts?: CompressQueryOptions): string;
@@ -0,0 +1,60 @@
1
+ import { countTokens } from "../tokens.js";
2
+ export function compressStoreQuery(input, opts) {
3
+ let envelope;
4
+ try {
5
+ envelope = JSON.parse(input);
6
+ }
7
+ catch {
8
+ return input;
9
+ }
10
+ if (!envelope.results || !Array.isArray(envelope.results))
11
+ return input;
12
+ const keepExcerpts = opts?.keepExcerpts ?? false;
13
+ const maxExcerptLines = opts?.maxExcerptLines ?? 5;
14
+ const maxResults = opts?.maxResults ?? envelope.results.length;
15
+ const results = envelope.results.slice(0, maxResults).map((r) => {
16
+ const entry = {
17
+ id: r.id,
18
+ title: r.title,
19
+ status: r.status,
20
+ type: r.type,
21
+ };
22
+ if (r.relationships && Object.keys(r.relationships).length > 0) {
23
+ const rels = {};
24
+ for (const [k, v] of Object.entries(r.relationships)) {
25
+ if (v !== null && v !== undefined)
26
+ rels[k] = v;
27
+ }
28
+ if (Object.keys(rels).length > 0)
29
+ entry.relationships = rels;
30
+ }
31
+ if (keepExcerpts && r.excerpt) {
32
+ const lines = r.excerpt.split("\n");
33
+ entry.excerpt =
34
+ lines.length > maxExcerptLines
35
+ ? lines.slice(0, maxExcerptLines).join("\n") +
36
+ `\n... (${lines.length - maxExcerptLines} more lines)`
37
+ : r.excerpt;
38
+ }
39
+ return entry;
40
+ });
41
+ const compact = { results };
42
+ if (envelope.query)
43
+ compact.query = envelope.query;
44
+ if (envelope.path)
45
+ compact.path = envelope.path;
46
+ if (typeof envelope.count === "number")
47
+ compact.count = envelope.count;
48
+ if (typeof envelope.totalMatched === "number")
49
+ compact.totalMatched = envelope.totalMatched;
50
+ if (typeof envelope.returned === "number")
51
+ compact.returned = envelope.returned;
52
+ if (envelope.results.length > maxResults) {
53
+ compact.truncated = envelope.results.length - maxResults;
54
+ }
55
+ let output = JSON.stringify(compact, null, 2);
56
+ if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
57
+ output = JSON.stringify(compact);
58
+ }
59
+ return output;
60
+ }
@@ -0,0 +1 @@
1
+ export declare function compressValidateStore(input: string): string;