@agentic-coding-framework/orchestrator-core 0.7.3 → 0.8.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/dist/dispatch.js CHANGED
@@ -25,12 +25,54 @@ exports.queryProjectStatus = queryProjectStatus;
25
25
  exports.listProjects = listProjects;
26
26
  exports.startCustom = startCustom;
27
27
  exports.rollback = rollback;
28
+ exports.reopen = reopen;
29
+ exports.review = review;
30
+ exports.triage = triage;
28
31
  exports.checkPrerequisites = checkPrerequisites;
29
32
  exports.generateChecklist = generateChecklist;
30
33
  const fs_1 = require("fs");
31
34
  const path_1 = require("path");
32
35
  const state_1 = require("./state");
33
36
  const rules_1 = require("./rules");
37
+ // ─── Config Constants ────────────────────────────────────────────────────────
38
+ /** After N stories reach "done", automatically suggest/trigger a review session */
39
+ const REVIEW_TRIGGER_THRESHOLD = 3;
40
+ /**
41
+ * Step name alias map — CC executors derive step names from display_name
42
+ * or prompt context, which may not match ACO's internal step identifiers.
43
+ * Applied during applyHandoff() to normalize HANDOFF.step before the
44
+ * stale guard comparison.
45
+ *
46
+ * [FIX P0] Without this, valid HANDOFFs are rejected as "stale" because
47
+ * e.g. "api-contract" !== "contract", causing the pipeline to stall.
48
+ */
49
+ const STEP_ALIAS_MAP = {
50
+ // contract step aliases
51
+ "api-contract": "contract",
52
+ "api_contract": "contract",
53
+ "contract-update": "contract",
54
+ "api-contract-update": "contract",
55
+ // scaffold step aliases
56
+ "test-scaffolding": "scaffold",
57
+ "test_scaffolding": "scaffold",
58
+ "scaffolding": "scaffold",
59
+ "test-scaffold": "scaffold",
60
+ // sdd-delta aliases
61
+ "sdd_delta": "sdd-delta",
62
+ "sdd": "sdd-delta",
63
+ "delta": "sdd-delta",
64
+ "delta-spec": "sdd-delta",
65
+ // verify aliases
66
+ "quality-gate": "verify",
67
+ "quality_gate": "verify",
68
+ // update-memory aliases
69
+ "update_memory": "update-memory",
70
+ "memory-update": "update-memory",
71
+ // impl aliases
72
+ "implementation": "impl",
73
+ // commit aliases
74
+ "commit-changes": "commit",
75
+ };
34
76
  // ─── Main Dispatch Function ──────────────────────────────────────────────────
35
77
  /**
36
78
  * Core dispatch logic — direct translation of Protocol's dispatch(project).
@@ -167,13 +209,25 @@ function _dispatchInner(projectRoot, state, dryRun) {
167
209
  state.files_changed = [];
168
210
  // Check if we just reached "done"
169
211
  if (state.step === "done") {
212
+ // Clear reopened_from when story completes
213
+ state.reopened_from = null;
214
+ // Feature 3: Check if review should be triggered
215
+ let review_suggested = false;
216
+ const completedCount = countCompletedStories(projectRoot);
217
+ if (completedCount >= REVIEW_TRIGGER_THRESHOLD) {
218
+ review_suggested = true;
219
+ }
170
220
  if (!dryRun)
171
221
  (0, state_1.writeState)(projectRoot, state);
172
- return {
222
+ const result = {
173
223
  type: "done",
174
224
  story: state.story ?? "(no story)",
175
225
  summary: `Story ${state.story} completed. All steps passed.`,
176
226
  };
227
+ if (review_suggested) {
228
+ result.review_suggested = true;
229
+ }
230
+ return result;
177
231
  }
178
232
  // Recurse: the new step might also require human
179
233
  const newRule = (0, rules_1.getRule)(state.step);
@@ -208,7 +262,25 @@ function _dispatchInner(projectRoot, state, dryRun) {
208
262
  : "No specific reason."),
209
263
  };
210
264
  }
211
- const target = (0, rules_1.getFailTarget)(state.step, state.reason);
265
+ // Feature 1: Escalation logic for post-reopen verify failures
266
+ // If verify fails after reopen with no explicit reason, escalate by rolling back one step deeper
267
+ let target = (0, rules_1.getFailTarget)(state.step, state.reason);
268
+ if (state.step === "verify" &&
269
+ state.reopened_from !== null &&
270
+ state.reason === null // only escalate for pure RED failure (no specific reason)
271
+ ) {
272
+ // Find the step before reopened_from
273
+ const earlierStep = getEarlierStep(state.reopened_from);
274
+ if (earlierStep !== null) {
275
+ // Escalate to the earlier step (overriding normal routing)
276
+ target = earlierStep;
277
+ if (!dryRun) {
278
+ (0, state_1.appendLog)(projectRoot, "INFO", "dispatch", `Post-reopen verify failure escalation: ${state.step} → ${earlierStep} (was reopened at ${state.reopened_from})`);
279
+ }
280
+ // Clear reopened_from so we only escalate once
281
+ state.reopened_from = null;
282
+ }
283
+ }
212
284
  if (target !== state.step) {
213
285
  // Route to different step
214
286
  state.step = target;
@@ -244,12 +316,12 @@ function _dispatchInner(projectRoot, state, dryRun) {
244
316
  lines.push("");
245
317
  prompt = prompt + "\n" + lines.join("\n");
246
318
  }
319
+ // Detect framework adoption level so caller knows the context richness
320
+ const framework = detectFramework(projectRoot);
247
321
  if (!dryRun) {
248
322
  const running = (0, state_1.markRunning)(state);
249
323
  (0, state_1.writeState)(projectRoot, running);
250
324
  }
251
- // Detect framework adoption level so caller knows the context richness
252
- const framework = detectFramework(projectRoot);
253
325
  return {
254
326
  type: "dispatched",
255
327
  project: state.project,
@@ -568,6 +640,16 @@ function applyHandoff(projectRoot) {
568
640
  message: state.last_error,
569
641
  };
570
642
  }
643
+ // [FIX P0] Normalize HANDOFF step name — CC executors may write display-name
644
+ // variants (e.g. "api-contract" instead of "contract", "test-scaffolding"
645
+ // instead of "scaffold"). Without this, valid HANDOFFs are rejected as stale.
646
+ if (handoff.step) {
647
+ const normalized = STEP_ALIAS_MAP[handoff.step.toLowerCase()];
648
+ if (normalized) {
649
+ (0, state_1.appendLog)(projectRoot, "INFO", "applyHandoff", `Normalized HANDOFF step: "${handoff.step}" → "${normalized}"`);
650
+ handoff.step = normalized;
651
+ }
652
+ }
571
653
  // [FIX P0] Stale HANDOFF guard: if HANDOFF.step doesn't match STATE.step,
572
654
  // this HANDOFF is from a previous step (e.g., hook re-fires after dispatch
573
655
  // already advanced). Applying it would overwrite the current step's status
@@ -1103,6 +1185,393 @@ function rollback(projectRoot, targetStep, options = {}) {
1103
1185
  message: `Rolled back to "${targetStep}" (attempt 1, status: pending)`,
1104
1186
  };
1105
1187
  }
1188
+ // ─── Helper Functions for Features ────────────────────────────────────────────
1189
+ /**
1190
+ * Count completed stories since last review by reading .ai/history.md
1191
+ * and matching entries with reopen pattern.
1192
+ */
1193
+ function countCompletedStories(projectRoot) {
1194
+ try {
1195
+ const historyPath = (0, path_1.join)(projectRoot, ".ai", "history.md");
1196
+ if (!(0, fs_1.existsSync)(historyPath))
1197
+ return 0;
1198
+ const content = (0, fs_1.readFileSync)(historyPath, "utf-8");
1199
+ // Count lines that match "### Reopen" pattern (each represents a completed story that was reopened)
1200
+ // Plus we need to count stories that completed naturally
1201
+ // For now, count any story-related lines in history
1202
+ const matches = content.match(/### Reopen/g) || [];
1203
+ return matches.length;
1204
+ }
1205
+ catch {
1206
+ return 0;
1207
+ }
1208
+ }
1209
+ /**
1210
+ * Find the step that comes before a given step in the sequence.
1211
+ * Returns null if the step is the first one (no earlier step).
1212
+ */
1213
+ function getEarlierStep(step) {
1214
+ const sequence = (0, rules_1.getStepSequence)();
1215
+ const index = sequence.indexOf(step);
1216
+ if (index <= 0)
1217
+ return null;
1218
+ return sequence[index - 1];
1219
+ }
1220
+ // ─── Reopen ──────────────────────────────────────────────────────────────────
1221
+ /**
1222
+ * Reopen a completed story at a specified step.
1223
+ *
1224
+ * Unlike rollback() which moves backwards within an active story,
1225
+ * reopen() specifically targets stories that have reached step "done".
1226
+ * It resets state to the target step so the pipeline can re-execute from there.
1227
+ *
1228
+ * Use case: Review Session or triage found issues in a completed US —
1229
+ * human decides to reopen it at a specific step (e.g., "impl", "verify").
1230
+ *
1231
+ * Guards:
1232
+ * - Story must be in "done" state (use rollback for active stories)
1233
+ * - Target step must be a valid pipeline step
1234
+ * - Cannot reopen to "done" (that's a no-op)
1235
+ *
1236
+ * [v0.8.0] FB-009: Review → Triage → Re-entry
1237
+ */
1238
+ function reopen(projectRoot, targetStep, options = {}) {
1239
+ let state;
1240
+ try {
1241
+ state = (0, state_1.readState)(projectRoot);
1242
+ }
1243
+ catch (err) {
1244
+ return { type: "error", code: "STATE_NOT_FOUND", message: err.message, recoverable: false };
1245
+ }
1246
+ // Guard: story must be completed
1247
+ if (state.step !== "done") {
1248
+ (0, state_1.appendLog)(projectRoot, "ERROR", "reopen", `NOT_DONE: current step is "${state.step}", not "done". Use rollback instead.`);
1249
+ return {
1250
+ type: "error",
1251
+ code: "NOT_DONE",
1252
+ message: `Cannot reopen: story is at step "${state.step}", not "done". Use "rollback" for active stories.`,
1253
+ recoverable: false,
1254
+ };
1255
+ }
1256
+ // Cannot reopen to "done" (check before sequence validation since "done" isn't in sequence)
1257
+ if (targetStep === "done") {
1258
+ return {
1259
+ type: "error",
1260
+ code: "REOPEN_TO_DONE",
1261
+ message: `Cannot reopen to "done" — story is already done.`,
1262
+ recoverable: false,
1263
+ };
1264
+ }
1265
+ // Validate target step
1266
+ const sequence = (0, rules_1.getStepSequence)();
1267
+ const targetIndex = targetStep === "bootstrap" ? -1 : sequence.indexOf(targetStep);
1268
+ if (targetStep !== "bootstrap" && targetIndex === -1) {
1269
+ (0, state_1.appendLog)(projectRoot, "ERROR", "reopen", `INVALID_TARGET: "${targetStep}"`);
1270
+ return {
1271
+ type: "error",
1272
+ code: "INVALID_TARGET",
1273
+ message: `Invalid reopen target "${targetStep}". Valid steps: bootstrap, ${sequence.join(", ")}`,
1274
+ recoverable: false,
1275
+ };
1276
+ }
1277
+ // Reset state to target step
1278
+ const previousStep = state.step; // "done"
1279
+ const rule = targetStep === "bootstrap"
1280
+ ? (0, rules_1.getRule)("bootstrap")
1281
+ : (0, rules_1.getRule)(targetStep);
1282
+ state.step = targetStep;
1283
+ state.status = "pending";
1284
+ state.attempt = 1;
1285
+ state.max_attempts = rule.max_attempts;
1286
+ state.timeout_min = rule.timeout_min;
1287
+ state.reason = null;
1288
+ state.last_error = null;
1289
+ state.files_changed = [];
1290
+ state.dispatched_at = null;
1291
+ state.completed_at = null;
1292
+ state.tests = null;
1293
+ state.failing_tests = [];
1294
+ state.lint_pass = null;
1295
+ state.human_note = options.humanNote ?? null;
1296
+ state.reopened_from = targetStep; // Feature 1: Track reopen target for escalation
1297
+ (0, state_1.writeState)(projectRoot, state);
1298
+ (0, state_1.appendLog)(projectRoot, "INFO", "reopen", `Reopened story "${state.story}" from "${previousStep}" to "${targetStep}"${options.humanNote ? ` note: "${options.humanNote}"` : ""}`);
1299
+ // Feature 2: Append entry to .ai/history.md
1300
+ const isoTimestamp = new Date().toISOString();
1301
+ const historyEntry = `### Reopen — ${state.story} → ${targetStep}
1302
+ - **Date**: ${isoTimestamp}
1303
+ - **From**: ${previousStep} → ${targetStep}
1304
+ - **Note**: ${options.humanNote ?? "N/A"}`;
1305
+ (0, state_1.appendHistory)(projectRoot, historyEntry);
1306
+ return {
1307
+ type: "ok",
1308
+ state,
1309
+ message: `Reopened story "${state.story}" at step "${targetStep}" (attempt 1, status: pending)`,
1310
+ };
1311
+ }
1312
+ // ─── Review (On-Demand Review Session) ───────────────────────────────────────
1313
+ /**
1314
+ * Generate a Review Session prompt for the current project.
1315
+ *
1316
+ * This is a STATELESS operation — it reads project state but does NOT
1317
+ * mutate STATE.json. The caller (CC executor) receives the prompt and
1318
+ * runs the review; results are acted on by the human (reopen, new US, etc.).
1319
+ *
1320
+ * Works on non-ACF projects too (detectFramework level 0): generates a
1321
+ * lighter review prompt based on whatever files exist.
1322
+ *
1323
+ * Review Session checks (from Lifecycle v0.9):
1324
+ * 1. Code Review — diff quality, naming, duplication
1325
+ * 2. Spec-Code Coherence — BDD ↔ tests ↔ impl alignment
1326
+ * 3. Regression — all existing tests still pass
1327
+ * 4. Security Scan — no hardcoded secrets, .gitignore coverage
1328
+ * 5. Memory Audit — PROJECT_MEMORY accuracy
1329
+ *
1330
+ * [v0.8.0] FB-009: Review → Triage → Re-entry
1331
+ */
1332
+ function review(projectRoot) {
1333
+ const framework = detectFramework(projectRoot);
1334
+ const prompt = buildReviewPrompt(projectRoot, framework.level);
1335
+ return {
1336
+ type: "review_prompt",
1337
+ prompt,
1338
+ fw_lv: framework.level,
1339
+ };
1340
+ }
1341
+ /**
1342
+ * Build the review session prompt based on framework adoption level.
1343
+ */
1344
+ function buildReviewPrompt(projectRoot, fwLevel) {
1345
+ const lines = [];
1346
+ lines.push("═══════════════════════════════════════════════════════════════");
1347
+ lines.push(" ON-DEMAND REVIEW SESSION");
1348
+ lines.push("═══════════════════════════════════════════════════════════════");
1349
+ lines.push("");
1350
+ if (fwLevel >= 2) {
1351
+ // Full ACF project — rich review
1352
+ let state = null;
1353
+ try {
1354
+ state = (0, state_1.readState)(projectRoot);
1355
+ }
1356
+ catch { /* no state */ }
1357
+ lines.push(`Project: ${state?.project ?? "(unknown)"}`);
1358
+ lines.push(`Story: ${state?.story ?? "(no active story)"}`);
1359
+ lines.push(`Current Step: ${state?.step ?? "(unknown)"}`);
1360
+ lines.push("");
1361
+ lines.push("## Review Checklist");
1362
+ lines.push("");
1363
+ lines.push("### 1. Code Review");
1364
+ lines.push("- Check recent changes for naming consistency, duplication, dead code");
1365
+ lines.push("- Verify diff-only discipline: only files related to the story should be modified");
1366
+ lines.push("");
1367
+ lines.push("### 2. Spec-Code Coherence");
1368
+ lines.push("- Read BDD scenarios in docs/bdd/ and verify each has a corresponding test");
1369
+ lines.push("- Read SDD Delta in docs/deltas/ and verify implementation matches");
1370
+ lines.push("- Check that API contracts in docs/api/ are synchronized");
1371
+ lines.push("");
1372
+ lines.push("### 3. Regression");
1373
+ lines.push("- Run the project's test suite and confirm all tests pass");
1374
+ lines.push("- If tests fail, record them in ISSUES section of PROJECT_MEMORY.md");
1375
+ lines.push("");
1376
+ lines.push("### 4. Security Scan");
1377
+ lines.push("- grep for common secret patterns: password=, apikey=, token=, BEGIN RSA PRIVATE KEY");
1378
+ lines.push("- Verify .gitignore covers: .env, *.key, credentials.json, *.pem");
1379
+ lines.push("- Confirm test fixtures use mock/fake values, not real credentials");
1380
+ lines.push("");
1381
+ lines.push("### 5. Memory Audit");
1382
+ lines.push("- Read PROJECT_MEMORY.md and verify NOW/TESTS/NEXT/ISSUES sections are accurate");
1383
+ lines.push("- Check .ai/history.md for completeness");
1384
+ lines.push("- Verify HANDOFF.md reflects the latest session state");
1385
+ }
1386
+ else if (fwLevel === 1) {
1387
+ // Partial ACF — moderate review
1388
+ lines.push("## Review Checklist (Partial ACF Project)");
1389
+ lines.push("");
1390
+ lines.push("### 1. Code Review");
1391
+ lines.push("- Check recent changes for quality, naming, duplication");
1392
+ lines.push("");
1393
+ lines.push("### 2. Spec Coherence");
1394
+ lines.push("- If BDD/SDD docs exist, verify alignment with code");
1395
+ lines.push("");
1396
+ lines.push("### 3. Regression");
1397
+ lines.push("- Run available test suite and confirm all tests pass");
1398
+ lines.push("");
1399
+ lines.push("### 4. Security Scan");
1400
+ lines.push("- grep for hardcoded secrets (password=, apikey=, token=)");
1401
+ lines.push("- Verify .gitignore covers sensitive file patterns");
1402
+ lines.push("");
1403
+ lines.push("### 5. Memory Check");
1404
+ lines.push("- If PROJECT_MEMORY.md exists, verify it is up to date");
1405
+ }
1406
+ else {
1407
+ // Non-ACF project — lightweight review
1408
+ lines.push("## Review Checklist (Non-ACF Project)");
1409
+ lines.push("");
1410
+ lines.push("### 1. Code Review");
1411
+ lines.push("- Review recent git changes (git log + git diff) for quality");
1412
+ lines.push("- Check for dead code, duplication, naming issues");
1413
+ lines.push("");
1414
+ lines.push("### 2. Test Check");
1415
+ lines.push("- Locate and run the project's test suite (if any)");
1416
+ lines.push("- Report test results");
1417
+ lines.push("");
1418
+ lines.push("### 3. Security Scan");
1419
+ lines.push("- grep for hardcoded secrets (password=, apikey=, token=)");
1420
+ lines.push("- Check .gitignore for sensitive patterns");
1421
+ }
1422
+ lines.push("");
1423
+ lines.push("═══════════════════════════════════════════════════════════════");
1424
+ lines.push(" OUTPUT FORMAT");
1425
+ lines.push("═══════════════════════════════════════════════════════════════");
1426
+ lines.push("");
1427
+ lines.push("For each check, report:");
1428
+ lines.push(" PASS — <brief note>");
1429
+ lines.push(" WARN — <issue description>");
1430
+ lines.push(" FAIL — <issue description + suggested action>");
1431
+ lines.push("");
1432
+ lines.push("At the end, provide a summary with recommended actions:");
1433
+ lines.push(" - REOPEN <US-XXX> at <step> — if existing story needs rework");
1434
+ lines.push(" - NEW US — <brief description> — if a new story is needed");
1435
+ lines.push(" - ISSUE — <description> — record in PROJECT_MEMORY.md ISSUES");
1436
+ lines.push(" - ALL CLEAR — no issues found");
1437
+ lines.push("");
1438
+ return lines.join("\n");
1439
+ }
1440
+ // ─── Triage (ISSUES → Actionable Plan) ──────────────────────────────────────
1441
+ /**
1442
+ * Read unfixed ISSUES from PROJECT_MEMORY.md and generate a triage prompt.
1443
+ *
1444
+ * This is a STATELESS operation — reads files but does NOT mutate STATE.json.
1445
+ * The triage prompt asks the executor (CC) to classify each issue and
1446
+ * recommend actions (reopen US, create new US, or dismiss).
1447
+ *
1448
+ * Requires PROJECT_MEMORY.md to exist with an ISSUES section.
1449
+ *
1450
+ * [v0.8.0] FB-009: Review → Triage → Re-entry
1451
+ */
1452
+ function triage(projectRoot) {
1453
+ const framework = detectFramework(projectRoot);
1454
+ // Read PROJECT_MEMORY.md
1455
+ const memoryPath = (0, path_1.join)(projectRoot, "PROJECT_MEMORY.md");
1456
+ if (!(0, fs_1.existsSync)(memoryPath)) {
1457
+ return {
1458
+ type: "error",
1459
+ code: "NO_MEMORY",
1460
+ message: "PROJECT_MEMORY.md not found. Cannot triage without ISSUES section.",
1461
+ recoverable: false,
1462
+ };
1463
+ }
1464
+ const memory = (0, fs_1.readFileSync)(memoryPath, "utf-8");
1465
+ const issues = parseIssuesFromMemory(memory);
1466
+ if (issues.length === 0) {
1467
+ return {
1468
+ type: "error",
1469
+ code: "NO_ISSUES",
1470
+ message: "No unfixed ISSUES found in PROJECT_MEMORY.md. Nothing to triage.",
1471
+ recoverable: false,
1472
+ };
1473
+ }
1474
+ const prompt = buildTriagePrompt(projectRoot, issues, framework.level);
1475
+ return {
1476
+ type: "triage_prompt",
1477
+ prompt,
1478
+ issues,
1479
+ fw_lv: framework.level,
1480
+ };
1481
+ }
1482
+ /**
1483
+ * Parse ISSUES lines from PROJECT_MEMORY.md.
1484
+ * Looks for lines starting with "- [ ]" under a section containing "ISSUES".
1485
+ * Filters out already-checked items "- [x]".
1486
+ */
1487
+ function parseIssuesFromMemory(memory) {
1488
+ const lines = memory.split("\n");
1489
+ let inIssuesSection = false;
1490
+ const issues = [];
1491
+ for (const line of lines) {
1492
+ // Detect ISSUES section header (## ISSUES, ### ISSUES, or just ISSUES:)
1493
+ if (/^#{1,4}\s*ISSUES/i.test(line) || /^ISSUES\s*:/i.test(line)) {
1494
+ inIssuesSection = true;
1495
+ continue;
1496
+ }
1497
+ // Exit ISSUES section on next header
1498
+ if (inIssuesSection && /^#{1,4}\s/.test(line) && !/ISSUES/i.test(line)) {
1499
+ inIssuesSection = false;
1500
+ continue;
1501
+ }
1502
+ // Collect unchecked items
1503
+ if (inIssuesSection && /^-\s*\[\s*\]/.test(line)) {
1504
+ issues.push(line.replace(/^-\s*\[\s*\]\s*/, "").trim());
1505
+ }
1506
+ }
1507
+ return issues;
1508
+ }
1509
+ /**
1510
+ * Build the triage prompt with parsed issues.
1511
+ */
1512
+ function buildTriagePrompt(projectRoot, issues, fwLevel) {
1513
+ const lines = [];
1514
+ lines.push("═══════════════════════════════════════════════════════════════");
1515
+ lines.push(" TRIAGE SESSION");
1516
+ lines.push("═══════════════════════════════════════════════════════════════");
1517
+ lines.push("");
1518
+ let state = null;
1519
+ try {
1520
+ state = (0, state_1.readState)(projectRoot);
1521
+ }
1522
+ catch { /* no state */ }
1523
+ if (state) {
1524
+ lines.push(`Project: ${state.project ?? "(unknown)"}`);
1525
+ lines.push(`Current Story: ${state.story ?? "(none)"}`);
1526
+ lines.push(`Current Step: ${state.step}`);
1527
+ lines.push("");
1528
+ }
1529
+ lines.push(`## Unfixed ISSUES (${issues.length})`);
1530
+ lines.push("");
1531
+ for (let i = 0; i < issues.length; i++) {
1532
+ lines.push(` ${i + 1}. ${issues[i]}`);
1533
+ }
1534
+ lines.push("");
1535
+ lines.push("## Triage Instructions");
1536
+ lines.push("");
1537
+ lines.push("For each issue above, analyze and classify:");
1538
+ lines.push("");
1539
+ lines.push(" A. REOPEN <US-XXX> at <step>");
1540
+ lines.push(" → Issue belongs to an existing story; reopen it at the appropriate step.");
1541
+ lines.push(" → The human will run: orchestrator reopen <project> <step>");
1542
+ lines.push("");
1543
+ lines.push(" B. NEW US: <title>");
1544
+ lines.push(" → Issue requires a new User Story. Write a 1-2 sentence description.");
1545
+ lines.push(" → The human will create the US and run: orchestrator start-story <project> <US-XXX>");
1546
+ lines.push("");
1547
+ lines.push(" C. DISMISS: <reason>");
1548
+ lines.push(" → Issue is resolved, duplicate, or no longer relevant.");
1549
+ lines.push(" → The human will mark it [x] in PROJECT_MEMORY.md.");
1550
+ lines.push("");
1551
+ if (fwLevel >= 2) {
1552
+ lines.push("## Context Files");
1553
+ lines.push("Read these files to inform your triage decisions:");
1554
+ lines.push(" - PROJECT_MEMORY.md (full context: NOW, TESTS, NEXT, ISSUES)");
1555
+ lines.push(" - .ai/history.md (completed work log)");
1556
+ lines.push(" - docs/bdd/ (BDD scenarios for existing stories)");
1557
+ lines.push(" - docs/deltas/ (SDD deltas for existing stories)");
1558
+ lines.push("");
1559
+ }
1560
+ lines.push("═══════════════════════════════════════════════════════════════");
1561
+ lines.push(" OUTPUT FORMAT");
1562
+ lines.push("═══════════════════════════════════════════════════════════════");
1563
+ lines.push("");
1564
+ lines.push("Produce a triage plan as a numbered list matching the issues above:");
1565
+ lines.push("");
1566
+ lines.push(" 1. [A] REOPEN US-001 at impl — <reason>");
1567
+ lines.push(" 2. [B] NEW US: \"Add input validation for email field\" — <reason>");
1568
+ lines.push(" 3. [C] DISMISS — resolved in commit abc123");
1569
+ lines.push("");
1570
+ lines.push("End with a SUMMARY: X to reopen, Y new US, Z dismissed.");
1571
+ lines.push("This plan is HUMAN-GATED — no actions will be taken automatically.");
1572
+ lines.push("");
1573
+ return lines.join("\n");
1574
+ }
1106
1575
  /**
1107
1576
  * Check if prerequisite files (claude_reads) exist before dispatching.
1108
1577
  * Only checks concrete paths (skips wildcards like *.go, **\/*.ts).