iriai-build 0.2.8 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iriai-build",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "description": "Iriai Build tool — AI agent orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -353,7 +353,7 @@ export class SlackAdapter extends InterfaceAdapter {
353
353
  const slug = slugify(featureDesc);
354
354
 
355
355
  if (this._orchestrator) {
356
- await this._orchestrator.initializeFeature(slug, messageTs, userId);
356
+ await this._orchestrator.initializeFeature(slug, messageTs, userId, featureDesc);
357
357
  }
358
358
  }
359
359
 
package/v3/constants.js CHANGED
@@ -143,6 +143,7 @@ export const SIGNAL = {
143
143
  NEEDS_REPOS: ".needs-repos",
144
144
  GATE_EVIDENCE: ".gate-evidence.yaml",
145
145
  OUTPUT_PARTIAL: ".output.partial",
146
+ SCOPING_COMPLETE: ".scoping-complete",
146
147
  };
147
148
 
148
149
  // ─── Known Repos ─────────────────────────────────────────────────────────────
package/v3/file-io.js CHANGED
@@ -51,6 +51,8 @@ export class FileIO extends EventEmitter {
51
51
  this.emit("impl:userMessage", { slug, agent: "operator", filePath });
52
52
  } else if (fileName === ".needs-repos") {
53
53
  this.emit("impl:needsRepos", { slug, filePath });
54
+ } else if (fileName === ".scoping-complete") {
55
+ this.emit("planning:scopingComplete", { slug, filePath });
54
56
  }
55
57
  return;
56
58
  }
@@ -14,6 +14,7 @@ import { invokeOperator, invokeOperatorRelay, parseOperatorResponse } from "./op
14
14
  import {
15
15
  buildPlanningRolePrompt, buildArtifactSummarizerPrompt, buildRolePrompt, buildOrchestratorPrompt,
16
16
  buildFeatureLeadInitPrompt, buildFeatureLeadRefreshPrompt, buildFeatureLeadTriggerPrompt,
17
+ buildScopingPrompt,
17
18
  } from "./prompt-builder.js";
18
19
  import { planningComplete } from "./planning-complete.js";
19
20
  import { launchImpl } from "./launch-impl.js";
@@ -70,9 +71,9 @@ export class Orchestrator {
70
71
 
71
72
  /**
72
73
  * Initialize a feature from [FEATURE] detection: create per-feature dirs,
73
- * feature channel, operator record, and start planning pipeline.
74
+ * feature channel, operator record, and start scoping phase.
74
75
  */
75
- async initializeFeature(slug, messageTs, userId) {
76
+ async initializeFeature(slug, messageTs, userId, featureDescription = "") {
76
77
  const featureDir = path.join(IMPL_BASE, "features", slug);
77
78
 
78
79
  // 1. Create directories
@@ -100,9 +101,14 @@ export class Orchestrator {
100
101
  try { fs.unlinkSync(opDest); } catch { /* ok */ }
101
102
  fs.symlinkSync(opSrc, opDest);
102
103
 
103
- // 3. Create feature in SQLite
104
+ // 3. Create feature in SQLite — store the original description in metadata
104
105
  const feature = queries.createFeature({ slug, threadTs: messageTs, signalDir: featureDir });
105
106
  queries.insertEvent(feature.id, "system", `user:${userId}`, `Feature requested: ${slug}`);
107
+ if (featureDescription) {
108
+ const meta = queries.getFeatureMetadata(feature.id);
109
+ meta.feature_description = featureDescription;
110
+ queries.updateFeatureMetadata(feature.id, meta);
111
+ }
106
112
 
107
113
  // 4. Create feature channel immediately
108
114
  const channelId = await this.adapter.createFeatureChannel(feature.id, slug);
@@ -126,7 +132,7 @@ export class Orchestrator {
126
132
  await this.adapter.postThreadMessage(feature.id,
127
133
  `*[Pipeline]* Starting planning for *${slug}*. Implementation channel: <#${channelId}>`);
128
134
  await this.adapter.postMessage(feature.id,
129
- `*[Pipeline]* Planning pipeline started for feature: *${slug}*\nPhase 1: Product Manager interview`);
135
+ `*[Pipeline]* Feature scoping started for: *${slug}*`);
130
136
 
131
137
  // Pin artifact portal URL in the feature channel
132
138
  const portalUrl = `http://localhost:${PORTAL_PORT}/features/${slug}`;
@@ -141,7 +147,7 @@ export class Orchestrator {
141
147
  }
142
148
  } else {
143
149
  await this.adapter.postThreadMessage(feature.id,
144
- `*[Pipeline]* Starting planning pipeline for: *${slug}*\nPhase 1: Product Manager interview`);
150
+ `*[Pipeline]* Starting feature scoping for: *${slug}*`);
145
151
  }
146
152
 
147
153
  // 7. Cache signal tree with planning field
@@ -158,13 +164,95 @@ export class Orchestrator {
158
164
  // 8. Start watching per-feature planning + operator signals
159
165
  this.fileIO.watchFeaturePlanningSignals(slug, planningTree, operatorDir);
160
166
 
161
- // 9. Set active planning role and dispatch PM
162
- queries.updateFeaturePlanningRole(feature.id, "pm");
163
- this.dispatchPlanningRole(feature.id, "pm");
167
+ // 9. Dispatch scoping phase Operator converses with user to establish
168
+ // scope, blast radius, constraints before PM starts.
169
+ this._dispatchScoping(feature, operatorDir, featureDescription);
164
170
 
165
171
  return feature;
166
172
  }
167
173
 
174
+ // ═══════════════════════════════════════════════════════════════════════════
175
+ // SCOPING PHASE (Operator ↔ User conversation before PM)
176
+ // ═══════════════════════════════════════════════════════════════════════════
177
+
178
+ /**
179
+ * Dispatch the Operator for feature scoping. This is the Operator's FIRST
180
+ * invocation — it converses with the user to establish scope, blast radius,
181
+ * and constraints. The same Operator session persists via --continue for
182
+ * the rest of the feature lifecycle, preserving scoping context.
183
+ *
184
+ * When done, the Operator writes:
185
+ * - .task in PM signal dir (structured PRD request)
186
+ * - .needs-repos in operator dir (repos to pull in)
187
+ * - .scoping-complete (triggers PM dispatch)
188
+ */
189
+ _dispatchScoping(feature, operatorDir, featureDescription) {
190
+ const agent = queries.getAgentByKey(`op-${feature.slug}`);
191
+ if (!agent) {
192
+ console.error(`[orchestrator] No operator agent for ${feature.slug}`);
193
+ return;
194
+ }
195
+
196
+ const projectRoot = PROJECT_ROOT;
197
+ const directoryMapPath = path.join(projectRoot, "DIRECTORY_MAP.MD");
198
+ const hasDirectoryMap = fs.existsSync(directoryMapPath);
199
+ const pmSignalDir = path.join(feature.signal_dir, "planning", "pm");
200
+
201
+ const prompt = buildScopingPrompt({
202
+ featureName: feature.slug,
203
+ featureDescription,
204
+ operatorDir,
205
+ pmSignalDir,
206
+ planDir: path.join(feature.signal_dir, "plans"),
207
+ directoryMapPath: hasDirectoryMap ? directoryMapPath : null,
208
+ projectRoot,
209
+ });
210
+
211
+ queries.updateFeaturePlanningRole(feature.id, "scoping");
212
+ queries.insertEvent(feature.id, "system", "bridge", "Scoping phase started — Operator conversing with user");
213
+
214
+ // Fresh session — this becomes the Operator's long-lived session
215
+ // that all future --continue invocations build on.
216
+ this.supervisor.spawn(agent.id, prompt, { continue: false });
217
+ console.log(`[orchestrator] Dispatched Operator scoping for ${feature.slug}`);
218
+ }
219
+
220
+ /**
221
+ * Handle .scoping-complete signal from Operator — transition to PM dispatch.
222
+ * At this point: .task exists in PM signal dir, .needs-repos may have been written.
223
+ */
224
+ async _handleScopingComplete(slug) {
225
+ const feature = queries.getFeatureBySlug(slug);
226
+ if (!feature) return;
227
+
228
+ // Process any pending .needs-repos that may not have been picked up yet
229
+ const tree = this._signalTrees[feature.slug];
230
+ if (tree?.operator) {
231
+ const reposFile = path.join(tree.operator, SIGNAL.NEEDS_REPOS);
232
+ if (fs.existsSync(reposFile)) {
233
+ const content = readSignal(reposFile, { deleteAfter: true });
234
+ if (content) await this._handleNeedsRepos(feature, content);
235
+ }
236
+ }
237
+
238
+ // Verify the PM .task file was written
239
+ const pmSignalDir = path.join(feature.signal_dir, "planning", "pm");
240
+ const taskFile = path.join(pmSignalDir, SIGNAL.TASK);
241
+ if (!fs.existsSync(taskFile)) {
242
+ console.warn(`[orchestrator] Scoping complete for ${slug} but no .task file for PM — using feature description`);
243
+ const meta = queries.getFeatureMetadata(feature.id);
244
+ const desc = meta.feature_description || feature.slug;
245
+ writeSignal(taskFile, `Feature request: ${desc}`);
246
+ }
247
+
248
+ queries.insertEvent(feature.id, "phase-transition", "bridge", "Scoping complete — dispatching PM");
249
+ await this.adapter.postPipelineMessage(feature.id, "Scoping complete. Starting Product Manager interview.");
250
+
251
+ // Dispatch PM with the structured .task from scoping
252
+ queries.updateFeaturePlanningRole(feature.id, "pm");
253
+ this.dispatchPlanningRole(feature.id, "pm");
254
+ }
255
+
168
256
  // ═══════════════════════════════════════════════════════════════════════════
169
257
  // PLANNING PIPELINE
170
258
  // ═══════════════════════════════════════════════════════════════════════════
@@ -381,18 +469,43 @@ export class Orchestrator {
381
469
  this._notifyOperatorOfDecision(feature, planDecision);
382
470
  }
383
471
  } else if (role === "ux-designer") {
384
- // Compound design step: ux-designer done dispatch ui-designer (no phase-review yet)
385
- await this.adapter.postPipelineMessage(feature.id,
386
- `UX Designer complete. Starting UI Designer...`);
387
- queries.updateFeaturePlanningRole(feature.id, "ui-designer");
388
- this.dispatchPlanningRole(feature.id, "ui-designer");
472
+ // Validate UX designer output before proceeding to UI designer
473
+ const planDir = path.join(feature.signal_dir, "plans");
474
+ const validation = this._validatePlanningOutput(role, planDir);
475
+ if (!validation.pass) {
476
+ await this.adapter.postPipelineMessage(feature.id,
477
+ `UX Designer output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
478
+ this._redispatchForMissingArtifacts(feature, role, validation);
479
+ } else {
480
+ // Compound design step: ux-designer done → dispatch ui-designer (no phase-review yet)
481
+ await this.adapter.postPipelineMessage(feature.id,
482
+ `UX Designer complete. Starting UI Designer...`);
483
+ queries.updateFeaturePlanningRole(feature.id, "ui-designer");
484
+ this.dispatchPlanningRole(feature.id, "ui-designer");
485
+ }
389
486
  } else if (role === "ui-designer") {
390
- // Compound design step: ui-designer done surface as "designer" phase-review
391
- // Both sub-roles are complete; present combined output for user approval
392
- await this._requestPhaseReview(feature, "designer", output);
487
+ // Validate UI designer output before surfacing combined design review
488
+ const planDir = path.join(feature.signal_dir, "plans");
489
+ const validation = this._validatePlanningOutput(role, planDir);
490
+ if (!validation.pass) {
491
+ await this.adapter.postPipelineMessage(feature.id,
492
+ `UI Designer output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
493
+ this._redispatchForMissingArtifacts(feature, role, validation);
494
+ } else {
495
+ // Both sub-roles complete; present combined output for user approval
496
+ await this._requestPhaseReview(feature, "designer", output);
497
+ }
393
498
  } else {
394
- // Non-final role summary + artifact upload + Block Kit review gate (all sequential)
395
- await this._requestPhaseReview(feature, role, output);
499
+ // All other roles (pm, architect, designer): validate then review gate
500
+ const planDir = path.join(feature.signal_dir, "plans");
501
+ const validation = this._validatePlanningOutput(role, planDir);
502
+ if (!validation.pass) {
503
+ await this.adapter.postPipelineMessage(feature.id,
504
+ `${PLANNING_ROLE_LABELS[role] || role} output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
505
+ this._redispatchForMissingArtifacts(feature, role, validation);
506
+ } else {
507
+ await this._requestPhaseReview(feature, role, output);
508
+ }
396
509
  }
397
510
 
398
511
  } finally {
@@ -401,6 +514,127 @@ export class Orchestrator {
401
514
  }
402
515
  }
403
516
 
517
+ /**
518
+ * Artifact requirements per planning role.
519
+ * Each entry: { label, check(planDir) → boolean }
520
+ */
521
+ static ROLE_ARTIFACTS = {
522
+ pm: [
523
+ { label: "PRD document (*-prd.md)", check: (d) => !!findArtifact("prd", d) },
524
+ ],
525
+ "ux-designer": [
526
+ { label: "design-decisions.md", check: (d) => !!findArtifact("design-decisions", d) },
527
+ ],
528
+ "ui-designer": [
529
+ { label: "design-decisions.md (with Visual Design Language)", check: (d) => {
530
+ const content = findArtifact("design-decisions", d);
531
+ if (!content) return false;
532
+ try {
533
+ const text = fs.readFileSync(content, "utf-8");
534
+ return text.includes("Visual Design Language") || text.includes("Color Palette");
535
+ } catch { return false; }
536
+ }},
537
+ { label: "mockup.html", check: (d) => fs.existsSync(path.join(d, "mockup.html")) },
538
+ ],
539
+ designer: [
540
+ { label: "design-decisions.md", check: (d) => !!findArtifact("design-decisions", d) },
541
+ { label: "mockup.html", check: (d) => fs.existsSync(path.join(d, "mockup.html")) },
542
+ ],
543
+ architect: [
544
+ { label: "context.md (investigation notes)", check: (d) => !!findArtifact("context", d) || !!findArtifact("architecture", d) },
545
+ { label: "plan.yaml (phase DAG)", check: (d) => fs.existsSync(path.join(d, "plan.yaml")) },
546
+ { label: "phases/ (task files)", check: (d) => {
547
+ try {
548
+ return fs.readdirSync(path.join(d, "phases"), { withFileTypes: true }).some(e => e.isDirectory());
549
+ } catch { return false; }
550
+ }},
551
+ { label: "journeys/ (test journeys)", check: (d) => {
552
+ try {
553
+ return fs.readdirSync(path.join(d, "journeys")).some(e => e.endsWith(".md"));
554
+ } catch { return false; }
555
+ }},
556
+ ],
557
+ };
558
+
559
+ /**
560
+ * Validate that a planning role produced its required artifacts.
561
+ * Runs auto-normalization (e.g. architecture.md → context.md) before checking.
562
+ * Returns { pass, found, missing }
563
+ */
564
+ _validatePlanningOutput(role, planDir) {
565
+ // Auto-normalize known filename drift
566
+ if (role === "architect") {
567
+ const archFile = path.join(planDir, "architecture.md");
568
+ const contextFile = path.join(planDir, "context.md");
569
+ if (fs.existsSync(archFile) && !fs.existsSync(contextFile)) {
570
+ try {
571
+ fs.renameSync(archFile, contextFile);
572
+ console.log(`[orchestrator] Renamed architecture.md → context.md in ${planDir}`);
573
+ } catch { /* ok */ }
574
+ }
575
+ }
576
+
577
+ const artifacts = Orchestrator.ROLE_ARTIFACTS[role];
578
+ if (!artifacts) return { pass: true, found: [], missing: [] };
579
+
580
+ const found = [];
581
+ const missing = [];
582
+ for (const art of artifacts) {
583
+ if (art.check(planDir)) {
584
+ found.push(art.label);
585
+ } else {
586
+ missing.push(art.label);
587
+ }
588
+ }
589
+
590
+ return { pass: missing.length === 0, found, missing };
591
+ }
592
+
593
+ /**
594
+ * Redispatch a planning role that produced incomplete output.
595
+ * Writes focused instructions to .user-message and re-spawns with fresh context.
596
+ */
597
+ _redispatchForMissingArtifacts(feature, role, validation) {
598
+ const slug = feature.slug;
599
+ console.log(`[orchestrator] ${role} output incomplete for ${slug}: missing ${validation.missing.join(", ")}`);
600
+
601
+ const signalDir = path.join(feature.signal_dir, "planning", role);
602
+ ensureDir(signalDir);
603
+
604
+ const roleName = PLANNING_ROLE_LABELS[role] || role;
605
+ const lines = [
606
+ `INCOMPLETE OUTPUT — REDISPATCH`,
607
+ ``,
608
+ `You signaled .done but required artifacts are missing.`,
609
+ `Read your CLAUDE.md output format section and produce the missing items.`,
610
+ ``,
611
+ `WHAT EXISTS (do NOT redo):`,
612
+ ...validation.found.map(f => ` ✓ ${f}`),
613
+ ``,
614
+ `WHAT IS MISSING (you MUST create these):`,
615
+ ...validation.missing.map(f => ` ✗ ${f}`),
616
+ ``,
617
+ `Write the missing artifacts to $PLAN_DIR/, then signal .done + .output again.`,
618
+ ];
619
+
620
+ // Role-specific guidance
621
+ if (role === "architect") {
622
+ lines.push(
623
+ ``,
624
+ `ARCHITECT-SPECIFIC:`,
625
+ `1. Read $PLAN_DIR/context.md for your investigation notes — do NOT re-explore the entire codebase`,
626
+ `2. Do targeted source code reads ONLY where needed for specific file paths or function signatures`,
627
+ `3. Every task file must cite real file paths and code evidence from the codebase`,
628
+ );
629
+ }
630
+
631
+ writeSignal(path.join(signalDir, SIGNAL.USER_MESSAGE), lines.join("\n"));
632
+ queries.updateFeaturePlanningRole(feature.id, role);
633
+
634
+ // Fresh context for the structured output pass
635
+ this.dispatchPlanningRole(feature.id, role, { continue: false });
636
+ }
637
+
404
638
  /**
405
639
  * Post phase summary, upload artifact, then post Block Kit approve/reject buttons.
406
640
  * Everything is sequential to guarantee correct message ordering.
@@ -1839,6 +2073,17 @@ export class Orchestrator {
1839
2073
  }
1840
2074
  });
1841
2075
 
2076
+ // Scoping complete — Operator finished scoping conversation, dispatch PM
2077
+ this.fileIO.on("planning:scopingComplete", async ({ slug, filePath }) => {
2078
+ try {
2079
+ // Clean up the signal file
2080
+ try { fs.unlinkSync(filePath); } catch { /* ok */ }
2081
+ await this._handleScopingComplete(slug);
2082
+ } catch (err) {
2083
+ console.error("[orchestrator] Scoping complete error:", err.message);
2084
+ }
2085
+ });
2086
+
1842
2087
  // Implementation signals — FL .agent-response routed through Operator relay
1843
2088
  this.fileIO.on("impl:response", async ({ slug, agent, filePath }) => {
1844
2089
  try {
@@ -2090,6 +2335,9 @@ export class Orchestrator {
2090
2335
  if (agentName !== "operator") return;
2091
2336
  const feature = queries.getFeatureBySlug(slug);
2092
2337
  if (!feature) return;
2338
+ // During scoping phase, the long-running scoping Operator polls for
2339
+ // .user-message directly — don't consume it with ephemeral Operator.
2340
+ if (feature.active_planning_role === "scoping") return;
2093
2341
  this._handleOperatorMessage(feature);
2094
2342
  });
2095
2343
 
@@ -2286,6 +2534,23 @@ export class Orchestrator {
2286
2534
 
2287
2535
  console.log(`[orchestrator] Operator exited with code ${exitCode} after ${elapsed}ms — scheduling retry`);
2288
2536
 
2537
+ // If still in scoping phase, re-dispatch scoping (not ephemeral Operator)
2538
+ if (feature.active_planning_role === "scoping") {
2539
+ const retried = this.supervisor.scheduleRetry(agentId, async () => {
2540
+ console.log(`[orchestrator] Operator scoping retry for ${feature.slug}`);
2541
+ const freshFeature = queries.getFeatureById(feature.id) || feature;
2542
+ const meta = queries.getFeatureMetadata(freshFeature.id);
2543
+ this._dispatchScoping(freshFeature, tree.operator, meta.feature_description || "");
2544
+ return null;
2545
+ });
2546
+ if (!retried) {
2547
+ console.error(`[orchestrator] Operator scoping for ${feature.slug} exhausted retries`);
2548
+ // Fallback: skip scoping and dispatch PM directly
2549
+ this._handleScopingComplete(feature.slug);
2550
+ }
2551
+ return;
2552
+ }
2553
+
2289
2554
  // Capture stashed context for the retry closure
2290
2555
  const stashedCtx = this._lastOperatorContext?.[feature.slug];
2291
2556
 
@@ -27,8 +27,8 @@ export function compilePlanReviewHtml(planDir) {
27
27
  tabs.push({ id: "design", label: "Design Decisions", content: designContent });
28
28
  }
29
29
 
30
- // Architecture (context.md)
31
- const contextContent = readArtifact(planDir, "context");
30
+ // Architecture (context.md or architecture.md — agents may use either name)
31
+ const contextContent = readArtifact(planDir, "context") || readArtifact(planDir, "architecture");
32
32
  if (contextContent) {
33
33
  tabs.push({ id: "architecture", label: "Architecture", content: contextContent });
34
34
  }
@@ -3,6 +3,170 @@
3
3
 
4
4
  import { IRIAI_TEAM_DIR, IMPL_BASE, V3_ROLES_DIR } from "./constants.js";
5
5
 
6
+ // ─── Scoping Phase (Operator as long-running scoping agent) ──────────────────
7
+
8
+ /**
9
+ * Build prompt for the scoping phase — Operator converses with user to
10
+ * establish scope, blast radius, and constraints before PM starts.
11
+ */
12
+ export function buildScopingPrompt({ featureName, featureDescription, operatorDir, pmSignalDir, planDir, directoryMapPath, projectRoot }) {
13
+ const directoryMapSection = directoryMapPath
14
+ ? `## Directory Map (Codebase Topology)
15
+ Read the directory map for high-level understanding of the project:
16
+ DIRECTORY_MAP=${directoryMapPath}
17
+
18
+ Use this to identify which repos/services are affected by this feature.
19
+ Do NOT read source code files — only use the directory map for repo identification.`
20
+ : `## No Directory Map Available
21
+ There is no DIRECTORY_MAP.MD in this project. Use the feature description
22
+ and your conversation with the user to understand the scope. You may explore
23
+ the top-level directory structure (ls, not reading source files) to identify repos.`;
24
+
25
+ return `You are the Operator for feature '${featureName}'.
26
+
27
+ Read your CLAUDE.md for your full role definition. This is your FIRST invocation —
28
+ your session will persist via --continue for the entire feature lifecycle.
29
+
30
+ ## CURRENT TASK: Feature Scoping
31
+
32
+ Before any planning agents start, you must scope this feature with the user.
33
+ Your job is boundary-setting — narrowing the sandbox that downstream agents work within.
34
+
35
+ You are NOT:
36
+ - Writing requirements (that's the PM's job)
37
+ - Making design decisions (that's the Designer's job)
38
+ - Investigating source code (that's the Architect's job)
39
+
40
+ You ARE:
41
+ - Establishing what repos/services are in scope
42
+ - Identifying blast radius (what else might be affected)
43
+ - Capturing non-functional requirements and constraints
44
+ - Recording any upfront decisions the user already has opinions on
45
+ - Identifying if new repos need to be created
46
+
47
+ ## Signal Directories
48
+ OPERATOR_DIR=${operatorDir}
49
+ PM_SIGNAL_DIR=${pmSignalDir}
50
+ PLAN_DIR=${planDir}
51
+ PROJECT_ROOT=${projectRoot}
52
+
53
+ ${directoryMapSection}
54
+
55
+ ## Feature Description (from user)
56
+ ${featureDescription || featureName}
57
+
58
+ ## Communication Protocol
59
+ 1. To send a message to the user: write to .agent-response
60
+ cat > ${operatorDir}/.agent-response << 'MSG_EOF'
61
+ your message here
62
+ MSG_EOF
63
+ The bridge posts it to the user and deletes the file.
64
+
65
+ 2. To receive a reply: poll for .user-message
66
+ while [ ! -f ${operatorDir}/.user-message ]; do sleep 5; done
67
+ MSG=$(cat ${operatorDir}/.user-message) && rm -f ${operatorDir}/.user-message
68
+
69
+ 3. After each .agent-response write, wait 2 seconds before polling.
70
+
71
+ Format for mobile: under 300 words per message, numbered options, bold key questions.
72
+
73
+ ## Conversation Flow
74
+
75
+ 1. **Read the directory map** (if available) to understand the project topology.
76
+
77
+ 2. **Greet the user and confirm the feature request.** Restate your understanding
78
+ of what they want in 2-3 sentences. Ask if that's correct.
79
+
80
+ 3. **Ask scoping questions one at a time.** Focus on:
81
+ - What repos/services are affected? (Use directory map to suggest candidates)
82
+ - Is this extending existing functionality or building something new?
83
+ - If new: what's the new repo/app name and where does it live?
84
+ - What are the constraints? (Performance, security, accessibility, compatibility)
85
+ - Are there any hard non-negotiable requirements?
86
+ - What is explicitly OUT of scope?
87
+
88
+ 4. **Keep it brief.** 3-5 questions max. Don't duplicate PM/Designer/Architect work.
89
+ If the user gives you enough context in the first message, you can skip to the summary.
90
+
91
+ 5. **Summarize and confirm.** Present a scoping summary for user approval:
92
+ - Scope (what's being built)
93
+ - Affected repos/services
94
+ - New repos to create (if any)
95
+ - Constraints/NFRs
96
+ - Out of scope
97
+
98
+ ## When Scoping Is Complete
99
+
100
+ After the user confirms your summary, do THREE things in order:
101
+
102
+ ### 1. Write the structured PRD request for the PM
103
+ Write a .task file in the PM's signal directory. This is the PM's starting brief.
104
+
105
+ cat > ${pmSignalDir}/.task << 'TASK_EOF'
106
+ FEATURE: ${featureName}
107
+
108
+ ## User's Request
109
+ <restate the feature request clearly>
110
+
111
+ ## Scope
112
+ <what is in scope — be specific>
113
+
114
+ ## Affected Repos/Services
115
+ <list repos identified from directory map + conversation>
116
+
117
+ ## New Repos
118
+ <any new repos to create, or "none">
119
+
120
+ ## Constraints & Non-Functional Requirements
121
+ <performance, security, accessibility, compatibility requirements>
122
+
123
+ ## Out of Scope
124
+ <what is explicitly excluded>
125
+
126
+ ## User Decisions
127
+ <any decisions the user made during scoping>
128
+ TASK_EOF
129
+
130
+ ### 2. Write .needs-repos
131
+ Identify the repos from the directory map that need worktrees.
132
+ Write their paths (relative to PROJECT_ROOT) to .needs-repos.
133
+ ${directoryMapPath ? `Read DIRECTORY_MAP for the exact paths.` : `List the repo paths based on your conversation with the user.`}
134
+
135
+ For existing repos:
136
+ cat > ${operatorDir}/.needs-repos << 'REPOS_EOF'
137
+ path/to/repo1
138
+ path/to/repo2
139
+ REPOS_EOF
140
+
141
+ For NEW repos that don't exist yet, use the + prefix:
142
+ +<local-path>:<github-name>[:<template>]
143
+
144
+ Available templates: fastapi-postgres, react-parcel
145
+ If no template fits, omit it (bare scaffold with README + .gitignore).
146
+
147
+ ### 3. Signal scoping complete
148
+ echo "DONE" > ${operatorDir}/.scoping-complete
149
+
150
+ This tells the bridge to dispatch the PM with your .task file.
151
+ After this signal, you will continue operating as the Operator for this feature
152
+ (relaying messages, routing decisions, pulling in additional repos as needed).
153
+
154
+ ## Important
155
+ - Do NOT write .scoping-complete until AFTER .task and .needs-repos are written
156
+ - Do NOT read source code — only the directory map for high-level repo identification
157
+ - Keep the conversation focused — you're setting boundaries, not doing analysis
158
+ - If the user wants to skip scoping (e.g., "just build it"), write a minimal .task
159
+ from the feature description and proceed
160
+
161
+ ## Context Preservation
162
+ This scoping conversation is critical context for your entire lifecycle as Operator.
163
+ The repos you identified, the user's constraints, and their decisions must be preserved.
164
+ If you ever need to write a .handover file for context refresh, ALWAYS include the full
165
+ scoping summary (repos, scope, constraints, user decisions) verbatim — do NOT summarize
166
+ or truncate it.
167
+ `;
168
+ }
169
+
6
170
  // ─── Operator (Ephemeral) ────────────────────────────────────────────────────
7
171
 
8
172
  /**
@@ -249,7 +249,7 @@ export class SlackAdapter {
249
249
  const slug = slugify(featureDesc);
250
250
 
251
251
  if (this._orchestrator) {
252
- await this._orchestrator.initializeFeature(slug, messageTs, userId);
252
+ await this._orchestrator.initializeFeature(slug, messageTs, userId, featureDesc);
253
253
  }
254
254
  }
255
255