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,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Building Module
|
|
3
|
+
*
|
|
4
|
+
* Handles template loading and prompt assembly for all job types.
|
|
5
|
+
* Extracted from runner.ts for modularity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
// Types needed for prompt building
|
|
13
|
+
export interface Assignment {
|
|
14
|
+
_id: string;
|
|
15
|
+
_creationTime: number;
|
|
16
|
+
namespaceId: string;
|
|
17
|
+
northStar: string;
|
|
18
|
+
status: "pending" | "active" | "blocked" | "complete";
|
|
19
|
+
blockedReason?: string;
|
|
20
|
+
independent: boolean;
|
|
21
|
+
priority: number;
|
|
22
|
+
artifacts: string;
|
|
23
|
+
decisions: string;
|
|
24
|
+
headGroupId?: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JobGroup {
|
|
30
|
+
_id: string;
|
|
31
|
+
_creationTime: number;
|
|
32
|
+
assignmentId: string;
|
|
33
|
+
nextGroupId?: string;
|
|
34
|
+
status: "pending" | "running" | "complete" | "failed";
|
|
35
|
+
aggregatedResult?: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Job {
|
|
40
|
+
_id: string;
|
|
41
|
+
_creationTime: number;
|
|
42
|
+
groupId: string;
|
|
43
|
+
jobType: string;
|
|
44
|
+
harness: "claude" | "codex" | "gemini";
|
|
45
|
+
context?: string;
|
|
46
|
+
status: "pending" | "running" | "complete" | "failed";
|
|
47
|
+
result?: string;
|
|
48
|
+
prompt?: string;
|
|
49
|
+
startedAt?: number;
|
|
50
|
+
completedAt?: number;
|
|
51
|
+
createdAt: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ChatMessage {
|
|
55
|
+
_id: string;
|
|
56
|
+
threadId: string;
|
|
57
|
+
role: "user" | "assistant" | "pm";
|
|
58
|
+
content: string;
|
|
59
|
+
createdAt: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ChatJobContext {
|
|
63
|
+
threadId: string;
|
|
64
|
+
namespaceId: string;
|
|
65
|
+
mode: "jam" | "cook" | "guardian";
|
|
66
|
+
// For differential prompting
|
|
67
|
+
effectivePromptMode: "jam" | "cook";
|
|
68
|
+
lastPromptMode?: "jam" | "cook";
|
|
69
|
+
messages: ChatMessage[];
|
|
70
|
+
latestUserMessage: string;
|
|
71
|
+
claudeSessionId?: string;
|
|
72
|
+
// Guardian mode context
|
|
73
|
+
assignmentId?: string;
|
|
74
|
+
isGuardianEvaluation?: boolean; // True when PO is evaluating PM response
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Prompt types for differential prompting
|
|
78
|
+
export type PromptType = "full" | "mode_activation" | "minimal" | "guardian_eval";
|
|
79
|
+
|
|
80
|
+
// Resolve templates directory relative to this module
|
|
81
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
const TEMPLATES_DIR = join(__dirname, "..", "templates");
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load a template file by job type
|
|
86
|
+
*/
|
|
87
|
+
export function loadTemplate(jobType: string): string {
|
|
88
|
+
const templatePath = join(TEMPLATES_DIR, `${jobType}.md`);
|
|
89
|
+
try {
|
|
90
|
+
return readFileSync(templatePath, "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
console.error(`Template not found: ${jobType}.md`);
|
|
93
|
+
return "Execute the task as described.\n\n{{CONTEXT}}";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Accumulated job result for PM jobs
|
|
98
|
+
export interface AccumulatedJobResult {
|
|
99
|
+
jobType: string;
|
|
100
|
+
harness: string; // Kept for internal tracking, not shown to PM
|
|
101
|
+
result: string;
|
|
102
|
+
groupId?: string;
|
|
103
|
+
groupIndex?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format accumulated results for PM prompt
|
|
108
|
+
* Uses jobType with A/B/C suffixes for multiple jobs of same type
|
|
109
|
+
* Does NOT expose harness names - PM sees only jobType labels
|
|
110
|
+
*/
|
|
111
|
+
function formatAccumulatedResults(
|
|
112
|
+
accumulatedResults: AccumulatedJobResult[]
|
|
113
|
+
): string {
|
|
114
|
+
if (!accumulatedResults || accumulatedResults.length === 0) {
|
|
115
|
+
return "(no previous results)";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Group by jobType to determine if we need A/B/C suffixes
|
|
119
|
+
const byJobType = new Map<string, AccumulatedJobResult[]>();
|
|
120
|
+
for (const r of accumulatedResults) {
|
|
121
|
+
const list = byJobType.get(r.jobType) || [];
|
|
122
|
+
list.push(r);
|
|
123
|
+
byJobType.set(r.jobType, list);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build sections with A/B/C suffixes for duplicates
|
|
127
|
+
const sections: string[] = [];
|
|
128
|
+
const suffixes = ["A", "B", "C", "D", "E", "F", "G", "H"];
|
|
129
|
+
|
|
130
|
+
for (const [jobType, results] of byJobType) {
|
|
131
|
+
if (results.length === 1) {
|
|
132
|
+
// Single job of this type - no suffix
|
|
133
|
+
sections.push(`## ${jobType}\n\n${results[0].result}`);
|
|
134
|
+
} else {
|
|
135
|
+
// Multiple jobs of same type - add A/B/C suffixes
|
|
136
|
+
for (let i = 0; i < results.length; i++) {
|
|
137
|
+
const suffix = suffixes[i] || `${i + 1}`;
|
|
138
|
+
sections.push(`## ${jobType} ${suffix}\n\n${results[i].result}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return sections.join("\n\n---\n\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface GroupedResults {
|
|
147
|
+
groupId: string;
|
|
148
|
+
groupIndex: number;
|
|
149
|
+
results: AccumulatedJobResult[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function groupAccumulatedResults(
|
|
153
|
+
accumulatedResults: AccumulatedJobResult[]
|
|
154
|
+
): GroupedResults[] {
|
|
155
|
+
if (!accumulatedResults || accumulatedResults.length === 0) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const groups = new Map<string, GroupedResults>();
|
|
160
|
+
let fallbackIndex = 0;
|
|
161
|
+
|
|
162
|
+
for (const result of accumulatedResults) {
|
|
163
|
+
const groupId = result.groupId || `unknown-${fallbackIndex}`;
|
|
164
|
+
const groupIndex =
|
|
165
|
+
typeof result.groupIndex === "number" ? result.groupIndex : fallbackIndex;
|
|
166
|
+
|
|
167
|
+
if (!groups.has(groupId)) {
|
|
168
|
+
groups.set(groupId, {
|
|
169
|
+
groupId,
|
|
170
|
+
groupIndex,
|
|
171
|
+
results: [],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
groups.get(groupId)!.results.push(result);
|
|
176
|
+
fallbackIndex += 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Array.from(groups.values()).sort((a, b) => a.groupIndex - b.groupIndex);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isReviewJobType(jobType: string): boolean {
|
|
183
|
+
return jobType === "review" || jobType.endsWith("review");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function includesJobType(results: AccumulatedJobResult[], jobType: string): boolean {
|
|
187
|
+
return results.some((r) => r.jobType === jobType);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function inferPrimaryJobType(results: AccumulatedJobResult[]): string {
|
|
191
|
+
if (includesJobType(results, "implement")) return "implement";
|
|
192
|
+
if (includesJobType(results, "plan")) return "plan";
|
|
193
|
+
if (includesJobType(results, "document")) return "document";
|
|
194
|
+
if (includesJobType(results, "uat")) return "uat";
|
|
195
|
+
return results[0]?.jobType || "unknown";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildPmModules(
|
|
199
|
+
accumulatedResults: AccumulatedJobResult[],
|
|
200
|
+
r1GroupResults?: AccumulatedJobResult[]
|
|
201
|
+
): string {
|
|
202
|
+
const grouped = groupAccumulatedResults(accumulatedResults);
|
|
203
|
+
if (grouped.length === 0) {
|
|
204
|
+
return "No prior job results found. Ask clarifying questions if needed and decide the first actionable job.";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const latestGroup = grouped[grouped.length - 1];
|
|
208
|
+
const latestHasReview = latestGroup.results.some((r) => isReviewJobType(r.jobType));
|
|
209
|
+
const latestHasImplement = latestGroup.results.some((r) => r.jobType === "implement");
|
|
210
|
+
const latestHasPlan = latestGroup.results.some((r) => r.jobType === "plan");
|
|
211
|
+
|
|
212
|
+
if (latestHasReview) {
|
|
213
|
+
let reviewGroupIndex = -1;
|
|
214
|
+
for (let i = grouped.length - 1; i >= 0; i--) {
|
|
215
|
+
if (grouped[i].results.some((r) => isReviewJobType(r.jobType))) {
|
|
216
|
+
reviewGroupIndex = i;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const hasR1Override = r1GroupResults && r1GroupResults.length > 0;
|
|
222
|
+
const priorGroup =
|
|
223
|
+
reviewGroupIndex > 0 ? grouped[reviewGroupIndex - 1] : undefined;
|
|
224
|
+
const p1JobType = hasR1Override
|
|
225
|
+
? inferPrimaryJobType(r1GroupResults!)
|
|
226
|
+
: priorGroup
|
|
227
|
+
? inferPrimaryJobType(priorGroup.results)
|
|
228
|
+
: "unknown";
|
|
229
|
+
const priorResults = hasR1Override
|
|
230
|
+
? formatAccumulatedResults(r1GroupResults!)
|
|
231
|
+
: priorGroup
|
|
232
|
+
? formatAccumulatedResults(priorGroup.results)
|
|
233
|
+
: "(no prior group found)";
|
|
234
|
+
|
|
235
|
+
return `### Post-Review Decision\n\n#### R-1 Context (Group Before Review)\n${priorResults}\n\n#### Decision Logic (P-1 = ${p1JobType})\n1. **If fundamental decisions are required** (cannot be inferred from mental-model + north star with high confidence):\n - Ask clarifying questions\n - Block the assignment until resolved\n2. **If high-severity issues have a clear optimal solution and reviewers concur**:\n - Append a new **${p1JobType}** job to address/refine\n - Include the issues raised and rationale in your PM response\n3. **If only medium/low/no issues and reviewers (and UAT, if present) approve**:\n - Filter issues for alignment with mental model and real product value\n - If **P-1 = plan**: update the plan doc yourself, then append **implement** (or complete if planning-only)\n - If **P-1 = implement**: append **implement** to address items, or append **document** if no further changes are warranted\n\nAlways include the issues raised and your decision rationale.\n`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (latestHasImplement) {
|
|
239
|
+
return `### Post-Implement Decision\n\n- Append a **review** job to assess the implementation quality and alignment.\n- If the implementation impacts UX, include **uat** in the same job group.\n- Provide clear context for reviewers/UAT (what changed, success criteria, files/flows).\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (latestHasPlan) {
|
|
243
|
+
return `### Post-Plan Decision\n\n- If the plan/spec represents **non-trivial change** (5+ files, backend + frontend, foundational schema, or core system building blocks), append a **review** job.\n- If the plan is **trivial** or already reviewed and approved, append **implement** to execute the full plan.\n - Note: the implement job is powerful and can candle multiple WP and sequence accordingly. Assign suffiecient work to the implement job so it can deliver a full vertical slice (ready for review).\n- Ensure the plan doc path is recorded in artifacts.\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return "No matching PM module found. Decide next steps based on the latest results and north star alignment.";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Build prompt for assignment-based jobs
|
|
251
|
+
* Each job has its own jobType and context
|
|
252
|
+
*/
|
|
253
|
+
export function buildPrompt(
|
|
254
|
+
group: JobGroup,
|
|
255
|
+
assignment: Assignment,
|
|
256
|
+
job: Job,
|
|
257
|
+
accumulatedResults: AccumulatedJobResult[],
|
|
258
|
+
previousNonPmGroupResults: AccumulatedJobResult[],
|
|
259
|
+
r1GroupResults: AccumulatedJobResult[]
|
|
260
|
+
): string {
|
|
261
|
+
const template = loadTemplate(job.jobType);
|
|
262
|
+
|
|
263
|
+
// Format accumulated results for PM/retrospect jobs
|
|
264
|
+
const previousResults =
|
|
265
|
+
job.jobType === "pm" ? accumulatedResults : previousNonPmGroupResults;
|
|
266
|
+
const resultText = formatAccumulatedResults(previousResults);
|
|
267
|
+
const pmModules =
|
|
268
|
+
job.jobType === "pm" ? buildPmModules(accumulatedResults, r1GroupResults) : "";
|
|
269
|
+
|
|
270
|
+
return template
|
|
271
|
+
.replace(/\{\{NORTH_STAR\}\}/g, assignment.northStar)
|
|
272
|
+
.replace(/\{\{ARTIFACTS\}\}/g, assignment.artifacts || "(none)")
|
|
273
|
+
.replace(/\{\{DECISIONS\}\}/g, assignment.decisions || "(none)")
|
|
274
|
+
.replace(/\{\{CONTEXT\}\}/g, job.context || "(no specific context)")
|
|
275
|
+
.replace(/\{\{PREVIOUS_RESULT\}\}/g, resultText)
|
|
276
|
+
.replace(/\{\{ASSIGNMENT_ID\}\}/g, assignment._id)
|
|
277
|
+
.replace(/\{\{GROUP_ID\}\}/g, group._id)
|
|
278
|
+
.replace(/\{\{CURRENT_JOB_ID\}\}/g, job._id)
|
|
279
|
+
.replace(/\{\{HARNESS\}\}/g, job.harness)
|
|
280
|
+
.replace(/\{\{PM_MODULES\}\}/g, pmModules);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Determine which prompt type to use based on context
|
|
285
|
+
*/
|
|
286
|
+
export function determinePromptType(chatContext: ChatJobContext): PromptType {
|
|
287
|
+
const isNewSession = !chatContext.claudeSessionId;
|
|
288
|
+
const isGuardianEval = chatContext.isGuardianEvaluation === true;
|
|
289
|
+
|
|
290
|
+
// Guardian evaluation always gets its special prompt
|
|
291
|
+
if (isGuardianEval) {
|
|
292
|
+
return "guardian_eval";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// New session (no claudeSessionId) - send full prompt
|
|
296
|
+
if (isNewSession) {
|
|
297
|
+
return "full";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check if mode changed since last prompt
|
|
301
|
+
const modeChanged = chatContext.lastPromptMode !== chatContext.effectivePromptMode;
|
|
302
|
+
|
|
303
|
+
if (modeChanged) {
|
|
304
|
+
return "mode_activation";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Same mode, resuming session - minimal prompt
|
|
308
|
+
return "minimal";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Build prompt for chat jobs with differential prompting
|
|
313
|
+
* - Full prompt for new sessions
|
|
314
|
+
* - Mode activation snippet when mode changes
|
|
315
|
+
* - Minimal prompt for same-mode resume
|
|
316
|
+
* - Guardian eval injection for PM report evaluation
|
|
317
|
+
*/
|
|
318
|
+
export function buildChatPrompt(chatContext: ChatJobContext, namespace: string): string {
|
|
319
|
+
const promptType = determinePromptType(chatContext);
|
|
320
|
+
const template = loadTemplate("product-owner");
|
|
321
|
+
|
|
322
|
+
// Variable replacements used across all prompt types
|
|
323
|
+
const replaceVars = (text: string): string => {
|
|
324
|
+
return text
|
|
325
|
+
.replace(/\{\{THREAD_ID\}\}/g, chatContext.threadId)
|
|
326
|
+
.replace(/\{\{NAMESPACE\}\}/g, namespace)
|
|
327
|
+
.replace(/\{\{MODE\}\}/g, chatContext.effectivePromptMode)
|
|
328
|
+
.replace(/\{\{LATEST_MESSAGE\}\}/g, chatContext.latestUserMessage)
|
|
329
|
+
.replace(/\{\{ASSIGNMENT_ID\}\}/g, chatContext.assignmentId || "(no assignment linked)");
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
switch (promptType) {
|
|
333
|
+
case "full":
|
|
334
|
+
return buildFullPrompt(template, chatContext, replaceVars);
|
|
335
|
+
|
|
336
|
+
case "mode_activation":
|
|
337
|
+
return buildModeActivationPrompt(chatContext, replaceVars);
|
|
338
|
+
|
|
339
|
+
case "minimal":
|
|
340
|
+
return buildMinimalPrompt(chatContext, replaceVars);
|
|
341
|
+
|
|
342
|
+
case "guardian_eval":
|
|
343
|
+
return buildGuardianEvalPrompt(template, chatContext, replaceVars);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Build full prompt for new sessions
|
|
349
|
+
*/
|
|
350
|
+
function buildFullPrompt(
|
|
351
|
+
template: string,
|
|
352
|
+
chatContext: ChatJobContext,
|
|
353
|
+
replaceVars: (text: string) => string
|
|
354
|
+
): string {
|
|
355
|
+
let processed = template;
|
|
356
|
+
|
|
357
|
+
// Remove GUARDIAN_MODE blocks (not applicable for full prompt)
|
|
358
|
+
const guardianModeRegex = /\{\{#if GUARDIAN_MODE\}\}[\s\S]*?\{\{\/if\}\}/g;
|
|
359
|
+
processed = processed.replace(guardianModeRegex, "");
|
|
360
|
+
|
|
361
|
+
// Handle {{#if COOK_MODE}}...{{else}}...{{/if}}
|
|
362
|
+
const cookModeRegex = /\{\{#if COOK_MODE\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
363
|
+
processed = processed.replace(cookModeRegex, (_match, cookContent, jamContent) => {
|
|
364
|
+
return chatContext.effectivePromptMode === "cook" ? cookContent : jamContent;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Handle simple {{#if COOK_MODE}}...{{/if}} without else
|
|
368
|
+
const simpleCookModeRegex = /\{\{#if COOK_MODE\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
369
|
+
processed = processed.replace(simpleCookModeRegex, (_match, content) => {
|
|
370
|
+
return chatContext.effectivePromptMode === "cook" ? content : "";
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Handle {{#if NEW_SESSION}}...{{/if}} - include for new sessions
|
|
374
|
+
const newSessionRegex = /\{\{#if NEW_SESSION\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
375
|
+
processed = processed.replace(newSessionRegex, (_match, content) => content);
|
|
376
|
+
|
|
377
|
+
return replaceVars(processed);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build mode activation prompt when mode changes during session
|
|
382
|
+
*/
|
|
383
|
+
function buildModeActivationPrompt(
|
|
384
|
+
chatContext: ChatJobContext,
|
|
385
|
+
replaceVars: (text: string) => string
|
|
386
|
+
): string {
|
|
387
|
+
const mode = chatContext.effectivePromptMode.toUpperCase();
|
|
388
|
+
|
|
389
|
+
let modeContent: string;
|
|
390
|
+
|
|
391
|
+
if (chatContext.effectivePromptMode === "cook") {
|
|
392
|
+
modeContent = `## COOK MODE ACTIVATED
|
|
393
|
+
|
|
394
|
+
You now have **FULL AUTONOMY** to take action. Adopt COOK mode behavior until explicitly changed.
|
|
395
|
+
|
|
396
|
+
### Your Powers in Cook Mode
|
|
397
|
+
|
|
398
|
+
- CREATE new assignments via CLI
|
|
399
|
+
- INSERT jobs into the workflow queue
|
|
400
|
+
- Make product decisions and execute them
|
|
401
|
+
|
|
402
|
+
When the user wants work to be done:
|
|
403
|
+
1. **Confirm** your understanding of requirements
|
|
404
|
+
2. **Create** an assignment with a **verbose north star** (include user perspective + success criteria)
|
|
405
|
+
3. **Insert** an initial job to begin work (usually \`plan\` type)
|
|
406
|
+
4. **Immediately update** \`docs/project/spec/mental-model.md\` with new insights from the conversation
|
|
407
|
+
5. **Inform** the user what you've initiated
|
|
408
|
+
|
|
409
|
+
### CLI Commands Available
|
|
410
|
+
|
|
411
|
+
\`\`\`bash
|
|
412
|
+
# Create a new assignment (auto-linked to this thread)
|
|
413
|
+
npx tsx .agents/tools/workflow/cli.ts create "<north-star-description>" --priority <N>
|
|
414
|
+
|
|
415
|
+
# Insert job(s) - jobs in the same array run in parallel
|
|
416
|
+
npx tsx .agents/tools/workflow/cli.ts insert-job <assignmentId> \\
|
|
417
|
+
--jobs '[{"jobType":"plan","context":"<context>"}]'
|
|
418
|
+
|
|
419
|
+
# Append to existing chain (use --append to link after current tail)
|
|
420
|
+
npx tsx .agents/tools/workflow/cli.ts insert-job <assignmentId> --append \\
|
|
421
|
+
--jobs '[{"jobType":"implement","context":"Build auth"},{"jobType":"uat","context":"Test login"}]'
|
|
422
|
+
|
|
423
|
+
# View assignments and queue
|
|
424
|
+
npx tsx .agents/tools/workflow/cli.ts assignments
|
|
425
|
+
npx tsx .agents/tools/workflow/cli.ts queue
|
|
426
|
+
|
|
427
|
+
# Delete assignment
|
|
428
|
+
npx tsx .agents/tools/workflow/cli.ts delete-assignment <assignmentId>
|
|
429
|
+
\`\`\`
|
|
430
|
+
|
|
431
|
+
### Job Types You Can Create
|
|
432
|
+
|
|
433
|
+
| Type | Use When |
|
|
434
|
+
|------|----------|
|
|
435
|
+
| \`plan\` | Need a spec doc and work-package breakdown |
|
|
436
|
+
| \`implement\` | Clear requirements ready for implementation |
|
|
437
|
+
| \`review\` | Engineering quality review of plan/spec or implementation |
|
|
438
|
+
| \`uat\` | Need user-perspective testing |
|
|
439
|
+
| \`document\` | Update docs and finalize assignment |`;
|
|
440
|
+
} else {
|
|
441
|
+
modeContent = `## JAM MODE ACTIVATED
|
|
442
|
+
|
|
443
|
+
You are now in **READ-ONLY** ideation mode. Adopt JAM mode behavior until explicitly changed.
|
|
444
|
+
|
|
445
|
+
### Your Role in Jam Mode
|
|
446
|
+
|
|
447
|
+
- You CANNOT create assignments or jobs
|
|
448
|
+
- You CAN help spec out ideas
|
|
449
|
+
- You CAN ask clarifying questions
|
|
450
|
+
- You CAN suggest approaches and trade-offs
|
|
451
|
+
- You CAN help refine requirements
|
|
452
|
+
- You CAN explore the codebase and existing work
|
|
453
|
+
|
|
454
|
+
Help the user think through their ideas:
|
|
455
|
+
- Ask probing questions to clarify requirements
|
|
456
|
+
- Identify potential challenges and edge cases
|
|
457
|
+
- Suggest technical approaches
|
|
458
|
+
- Help prioritize features
|
|
459
|
+
- Draft acceptance criteria
|
|
460
|
+
- Explore trade-offs between options
|
|
461
|
+
|
|
462
|
+
### When to Suggest Cook Mode
|
|
463
|
+
|
|
464
|
+
If the user says things like "Let's do it", "Make it happen", "Start working on this" - suggest they switch to **Cook mode** to take action.`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const prompt = `${modeContent}
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Current Message
|
|
472
|
+
|
|
473
|
+
**User says:**
|
|
474
|
+
${chatContext.latestUserMessage}`;
|
|
475
|
+
|
|
476
|
+
return replaceVars(prompt);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Build minimal prompt for same-mode resume
|
|
481
|
+
*/
|
|
482
|
+
function buildMinimalPrompt(
|
|
483
|
+
chatContext: ChatJobContext,
|
|
484
|
+
replaceVars: (text: string) => string
|
|
485
|
+
): string {
|
|
486
|
+
const prompt = `## Current Message
|
|
487
|
+
|
|
488
|
+
**User says:**
|
|
489
|
+
${chatContext.latestUserMessage}`;
|
|
490
|
+
|
|
491
|
+
return replaceVars(prompt);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Build guardian evaluation prompt for PM report evaluation
|
|
496
|
+
*/
|
|
497
|
+
function buildGuardianEvalPrompt(
|
|
498
|
+
template: string,
|
|
499
|
+
chatContext: ChatJobContext,
|
|
500
|
+
replaceVars: (text: string) => string
|
|
501
|
+
): string {
|
|
502
|
+
// Extract the GUARDIAN_MODE block from template
|
|
503
|
+
const guardianModeRegex = /\{\{#if GUARDIAN_MODE\}\}([\s\S]*?)\{\{\/if\}\}/;
|
|
504
|
+
const match = template.match(guardianModeRegex);
|
|
505
|
+
|
|
506
|
+
if (match && match[1]) {
|
|
507
|
+
return replaceVars(match[1].trim());
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Fallback if template doesn't have guardian block
|
|
511
|
+
return replaceVars(`## GUARDIAN MODE - ALIGNMENT EVALUATION
|
|
512
|
+
|
|
513
|
+
You are monitoring assignment alignment. A PM has reported on work progress.
|
|
514
|
+
|
|
515
|
+
**Assignment ID:** ${chatContext.assignmentId || "(unknown)"}
|
|
516
|
+
|
|
517
|
+
### PM Progress Report
|
|
518
|
+
${chatContext.latestUserMessage}
|
|
519
|
+
|
|
520
|
+
### Alignment Response
|
|
521
|
+
|
|
522
|
+
Respond with **ONE** of:
|
|
523
|
+
|
|
524
|
+
**🟢** - Trajectory aligned with intent. Just the emoji, nothing else.
|
|
525
|
+
|
|
526
|
+
**🟠** - Uncertain. Include 2-3 sentence rationale explaining the concern.
|
|
527
|
+
|
|
528
|
+
**🔴** - Misaligned. Include rationale and block the assignment.`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Parse chat context from job context JSON field
|
|
533
|
+
*/
|
|
534
|
+
export function parseChatContext(contextStr: string): ChatJobContext | null {
|
|
535
|
+
try {
|
|
536
|
+
return JSON.parse(contextStr) as ChatJobContext;
|
|
537
|
+
} catch {
|
|
538
|
+
console.error("Failed to parse chat context:", contextStr);
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Check if a job is a chat job based on its type
|
|
545
|
+
*/
|
|
546
|
+
export function isChatJob(job: Job): boolean {
|
|
547
|
+
return job.jobType === "chat" || job.jobType === "product-owner";
|
|
548
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for stream handlers module
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx tsx --test lib/streams.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it } from "node:test";
|
|
8
|
+
import assert from "node:assert";
|
|
9
|
+
import {
|
|
10
|
+
ClaudeStreamHandler,
|
|
11
|
+
CodexStreamHandler,
|
|
12
|
+
GeminiStreamHandler,
|
|
13
|
+
buildCommand,
|
|
14
|
+
} from "./streams.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// ClaudeStreamHandler tests
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
describe("ClaudeStreamHandler", () => {
|
|
21
|
+
it("extracts text and captures session_id on success", () => {
|
|
22
|
+
const handler = new ClaudeStreamHandler();
|
|
23
|
+
|
|
24
|
+
// Simulate event stream
|
|
25
|
+
handler.onEvent({
|
|
26
|
+
type: "assistant",
|
|
27
|
+
message: { content: [{ type: "text", text: "Hello" }] },
|
|
28
|
+
});
|
|
29
|
+
handler.onEvent({
|
|
30
|
+
type: "result",
|
|
31
|
+
subtype: "success",
|
|
32
|
+
result: "Final result",
|
|
33
|
+
session_id: "session_123",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.strictEqual(handler.getResult(), "Final result");
|
|
37
|
+
assert.strictEqual(handler.isComplete(), true);
|
|
38
|
+
assert.strictEqual(handler.getSessionId(), "session_123");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not mark complete on failure", () => {
|
|
42
|
+
const handler = new ClaudeStreamHandler();
|
|
43
|
+
|
|
44
|
+
handler.onEvent({ type: "result", subtype: "error" });
|
|
45
|
+
|
|
46
|
+
assert.strictEqual(handler.isComplete(), false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// CodexStreamHandler tests
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
describe("CodexStreamHandler", () => {
|
|
55
|
+
it("extracts agent_message text and marks complete on turn.completed", () => {
|
|
56
|
+
const handler = new CodexStreamHandler();
|
|
57
|
+
|
|
58
|
+
handler.onEvent({
|
|
59
|
+
type: "item.completed",
|
|
60
|
+
item: { type: "agent_message", text: "First message" },
|
|
61
|
+
});
|
|
62
|
+
handler.onEvent({
|
|
63
|
+
type: "item.completed",
|
|
64
|
+
item: { type: "agent_message", text: "Second message" },
|
|
65
|
+
});
|
|
66
|
+
handler.onEvent({ type: "turn.completed" });
|
|
67
|
+
|
|
68
|
+
assert.ok(handler.getResult().includes("First message"));
|
|
69
|
+
assert.ok(handler.getResult().includes("Second message"));
|
|
70
|
+
assert.strictEqual(handler.isComplete(), true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("ignores non-agent_message items", () => {
|
|
74
|
+
const handler = new CodexStreamHandler();
|
|
75
|
+
|
|
76
|
+
handler.onEvent({
|
|
77
|
+
type: "item.completed",
|
|
78
|
+
item: { type: "reasoning", text: "Thinking..." },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.strictEqual(handler.getResult(), "");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// GeminiStreamHandler tests
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
describe("GeminiStreamHandler", () => {
|
|
90
|
+
it("accumulates assistant content and marks complete on result", () => {
|
|
91
|
+
const handler = new GeminiStreamHandler();
|
|
92
|
+
|
|
93
|
+
handler.onEvent({ type: "message", role: "assistant", content: "Hello " });
|
|
94
|
+
handler.onEvent({ type: "message", role: "assistant", content: "world" });
|
|
95
|
+
handler.onEvent({ type: "result" });
|
|
96
|
+
|
|
97
|
+
assert.strictEqual(handler.getResult(), "Hello world");
|
|
98
|
+
assert.strictEqual(handler.isComplete(), true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("ignores non-assistant messages", () => {
|
|
102
|
+
const handler = new GeminiStreamHandler();
|
|
103
|
+
|
|
104
|
+
handler.onEvent({ type: "message", role: "user", content: "User input" });
|
|
105
|
+
handler.onEvent({ type: "tool_use", tool_name: "shell" });
|
|
106
|
+
|
|
107
|
+
assert.strictEqual(handler.getResult(), "");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// buildCommand tests
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
describe("buildCommand", () => {
|
|
116
|
+
it("builds claude command with optional session resume", () => {
|
|
117
|
+
const basic = buildCommand("claude", "test");
|
|
118
|
+
assert.strictEqual(basic.cmd, "claude");
|
|
119
|
+
assert.ok(basic.args.includes("--output-format"));
|
|
120
|
+
assert.ok(!basic.args.includes("--resume"));
|
|
121
|
+
|
|
122
|
+
const withSession = buildCommand("claude", "test", { sessionId: "s123" });
|
|
123
|
+
assert.ok(withSession.args.includes("--resume"));
|
|
124
|
+
assert.ok(withSession.args.includes("s123"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("builds codex and gemini commands", () => {
|
|
128
|
+
const codex = buildCommand("codex", "test");
|
|
129
|
+
assert.strictEqual(codex.cmd, "codex");
|
|
130
|
+
assert.ok(codex.args.includes("--json"));
|
|
131
|
+
|
|
132
|
+
const gemini = buildCommand("gemini", "test");
|
|
133
|
+
assert.strictEqual(gemini.cmd, "gemini");
|
|
134
|
+
assert.ok(gemini.args.includes("stream-json"));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("throws for unknown harness", () => {
|
|
138
|
+
assert.throws(() => buildCommand("unknown", "test"), /Unknown harness/);
|
|
139
|
+
});
|
|
140
|
+
});
|