sidecar-cli 0.1.0-beta.5 → 0.1.0-beta.7

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
@@ -144,14 +144,28 @@ sidecar session end --summary "Initialization and recording flow implemented"
144
144
  ## AI agent usage
145
145
 
146
146
  Sidecar generates `.sidecar/AGENTS.md` during `init`.
147
+ This repo also includes a root `AGENTS.md` so the policy is visible before any `.sidecar` lookup.
147
148
 
148
- That file explains:
149
+ Required minimum for any code change:
149
150
 
150
- - this repo uses Sidecar
151
- - required workflow for agents
152
- - when to record notes, decisions, worklogs, and tasks
153
- - recommended commands
154
- - a practical session checklist and example
151
+ 1. `sidecar context --format markdown`
152
+ 2. `sidecar worklog record --done "<what changed>" --files <paths> --by agent`
153
+ 3. if behavior/design changed: `sidecar decision record ...`
154
+ 4. if follow-up exists: `sidecar task add ...`
155
+ 5. `sidecar summary refresh`
156
+
157
+ Optional local enforcement:
158
+
159
+ ```bash
160
+ npm run install:hooks
161
+ ```
162
+
163
+ This installs a non-blocking pre-commit reminder that runs `npm run sidecar:reminder`.
164
+ If a pre-commit hook already exists, Sidecar will not overwrite it unless you run:
165
+
166
+ ```bash
167
+ npm run install:hooks -- --force
168
+ ```
155
169
 
156
170
  Agents can discover the CLI surface programmatically with:
157
171
 
@@ -214,6 +228,7 @@ Required configuration:
214
228
  - `HOMEBREW_TAP_GITHUB_TOKEN` (secret, optional)
215
229
 
216
230
  See [RELEASE.md](./RELEASE.md) for full release steps and examples.
231
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for code structure and contribution guidelines.
217
232
 
218
233
  Quick preflight:
219
234
 
package/dist/cli.js CHANGED
@@ -58,6 +58,25 @@ function respondSuccess(command, asJson, data, lines = []) {
58
58
  console.log(line);
59
59
  }
60
60
  }
61
+ function summaryWasRefreshedRecently(db, projectId) {
62
+ return Boolean(db
63
+ .prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'summary_generated' AND created_at >= datetime('now', '-3 day') LIMIT 1`)
64
+ .get(projectId));
65
+ }
66
+ function renderSessionHygiene(command, asJson, warnings) {
67
+ if (asJson) {
68
+ printJsonEnvelope(jsonSuccess(command, { warnings, healthy: warnings.length === 0 }));
69
+ return;
70
+ }
71
+ if (warnings.length === 0) {
72
+ console.log('Session hygiene looks good.');
73
+ return;
74
+ }
75
+ console.log('Session hygiene warnings:');
76
+ for (const warning of warnings) {
77
+ console.log(`- ${warning}`);
78
+ }
79
+ }
61
80
  function renderContextText(data) {
62
81
  const lines = [];
63
82
  lines.push(`Project: ${data.projectName}`);
@@ -177,6 +196,15 @@ program
177
196
  const rootPath = process.cwd();
178
197
  const sidecar = getSidecarPaths(rootPath);
179
198
  const projectName = opts.name?.trim() || path.basename(rootPath);
199
+ if (fs.existsSync(sidecar.sidecarPath)) {
200
+ const stat = fs.lstatSync(sidecar.sidecarPath);
201
+ if (stat.isSymbolicLink()) {
202
+ fail('Refusing to initialize: .sidecar is a symbolic link. Remove it and run init again.');
203
+ }
204
+ if (!stat.isDirectory()) {
205
+ fail('Refusing to initialize: .sidecar exists but is not a directory.');
206
+ }
207
+ }
180
208
  if (fs.existsSync(sidecar.sidecarPath) && !opts.force) {
181
209
  fail('Sidecar is already initialized in this project. Re-run with --force to recreate .sidecar files.');
182
210
  }
@@ -187,6 +215,9 @@ program
187
215
  sidecar.agentsPath,
188
216
  sidecar.summaryPath,
189
217
  ];
218
+ const shouldWriteRootAgents = Boolean(opts.force) || !fs.existsSync(sidecar.rootAgentsPath);
219
+ if (shouldWriteRootAgents)
220
+ files.push(sidecar.rootAgentsPath);
190
221
  fs.mkdirSync(sidecar.sidecarPath, { recursive: true });
191
222
  if (opts.force) {
192
223
  for (const file of [
@@ -220,6 +251,9 @@ program
220
251
  output: { humanTime: true },
221
252
  }));
222
253
  fs.writeFileSync(sidecar.agentsPath, renderAgentsMarkdown(projectName));
254
+ if (shouldWriteRootAgents) {
255
+ fs.writeFileSync(sidecar.rootAgentsPath, renderAgentsMarkdown(projectName));
256
+ }
223
257
  const db2 = new Database(sidecar.dbPath);
224
258
  const refreshed = refreshSummaryFile(db2, rootPath, 1, 10);
225
259
  db2.close();
@@ -632,22 +666,9 @@ session
632
666
  const command = 'session verify';
633
667
  try {
634
668
  const { db, projectId } = requireInitialized();
635
- const summaryRecent = Boolean(db
636
- .prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'summary_generated' AND created_at >= datetime('now', '-3 day') LIMIT 1`)
637
- .get(projectId));
638
- const warnings = verifySessionHygiene(db, projectId, summaryRecent);
669
+ const warnings = verifySessionHygiene(db, projectId, summaryWasRefreshedRecently(db, projectId));
639
670
  db.close();
640
- if (opts.json)
641
- printJsonEnvelope(jsonSuccess(command, { warnings, healthy: warnings.length === 0 }));
642
- else {
643
- if (warnings.length === 0) {
644
- console.log('Session hygiene looks good.');
645
- return;
646
- }
647
- console.log('Session hygiene warnings:');
648
- for (const w of warnings)
649
- console.log(`- ${w}`);
650
- }
671
+ renderSessionHygiene(command, Boolean(opts.json), warnings);
651
672
  }
652
673
  catch (err) {
653
674
  handleCommandError(command, Boolean(opts.json), err);
@@ -662,22 +683,9 @@ program
662
683
  const command = 'doctor';
663
684
  try {
664
685
  const { db, projectId } = requireInitialized();
665
- const summaryRecent = Boolean(db
666
- .prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'summary_generated' AND created_at >= datetime('now', '-3 day') LIMIT 1`)
667
- .get(projectId));
668
- const warnings = verifySessionHygiene(db, projectId, summaryRecent);
686
+ const warnings = verifySessionHygiene(db, projectId, summaryWasRefreshedRecently(db, projectId));
669
687
  db.close();
670
- if (opts.json)
671
- printJsonEnvelope(jsonSuccess(command, { warnings, healthy: warnings.length === 0 }));
672
- else {
673
- if (warnings.length === 0) {
674
- console.log('Session hygiene looks good.');
675
- return;
676
- }
677
- console.log('Session hygiene warnings:');
678
- for (const w of warnings)
679
- console.log(`- ${w}`);
680
- }
688
+ renderSessionHygiene(command, Boolean(opts.json), warnings);
681
689
  }
682
690
  catch (err) {
683
691
  handleCommandError(command, Boolean(opts.json), err);
package/dist/lib/paths.js CHANGED
@@ -6,6 +6,7 @@ export function getSidecarPaths(rootPath) {
6
6
  return {
7
7
  rootPath,
8
8
  sidecarPath,
9
+ rootAgentsPath: path.join(rootPath, 'AGENTS.md'),
9
10
  dbPath: path.join(sidecarPath, 'sidecar.db'),
10
11
  configPath: path.join(sidecarPath, 'config.json'),
11
12
  preferencesPath: path.join(sidecarPath, 'preferences.json'),
@@ -16,8 +17,11 @@ export function getSidecarPaths(rootPath) {
16
17
  export function findSidecarRoot(startDir = process.cwd()) {
17
18
  let current = path.resolve(startDir);
18
19
  while (true) {
19
- if (fs.existsSync(path.join(current, SIDECAR_DIR))) {
20
- return current;
20
+ const candidate = path.join(current, SIDECAR_DIR);
21
+ if (fs.existsSync(candidate)) {
22
+ const stat = fs.lstatSync(candidate);
23
+ if (stat.isDirectory() && !stat.isSymbolicLink())
24
+ return current;
21
25
  }
22
26
  const parent = path.dirname(current);
23
27
  if (parent === current)
@@ -3,6 +3,16 @@ export function renderAgentsMarkdown(projectName) {
3
3
 
4
4
  Sidecar is the local project memory tool for this repository.
5
5
 
6
+ ## MUST before final response
7
+
8
+ If you changed code, you MUST run these commands in order:
9
+
10
+ 1. \`sidecar context --format markdown\`
11
+ 2. \`sidecar worklog record --done "<what changed>" --files <paths> --by agent\`
12
+ 3. if behavior/design/architecture changed: \`sidecar decision record --title "<decision>" --summary "<why>" --by agent\`
13
+ 4. if follow-up exists: \`sidecar task add "<follow-up>" --priority medium --by agent\`
14
+ 5. \`sidecar summary refresh\`
15
+
6
16
  ## Required workflow
7
17
 
8
18
  For any code change, run these in order before final response:
@@ -18,6 +28,7 @@ For any code change, run these in order before final response:
18
28
  - If files changed: always record a worklog.
19
29
  - If behavior/design choice changed: record a decision.
20
30
  - If follow-up work exists: add a task.
31
+ - Never skip summary refresh after recording work.
21
32
 
22
33
  ## Definition of Done
23
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecar-cli",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Local-first project memory and recording tool",
5
5
  "scripts": {
6
6
  "build": "npm run clean && tsc -p tsconfig.json",
@@ -14,7 +14,8 @@
14
14
  "release:stable": "node scripts/cut-release.mjs --channel stable",
15
15
  "release:beta": "node scripts/cut-release.mjs --channel beta",
16
16
  "release:rc": "node scripts/cut-release.mjs --channel rc",
17
- "sidecar:reminder": "node scripts/sidecar-reminder.mjs"
17
+ "sidecar:reminder": "node scripts/sidecar-reminder.mjs",
18
+ "install:hooks": "node scripts/install-git-hooks.mjs"
18
19
  },
19
20
  "keywords": [],
20
21
  "author": "",
@@ -1,31 +0,0 @@
1
- import { z } from 'zod';
2
- export const eventTypeSchema = z.enum([
3
- 'note',
4
- 'worklog',
5
- 'decision',
6
- 'task_update',
7
- 'summary',
8
- 'context',
9
- ]);
10
- export const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
11
- export const addEventSchema = z.object({
12
- type: eventTypeSchema,
13
- title: z.string().trim().min(1).max(140),
14
- body: z.string().trim().min(1),
15
- tags: z.array(z.string().trim().min(1)).default([]),
16
- });
17
- export const addTaskSchema = z.object({
18
- title: z.string().trim().min(1).max(240),
19
- priority: taskPrioritySchema.default('medium'),
20
- });
21
- export const completeTaskSchema = z.object({
22
- id: z.number().int().positive(),
23
- });
24
- export function parseTags(input) {
25
- if (!input)
26
- return [];
27
- return input
28
- .split(',')
29
- .map((t) => t.trim())
30
- .filter(Boolean);
31
- }