claude-comms 1.0.45 → 1.0.46
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/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
- package/.agents/tools/agent-job/agent_job.py +71 -1
- package/.agents/tools/workflow/CONSUMER_SETUP.md +114 -0
- package/.agents/tools/workflow/ONBOARD.md +167 -0
- package/.agents/tools/workflow/README.md +169 -0
- package/.agents/tools/workflow/cli.ts +726 -0
- package/.agents/tools/workflow/config.example.json +16 -0
- package/.agents/tools/workflow/init.ts +76 -0
- package/.agents/tools/workflow/lib/file-tracker.test.ts +81 -0
- package/.agents/tools/workflow/lib/file-tracker.ts +387 -0
- package/.agents/tools/workflow/lib/harness-executor.ts +649 -0
- package/.agents/tools/workflow/lib/prompts.scenario.test.ts +262 -0
- package/.agents/tools/workflow/lib/prompts.ts +548 -0
- package/.agents/tools/workflow/lib/streams.test.ts +140 -0
- package/.agents/tools/workflow/lib/streams.ts +254 -0
- package/.agents/tools/workflow/runner.ts +994 -0
- package/.agents/tools/workflow/templates/document.md +42 -0
- package/.agents/tools/workflow/templates/implement.md +117 -0
- package/.agents/tools/workflow/templates/plan.md +54 -0
- package/.agents/tools/workflow/templates/pm.md +84 -0
- package/.agents/tools/workflow/templates/product-owner.md +211 -0
- package/.agents/tools/workflow/templates/review.md +56 -0
- package/.agents/tools/workflow/templates/uat.md +71 -0
- package/.claude/agents/core/engineer.md +2 -13
- package/.claude/commands/action-feedback-item.md +14 -9
- package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
- package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
- package/.claude/settings.local.json +46 -0
- package/CLAUDE.md +1 -1
- package/bin/claude-setup.js +3 -4
- package/package.json +1 -1
- package/src/orchestrator/installer.js +5 -63
- package/.claude/agents/core/architect.md +0 -316
- package/.claude/agents/core/consultants/codex.md +0 -39
- package/.claude/agents/core/consultants/gemini.md +0 -39
- package/.claude/agents/core/designer.md +0 -181
- package/.claude/agents/core/planner.md +0 -251
- package/.claude/agents/core/uat.md +0 -147
- package/.claude/agents/util/feedback-executor.md +0 -29
- package/.claude/agents/util/meta-agent-builder.md +0 -309
- package/.claude/agents/util/session-review.md +0 -228
- package/.claude/commands/analyse-document.md +0 -10
- package/.claude/commands/await-feedback-orchestrator.md +0 -12
- package/.claude/commands/await-feedback.md +0 -21
- package/.claude/commands/cook.md +0 -119
- package/.claude/commands/feedback.md +0 -140
- package/.claude/commands/hello-world.md +0 -1
- package/.claude/commands/uat-refine.md +0 -121
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Workflow Engine CLI
|
|
4
|
+
*
|
|
5
|
+
* Interface to the Convex backend for managing assignments and job groups.
|
|
6
|
+
* Primary consumers: PM agent, PO agent
|
|
7
|
+
*
|
|
8
|
+
* Environment Variables (auto-set by runner):
|
|
9
|
+
* WORKFLOW_ASSIGNMENT_ID Current assignment (used as default for assignment commands)
|
|
10
|
+
* WORKFLOW_GROUP_ID Current group (used as default --after for insert-job)
|
|
11
|
+
* WORKFLOW_JOB_ID Current job
|
|
12
|
+
* WORKFLOW_THREAD_ID Current chat thread (used for auto-linking assignments)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* npx tsx cli.ts <command> [args]
|
|
16
|
+
*
|
|
17
|
+
* Commands:
|
|
18
|
+
* assignments [--status <status>] List assignments
|
|
19
|
+
* assignment <id> Get assignment details with groups
|
|
20
|
+
* groups [--status <status>] List job groups
|
|
21
|
+
* group <id> Get group details with jobs
|
|
22
|
+
* jobs [--status <status>] List jobs
|
|
23
|
+
* job <id> Get job details
|
|
24
|
+
* queue Show queue status
|
|
25
|
+
*
|
|
26
|
+
* create <northStar> [--priority N] [--independent] [--thread <threadId>] Create assignment
|
|
27
|
+
* insert-job [assignmentId] [--type <type>] [--jobs <json>] [--harness <harness>] [--context <ctx>] [--after <groupId>] [--append]
|
|
28
|
+
* assignmentId defaults to WORKFLOW_ASSIGNMENT_ID
|
|
29
|
+
* --jobs: JSON array of job definitions: [{"jobType":"review"},{"jobType":"implement","harness":"codex"}]
|
|
30
|
+
* Jobs in the same group run in parallel and share a groupId
|
|
31
|
+
* --type: single job type (shorthand for --jobs with one entry)
|
|
32
|
+
* --after defaults to WORKFLOW_GROUP_ID (auto-links in job context)
|
|
33
|
+
* --append finds tail group and links there (for PO adding to existing chain)
|
|
34
|
+
* update-assignment [id] [--artifacts <str>] [--decisions <str>] [--alignment <aligned|uncertain|misaligned>]
|
|
35
|
+
* complete [assignmentId] Mark assignment complete
|
|
36
|
+
* block [assignmentId] --reason <str> Block assignment
|
|
37
|
+
* unblock [assignmentId] Unblock assignment
|
|
38
|
+
* delete-assignment <id> Delete assignment and all its groups/jobs
|
|
39
|
+
*
|
|
40
|
+
* start-job <jobId> Mark job as running
|
|
41
|
+
* complete-job <jobId> --result <str> Mark job as complete
|
|
42
|
+
* fail-job <jobId> [--result <str>] Mark job as failed
|
|
43
|
+
*
|
|
44
|
+
* Chat Commands:
|
|
45
|
+
* chat-threads List chat threads
|
|
46
|
+
* chat-thread <threadId> Get thread with messages
|
|
47
|
+
* chat-create [--title <title>] Create a new chat thread
|
|
48
|
+
* chat-send <threadId> <message> Send message and create chat job
|
|
49
|
+
* chat-mode <threadId> <jam|cook|guardian> Change thread mode
|
|
50
|
+
* chat-title <threadId> <title> Update thread title
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
54
|
+
import { api } from "./workflow-engine/convex/_generated/api.js";
|
|
55
|
+
import { readFileSync } from "fs";
|
|
56
|
+
import { dirname, join } from "path";
|
|
57
|
+
import { fileURLToPath } from "url";
|
|
58
|
+
|
|
59
|
+
type Id<T extends string> = string & { __tableName: T };
|
|
60
|
+
|
|
61
|
+
// Load config
|
|
62
|
+
type Harness = "claude" | "codex" | "gemini";
|
|
63
|
+
|
|
64
|
+
interface Config {
|
|
65
|
+
convexUrl: string;
|
|
66
|
+
namespace: string;
|
|
67
|
+
timeoutMs: number;
|
|
68
|
+
harnessDefaults: {
|
|
69
|
+
default: Harness;
|
|
70
|
+
[jobType: string]: Harness;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Auto-expansion config: job types that automatically fan out to multiple harnesses
|
|
75
|
+
// CLI expands these before sending to Convex (not in Convex mutations)
|
|
76
|
+
const AUTO_EXPAND_CONFIG: Record<string, Harness[]> = {
|
|
77
|
+
review: ["claude", "codex", "gemini"],
|
|
78
|
+
"architecture-review": ["claude", "codex", "gemini"],
|
|
79
|
+
"spec-review": ["claude", "codex", "gemini"],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the harness for a job type from config
|
|
84
|
+
* Resolution order: config.harnessDefaults[jobType] -> config.harnessDefaults.default
|
|
85
|
+
*/
|
|
86
|
+
function getHarnessForJobType(jobType: string): Harness {
|
|
87
|
+
return config.harnessDefaults[jobType] || config.harnessDefaults.default;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
91
|
+
const configPath = join(__dirname, "config.json");
|
|
92
|
+
const config: Config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
93
|
+
|
|
94
|
+
const client = new ConvexHttpClient(config.convexUrl);
|
|
95
|
+
|
|
96
|
+
// Cached namespace ID (resolved at runtime)
|
|
97
|
+
let namespaceId: Id<"namespaces"> | null = null;
|
|
98
|
+
|
|
99
|
+
async function getNamespaceId(): Promise<Id<"namespaces">> {
|
|
100
|
+
if (namespaceId) return namespaceId;
|
|
101
|
+
|
|
102
|
+
const namespace = await client.query(api.namespaces.getByName, {
|
|
103
|
+
name: config.namespace,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!namespace) {
|
|
107
|
+
error(`Namespace "${config.namespace}" not found. Run 'npx tsx init.ts' first.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
namespaceId = namespace._id as Id<"namespaces">;
|
|
111
|
+
return namespaceId;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Argument parsing helpers
|
|
115
|
+
function parseArgs(args: string[]): { flags: Record<string, string>; positional: string[] } {
|
|
116
|
+
const flags: Record<string, string> = {};
|
|
117
|
+
const positional: string[] = [];
|
|
118
|
+
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < args.length) {
|
|
121
|
+
if (args[i].startsWith("--")) {
|
|
122
|
+
const key = args[i].slice(2);
|
|
123
|
+
const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : "true";
|
|
124
|
+
flags[key] = value;
|
|
125
|
+
i += value === "true" ? 1 : 2;
|
|
126
|
+
} else {
|
|
127
|
+
positional.push(args[i]);
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { flags, positional };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function output(data: unknown) {
|
|
136
|
+
console.log(JSON.stringify(data, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function error(message: string): never {
|
|
140
|
+
console.error(JSON.stringify({ error: message }));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Commands
|
|
145
|
+
async function listAssignments(status?: string) {
|
|
146
|
+
const validStatuses = ["pending", "active", "blocked", "complete"];
|
|
147
|
+
if (status && !validStatuses.includes(status)) {
|
|
148
|
+
error(`Invalid status. Must be one of: ${validStatuses.join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const nsId = await getNamespaceId();
|
|
152
|
+
const result = await client.query(api.assignments.list, {
|
|
153
|
+
namespaceId: nsId,
|
|
154
|
+
status: status as any,
|
|
155
|
+
});
|
|
156
|
+
output(result);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function getAssignment(id: string) {
|
|
160
|
+
const result = await client.query(api.assignments.getWithGroups, {
|
|
161
|
+
id: id as Id<"assignments">,
|
|
162
|
+
});
|
|
163
|
+
if (!result) error("Assignment not found");
|
|
164
|
+
output(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function listGroups(status?: string, assignmentId?: string) {
|
|
168
|
+
const validStatuses = ["pending", "running", "complete", "failed"];
|
|
169
|
+
if (status && !validStatuses.includes(status)) {
|
|
170
|
+
error(`Invalid status. Must be one of: ${validStatuses.join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await client.query(api.jobs.listGroups, {
|
|
174
|
+
status: status as any,
|
|
175
|
+
assignmentId: assignmentId as Id<"assignments"> | undefined,
|
|
176
|
+
});
|
|
177
|
+
output(result);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function getGroup(id: string) {
|
|
181
|
+
const result = await client.query(api.jobs.getGroupWithJobs, {
|
|
182
|
+
id: id as Id<"jobGroups">,
|
|
183
|
+
});
|
|
184
|
+
if (!result) error("Group not found");
|
|
185
|
+
output(result);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function listJobs(status?: string, groupId?: string) {
|
|
189
|
+
const validStatuses = ["pending", "running", "complete", "failed"];
|
|
190
|
+
if (status && !validStatuses.includes(status)) {
|
|
191
|
+
error(`Invalid status. Must be one of: ${validStatuses.join(", ")}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await client.query(api.jobs.list, {
|
|
195
|
+
status: status as any,
|
|
196
|
+
groupId: groupId as Id<"jobGroups"> | undefined,
|
|
197
|
+
});
|
|
198
|
+
output(result);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function getJob(id: string) {
|
|
202
|
+
const result = await client.query(api.jobs.getWithGroup, {
|
|
203
|
+
id: id as Id<"jobs">,
|
|
204
|
+
});
|
|
205
|
+
if (!result) error("Job not found");
|
|
206
|
+
output(result);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function getQueueStatus() {
|
|
210
|
+
const nsId = await getNamespaceId();
|
|
211
|
+
const result = await client.query(api.scheduler.getQueueStatus, {
|
|
212
|
+
namespaceId: nsId,
|
|
213
|
+
});
|
|
214
|
+
output(result);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function createAssignment(
|
|
218
|
+
northStar: string,
|
|
219
|
+
priority?: number,
|
|
220
|
+
independent?: boolean,
|
|
221
|
+
threadId?: string
|
|
222
|
+
) {
|
|
223
|
+
const nsId = await getNamespaceId();
|
|
224
|
+
const id = await client.mutation(api.assignments.create, {
|
|
225
|
+
namespaceId: nsId,
|
|
226
|
+
northStar,
|
|
227
|
+
priority,
|
|
228
|
+
independent,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Link to thread if provided, or use env var (set by runner for chat jobs)
|
|
232
|
+
const effectiveThreadId = threadId || process.env.WORKFLOW_THREAD_ID;
|
|
233
|
+
if (effectiveThreadId) {
|
|
234
|
+
await client.mutation(api.chatThreads.linkAssignment, {
|
|
235
|
+
id: effectiveThreadId as Id<"chatThreads">,
|
|
236
|
+
assignmentId: id as Id<"assignments">,
|
|
237
|
+
});
|
|
238
|
+
const source = threadId ? "explicit" : "env";
|
|
239
|
+
output({ id, threadId: effectiveThreadId, message: `Assignment created and linked to thread (${source})` });
|
|
240
|
+
} else {
|
|
241
|
+
output({ id, message: "Assignment created" });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Find the tail group of an assignment (for --append)
|
|
246
|
+
async function findTailGroup(assignmentId: string): Promise<string | null> {
|
|
247
|
+
const assignment = await client.query(api.assignments.get, {
|
|
248
|
+
id: assignmentId as Id<"assignments">,
|
|
249
|
+
});
|
|
250
|
+
if (!assignment || !assignment.headGroupId) return null;
|
|
251
|
+
|
|
252
|
+
// Walk the chain to find the tail
|
|
253
|
+
let currentGroupId: string | undefined = assignment.headGroupId;
|
|
254
|
+
let tailGroupId: string = assignment.headGroupId;
|
|
255
|
+
|
|
256
|
+
while (currentGroupId) {
|
|
257
|
+
const group = await client.query(api.jobs.getGroup, {
|
|
258
|
+
id: currentGroupId as Id<"jobGroups">,
|
|
259
|
+
});
|
|
260
|
+
if (!group) break;
|
|
261
|
+
tailGroupId = group._id;
|
|
262
|
+
currentGroupId = group.nextGroupId;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return tailGroupId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Job definition for CLI input (before expansion)
|
|
269
|
+
interface JobDefInput {
|
|
270
|
+
jobType: string;
|
|
271
|
+
harness?: "claude" | "codex" | "gemini"; // Optional - will use default or expand
|
|
272
|
+
context?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Job definition after expansion (ready for mutation)
|
|
276
|
+
interface JobDef {
|
|
277
|
+
jobType: string;
|
|
278
|
+
harness: "claude" | "codex" | "gemini";
|
|
279
|
+
context?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Expand jobs using AUTO_EXPAND_CONFIG
|
|
284
|
+
* e.g., { jobType: "review" } -> 3 jobs with different harnesses
|
|
285
|
+
*/
|
|
286
|
+
function expandJobs(jobs: JobDefInput[]): JobDef[] {
|
|
287
|
+
const expanded: JobDef[] = [];
|
|
288
|
+
|
|
289
|
+
for (const job of jobs) {
|
|
290
|
+
const expandHarnesses = AUTO_EXPAND_CONFIG[job.jobType];
|
|
291
|
+
|
|
292
|
+
if (expandHarnesses && !job.harness) {
|
|
293
|
+
// Auto-expand to multiple harnesses
|
|
294
|
+
for (const harness of expandHarnesses) {
|
|
295
|
+
expanded.push({
|
|
296
|
+
jobType: job.jobType,
|
|
297
|
+
harness,
|
|
298
|
+
context: job.context,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
// Single job - use specified harness or default
|
|
303
|
+
expanded.push({
|
|
304
|
+
jobType: job.jobType,
|
|
305
|
+
harness: job.harness || getHarnessForJobType(job.jobType),
|
|
306
|
+
context: job.context,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return expanded;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function insertJobs(
|
|
315
|
+
assignmentId: string,
|
|
316
|
+
jobs: JobDefInput[],
|
|
317
|
+
afterGroupId?: string,
|
|
318
|
+
append?: boolean
|
|
319
|
+
) {
|
|
320
|
+
if (jobs.length === 0) {
|
|
321
|
+
error("At least one job required");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Expand jobs (auto-expansion happens here, not in Convex)
|
|
325
|
+
const expandedJobs = expandJobs(jobs);
|
|
326
|
+
|
|
327
|
+
const validHarnesses = ["claude", "codex", "gemini"];
|
|
328
|
+
for (const job of expandedJobs) {
|
|
329
|
+
if (!validHarnesses.includes(job.harness)) {
|
|
330
|
+
error(`Invalid harness "${job.harness}". Must be one of: ${validHarnesses.join(", ")}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Determine effective afterGroupId
|
|
335
|
+
let effectiveAfterGroupId = afterGroupId;
|
|
336
|
+
|
|
337
|
+
if (append && !effectiveAfterGroupId) {
|
|
338
|
+
// --append: find tail group and insert after it
|
|
339
|
+
effectiveAfterGroupId = await findTailGroup(assignmentId) || undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let result: { groupId: string; jobIds: string[] };
|
|
343
|
+
let linkInfo: string;
|
|
344
|
+
|
|
345
|
+
if (effectiveAfterGroupId) {
|
|
346
|
+
result = await client.mutation(api.jobs.insertGroupAfter, {
|
|
347
|
+
afterGroupId: effectiveAfterGroupId as Id<"jobGroups">,
|
|
348
|
+
jobs: expandedJobs as any,
|
|
349
|
+
});
|
|
350
|
+
linkInfo = `linked after group ${effectiveAfterGroupId.slice(-8)}`;
|
|
351
|
+
} else {
|
|
352
|
+
result = await client.mutation(api.jobs.createGroup, {
|
|
353
|
+
assignmentId: assignmentId as Id<"assignments">,
|
|
354
|
+
jobs: expandedJobs as any,
|
|
355
|
+
});
|
|
356
|
+
linkInfo = "created as head group";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
output({
|
|
360
|
+
groupId: result.groupId,
|
|
361
|
+
jobIds: result.jobIds,
|
|
362
|
+
jobs: expandedJobs.map(j => ({ jobType: j.jobType, harness: j.harness })),
|
|
363
|
+
message: `Group ${linkInfo} with ${result.jobIds.length} job(s)`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function updateAssignment(
|
|
368
|
+
id: string,
|
|
369
|
+
artifacts?: string,
|
|
370
|
+
decisions?: string,
|
|
371
|
+
alignment?: string
|
|
372
|
+
) {
|
|
373
|
+
const validAlignments = ["aligned", "uncertain", "misaligned"];
|
|
374
|
+
if (alignment && !validAlignments.includes(alignment)) {
|
|
375
|
+
error(`Invalid alignment status. Must be one of: ${validAlignments.join(", ")}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await client.mutation(api.assignments.update, {
|
|
379
|
+
id: id as Id<"assignments">,
|
|
380
|
+
artifacts,
|
|
381
|
+
decisions,
|
|
382
|
+
alignmentStatus: alignment as "aligned" | "uncertain" | "misaligned" | undefined,
|
|
383
|
+
});
|
|
384
|
+
output({ message: "Assignment updated" });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function completeAssignment(id: string) {
|
|
388
|
+
await client.mutation(api.assignments.complete, {
|
|
389
|
+
id: id as Id<"assignments">,
|
|
390
|
+
});
|
|
391
|
+
output({ message: "Assignment completed" });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function blockAssignment(id: string, reason: string) {
|
|
395
|
+
await client.mutation(api.assignments.block, {
|
|
396
|
+
id: id as Id<"assignments">,
|
|
397
|
+
reason,
|
|
398
|
+
});
|
|
399
|
+
output({ message: "Assignment blocked" });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function unblockAssignment(id: string) {
|
|
403
|
+
await client.mutation(api.assignments.unblock, {
|
|
404
|
+
id: id as Id<"assignments">,
|
|
405
|
+
});
|
|
406
|
+
output({ message: "Assignment unblocked" });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function deleteAssignment(id: string) {
|
|
410
|
+
const result = await client.mutation(api.assignments.remove, {
|
|
411
|
+
id: id as Id<"assignments">,
|
|
412
|
+
});
|
|
413
|
+
output({ message: `Assignment deleted (${result.groupsDeleted} groups, ${result.jobsDeleted} jobs removed)` });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function startJob(id: string) {
|
|
417
|
+
await client.mutation(api.jobs.start, {
|
|
418
|
+
id: id as Id<"jobs">,
|
|
419
|
+
});
|
|
420
|
+
output({ message: "Job started" });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function completeJob(id: string, result: string) {
|
|
424
|
+
await client.mutation(api.jobs.complete, {
|
|
425
|
+
id: id as Id<"jobs">,
|
|
426
|
+
result,
|
|
427
|
+
});
|
|
428
|
+
output({ message: "Job completed" });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function failJob(id: string, result?: string) {
|
|
432
|
+
await client.mutation(api.jobs.fail, {
|
|
433
|
+
id: id as Id<"jobs">,
|
|
434
|
+
result,
|
|
435
|
+
});
|
|
436
|
+
output({ message: "Job failed" });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Chat commands
|
|
440
|
+
|
|
441
|
+
async function listChatThreads() {
|
|
442
|
+
const nsId = await getNamespaceId();
|
|
443
|
+
const result = await client.query(api.chatThreads.list, {
|
|
444
|
+
namespaceId: nsId,
|
|
445
|
+
});
|
|
446
|
+
output(result);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function getChatThread(threadId: string) {
|
|
450
|
+
const thread = await client.query(api.chatThreads.get, {
|
|
451
|
+
id: threadId as Id<"chatThreads">,
|
|
452
|
+
});
|
|
453
|
+
if (!thread) error("Thread not found");
|
|
454
|
+
|
|
455
|
+
const messages = await client.query(api.chatMessages.list, {
|
|
456
|
+
threadId: threadId as Id<"chatThreads">,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
output({ thread, messages });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function createChatThread(title?: string) {
|
|
463
|
+
const nsId = await getNamespaceId();
|
|
464
|
+
const threadId = await client.mutation(api.chatThreads.create, {
|
|
465
|
+
namespaceId: nsId,
|
|
466
|
+
title,
|
|
467
|
+
mode: "jam", // Default to safe mode
|
|
468
|
+
});
|
|
469
|
+
output({ threadId, message: "Chat thread created" });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function changeChatMode(threadId: string, mode: string, assignmentId?: string) {
|
|
473
|
+
if (mode !== "jam" && mode !== "cook" && mode !== "guardian") {
|
|
474
|
+
error("Mode must be 'jam', 'cook', or 'guardian'");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Guardian mode requires an assignment link and uses atomic mutation
|
|
478
|
+
if (mode === "guardian") {
|
|
479
|
+
if (!assignmentId) {
|
|
480
|
+
error("Guardian mode requires --assignment <id> to link to an assignment");
|
|
481
|
+
}
|
|
482
|
+
// Use atomic mutation to link, set alignment, and change mode
|
|
483
|
+
await client.mutation(api.chatThreads.enableGuardianMode, {
|
|
484
|
+
threadId: threadId as Id<"chatThreads">,
|
|
485
|
+
assignmentId: assignmentId as Id<"assignments">,
|
|
486
|
+
});
|
|
487
|
+
output({ message: `Thread mode changed to guardian, linked to assignment ${assignmentId}` });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await client.mutation(api.chatThreads.updateMode, {
|
|
492
|
+
id: threadId as Id<"chatThreads">,
|
|
493
|
+
mode: mode as "jam" | "cook",
|
|
494
|
+
});
|
|
495
|
+
output({ message: `Thread mode changed to ${mode}` });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function updateChatTitle(threadId: string, title: string) {
|
|
499
|
+
await client.mutation(api.chatThreads.updateTitle, {
|
|
500
|
+
id: threadId as Id<"chatThreads">,
|
|
501
|
+
title,
|
|
502
|
+
});
|
|
503
|
+
output({ message: `Thread title updated to "${title}"` });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function sendChatMessage(threadId: string, message: string, harness?: string) {
|
|
507
|
+
// Get thread to check it exists
|
|
508
|
+
const thread = await client.query(api.chatThreads.get, {
|
|
509
|
+
id: threadId as Id<"chatThreads">,
|
|
510
|
+
});
|
|
511
|
+
if (!thread) error("Thread not found");
|
|
512
|
+
|
|
513
|
+
// Add user message to thread
|
|
514
|
+
await client.mutation(api.chatMessages.add, {
|
|
515
|
+
threadId: threadId as Id<"chatThreads">,
|
|
516
|
+
role: "user",
|
|
517
|
+
content: message,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Trigger chat job (uses chatJobs table, not assignments/jobs)
|
|
521
|
+
// The trigger mutation builds context internally
|
|
522
|
+
const result = await client.mutation(api.chatJobs.trigger, {
|
|
523
|
+
threadId: threadId as Id<"chatThreads">,
|
|
524
|
+
harness: (harness || getHarnessForJobType("chat")) as Harness,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
output({
|
|
528
|
+
threadId,
|
|
529
|
+
jobId: result.jobId,
|
|
530
|
+
mode: result.mode,
|
|
531
|
+
message: "Chat message sent, job created",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Main
|
|
536
|
+
async function main() {
|
|
537
|
+
const args = process.argv.slice(2);
|
|
538
|
+
if (args.length === 0) {
|
|
539
|
+
error("No command provided. Use --help for usage.");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const command = args[0];
|
|
543
|
+
const { flags, positional } = parseArgs(args.slice(1));
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
switch (command) {
|
|
547
|
+
case "assignments":
|
|
548
|
+
await listAssignments(flags.status);
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
case "assignment":
|
|
552
|
+
if (!positional[0]) error("Assignment ID required");
|
|
553
|
+
await getAssignment(positional[0]);
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case "groups":
|
|
557
|
+
await listGroups(flags.status, flags.assignment);
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
case "group":
|
|
561
|
+
if (!positional[0]) error("Group ID required");
|
|
562
|
+
await getGroup(positional[0]);
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
case "jobs":
|
|
566
|
+
await listJobs(flags.status, flags.group);
|
|
567
|
+
break;
|
|
568
|
+
|
|
569
|
+
case "job":
|
|
570
|
+
if (!positional[0]) error("Job ID required");
|
|
571
|
+
await getJob(positional[0]);
|
|
572
|
+
break;
|
|
573
|
+
|
|
574
|
+
case "queue":
|
|
575
|
+
await getQueueStatus();
|
|
576
|
+
break;
|
|
577
|
+
|
|
578
|
+
case "create":
|
|
579
|
+
if (!positional[0]) error("North star text required");
|
|
580
|
+
await createAssignment(
|
|
581
|
+
positional[0],
|
|
582
|
+
flags.priority ? parseInt(flags.priority) : undefined,
|
|
583
|
+
flags.independent === "true",
|
|
584
|
+
flags.thread
|
|
585
|
+
);
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case "insert-job": {
|
|
589
|
+
// Assignment ID: positional arg > env var
|
|
590
|
+
const assignmentId = positional[0] || process.env.WORKFLOW_ASSIGNMENT_ID;
|
|
591
|
+
if (!assignmentId) error("Assignment ID required (or set WORKFLOW_ASSIGNMENT_ID)");
|
|
592
|
+
|
|
593
|
+
// After group ID: --after flag > env var (auto-link in job context)
|
|
594
|
+
const afterGroupId = flags.after || process.env.WORKFLOW_GROUP_ID;
|
|
595
|
+
|
|
596
|
+
// Build jobs array - either from --jobs JSON or from --type (single job)
|
|
597
|
+
// Keep harness undefined to allow expandJobs() to handle auto-expansion
|
|
598
|
+
let jobs: JobDefInput[];
|
|
599
|
+
|
|
600
|
+
if (flags.jobs) {
|
|
601
|
+
// Parse JSON array of jobs: [{"jobType":"review","harness":"claude"},...]
|
|
602
|
+
try {
|
|
603
|
+
const parsed = JSON.parse(flags.jobs);
|
|
604
|
+
if (!Array.isArray(parsed)) {
|
|
605
|
+
error("--jobs must be a JSON array");
|
|
606
|
+
}
|
|
607
|
+
jobs = parsed.map((j: any) => ({
|
|
608
|
+
jobType: j.jobType,
|
|
609
|
+
harness: j.harness, // Don't apply default - let expandJobs handle it
|
|
610
|
+
context: j.context,
|
|
611
|
+
}));
|
|
612
|
+
} catch (e) {
|
|
613
|
+
error(`Invalid --jobs JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
614
|
+
}
|
|
615
|
+
} else if (flags.type) {
|
|
616
|
+
// Single job via --type (backward compatible)
|
|
617
|
+
jobs = [{
|
|
618
|
+
jobType: flags.type,
|
|
619
|
+
harness: flags.harness as "claude" | "codex" | "gemini" | undefined,
|
|
620
|
+
context: flags.context,
|
|
621
|
+
}];
|
|
622
|
+
} else {
|
|
623
|
+
error("Either --jobs (JSON array) or --type required");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
await insertJobs(
|
|
627
|
+
assignmentId,
|
|
628
|
+
jobs,
|
|
629
|
+
afterGroupId,
|
|
630
|
+
flags.append === "true"
|
|
631
|
+
);
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
case "update-assignment": {
|
|
636
|
+
const assignmentId = positional[0] || process.env.WORKFLOW_ASSIGNMENT_ID;
|
|
637
|
+
if (!assignmentId) error("Assignment ID required (or set WORKFLOW_ASSIGNMENT_ID)");
|
|
638
|
+
await updateAssignment(assignmentId, flags.artifacts, flags.decisions, flags.alignment);
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
case "complete": {
|
|
643
|
+
const assignmentId = positional[0] || process.env.WORKFLOW_ASSIGNMENT_ID;
|
|
644
|
+
if (!assignmentId) error("Assignment ID required (or set WORKFLOW_ASSIGNMENT_ID)");
|
|
645
|
+
await completeAssignment(assignmentId);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
case "block": {
|
|
650
|
+
const assignmentId = positional[0] || process.env.WORKFLOW_ASSIGNMENT_ID;
|
|
651
|
+
if (!assignmentId) error("Assignment ID required (or set WORKFLOW_ASSIGNMENT_ID)");
|
|
652
|
+
if (!flags.reason) error("--reason required");
|
|
653
|
+
await blockAssignment(assignmentId, flags.reason);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
case "unblock": {
|
|
658
|
+
const assignmentId = positional[0] || process.env.WORKFLOW_ASSIGNMENT_ID;
|
|
659
|
+
if (!assignmentId) error("Assignment ID required (or set WORKFLOW_ASSIGNMENT_ID)");
|
|
660
|
+
await unblockAssignment(assignmentId);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
case "delete-assignment": {
|
|
665
|
+
if (!positional[0]) error("Assignment ID required");
|
|
666
|
+
await deleteAssignment(positional[0]);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
case "start-job":
|
|
671
|
+
if (!positional[0]) error("Job ID required");
|
|
672
|
+
await startJob(positional[0]);
|
|
673
|
+
break;
|
|
674
|
+
|
|
675
|
+
case "complete-job":
|
|
676
|
+
if (!positional[0]) error("Job ID required");
|
|
677
|
+
if (!flags.result) error("--result required");
|
|
678
|
+
await completeJob(positional[0], flags.result);
|
|
679
|
+
break;
|
|
680
|
+
|
|
681
|
+
case "fail-job":
|
|
682
|
+
if (!positional[0]) error("Job ID required");
|
|
683
|
+
await failJob(positional[0], flags.result);
|
|
684
|
+
break;
|
|
685
|
+
|
|
686
|
+
// Chat commands
|
|
687
|
+
case "chat-threads":
|
|
688
|
+
await listChatThreads();
|
|
689
|
+
break;
|
|
690
|
+
|
|
691
|
+
case "chat-thread":
|
|
692
|
+
if (!positional[0]) error("Thread ID required");
|
|
693
|
+
await getChatThread(positional[0]);
|
|
694
|
+
break;
|
|
695
|
+
|
|
696
|
+
case "chat-create":
|
|
697
|
+
await createChatThread(flags.title);
|
|
698
|
+
break;
|
|
699
|
+
|
|
700
|
+
case "chat-send":
|
|
701
|
+
if (!positional[0]) error("Thread ID required");
|
|
702
|
+
if (!positional[1]) error("Message required");
|
|
703
|
+
await sendChatMessage(positional[0], positional[1], flags.harness);
|
|
704
|
+
break;
|
|
705
|
+
|
|
706
|
+
case "chat-mode":
|
|
707
|
+
if (!positional[0]) error("Thread ID required");
|
|
708
|
+
if (!positional[1]) error("Mode required (jam, cook, or guardian)");
|
|
709
|
+
await changeChatMode(positional[0], positional[1], flags.assignment);
|
|
710
|
+
break;
|
|
711
|
+
|
|
712
|
+
case "chat-title":
|
|
713
|
+
if (!positional[0]) error("Thread ID required");
|
|
714
|
+
if (!positional[1]) error("Title required");
|
|
715
|
+
await updateChatTitle(positional[0], positional[1]);
|
|
716
|
+
break;
|
|
717
|
+
|
|
718
|
+
default:
|
|
719
|
+
error(`Unknown command: ${command}`);
|
|
720
|
+
}
|
|
721
|
+
} catch (e) {
|
|
722
|
+
error(e instanceof Error ? e.message : String(e));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
main();
|