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.
- package/README.md +16 -42
- package/dist/cli.js +115 -65
- package/dist/prompts/packet-sections.js +41 -103
- package/dist/prompts/prompt-compiler.js +4 -4
- package/dist/prompts/prompt-service.js +2 -2
- package/dist/services/capabilities-service.js +18 -22
- package/dist/services/run-orchestrator-service.js +8 -13
- package/dist/services/run-review-service.js +14 -15
- package/dist/services/task-orchestration-service.js +43 -56
- package/dist/services/task-status-service.js +29 -0
- package/dist/tasks/task-packet.js +150 -119
- package/dist/tasks/task-repository.js +64 -26
- package/dist/tasks/task-service.js +12 -46
- package/dist/templates/agents.js +118 -52
- package/package.json +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { nowIso, stringifyJson } from '../lib/format.js';
|
|
4
4
|
import { SidecarError } from '../lib/errors.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getSidecarPaths } from '../lib/paths.js';
|
|
6
6
|
import { taskPacketSchema } from './task-packet.js';
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
33
|
+
allTaskFiles() {
|
|
26
34
|
this.ensureStorage();
|
|
27
|
-
const files =
|
|
28
|
-
|
|
29
|
-
for (const
|
|
30
|
-
if (!
|
|
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
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
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 =
|
|
46
|
-
if (!
|
|
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
|
-
|
|
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
|
|
103
|
+
for (const filePath of this.allTaskFiles()) {
|
|
67
104
|
try {
|
|
68
105
|
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
69
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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) {
|
package/dist/templates/agents.js
CHANGED
|
@@ -1,68 +1,134 @@
|
|
|
1
1
|
function renderSharedGuide(projectName, heading) {
|
|
2
|
-
return `# Sidecar
|
|
2
|
+
return `# Sidecar
|
|
3
3
|
|
|
4
4
|
${heading}
|
|
5
5
|
|
|
6
|
-
Sidecar is
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
## Read before you write
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
24
|
-
3.
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|