@agentic-coding-framework/orchestrator-core 0.7.2 → 0.8.0

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,18 @@ 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;
34
40
  // ─── Main Dispatch Function ──────────────────────────────────────────────────
35
41
  /**
36
42
  * Core dispatch logic — direct translation of Protocol's dispatch(project).
@@ -104,7 +110,7 @@ function _dispatchInner(projectRoot, state, dryRun) {
104
110
  };
105
111
  }
106
112
  const rule = (0, rules_1.getRule)(state.step);
107
- // ── Timeout check ──
113
+ // ── Running state check ──
108
114
  if (state.status === "running") {
109
115
  if ((0, state_1.isTimedOut)(state)) {
110
116
  const elapsed = elapsedMinutes(state.dispatched_at);
@@ -122,13 +128,17 @@ function _dispatchInner(projectRoot, state, dryRun) {
122
128
  last_error: state.last_error,
123
129
  };
124
130
  }
125
- // Still running
126
- return {
127
- type: "already_running",
128
- step: state.step,
129
- elapsed_min: elapsedMinutes(state.dispatched_at),
130
- last_error: state.last_error,
131
- };
131
+ // Still running — real dispatch returns "already_running",
132
+ // but peek (dryRun) falls through to generate the prompt so
133
+ // dispatch-claude-code.sh can use it after external dispatch.
134
+ if (!dryRun) {
135
+ return {
136
+ type: "already_running",
137
+ step: state.step,
138
+ elapsed_min: elapsedMinutes(state.dispatched_at),
139
+ last_error: state.last_error,
140
+ };
141
+ }
132
142
  }
133
143
  // ── Requires human (review checkpoint) ──
134
144
  if (rule.requires_human && state.status !== "pass") {
@@ -163,13 +173,25 @@ function _dispatchInner(projectRoot, state, dryRun) {
163
173
  state.files_changed = [];
164
174
  // Check if we just reached "done"
165
175
  if (state.step === "done") {
176
+ // Clear reopened_from when story completes
177
+ state.reopened_from = null;
178
+ // Feature 3: Check if review should be triggered
179
+ let review_suggested = false;
180
+ const completedCount = countCompletedStories(projectRoot);
181
+ if (completedCount >= REVIEW_TRIGGER_THRESHOLD) {
182
+ review_suggested = true;
183
+ }
166
184
  if (!dryRun)
167
185
  (0, state_1.writeState)(projectRoot, state);
168
- return {
186
+ const result = {
169
187
  type: "done",
170
188
  story: state.story ?? "(no story)",
171
189
  summary: `Story ${state.story} completed. All steps passed.`,
172
190
  };
191
+ if (review_suggested) {
192
+ result.review_suggested = true;
193
+ }
194
+ return result;
173
195
  }
174
196
  // Recurse: the new step might also require human
175
197
  const newRule = (0, rules_1.getRule)(state.step);
@@ -204,7 +226,25 @@ function _dispatchInner(projectRoot, state, dryRun) {
204
226
  : "No specific reason."),
205
227
  };
206
228
  }
207
- const target = (0, rules_1.getFailTarget)(state.step, state.reason);
229
+ // Feature 1: Escalation logic for post-reopen verify failures
230
+ // If verify fails after reopen with no explicit reason, escalate by rolling back one step deeper
231
+ let target = (0, rules_1.getFailTarget)(state.step, state.reason);
232
+ if (state.step === "verify" &&
233
+ state.reopened_from !== null &&
234
+ state.reason === null // only escalate for pure RED failure (no specific reason)
235
+ ) {
236
+ // Find the step before reopened_from
237
+ const earlierStep = getEarlierStep(state.reopened_from);
238
+ if (earlierStep !== null) {
239
+ // Escalate to the earlier step (overriding normal routing)
240
+ target = earlierStep;
241
+ if (!dryRun) {
242
+ (0, state_1.appendLog)(projectRoot, "INFO", "dispatch", `Post-reopen verify failure escalation: ${state.step} → ${earlierStep} (was reopened at ${state.reopened_from})`);
243
+ }
244
+ // Clear reopened_from so we only escalate once
245
+ state.reopened_from = null;
246
+ }
247
+ }
208
248
  if (target !== state.step) {
209
249
  // Route to different step
210
250
  state.step = target;
@@ -1099,6 +1139,393 @@ function rollback(projectRoot, targetStep, options = {}) {
1099
1139
  message: `Rolled back to "${targetStep}" (attempt 1, status: pending)`,
1100
1140
  };
1101
1141
  }
1142
+ // ─── Helper Functions for Features ────────────────────────────────────────────
1143
+ /**
1144
+ * Count completed stories since last review by reading .ai/history.md
1145
+ * and matching entries with reopen pattern.
1146
+ */
1147
+ function countCompletedStories(projectRoot) {
1148
+ try {
1149
+ const historyPath = (0, path_1.join)(projectRoot, ".ai", "history.md");
1150
+ if (!(0, fs_1.existsSync)(historyPath))
1151
+ return 0;
1152
+ const content = (0, fs_1.readFileSync)(historyPath, "utf-8");
1153
+ // Count lines that match "### Reopen" pattern (each represents a completed story that was reopened)
1154
+ // Plus we need to count stories that completed naturally
1155
+ // For now, count any story-related lines in history
1156
+ const matches = content.match(/### Reopen/g) || [];
1157
+ return matches.length;
1158
+ }
1159
+ catch {
1160
+ return 0;
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Find the step that comes before a given step in the sequence.
1165
+ * Returns null if the step is the first one (no earlier step).
1166
+ */
1167
+ function getEarlierStep(step) {
1168
+ const sequence = (0, rules_1.getStepSequence)();
1169
+ const index = sequence.indexOf(step);
1170
+ if (index <= 0)
1171
+ return null;
1172
+ return sequence[index - 1];
1173
+ }
1174
+ // ─── Reopen ──────────────────────────────────────────────────────────────────
1175
+ /**
1176
+ * Reopen a completed story at a specified step.
1177
+ *
1178
+ * Unlike rollback() which moves backwards within an active story,
1179
+ * reopen() specifically targets stories that have reached step "done".
1180
+ * It resets state to the target step so the pipeline can re-execute from there.
1181
+ *
1182
+ * Use case: Review Session or triage found issues in a completed US —
1183
+ * human decides to reopen it at a specific step (e.g., "impl", "verify").
1184
+ *
1185
+ * Guards:
1186
+ * - Story must be in "done" state (use rollback for active stories)
1187
+ * - Target step must be a valid pipeline step
1188
+ * - Cannot reopen to "done" (that's a no-op)
1189
+ *
1190
+ * [v0.8.0] FB-009: Review → Triage → Re-entry
1191
+ */
1192
+ function reopen(projectRoot, targetStep, options = {}) {
1193
+ let state;
1194
+ try {
1195
+ state = (0, state_1.readState)(projectRoot);
1196
+ }
1197
+ catch (err) {
1198
+ return { type: "error", code: "STATE_NOT_FOUND", message: err.message, recoverable: false };
1199
+ }
1200
+ // Guard: story must be completed
1201
+ if (state.step !== "done") {
1202
+ (0, state_1.appendLog)(projectRoot, "ERROR", "reopen", `NOT_DONE: current step is "${state.step}", not "done". Use rollback instead.`);
1203
+ return {
1204
+ type: "error",
1205
+ code: "NOT_DONE",
1206
+ message: `Cannot reopen: story is at step "${state.step}", not "done". Use "rollback" for active stories.`,
1207
+ recoverable: false,
1208
+ };
1209
+ }
1210
+ // Cannot reopen to "done" (check before sequence validation since "done" isn't in sequence)
1211
+ if (targetStep === "done") {
1212
+ return {
1213
+ type: "error",
1214
+ code: "REOPEN_TO_DONE",
1215
+ message: `Cannot reopen to "done" — story is already done.`,
1216
+ recoverable: false,
1217
+ };
1218
+ }
1219
+ // Validate target step
1220
+ const sequence = (0, rules_1.getStepSequence)();
1221
+ const targetIndex = targetStep === "bootstrap" ? -1 : sequence.indexOf(targetStep);
1222
+ if (targetStep !== "bootstrap" && targetIndex === -1) {
1223
+ (0, state_1.appendLog)(projectRoot, "ERROR", "reopen", `INVALID_TARGET: "${targetStep}"`);
1224
+ return {
1225
+ type: "error",
1226
+ code: "INVALID_TARGET",
1227
+ message: `Invalid reopen target "${targetStep}". Valid steps: bootstrap, ${sequence.join(", ")}`,
1228
+ recoverable: false,
1229
+ };
1230
+ }
1231
+ // Reset state to target step
1232
+ const previousStep = state.step; // "done"
1233
+ const rule = targetStep === "bootstrap"
1234
+ ? (0, rules_1.getRule)("bootstrap")
1235
+ : (0, rules_1.getRule)(targetStep);
1236
+ state.step = targetStep;
1237
+ state.status = "pending";
1238
+ state.attempt = 1;
1239
+ state.max_attempts = rule.max_attempts;
1240
+ state.timeout_min = rule.timeout_min;
1241
+ state.reason = null;
1242
+ state.last_error = null;
1243
+ state.files_changed = [];
1244
+ state.dispatched_at = null;
1245
+ state.completed_at = null;
1246
+ state.tests = null;
1247
+ state.failing_tests = [];
1248
+ state.lint_pass = null;
1249
+ state.human_note = options.humanNote ?? null;
1250
+ state.reopened_from = targetStep; // Feature 1: Track reopen target for escalation
1251
+ (0, state_1.writeState)(projectRoot, state);
1252
+ (0, state_1.appendLog)(projectRoot, "INFO", "reopen", `Reopened story "${state.story}" from "${previousStep}" to "${targetStep}"${options.humanNote ? ` note: "${options.humanNote}"` : ""}`);
1253
+ // Feature 2: Append entry to .ai/history.md
1254
+ const isoTimestamp = new Date().toISOString();
1255
+ const historyEntry = `### Reopen — ${state.story} → ${targetStep}
1256
+ - **Date**: ${isoTimestamp}
1257
+ - **From**: ${previousStep} → ${targetStep}
1258
+ - **Note**: ${options.humanNote ?? "N/A"}`;
1259
+ (0, state_1.appendHistory)(projectRoot, historyEntry);
1260
+ return {
1261
+ type: "ok",
1262
+ state,
1263
+ message: `Reopened story "${state.story}" at step "${targetStep}" (attempt 1, status: pending)`,
1264
+ };
1265
+ }
1266
+ // ─── Review (On-Demand Review Session) ───────────────────────────────────────
1267
+ /**
1268
+ * Generate a Review Session prompt for the current project.
1269
+ *
1270
+ * This is a STATELESS operation — it reads project state but does NOT
1271
+ * mutate STATE.json. The caller (CC executor) receives the prompt and
1272
+ * runs the review; results are acted on by the human (reopen, new US, etc.).
1273
+ *
1274
+ * Works on non-ACF projects too (detectFramework level 0): generates a
1275
+ * lighter review prompt based on whatever files exist.
1276
+ *
1277
+ * Review Session checks (from Lifecycle v0.9):
1278
+ * 1. Code Review — diff quality, naming, duplication
1279
+ * 2. Spec-Code Coherence — BDD ↔ tests ↔ impl alignment
1280
+ * 3. Regression — all existing tests still pass
1281
+ * 4. Security Scan — no hardcoded secrets, .gitignore coverage
1282
+ * 5. Memory Audit — PROJECT_MEMORY accuracy
1283
+ *
1284
+ * [v0.8.0] FB-009: Review → Triage → Re-entry
1285
+ */
1286
+ function review(projectRoot) {
1287
+ const framework = detectFramework(projectRoot);
1288
+ const prompt = buildReviewPrompt(projectRoot, framework.level);
1289
+ return {
1290
+ type: "review_prompt",
1291
+ prompt,
1292
+ fw_lv: framework.level,
1293
+ };
1294
+ }
1295
+ /**
1296
+ * Build the review session prompt based on framework adoption level.
1297
+ */
1298
+ function buildReviewPrompt(projectRoot, fwLevel) {
1299
+ const lines = [];
1300
+ lines.push("═══════════════════════════════════════════════════════════════");
1301
+ lines.push(" ON-DEMAND REVIEW SESSION");
1302
+ lines.push("═══════════════════════════════════════════════════════════════");
1303
+ lines.push("");
1304
+ if (fwLevel >= 2) {
1305
+ // Full ACF project — rich review
1306
+ let state = null;
1307
+ try {
1308
+ state = (0, state_1.readState)(projectRoot);
1309
+ }
1310
+ catch { /* no state */ }
1311
+ lines.push(`Project: ${state?.project ?? "(unknown)"}`);
1312
+ lines.push(`Story: ${state?.story ?? "(no active story)"}`);
1313
+ lines.push(`Current Step: ${state?.step ?? "(unknown)"}`);
1314
+ lines.push("");
1315
+ lines.push("## Review Checklist");
1316
+ lines.push("");
1317
+ lines.push("### 1. Code Review");
1318
+ lines.push("- Check recent changes for naming consistency, duplication, dead code");
1319
+ lines.push("- Verify diff-only discipline: only files related to the story should be modified");
1320
+ lines.push("");
1321
+ lines.push("### 2. Spec-Code Coherence");
1322
+ lines.push("- Read BDD scenarios in docs/bdd/ and verify each has a corresponding test");
1323
+ lines.push("- Read SDD Delta in docs/deltas/ and verify implementation matches");
1324
+ lines.push("- Check that API contracts in docs/api/ are synchronized");
1325
+ lines.push("");
1326
+ lines.push("### 3. Regression");
1327
+ lines.push("- Run the project's test suite and confirm all tests pass");
1328
+ lines.push("- If tests fail, record them in ISSUES section of PROJECT_MEMORY.md");
1329
+ lines.push("");
1330
+ lines.push("### 4. Security Scan");
1331
+ lines.push("- grep for common secret patterns: password=, apikey=, token=, BEGIN RSA PRIVATE KEY");
1332
+ lines.push("- Verify .gitignore covers: .env, *.key, credentials.json, *.pem");
1333
+ lines.push("- Confirm test fixtures use mock/fake values, not real credentials");
1334
+ lines.push("");
1335
+ lines.push("### 5. Memory Audit");
1336
+ lines.push("- Read PROJECT_MEMORY.md and verify NOW/TESTS/NEXT/ISSUES sections are accurate");
1337
+ lines.push("- Check .ai/history.md for completeness");
1338
+ lines.push("- Verify HANDOFF.md reflects the latest session state");
1339
+ }
1340
+ else if (fwLevel === 1) {
1341
+ // Partial ACF — moderate review
1342
+ lines.push("## Review Checklist (Partial ACF Project)");
1343
+ lines.push("");
1344
+ lines.push("### 1. Code Review");
1345
+ lines.push("- Check recent changes for quality, naming, duplication");
1346
+ lines.push("");
1347
+ lines.push("### 2. Spec Coherence");
1348
+ lines.push("- If BDD/SDD docs exist, verify alignment with code");
1349
+ lines.push("");
1350
+ lines.push("### 3. Regression");
1351
+ lines.push("- Run available test suite and confirm all tests pass");
1352
+ lines.push("");
1353
+ lines.push("### 4. Security Scan");
1354
+ lines.push("- grep for hardcoded secrets (password=, apikey=, token=)");
1355
+ lines.push("- Verify .gitignore covers sensitive file patterns");
1356
+ lines.push("");
1357
+ lines.push("### 5. Memory Check");
1358
+ lines.push("- If PROJECT_MEMORY.md exists, verify it is up to date");
1359
+ }
1360
+ else {
1361
+ // Non-ACF project — lightweight review
1362
+ lines.push("## Review Checklist (Non-ACF Project)");
1363
+ lines.push("");
1364
+ lines.push("### 1. Code Review");
1365
+ lines.push("- Review recent git changes (git log + git diff) for quality");
1366
+ lines.push("- Check for dead code, duplication, naming issues");
1367
+ lines.push("");
1368
+ lines.push("### 2. Test Check");
1369
+ lines.push("- Locate and run the project's test suite (if any)");
1370
+ lines.push("- Report test results");
1371
+ lines.push("");
1372
+ lines.push("### 3. Security Scan");
1373
+ lines.push("- grep for hardcoded secrets (password=, apikey=, token=)");
1374
+ lines.push("- Check .gitignore for sensitive patterns");
1375
+ }
1376
+ lines.push("");
1377
+ lines.push("═══════════════════════════════════════════════════════════════");
1378
+ lines.push(" OUTPUT FORMAT");
1379
+ lines.push("═══════════════════════════════════════════════════════════════");
1380
+ lines.push("");
1381
+ lines.push("For each check, report:");
1382
+ lines.push(" PASS — <brief note>");
1383
+ lines.push(" WARN — <issue description>");
1384
+ lines.push(" FAIL — <issue description + suggested action>");
1385
+ lines.push("");
1386
+ lines.push("At the end, provide a summary with recommended actions:");
1387
+ lines.push(" - REOPEN <US-XXX> at <step> — if existing story needs rework");
1388
+ lines.push(" - NEW US — <brief description> — if a new story is needed");
1389
+ lines.push(" - ISSUE — <description> — record in PROJECT_MEMORY.md ISSUES");
1390
+ lines.push(" - ALL CLEAR — no issues found");
1391
+ lines.push("");
1392
+ return lines.join("\n");
1393
+ }
1394
+ // ─── Triage (ISSUES → Actionable Plan) ──────────────────────────────────────
1395
+ /**
1396
+ * Read unfixed ISSUES from PROJECT_MEMORY.md and generate a triage prompt.
1397
+ *
1398
+ * This is a STATELESS operation — reads files but does NOT mutate STATE.json.
1399
+ * The triage prompt asks the executor (CC) to classify each issue and
1400
+ * recommend actions (reopen US, create new US, or dismiss).
1401
+ *
1402
+ * Requires PROJECT_MEMORY.md to exist with an ISSUES section.
1403
+ *
1404
+ * [v0.8.0] FB-009: Review → Triage → Re-entry
1405
+ */
1406
+ function triage(projectRoot) {
1407
+ const framework = detectFramework(projectRoot);
1408
+ // Read PROJECT_MEMORY.md
1409
+ const memoryPath = (0, path_1.join)(projectRoot, "PROJECT_MEMORY.md");
1410
+ if (!(0, fs_1.existsSync)(memoryPath)) {
1411
+ return {
1412
+ type: "error",
1413
+ code: "NO_MEMORY",
1414
+ message: "PROJECT_MEMORY.md not found. Cannot triage without ISSUES section.",
1415
+ recoverable: false,
1416
+ };
1417
+ }
1418
+ const memory = (0, fs_1.readFileSync)(memoryPath, "utf-8");
1419
+ const issues = parseIssuesFromMemory(memory);
1420
+ if (issues.length === 0) {
1421
+ return {
1422
+ type: "error",
1423
+ code: "NO_ISSUES",
1424
+ message: "No unfixed ISSUES found in PROJECT_MEMORY.md. Nothing to triage.",
1425
+ recoverable: false,
1426
+ };
1427
+ }
1428
+ const prompt = buildTriagePrompt(projectRoot, issues, framework.level);
1429
+ return {
1430
+ type: "triage_prompt",
1431
+ prompt,
1432
+ issues,
1433
+ fw_lv: framework.level,
1434
+ };
1435
+ }
1436
+ /**
1437
+ * Parse ISSUES lines from PROJECT_MEMORY.md.
1438
+ * Looks for lines starting with "- [ ]" under a section containing "ISSUES".
1439
+ * Filters out already-checked items "- [x]".
1440
+ */
1441
+ function parseIssuesFromMemory(memory) {
1442
+ const lines = memory.split("\n");
1443
+ let inIssuesSection = false;
1444
+ const issues = [];
1445
+ for (const line of lines) {
1446
+ // Detect ISSUES section header (## ISSUES, ### ISSUES, or just ISSUES:)
1447
+ if (/^#{1,4}\s*ISSUES/i.test(line) || /^ISSUES\s*:/i.test(line)) {
1448
+ inIssuesSection = true;
1449
+ continue;
1450
+ }
1451
+ // Exit ISSUES section on next header
1452
+ if (inIssuesSection && /^#{1,4}\s/.test(line) && !/ISSUES/i.test(line)) {
1453
+ inIssuesSection = false;
1454
+ continue;
1455
+ }
1456
+ // Collect unchecked items
1457
+ if (inIssuesSection && /^-\s*\[\s*\]/.test(line)) {
1458
+ issues.push(line.replace(/^-\s*\[\s*\]\s*/, "").trim());
1459
+ }
1460
+ }
1461
+ return issues;
1462
+ }
1463
+ /**
1464
+ * Build the triage prompt with parsed issues.
1465
+ */
1466
+ function buildTriagePrompt(projectRoot, issues, fwLevel) {
1467
+ const lines = [];
1468
+ lines.push("═══════════════════════════════════════════════════════════════");
1469
+ lines.push(" TRIAGE SESSION");
1470
+ lines.push("═══════════════════════════════════════════════════════════════");
1471
+ lines.push("");
1472
+ let state = null;
1473
+ try {
1474
+ state = (0, state_1.readState)(projectRoot);
1475
+ }
1476
+ catch { /* no state */ }
1477
+ if (state) {
1478
+ lines.push(`Project: ${state.project ?? "(unknown)"}`);
1479
+ lines.push(`Current Story: ${state.story ?? "(none)"}`);
1480
+ lines.push(`Current Step: ${state.step}`);
1481
+ lines.push("");
1482
+ }
1483
+ lines.push(`## Unfixed ISSUES (${issues.length})`);
1484
+ lines.push("");
1485
+ for (let i = 0; i < issues.length; i++) {
1486
+ lines.push(` ${i + 1}. ${issues[i]}`);
1487
+ }
1488
+ lines.push("");
1489
+ lines.push("## Triage Instructions");
1490
+ lines.push("");
1491
+ lines.push("For each issue above, analyze and classify:");
1492
+ lines.push("");
1493
+ lines.push(" A. REOPEN <US-XXX> at <step>");
1494
+ lines.push(" → Issue belongs to an existing story; reopen it at the appropriate step.");
1495
+ lines.push(" → The human will run: orchestrator reopen <project> <step>");
1496
+ lines.push("");
1497
+ lines.push(" B. NEW US: <title>");
1498
+ lines.push(" → Issue requires a new User Story. Write a 1-2 sentence description.");
1499
+ lines.push(" → The human will create the US and run: orchestrator start-story <project> <US-XXX>");
1500
+ lines.push("");
1501
+ lines.push(" C. DISMISS: <reason>");
1502
+ lines.push(" → Issue is resolved, duplicate, or no longer relevant.");
1503
+ lines.push(" → The human will mark it [x] in PROJECT_MEMORY.md.");
1504
+ lines.push("");
1505
+ if (fwLevel >= 2) {
1506
+ lines.push("## Context Files");
1507
+ lines.push("Read these files to inform your triage decisions:");
1508
+ lines.push(" - PROJECT_MEMORY.md (full context: NOW, TESTS, NEXT, ISSUES)");
1509
+ lines.push(" - .ai/history.md (completed work log)");
1510
+ lines.push(" - docs/bdd/ (BDD scenarios for existing stories)");
1511
+ lines.push(" - docs/deltas/ (SDD deltas for existing stories)");
1512
+ lines.push("");
1513
+ }
1514
+ lines.push("═══════════════════════════════════════════════════════════════");
1515
+ lines.push(" OUTPUT FORMAT");
1516
+ lines.push("═══════════════════════════════════════════════════════════════");
1517
+ lines.push("");
1518
+ lines.push("Produce a triage plan as a numbered list matching the issues above:");
1519
+ lines.push("");
1520
+ lines.push(" 1. [A] REOPEN US-001 at impl — <reason>");
1521
+ lines.push(" 2. [B] NEW US: \"Add input validation for email field\" — <reason>");
1522
+ lines.push(" 3. [C] DISMISS — resolved in commit abc123");
1523
+ lines.push("");
1524
+ lines.push("End with a SUMMARY: X to reopen, Y new US, Z dismissed.");
1525
+ lines.push("This plan is HUMAN-GATED — no actions will be taken automatically.");
1526
+ lines.push("");
1527
+ return lines.join("\n");
1528
+ }
1102
1529
  /**
1103
1530
  * Check if prerequisite files (claude_reads) exist before dispatching.
1104
1531
  * Only checks concrete paths (skips wildcards like *.go, **\/*.ts).