sidecar-cli 0.1.4 → 0.1.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,6 +179,29 @@ Artifacts:
179
179
  - `sidecar artifact add <path> [--kind file|doc|screenshot|other] [--note <text>] [--json]`
180
180
  - `sidecar artifact list [--json]`
181
181
 
182
+ ## Automatic prompt token budgeting
183
+
184
+ When Sidecar compiles task prompts (`sidecar prompt compile` and run execution flows), it automatically applies a token budget to reduce context size without degrading execution quality.
185
+
186
+ Current behavior:
187
+
188
+ - Keeps required sections intact (task, objective, constraints, validation, definition of done).
189
+ - Deduplicates repeated list items.
190
+ - Trims only optional high-volume sections when needed (for example: in-scope lists, linked notes/decisions, long file lists).
191
+ - Adds compact overflow lines such as `+ N more ... (see task packet for full list)`.
192
+
193
+ Current defaults:
194
+
195
+ - Target budget: ~1200 estimated tokens
196
+ - Safety ceiling: ~1500 estimated tokens
197
+
198
+ Prompt optimization data is included in compile output and stored on run records:
199
+
200
+ - `prompt_tokens_estimated_before`
201
+ - `prompt_tokens_estimated_after`
202
+ - `prompt_budget_target`
203
+ - `prompt_trimmed_sections`
204
+
182
205
  ## Example workflow
183
206
 
184
207
  ```bash
package/dist/cli.js CHANGED
@@ -416,6 +416,7 @@ program
416
416
  'Documentation: https://usesidecar.dev/',
417
417
  ...(resolvedInstructions ? ['', `Loaded instructions.md from ${resolvedInstructions.sourceLabel}`] : []),
418
418
  ]);
419
+ maybePrintUpdateNotice();
419
420
  }
420
421
  catch (err) {
421
422
  handleCommandError(command, Boolean(opts.json), err);
@@ -974,11 +975,16 @@ prompt
974
975
  runner_type: compiled.runner_type,
975
976
  agent_role: compiled.agent_role,
976
977
  prompt_path: compiled.prompt_path,
978
+ prompt_optimization: compiled.prompt_optimization,
977
979
  preview: opts.preview ? compiled.prompt_markdown : null,
978
980
  }, [
979
981
  `Compiled prompt for ${compiled.task_id}.`,
980
982
  `Run: ${compiled.run_id}`,
981
983
  `Path: ${compiled.prompt_path}`,
984
+ `Prompt estimate: ${compiled.prompt_optimization.estimated_tokens_before} -> ${compiled.prompt_optimization.estimated_tokens_after} tokens (target ${compiled.prompt_optimization.budget_target})`,
985
+ ...(compiled.prompt_optimization.trimmed_sections.length > 0
986
+ ? [`Trimmed: ${compiled.prompt_optimization.trimmed_sections.join(', ')}`]
987
+ : []),
982
988
  ...(opts.preview ? ['', compiled.prompt_markdown] : []),
983
989
  ]);
984
990
  }
@@ -1353,7 +1359,6 @@ if (process.argv.length === 2) {
1353
1359
  console.log('');
1354
1360
  }
1355
1361
  program.outputHelp();
1356
- maybePrintUpdateNotice();
1357
1362
  process.exit(0);
1358
1363
  }
1359
1364
  if (process.argv[2] === 'run' &&
@@ -1369,4 +1374,3 @@ if (process.argv[2] === 'run' &&
1369
1374
  process.argv.splice(2, 1, 'run-exec');
1370
1375
  }
1371
1376
  program.parse(process.argv);
1372
- maybePrintUpdateNotice();
@@ -2,6 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { nowIso } from '../lib/format.js';
4
4
  import { getSidecarPaths } from '../lib/paths.js';
5
+ const PROMPT_BUDGET_TARGET = 1200;
6
+ const PROMPT_BUDGET_MAX = 1500;
5
7
  function section(title, lines) {
6
8
  return [`## ${title}`, ...lines, ''].join('\n');
7
9
  }
@@ -40,8 +42,44 @@ function runnerGuidance(runner) {
40
42
  'Provide a clear summary with validation and follow-up notes at the end.',
41
43
  ];
42
44
  }
43
- export function compilePromptMarkdown(input) {
44
- const { task, run, runner, agentRole, linkedContext } = input;
45
+ function estimateTokens(text) {
46
+ return Math.ceil(text.length / 4);
47
+ }
48
+ function dedupe(items) {
49
+ const seen = new Set();
50
+ const out = [];
51
+ for (const item of items.map((v) => v.trim()).filter(Boolean)) {
52
+ const key = item.toLowerCase();
53
+ if (seen.has(key))
54
+ continue;
55
+ seen.add(key);
56
+ out.push(item);
57
+ }
58
+ return out;
59
+ }
60
+ function withOverflow(items, fullCount, label) {
61
+ const overflow = fullCount - items.length;
62
+ if (overflow <= 0)
63
+ return items;
64
+ return [...items, `+ ${overflow} more ${label} (see task packet for full list)`];
65
+ }
66
+ function buildPromptLists(input) {
67
+ const { task, linkedContext } = input;
68
+ return {
69
+ inScope: dedupe(task.scope.in_scope),
70
+ outOfScope: dedupe(task.scope.out_of_scope),
71
+ filesToRead: dedupe(task.implementation.files_to_read),
72
+ filesToAvoid: dedupe(task.implementation.files_to_avoid),
73
+ relatedDecisions: dedupe(linkedContext?.related_decisions ?? task.context.related_decisions),
74
+ relatedNotes: dedupe(linkedContext?.related_notes ?? task.context.related_notes),
75
+ technicalConstraints: dedupe(task.constraints.technical),
76
+ designConstraints: dedupe(task.constraints.design),
77
+ validationCommands: dedupe(task.execution.commands.validation),
78
+ definitionOfDone: dedupe(task.definition_of_done),
79
+ };
80
+ }
81
+ function renderPrompt(input, lists) {
82
+ const { task, run, runner, agentRole } = input;
45
83
  const lines = [];
46
84
  lines.push('# Sidecar Execution Brief');
47
85
  lines.push('');
@@ -59,26 +97,91 @@ export function compilePromptMarkdown(input) {
59
97
  ]));
60
98
  lines.push(section('Objective', [task.goal]));
61
99
  lines.push(section('Why this matters', [task.summary]));
62
- lines.push(section('In scope', bullets(task.scope.in_scope)));
63
- lines.push(section('Out of scope', bullets(task.scope.out_of_scope)));
64
- lines.push(section('Read these first', bullets(task.implementation.files_to_read)));
65
- lines.push(section('Avoid changing', bullets(task.implementation.files_to_avoid)));
66
- const relatedDecisions = linkedContext?.related_decisions ?? task.context.related_decisions;
67
- const relatedNotes = linkedContext?.related_notes ?? task.context.related_notes;
100
+ lines.push(section('In scope', bullets(lists.inScope)));
101
+ lines.push(section('Out of scope', bullets(lists.outOfScope)));
102
+ lines.push(section('Read these first', bullets(lists.filesToRead)));
103
+ lines.push(section('Avoid changing', bullets(lists.filesToAvoid)));
68
104
  lines.push(section('Linked context', [
69
- ...bullets(relatedDecisions, '- no related decisions'),
70
- ...bullets(relatedNotes, '- no related notes'),
105
+ ...bullets(lists.relatedDecisions, '- no related decisions'),
106
+ ...bullets(lists.relatedNotes, '- no related notes'),
71
107
  ]));
72
108
  lines.push(section('Constraints', [
73
- ...bullets(task.constraints.technical, '- no technical constraints'),
74
- ...bullets(task.constraints.design, '- no design constraints'),
109
+ ...bullets(lists.technicalConstraints, '- no technical constraints'),
110
+ ...bullets(lists.designConstraints, '- no design constraints'),
75
111
  ]));
76
- lines.push(section('Validation', bullets(task.execution.commands.validation)));
77
- lines.push(section('Definition of done', bullets(task.definition_of_done)));
112
+ lines.push(section('Validation', bullets(lists.validationCommands)));
113
+ lines.push(section('Definition of done', bullets(lists.definitionOfDone)));
78
114
  lines.push(section('Runner guidance', runnerGuidance(runner)));
79
115
  lines.push(section('Final response format', finalResponseFormat(runner)));
80
116
  return `${lines.join('\n').trim()}\n`;
81
117
  }
118
+ function applyPromptBudget(input) {
119
+ const baseline = buildPromptLists(input);
120
+ const baselineMarkdown = renderPrompt(input, baseline);
121
+ const baselineTokens = estimateTokens(baselineMarkdown);
122
+ if (baselineTokens <= PROMPT_BUDGET_TARGET) {
123
+ return {
124
+ optimizedLists: baseline,
125
+ metadata: {
126
+ estimated_tokens_before: baselineTokens,
127
+ estimated_tokens_after: baselineTokens,
128
+ budget_target: PROMPT_BUDGET_TARGET,
129
+ budget_max: PROMPT_BUDGET_MAX,
130
+ trimmed_sections: [],
131
+ },
132
+ };
133
+ }
134
+ const optimized = {
135
+ ...baseline,
136
+ inScope: withOverflow(baseline.inScope.slice(0, 8), baseline.inScope.length, 'in-scope items'),
137
+ outOfScope: withOverflow(baseline.outOfScope.slice(0, 5), baseline.outOfScope.length, 'out-of-scope items'),
138
+ filesToRead: withOverflow(baseline.filesToRead.slice(0, 10), baseline.filesToRead.length, 'read-first files'),
139
+ filesToAvoid: withOverflow(baseline.filesToAvoid.slice(0, 5), baseline.filesToAvoid.length, 'avoid files'),
140
+ relatedDecisions: withOverflow(baseline.relatedDecisions.slice(0, 3), baseline.relatedDecisions.length, 'decisions'),
141
+ relatedNotes: withOverflow(baseline.relatedNotes.slice(0, 2), baseline.relatedNotes.length, 'notes'),
142
+ };
143
+ let optimizedMarkdown = renderPrompt(input, optimized);
144
+ let optimizedTokens = estimateTokens(optimizedMarkdown);
145
+ // Safety valve for unusually large tasks: preserve must-have sections and thin optional context further.
146
+ if (optimizedTokens > PROMPT_BUDGET_MAX) {
147
+ optimized.relatedDecisions = withOverflow(baseline.relatedDecisions.slice(0, 1), baseline.relatedDecisions.length, 'decisions');
148
+ optimized.relatedNotes = withOverflow([], baseline.relatedNotes.length, 'notes');
149
+ optimized.outOfScope = withOverflow(baseline.outOfScope.slice(0, 3), baseline.outOfScope.length, 'out-of-scope items');
150
+ optimized.filesToAvoid = withOverflow(baseline.filesToAvoid.slice(0, 3), baseline.filesToAvoid.length, 'avoid files');
151
+ optimizedMarkdown = renderPrompt(input, optimized);
152
+ optimizedTokens = estimateTokens(optimizedMarkdown);
153
+ }
154
+ const trimmedSections = [];
155
+ if (optimized.inScope.length < baseline.inScope.length)
156
+ trimmedSections.push('in_scope');
157
+ if (optimized.outOfScope.length < baseline.outOfScope.length)
158
+ trimmedSections.push('out_of_scope');
159
+ if (optimized.filesToRead.length < baseline.filesToRead.length)
160
+ trimmedSections.push('files_to_read');
161
+ if (optimized.filesToAvoid.length < baseline.filesToAvoid.length)
162
+ trimmedSections.push('files_to_avoid');
163
+ if (optimized.relatedDecisions.length < baseline.relatedDecisions.length)
164
+ trimmedSections.push('related_decisions');
165
+ if (optimized.relatedNotes.length < baseline.relatedNotes.length)
166
+ trimmedSections.push('related_notes');
167
+ return {
168
+ optimizedLists: optimized,
169
+ metadata: {
170
+ estimated_tokens_before: baselineTokens,
171
+ estimated_tokens_after: optimizedTokens,
172
+ budget_target: PROMPT_BUDGET_TARGET,
173
+ budget_max: PROMPT_BUDGET_MAX,
174
+ trimmed_sections: trimmedSections,
175
+ },
176
+ };
177
+ }
178
+ export function compilePromptMarkdown(input) {
179
+ const optimized = applyPromptBudget(input);
180
+ return {
181
+ markdown: renderPrompt(input, optimized.optimizedLists),
182
+ metadata: optimized.metadata,
183
+ };
184
+ }
82
185
  export function saveCompiledPrompt(rootPath, runId, markdown) {
83
186
  const promptsPath = getSidecarPaths(rootPath).promptsPath;
84
187
  fs.mkdirSync(promptsPath, { recursive: true });
@@ -11,18 +11,22 @@ export function compileTaskPrompt(input) {
11
11
  branch: task.tracking.branch,
12
12
  worktree: task.tracking.worktree,
13
13
  });
14
- const promptMarkdown = compilePromptMarkdown({
14
+ const compiledPrompt = compilePromptMarkdown({
15
15
  task,
16
16
  run: created.run,
17
17
  runner: input.runner,
18
18
  agentRole: input.agentRole,
19
19
  linkedContext: input.linkedContext,
20
20
  });
21
- const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id, promptMarkdown);
21
+ const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id, compiledPrompt.markdown);
22
22
  updateRunRecordEntry(input.rootPath, created.run.run_id, {
23
23
  status: 'queued',
24
24
  prompt_path: promptPath,
25
- summary: `Compiled prompt for task ${task.task_id}`,
25
+ prompt_tokens_estimated_before: compiledPrompt.metadata.estimated_tokens_before,
26
+ prompt_tokens_estimated_after: compiledPrompt.metadata.estimated_tokens_after,
27
+ prompt_budget_target: compiledPrompt.metadata.budget_target,
28
+ prompt_trimmed_sections: compiledPrompt.metadata.trimmed_sections,
29
+ summary: `Compiled prompt for task ${task.task_id} (${compiledPrompt.metadata.estimated_tokens_before} -> ${compiledPrompt.metadata.estimated_tokens_after} est. tokens)`,
26
30
  });
27
31
  return {
28
32
  run_id: created.run.run_id,
@@ -30,6 +34,7 @@ export function compileTaskPrompt(input) {
30
34
  runner_type: input.runner,
31
35
  agent_role: input.agentRole,
32
36
  prompt_path: promptPath,
33
- prompt_markdown: promptMarkdown,
37
+ prompt_markdown: compiledPrompt.markdown,
38
+ prompt_optimization: compiledPrompt.metadata,
34
39
  };
35
40
  }
@@ -28,6 +28,10 @@ export const runRecordSchema = z
28
28
  reviewed_at: z.string().datetime({ offset: true }).nullable().default(null),
29
29
  reviewed_by: z.string().default(''),
30
30
  review_note: z.string().default(''),
31
+ prompt_tokens_estimated_before: z.number().int().nonnegative().default(0),
32
+ prompt_tokens_estimated_after: z.number().int().nonnegative().default(0),
33
+ prompt_budget_target: z.number().int().nonnegative().default(0),
34
+ prompt_trimmed_sections: z.array(z.string()).default([]),
31
35
  })
32
36
  .strict();
33
37
  export const runRecordCreateInputSchema = runRecordSchema
@@ -49,6 +53,10 @@ export const runRecordCreateInputSchema = runRecordSchema
49
53
  reviewed_at: true,
50
54
  reviewed_by: true,
51
55
  review_note: true,
56
+ prompt_tokens_estimated_before: true,
57
+ prompt_tokens_estimated_after: true,
58
+ prompt_budget_target: true,
59
+ prompt_trimmed_sections: true,
52
60
  });
53
61
  export const runRecordUpdateInputSchema = z
54
62
  .object({
@@ -67,6 +75,10 @@ export const runRecordUpdateInputSchema = z
67
75
  reviewed_at: z.string().datetime({ offset: true }).nullable().optional(),
68
76
  reviewed_by: z.string().optional(),
69
77
  review_note: z.string().optional(),
78
+ prompt_tokens_estimated_before: z.number().int().nonnegative().optional(),
79
+ prompt_tokens_estimated_after: z.number().int().nonnegative().optional(),
80
+ prompt_budget_target: z.number().int().nonnegative().optional(),
81
+ prompt_trimmed_sections: z.array(z.string()).optional(),
70
82
  })
71
83
  .strict();
72
84
  export function createRunRecord(runId, input) {
@@ -92,6 +104,10 @@ export function createRunRecord(runId, input) {
92
104
  reviewed_at: input.reviewed_at ?? null,
93
105
  reviewed_by: input.reviewed_by ?? '',
94
106
  review_note: input.review_note ?? '',
107
+ prompt_tokens_estimated_before: input.prompt_tokens_estimated_before ?? 0,
108
+ prompt_tokens_estimated_after: input.prompt_tokens_estimated_after ?? 0,
109
+ prompt_budget_target: input.prompt_budget_target ?? 0,
110
+ prompt_trimmed_sections: input.prompt_trimmed_sections ?? [],
95
111
  };
96
112
  return runRecordSchema.parse(normalized);
97
113
  }
@@ -53,6 +53,7 @@ export class RunRecordRepository {
53
53
  validation_results: parsedPatch.validation_results ?? existing.validation_results,
54
54
  blockers: parsedPatch.blockers ?? existing.blockers,
55
55
  follow_ups: parsedPatch.follow_ups ?? existing.follow_ups,
56
+ prompt_trimmed_sections: parsedPatch.prompt_trimmed_sections ?? existing.prompt_trimmed_sections,
56
57
  };
57
58
  const validated = runRecordSchema.parse(merged);
58
59
  const filePath = runFilePath(this.runsPath, runId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecar-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5-beta.1",
4
4
  "description": "Local-first project memory and recording tool",
5
5
  "scripts": {
6
6
  "build": "npm run clean && tsc -p tsconfig.json",