sidecar-cli 0.1.5-rc.1 → 0.1.6-beta.2

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.
@@ -1,12 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { getSidecarPaths } from '../lib/paths.js';
3
+ import { nowIso, stringifyJson } from '../lib/format.js';
4
4
  import { SidecarError } from '../lib/errors.js';
5
- import { stringifyJson } from '../lib/format.js';
5
+ import { getSidecarPaths } from '../lib/paths.js';
6
6
  import { taskPacketSchema } from './task-packet.js';
7
- function taskFilePath(tasksPath, taskId) {
8
- return path.join(tasksPath, `${taskId}.json`);
9
- }
7
+ const TASK_STATUS_FOLDERS = {
8
+ active: 'active',
9
+ blocked: 'blocked',
10
+ done: 'done',
11
+ };
10
12
  function parseTaskIdOrdinal(taskId) {
11
13
  const match = /^T-(\d+)$/.exec(taskId);
12
14
  return match ? Number.parseInt(match[1], 10) : 0;
@@ -19,36 +21,77 @@ export class TaskPacketRepository {
19
21
  get tasksPath() {
20
22
  return getSidecarPaths(this.rootPath).tasksPath;
21
23
  }
24
+ statusPath(status) {
25
+ return path.join(this.tasksPath, TASK_STATUS_FOLDERS[status]);
26
+ }
22
27
  ensureStorage() {
23
28
  fs.mkdirSync(this.tasksPath, { recursive: true });
29
+ fs.mkdirSync(this.statusPath('active'), { recursive: true });
30
+ fs.mkdirSync(this.statusPath('blocked'), { recursive: true });
31
+ fs.mkdirSync(this.statusPath('done'), { recursive: true });
24
32
  }
25
- generateNextTaskId() {
33
+ allTaskFiles() {
26
34
  this.ensureStorage();
27
- const files = fs.readdirSync(this.tasksPath, { withFileTypes: true });
28
- let max = 0;
29
- for (const file of files) {
30
- if (!file.isFile() || !file.name.endsWith('.json'))
35
+ const files = [];
36
+ const roots = [this.tasksPath, this.statusPath('active'), this.statusPath('blocked'), this.statusPath('done')];
37
+ for (const root of roots) {
38
+ if (!fs.existsSync(root))
31
39
  continue;
32
- const id = file.name.slice(0, -'.json'.length);
40
+ const entries = fs.readdirSync(root, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
43
+ continue;
44
+ files.push(path.join(root, entry.name));
45
+ }
46
+ }
47
+ return Array.from(new Set(files)).sort();
48
+ }
49
+ generateNextTaskId() {
50
+ const files = this.allTaskFiles();
51
+ let max = 0;
52
+ for (const filePath of files) {
53
+ const name = path.basename(filePath);
54
+ const id = name.slice(0, -'.json'.length);
33
55
  max = Math.max(max, parseTaskIdOrdinal(id));
34
56
  }
35
57
  return `T-${String(max + 1).padStart(3, '0')}`;
36
58
  }
59
+ findTaskFile(taskId) {
60
+ const candidateName = `${taskId}.json`;
61
+ for (const filePath of this.allTaskFiles()) {
62
+ if (path.basename(filePath) === candidateName)
63
+ return filePath;
64
+ }
65
+ return null;
66
+ }
37
67
  save(packet) {
38
68
  this.ensureStorage();
39
69
  const validated = taskPacketSchema.parse(packet);
40
- const filePath = taskFilePath(this.tasksPath, validated.task_id);
41
- fs.writeFileSync(filePath, `${stringifyJson(validated)}\n`, 'utf8');
42
- return filePath;
70
+ const now = nowIso();
71
+ const withUpdatedAt = taskPacketSchema.parse({
72
+ ...validated,
73
+ created_at: validated.created_at || now,
74
+ updated_at: now,
75
+ });
76
+ const destination = path.join(this.statusPath(withUpdatedAt.status), `${withUpdatedAt.task_id}.json`);
77
+ const existing = this.findTaskFile(withUpdatedAt.task_id);
78
+ if (existing && existing !== destination && fs.existsSync(existing)) {
79
+ fs.unlinkSync(existing);
80
+ }
81
+ fs.writeFileSync(destination, `${stringifyJson(withUpdatedAt)}\n`, 'utf8');
82
+ return destination;
43
83
  }
44
84
  get(taskId) {
45
- const filePath = taskFilePath(this.tasksPath, taskId);
46
- if (!fs.existsSync(filePath)) {
85
+ const filePath = this.findTaskFile(taskId);
86
+ if (!filePath)
47
87
  throw new SidecarError(`Task not found: ${taskId}`);
48
- }
49
88
  try {
50
89
  const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
51
- return taskPacketSchema.parse(raw);
90
+ const parsed = taskPacketSchema.parse(raw);
91
+ if (parsed.status === 'active' && filePath.startsWith(this.statusPath('blocked'))) {
92
+ parsed.status = 'blocked';
93
+ }
94
+ return parsed;
52
95
  }
53
96
  catch (err) {
54
97
  const message = err instanceof Error ? err.message : String(err);
@@ -56,17 +99,12 @@ export class TaskPacketRepository {
56
99
  }
57
100
  }
58
101
  list() {
59
- this.ensureStorage();
60
- const files = fs
61
- .readdirSync(this.tasksPath, { withFileTypes: true })
62
- .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
63
- .map((entry) => path.join(this.tasksPath, entry.name))
64
- .sort();
65
102
  const packets = [];
66
- for (const filePath of files) {
103
+ for (const filePath of this.allTaskFiles()) {
67
104
  try {
68
105
  const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
69
- packets.push(taskPacketSchema.parse(raw));
106
+ const parsed = taskPacketSchema.parse(raw);
107
+ packets.push(parsed);
70
108
  }
71
109
  catch (err) {
72
110
  const message = err instanceof Error ? err.message : String(err);
@@ -1,51 +1,21 @@
1
1
  import { TaskPacketRepository } from './task-repository.js';
2
2
  import { createTaskPacket, } from './task-packet.js';
3
- import { normalizeValidationStep } from '../runs/capture.js';
4
3
  export function createTaskPacketRecord(rootPath, input) {
5
4
  const repo = new TaskPacketRepository(rootPath);
6
5
  const taskId = repo.generateNextTaskId();
7
6
  const packetInput = {
8
7
  title: input.title,
9
8
  summary: input.summary,
10
- goal: input.goal,
11
- type: input.type,
12
- status: input.status,
13
9
  priority: input.priority,
14
- scope: {
15
- in_scope: input.scope_in_scope ?? [],
16
- out_of_scope: input.scope_out_of_scope ?? [],
17
- },
18
- context: {
19
- related_decisions: input.related_decisions ?? [],
20
- related_notes: input.related_notes ?? [],
21
- },
22
- implementation: {
23
- files_to_read: input.files_to_read ?? [],
24
- files_to_avoid: input.files_to_avoid ?? [],
25
- },
26
- constraints: {
27
- technical: input.technical_constraints ?? [],
28
- design: input.design_constraints ?? [],
29
- },
30
- execution: {
31
- commands: {
32
- validation: (input.validation_commands ?? [])
33
- .map((v) => normalizeValidationStep(v))
34
- .filter((v) => v !== null),
35
- },
36
- },
37
- dependencies: input.dependencies ?? [],
38
- tags: input.tags ?? [],
39
- target_areas: input.target_areas ?? [],
40
- definition_of_done: input.definition_of_done ?? [],
41
- tracking: {
42
- branch: input.branch ?? '',
43
- worktree: input.worktree ?? '',
44
- assigned_agent_role: null,
45
- assigned_runner: null,
46
- assignment_reason: '',
47
- assigned_at: null,
10
+ status: input.status,
11
+ trigger: {
12
+ condition: input.trigger_condition,
13
+ ...(input.trigger_check_command ? { check_command: input.trigger_check_command } : {}),
14
+ depends_on: (input.trigger_depends_on ?? []).map((v) => v.toUpperCase()),
48
15
  },
16
+ entry_points: input.entry_points,
17
+ done_condition: input.done_condition,
18
+ validation_command: input.validation_command,
49
19
  };
50
20
  const packet = createTaskPacket(taskId, packetInput);
51
21
  const filePath = repo.save(packet);
@@ -54,13 +24,9 @@ export function createTaskPacketRecord(rootPath, input) {
54
24
  export function listTaskPackets(rootPath) {
55
25
  const repo = new TaskPacketRepository(rootPath);
56
26
  const order = {
57
- draft: 0,
58
- ready: 1,
59
- queued: 2,
60
- running: 3,
61
- review: 4,
62
- blocked: 5,
63
- done: 6,
27
+ active: 0,
28
+ blocked: 1,
29
+ done: 2,
64
30
  };
65
31
  return repo
66
32
  .list()
@@ -69,7 +35,7 @@ export function listTaskPackets(rootPath) {
69
35
  const byStatus = order[a.status] - order[b.status];
70
36
  if (byStatus !== 0)
71
37
  return byStatus;
72
- return a.task_id.localeCompare(b.task_id, undefined, { numeric: true });
38
+ return a.created_at.localeCompare(b.created_at) || a.task_id.localeCompare(b.task_id, undefined, { numeric: true });
73
39
  });
74
40
  }
75
41
  export function getTaskPacket(rootPath, taskId) {
@@ -1,68 +1,134 @@
1
1
  function renderSharedGuide(projectName, heading) {
2
- return `# Sidecar Agent Guide
2
+ return `# Sidecar
3
3
 
4
4
  ${heading}
5
5
 
6
- Sidecar is the local project memory tool for this repository.
6
+ Sidecar is this repo's decision log and worklog. Git already records
7
+ what changed; Sidecar exists to record why — and only when "why" is
8
+ non-obvious from the code or the diff.
7
9
 
8
- ## MUST before final response
10
+ If invoked without a specific instruction ("do the next thing", loop mode,
11
+ or autonomous mode), go straight to **Picking up the next task** below and
12
+ follow those rules exactly.
9
13
 
10
- If you changed code, you MUST run these commands in order:
14
+ ## Read before you write
11
15
 
12
- 1. \`sidecar context --format markdown\`
13
- 2. \`sidecar worklog record --done "<what changed>" --files <paths> --by agent\`
14
- 3. if behavior/design/architecture changed: \`sidecar decision record --title "<decision>" --summary "<why>" --by agent\`
15
- 4. if follow-up exists: \`sidecar task add "<follow-up>" --priority medium --by agent\`
16
- 5. \`sidecar summary refresh\`
16
+ Before any non-trivial change — infra, schema, moderation rules, queue
17
+ config, routing, anything that touches a past architectural choice
18
+ check the decisions log:
19
+
20
+ \`sidecar context --format markdown\`
21
+
22
+ If a recent decision constrains your change, follow it or surface it to
23
+ the user before overriding it. Do not silently re-litigate prior choices.
24
+
25
+ If your change reverses a prior decision, record a new decision that
26
+ references the old one. Do not edit history.
27
+
28
+ ## Record a decision when…
29
+
30
+ A decision is worth recording when a future agent reading the same code
31
+ would plausibly make a different call without this context.
32
+
33
+ Record:
34
+ - Trade-offs with a clear loser ("X over Y because Z")
35
+ - Deliberate non-actions ("did NOT add apex→www at the LB because…")
36
+ - Choices that look wrong at first glance
37
+ - Constraints imposed by external systems (cert SANs, RDS VPC, etc.)
38
+
39
+ Don't record:
40
+ - Style or naming
41
+ - Anything obvious from reading the touched file
42
+ - Restatement of the diff
43
+ - "Added X" — that's a worklog, not a decision
44
+
45
+ ## Record a worklog when…
46
+
47
+ A worklog is worth recording for multi-step or cross-cutting work where
48
+ the shape of the change isn't obvious from any single commit.
49
+
50
+ One-line fixes, typo corrections, and dependency bumps usually don't need
51
+ a worklog in general. But for this repo policy: if files changed, record
52
+ a worklog.
53
+
54
+ ## Tasks
17
55
 
18
- ## Required workflow
56
+ Create a task only when all of these are explicit:
57
+ - Trigger
58
+ - Entry points (1-3 files)
59
+ - Done condition
60
+ - Validation command
19
61
 
20
- For any code change, run these in order before final response:
62
+ Otherwise: surface the follow-up inline and let it die, or record it as a
63
+ decision if the call is "we're not doing this."
21
64
 
65
+ On task completion: write the worklog and decision entries the task
66
+ produced, then move the file to \`tasks/done/\`. No manual archival step.
67
+
68
+ ## Picking up the next task
69
+
70
+ When an agent is invoked without a specific instruction ("do the next
71
+ thing", "/loop", autonomous mode), follow this precedence exactly:
72
+
73
+ 1. List \`tasks/active/\`.
74
+ 2. For each task, verify the trigger is satisfied against current
75
+ state (run the query, check the dependency, read the threshold). Do
76
+ not assume — a trigger written months ago may have already fired or
77
+ may have been invalidated.
78
+ 3. Filter to tasks whose trigger is satisfied right now.
79
+ 4. From that filtered set, pick the highest \`priority\`. Ties broken by
80
+ oldest \`created_at\` (FIFO — prevents agents from re-prioritizing the
81
+ queue each run).
82
+ 5. Execute that one task. Do not batch multiple tasks per invocation
83
+ unless explicitly told to.
84
+
85
+ ## When nothing is ready, ask — do not invent work
86
+
87
+ If step 3 above returns an empty set:
88
+
89
+ - Ask the user. Surface the active task list with each task's
90
+ trigger and why it's not satisfied. Let the user pick, override a
91
+ trigger, or send you elsewhere.
92
+ - Do not pick the "closest" task and start anyway.
93
+ - Do not pick a task whose trigger is almost satisfied and satisfy it
94
+ yourself as a side quest.
95
+ - Do not create a new task on the spot to give yourself something to
96
+ do. Task creation requires the same trigger + entry points + done
97
+ condition + validation command bar as everything else; you cannot
98
+ bypass it by self-assigning.
99
+ - Do not start unsolicited refactoring, "cleanup", doc edits, or
100
+ speculative work to fill the gap.
101
+
102
+ If the user is unreachable (true autonomous mode, no human in loop):
103
+ exit cleanly with a status message naming the empty queue. Idle is a
104
+ valid outcome. Inventing work is not.
105
+
106
+ If a trigger requires information you can verify cheaply (a SQL count,
107
+ a \`git log\` check, a file existence test), verify it yourself. If it
108
+ requires information only the user has (a stakeholder commitment, a
109
+ business threshold), ask — don't guess.
110
+
111
+ ## Repo-required sequence for code changes
112
+
113
+ If you changed code, run:
22
114
  1. \`sidecar context --format markdown\`
23
- 2. implement changes
24
- 3. \`sidecar worklog record ...\`
25
- 4. if behavior/design/architecture changed: \`sidecar decision record ...\`
115
+ 2. \`sidecar worklog record --done "<what changed>" --files <paths> --by agent\`
116
+ 3. if behavior/design/architecture changed:
117
+ \`sidecar decision record --title "<decision>" --summary "<why + rejected alternative>" --by agent\`
118
+ 4. if follow-up exists and it meets task criteria:
119
+ \`sidecar task create --title "<follow-up>" --summary "<summary>" --trigger "<trigger>" --entry-points <path1,path2> --done-condition "<done>" --validate-cmd "<cmd>" --priority medium\`
26
120
  5. \`sidecar summary refresh\`
27
121
 
28
- ## Scope rules
29
-
30
- - If files changed: always record a worklog.
31
- - If behavior/design choice changed: record a decision.
32
- - If follow-up work exists: add a task.
33
- - Never skip summary refresh after recording work.
34
-
35
- ## Definition of Done
36
-
37
- - [ ] Context reviewed
38
- - \`sidecar context --format markdown\`
39
- - [ ] Work recorded
40
- - \`sidecar worklog record --done "<what changed>" --files <paths> --by agent\`
41
- - [ ] Decision recorded when needed
42
- - \`sidecar decision record --title "<decision>" --summary "<why>" --by agent\`
43
- - [ ] Follow-up task created when needed
44
- - \`sidecar task add "<follow-up>" --priority medium --by agent\`
45
- - [ ] Summary refreshed
46
- - \`sidecar summary refresh\`
47
-
48
- ## Command patterns
49
-
50
- - Context: \`sidecar context --format markdown\`
51
- - Worklog: \`sidecar worklog record --done "<what changed>" --files src/a.ts,src/b.ts --by agent\`
52
- - Decision: \`sidecar decision record --title "<title>" --summary "<summary>" --by agent\`
53
- - Task: \`sidecar task add "<follow-up>" --priority medium --by agent\`
54
- - Summary: \`sidecar summary refresh\`
55
-
56
- ## Example: small feature build
57
-
58
- \`\`\`bash
59
- sidecar context --format markdown
60
- # implement small todo app feature in src/app.ts and src/todo.ts
61
- sidecar worklog record --goal "todo feature" --done "Added todo CRUD handlers and wired routes" --files src/app.ts,src/todo.ts --by agent
62
- sidecar decision record --title "Use in-memory store for v1" --summary "Keeps implementation simple for initial feature" --by agent
63
- sidecar task add "Persist todos to sqlite" --priority medium --by agent
64
- sidecar summary refresh
65
- \`\`\`
122
+ Run \`sidecar summary refresh\` only after you actually recorded something.
123
+
124
+ ## Command reference
125
+
126
+ \`sidecar context --format markdown\`
127
+ \`sidecar worklog record --done "<what changed>" --files <paths> --by agent\`
128
+ \`sidecar decision record --title "<title>" --summary "<why + rejected alternative>" --by agent\`
129
+ \`sidecar task create --title "<follow-up>" --summary "<summary>" --trigger "<trigger>" --entry-points <paths> --done-condition "<done>" --validate-cmd "<cmd>" --priority medium\`
130
+ \`sidecar task set-status <task-id> --to active|blocked|done --reason "<why>" --by agent\`
131
+ \`sidecar summary refresh\`
66
132
 
67
133
  ## Optional hygiene reminder
68
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecar-cli",
3
- "version": "0.1.5-rc.1",
3
+ "version": "0.1.6-beta.2",
4
4
  "description": "Local-first project memory and recording tool",
5
5
  "scripts": {
6
6
  "build": "npm run clean && tsc -p tsconfig.json",