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 +1 -1
- package/v3/adapters/slack-adapter.js +1 -1
- package/v3/constants.js +1 -0
- package/v3/file-io.js +2 -0
- package/v3/orchestrator.js +283 -18
- package/v3/plan-compiler.js +2 -2
- package/v3/prompt-builder.js +164 -0
- package/v3/slack-adapter.js +1 -1
package/package.json
CHANGED
|
@@ -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
|
}
|
package/v3/orchestrator.js
CHANGED
|
@@ -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
|
|
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]*
|
|
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
|
|
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.
|
|
162
|
-
|
|
163
|
-
this.
|
|
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
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
//
|
|
395
|
-
|
|
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
|
|
package/v3/plan-compiler.js
CHANGED
|
@@ -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
|
}
|
package/v3/prompt-builder.js
CHANGED
|
@@ -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
|
/**
|
package/v3/slack-adapter.js
CHANGED
|
@@ -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
|
|