@papi-ai/adapter-md 0.1.0-alpha → 0.2.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.
Files changed (4) hide show
  1. package/LICENSE +98 -0
  2. package/dist/index.d.ts +1127 -194
  3. package/dist/index.js +577 -457
  4. package/package.json +9 -3
package/dist/index.js CHANGED
@@ -8,18 +8,27 @@ import {
8
8
  VALID_TRANSITIONS as _VALID_TRANSITIONS,
9
9
  isValidTransition as _isValidTransition,
10
10
  validateTransition as _validateTransition,
11
- isValidStatus as _isValidStatus
11
+ isValidStatus as _isValidStatus,
12
+ isLiveDecision as _isLiveDecision,
13
+ RETIRED_DECISION_OUTCOMES as _RETIRED_DECISION_OUTCOMES
12
14
  } from "@papi-ai/shared";
13
15
  var VALID_TRANSITIONS = _VALID_TRANSITIONS;
14
16
  var isValidTransition = _isValidTransition;
15
17
  var validateTransition = _validateTransition;
16
18
  var isValidStatus = _isValidStatus;
19
+ var isLiveDecision = _isLiveDecision;
17
20
  var TASK_TYPE_TIERS = {
18
21
  bug: 1,
19
22
  task: 1,
20
23
  research: 2,
24
+ spike: 2,
21
25
  idea: 3,
22
- feedback: 3
26
+ discovery: 1,
27
+ // Non-code brief types for non-technical Owners (AD-12)
28
+ "design-brief": 1,
29
+ "research-brief": 1,
30
+ "marketing-brief": 1,
31
+ "ops-brief": 1
23
32
  };
24
33
 
25
34
  // src/parsers/planning-log.ts
@@ -36,8 +45,8 @@ function extractSectionCompat(content, newHeading, legacyHeading) {
36
45
  const section = extractSection(content, newHeading);
37
46
  return section || extractSection(content, legacyHeading);
38
47
  }
39
- function parseSprintHealth(content) {
40
- const section = extractSectionCompat(content, "Sprint Health", "Session Health");
48
+ function parseCycleHealth(content) {
49
+ const section = extractSectionCompat(content, "Cycle Health", "Sprint Health") || extractSection(content, "Session Health");
41
50
  const rows = /* @__PURE__ */ new Map();
42
51
  for (const line of section.split("\n")) {
43
52
  const match = line.match(/^\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/);
@@ -48,27 +57,29 @@ function parseSprintHealth(content) {
48
57
  }
49
58
  const get = (key) => rows.get(key) ?? "";
50
59
  return {
51
- totalSprints: parseInt(get("total sprints") || get("total sessions"), 10) || 0,
52
- sprintsSinceLastStrategyReview: parseInt(get("sprints since last strategy review") || get("sessions since last strategy review"), 10) || 0,
60
+ totalCycles: parseInt(get("total cycles") || get("total sprints") || get("total sessions"), 10) || 0,
61
+ cyclesSinceLastStrategyReview: parseInt(get("cycles since last strategy review") || get("sprints since last strategy review") || get("sessions since last strategy review"), 10) || 0,
53
62
  strategyReviewDue: get("strategy review due"),
54
63
  boardHealth: get("board health"),
55
64
  strategicDirection: get("strategic direction"),
56
65
  lastFullMode: parseInt(get("last full mode"), 10) || 0
57
66
  };
58
67
  }
59
- function serializeSprintHealth(health, content) {
60
- const section = extractSectionCompat(content, "Sprint Health", "Session Health");
68
+ function serializeCycleHealth(health, content) {
69
+ const section = extractSectionCompat(content, "Cycle Health", "Sprint Health") || extractSection(content, "Session Health");
61
70
  const fieldMap = {
62
- "Total sprints": String(health.totalSprints),
63
- "Sprints since last Strategy Review": String(health.sprintsSinceLastStrategyReview),
71
+ "Total cycles": String(health.totalCycles),
72
+ "Cycles since last Strategy Review": String(health.cyclesSinceLastStrategyReview),
64
73
  "Strategy Review due": health.strategyReviewDue,
65
74
  "Board health": health.boardHealth,
66
75
  "Strategic direction": health.strategicDirection,
67
76
  "Last Full Mode": String(health.lastFullMode)
68
77
  };
69
78
  const legacyFieldMap = {
70
- "Total sessions": String(health.totalSprints),
71
- "Sessions since last Strategy Review": String(health.sprintsSinceLastStrategyReview)
79
+ "Total sprints": String(health.totalCycles),
80
+ "Sprints since last Strategy Review": String(health.cyclesSinceLastStrategyReview),
81
+ "Total sessions": String(health.totalCycles),
82
+ "Sessions since last Strategy Review": String(health.cyclesSinceLastStrategyReview)
72
83
  };
73
84
  let updatedSection = section;
74
85
  for (const [metric, value] of Object.entries({ ...fieldMap, ...legacyFieldMap })) {
@@ -86,8 +97,8 @@ function parseActiveDecisions(content) {
86
97
  );
87
98
  if (!headingMatch) return null;
88
99
  const metaMatch = block.match(/<!-- papi:(?:created_sprint=(\d+))?\s*(?:modified_sprint=(\d+)\s*)*(?:uuid=(\S+))? -->/);
89
- const createdSprint = metaMatch?.[1] ? parseInt(metaMatch[1], 10) : void 0;
90
- const modifiedSprint = metaMatch?.[2] ? parseInt(metaMatch[2], 10) : void 0;
100
+ const createdCycle = metaMatch?.[1] ? parseInt(metaMatch[1], 10) : void 0;
101
+ const modifiedCycle = metaMatch?.[2] ? parseInt(metaMatch[2], 10) : void 0;
91
102
  const uuid = metaMatch?.[3] ?? randomUUID();
92
103
  return {
93
104
  uuid,
@@ -97,8 +108,8 @@ function parseActiveDecisions(content) {
97
108
  confidence: headingMatch[3] ?? "HIGH",
98
109
  superseded: !!headingMatch[4],
99
110
  supersededBy: headingMatch[4],
100
- createdSprint,
101
- modifiedSprint,
111
+ createdCycle,
112
+ modifiedCycle,
102
113
  body: block
103
114
  };
104
115
  }).filter((d) => d !== null);
@@ -106,16 +117,16 @@ function parseActiveDecisions(content) {
106
117
  function stripTemporalMeta(body) {
107
118
  return body.replace(/\n?<!-- papi:(?:created_sprint=\d+)?\s*(?:modified_sprint=\d+\s*)*(?:uuid=\S+)? -->/g, "");
108
119
  }
109
- function buildTemporalMeta(createdSprint, modifiedSprint, uuid) {
120
+ function buildTemporalMeta(createdCycle, modifiedCycle, uuid) {
110
121
  const parts = [];
111
- if (createdSprint != null) parts.push(`created_sprint=${createdSprint}`);
112
- if (modifiedSprint != null) parts.push(`modified_sprint=${modifiedSprint}`);
122
+ if (createdCycle != null) parts.push(`created_sprint=${createdCycle}`);
123
+ if (modifiedCycle != null) parts.push(`modified_sprint=${modifiedCycle}`);
113
124
  if (uuid) parts.push(`uuid=${uuid}`);
114
125
  if (parts.length === 0) return "";
115
126
  return `
116
127
  <!-- papi:${parts.join(" ")} -->`;
117
128
  }
118
- function extractCreatedSprint(block) {
129
+ function extractCreatedCycle(block) {
119
130
  const m = block.match(/<!-- papi:(?:created_sprint=(\d+))/);
120
131
  return m?.[1] ? parseInt(m[1], 10) : void 0;
121
132
  }
@@ -123,19 +134,19 @@ function extractUuid(block) {
123
134
  const m = block.match(/<!-- papi:.*?uuid=(\S+)/);
124
135
  return m?.[1];
125
136
  }
126
- function updateActiveDecisionInContent(id, newBody, content, sprintNumber) {
137
+ function updateActiveDecisionInContent(id, newBody, content, cycleNumber) {
127
138
  if (!newBody) return content;
128
139
  const escapedId = id.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
129
140
  const pattern = new RegExp(`(### ${escapedId}:.*?)(?=^### AD-\\d+:|^## |$(?![\\s\\S]))`, "ms");
130
141
  const cleanBody = stripTemporalMeta(newBody);
131
142
  if (pattern.test(content)) {
132
143
  const existingMatch = content.match(pattern);
133
- const existingCreated = existingMatch ? extractCreatedSprint(existingMatch[0]) : void 0;
144
+ const existingCreated = existingMatch ? extractCreatedCycle(existingMatch[0]) : void 0;
134
145
  const existingUuid = existingMatch ? extractUuid(existingMatch[0]) : void 0;
135
- const meta2 = sprintNumber != null ? buildTemporalMeta(existingCreated, sprintNumber, existingUuid) : existingUuid ? buildTemporalMeta(existingCreated, void 0, existingUuid) : "";
146
+ const meta2 = cycleNumber != null ? buildTemporalMeta(existingCreated, cycleNumber, existingUuid) : existingUuid ? buildTemporalMeta(existingCreated, void 0, existingUuid) : "";
136
147
  return content.replace(pattern, cleanBody.trimEnd() + meta2 + "\n\n");
137
148
  }
138
- const meta = sprintNumber != null ? buildTemporalMeta(sprintNumber) : "";
149
+ const meta = cycleNumber != null ? buildTemporalMeta(cycleNumber) : "";
139
150
  const sectionPattern = /^(#{1,2} Active Decisions\n)([\s\S]*?)(?=^#{1,2} |$(?![\s\S]))/m;
140
151
  const sectionMatch = content.match(sectionPattern);
141
152
  if (sectionMatch) {
@@ -146,22 +157,22 @@ function updateActiveDecisionInContent(id, newBody, content, sprintNumber) {
146
157
  }
147
158
  return content;
148
159
  }
149
- function parseSprintLog(content, limit) {
150
- const section = extractSectionCompat(content, "Sprint Log", "Session Log");
151
- const chunks = section.split(/^(?=### (?:Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Sprint|Session) \d+ —/));
160
+ function parseCycleLog(content, limit) {
161
+ const section = extractSectionCompat(content, "Cycle Log", "Sprint Log");
162
+ const chunks = section.split(/^(?=### (?:Cycle|Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Cycle|Sprint|Session) \d+ —/));
152
163
  const entries = chunks.map((block) => {
153
- const headingMatch = block.match(/^### (?:Sprint|Session) (\d+) — (.+?)$/m);
154
- const sprintNumber = headingMatch ? parseInt(headingMatch[1], 10) : 0;
164
+ const headingMatch = block.match(/^### (?:Cycle|Sprint|Session) (\d+) — (.+?)$/m);
165
+ const cycleNumber = headingMatch ? parseInt(headingMatch[1], 10) : 0;
155
166
  const title = headingMatch ? headingMatch[2].trim() : block.split("\n")[0].replace(/^### /, "");
156
167
  const carryForwardMatch = block.match(/^- \*\*CARRY FORWARD:\*\*\s*(.+)$/m);
157
168
  const uuidMatch = block.match(/<!-- papi:.*?uuid=(\S+)/);
158
169
  const uuid = uuidMatch?.[1];
159
170
  const blockClean = block.replace(/\n?<!-- papi:.*?-->/g, "");
160
- const notesMatch = blockClean.match(/\*\*Sprint Notes:\*\*\s*([\s\S]*?)$/);
171
+ const notesMatch = blockClean.match(/\*\*Cycle Notes:\*\*\s*([\s\S]*?)$/);
161
172
  const notes = notesMatch ? notesMatch[1].trim() : void 0;
162
173
  return {
163
174
  uuid: uuid ?? randomUUID(),
164
- sprintNumber,
175
+ cycleNumber,
165
176
  title,
166
177
  content: block,
167
178
  carryForward: carryForwardMatch ? carryForwardMatch[1].trim() : void 0,
@@ -170,11 +181,11 @@ function parseSprintLog(content, limit) {
170
181
  });
171
182
  return limit ? entries.slice(0, limit) : entries;
172
183
  }
173
- function prependSprintLogEntry(entry, content) {
174
- const headingPattern = /^## (?:Sprint|Session) Log\s*$/m;
184
+ function prependCycleLogEntry(entry, content) {
185
+ const headingPattern = /^## (?:Cycle|Sprint|Session) Log\s*$/m;
175
186
  const headingMatch = content.match(headingPattern);
176
187
  if (!headingMatch || headingMatch.index === void 0) {
177
- throw new Error("Sprint Log section not found in Planning Log");
188
+ throw new Error("Cycle Log section not found in Planning Log");
178
189
  }
179
190
  const insertPos = headingMatch.index + headingMatch[0].length;
180
191
  const before = content.slice(0, insertPos);
@@ -183,7 +194,7 @@ function prependSprintLogEntry(entry, content) {
183
194
  if (entry.notes) {
184
195
  entryContent = `${entryContent}
185
196
 
186
- **Sprint Notes:** ${entry.notes}`;
197
+ **Cycle Notes:** ${entry.notes}`;
187
198
  }
188
199
  if (entry.uuid) {
189
200
  entryContent = `${entryContent}
@@ -197,17 +208,41 @@ ${after}`;
197
208
  function parseNorthStar(content) {
198
209
  return extractSection(content, "North Star").replace(/^## North Star\s*/m, "").trim();
199
210
  }
211
+ function upsertNorthStarInContent(content, statement) {
212
+ const headingPattern = /^## North Star\s*$/m;
213
+ const start = content.search(headingPattern);
214
+ if (start === -1) {
215
+ const cycleLogIdx = content.search(/^## (?:Cycle Log|Sprint Log)/m);
216
+ const newSection = `## North Star
217
+
218
+ ${statement}
219
+
220
+ `;
221
+ if (cycleLogIdx === -1) {
222
+ return content.trimEnd() + "\n\n" + newSection;
223
+ }
224
+ return content.slice(0, cycleLogIdx) + newSection + content.slice(cycleLogIdx);
225
+ }
226
+ const afterHeading = content.slice(start);
227
+ const nextSection = afterHeading.slice(1).search(/^## /m);
228
+ const sectionEnd = nextSection === -1 ? content.length : start + nextSection + 1;
229
+ return content.slice(0, start) + `## North Star
230
+
231
+ ${statement}
232
+
233
+ ` + content.slice(sectionEnd);
234
+ }
200
235
  function parseDeferred(content) {
201
236
  const section = extractSection(content, "Deferred / Parking Lot");
202
237
  return section.split("\n").filter((line) => line.match(/^-\s+/)).map((line) => line.replace(/^-\s+/, "").trim());
203
238
  }
204
- function compressSprintLogInContent(content, threshold, summary) {
205
- const section = extractSectionCompat(content, "Sprint Log", "Session Log");
206
- const chunks = section.split(/^(?=### (?:Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Sprint|Session) \d+ —/));
239
+ function compressCycleLogInContent(content, threshold, summary) {
240
+ const section = extractSectionCompat(content, "Cycle Log", "Sprint Log");
241
+ const chunks = section.split(/^(?=### (?:Cycle|Sprint|Session) \d+ —)/m).map((c) => c.trim()).filter((c) => c.match(/^### (?:Cycle|Sprint|Session) \d+ —/));
207
242
  const keep = [];
208
243
  let hasOld = false;
209
244
  for (const block of chunks) {
210
- const match = block.match(/^### (?:Sprint|Session) (\d+) —/);
245
+ const match = block.match(/^### (?:Cycle|Sprint|Session) (\d+) —/);
211
246
  const num = match ? parseInt(match[1], 10) : 0;
212
247
  if (num >= threshold) {
213
248
  keep.push(block);
@@ -216,27 +251,27 @@ function compressSprintLogInContent(content, threshold, summary) {
216
251
  }
217
252
  }
218
253
  if (!hasOld) return content;
219
- const summaryBlock = `### Sprints 1\u2013${threshold - 1} \u2014 Compressed Summary
254
+ const summaryBlock = `### Cycles 1\u2013${threshold - 1} \u2014 Compressed Summary
220
255
 
221
256
  ${summary}`;
222
257
  const newEntries = [...keep, summaryBlock].join("\n\n");
223
- const newSection = `## Sprint Log
258
+ const newSection = `## Cycle Log
224
259
 
225
260
  ${newEntries}
226
261
  `;
227
262
  return content.replace(section, newSection);
228
263
  }
229
- function parsePlanningLog(content, activeDecisionsContent, sprintLogContent) {
264
+ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
230
265
  return {
231
- sprintHealth: parseSprintHealth(content),
266
+ cycleHealth: parseCycleHealth(content),
232
267
  northStar: parseNorthStar(content),
233
268
  activeDecisions: parseActiveDecisions(activeDecisionsContent ?? content),
234
269
  deferred: parseDeferred(content),
235
- sprintLog: sprintLogContent ? parseSprintLog(sprintLogContent) : []
270
+ cycleLog: cycleLogContent ? parseCycleLog(cycleLogContent) : []
236
271
  };
237
272
  }
238
273
 
239
- // src/parsers/sprint-board.ts
274
+ // src/parsers/cycle-board.ts
240
275
  import { randomUUID as randomUUID3 } from "crypto";
241
276
  import yaml from "js-yaml";
242
277
 
@@ -248,6 +283,7 @@ var SECTION_HEADERS = [
248
283
  "SCOPE BOUNDARY (DO NOT DO THIS)",
249
284
  "ACCEPTANCE CRITERIA",
250
285
  "SECURITY CONSIDERATIONS",
286
+ "PRE-BUILD VERIFICATION",
251
287
  "FILES LIKELY TOUCHED",
252
288
  "EFFORT"
253
289
  ];
@@ -278,6 +314,9 @@ function splitSections(text) {
278
314
  function parseBulletList(text) {
279
315
  return text.split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
280
316
  }
317
+ function parseBulletsOnly(text) {
318
+ return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
319
+ }
281
320
  function parseChecklist(text) {
282
321
  return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
283
322
  }
@@ -285,11 +324,11 @@ function parseBuildHandoff(markdown) {
285
324
  if (!markdown.includes("BUILD HANDOFF")) return null;
286
325
  const taskIdMatch = markdown.match(/BUILD HANDOFF\s*—\s*(task-\d+)/);
287
326
  const taskTitleMatch = markdown.match(/^Task:\s*(.+)$/m);
288
- const sprintMatch = markdown.match(/^Sprint:\s*(\d+)$/m);
327
+ const cycleMatch = markdown.match(/^Cycle:\s*(\d+)$/m);
289
328
  const whyNowMatch = markdown.match(/^Why now:\s*([\s\S]*?)(?=\n\n|\nSCOPE)/m);
290
329
  const taskId = taskIdMatch?.[1] ?? "";
291
330
  const taskTitle = taskTitleMatch?.[1]?.trim() ?? "";
292
- const sprint = sprintMatch ? parseInt(sprintMatch[1], 10) : 0;
331
+ const cycle = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
293
332
  const whyNow = whyNowMatch?.[1]?.replace(/\s+/g, " ").trim() ?? "";
294
333
  const uuidMatch = markdown.match(/^UUID:\s*(\S+)$/m);
295
334
  const uuid = uuidMatch?.[1];
@@ -306,59 +345,92 @@ function parseBuildHandoff(markdown) {
306
345
  ...createdAt ? { createdAt } : {},
307
346
  taskId,
308
347
  taskTitle,
309
- sprint,
348
+ cycle,
310
349
  whyNow,
311
350
  scope: parseBulletList(sections.get("SCOPE (DO THIS)") ?? ""),
312
351
  scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
313
352
  acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
314
353
  securityConsiderations: (sections.get("SECURITY CONSIDERATIONS") ?? "").trim(),
354
+ verificationFiles: parseBulletsOnly(sections.get("PRE-BUILD VERIFICATION") ?? ""),
315
355
  filesLikelyTouched: parseBulletList(sections.get("FILES LIKELY TOUCHED") ?? ""),
316
356
  effort
317
357
  };
318
358
  }
319
- function serializeBuildHandoff(handoff) {
359
+ function ensureArray(value) {
360
+ if (Array.isArray(value)) return value;
361
+ if (typeof value === "string") {
362
+ try {
363
+ const parsed = JSON.parse(value);
364
+ if (Array.isArray(parsed)) return parsed;
365
+ } catch {
366
+ }
367
+ return value.trim() ? [value] : [];
368
+ }
369
+ return [];
370
+ }
371
+ function serializeBuildHandoff(raw) {
372
+ let handoff;
373
+ if (typeof raw === "string") {
374
+ try {
375
+ handoff = JSON.parse(raw);
376
+ } catch {
377
+ return raw;
378
+ }
379
+ } else {
380
+ handoff = raw;
381
+ }
320
382
  const lines = [];
321
383
  lines.push(`BUILD HANDOFF \u2014 ${handoff.taskId}`);
322
384
  if (handoff.uuid) lines.push(`UUID: ${handoff.uuid}`);
323
385
  if (handoff.displayId) lines.push(`Display ID: ${handoff.displayId}`);
324
386
  if (handoff.createdAt) lines.push(`Created: ${handoff.createdAt}`);
325
387
  lines.push(`Task: ${handoff.taskTitle}`);
326
- lines.push(`Sprint: ${handoff.sprint}`);
388
+ lines.push(`Cycle: ${handoff.cycle}`);
327
389
  lines.push(`Why now: ${handoff.whyNow}`);
328
390
  lines.push("");
329
391
  lines.push("SCOPE (DO THIS)");
330
- for (const item of handoff.scope) {
392
+ for (const item of ensureArray(handoff.scope)) {
331
393
  lines.push(`- ${item}`);
332
394
  }
333
395
  lines.push("");
334
396
  lines.push("SCOPE BOUNDARY (DO NOT DO THIS)");
335
- for (const item of handoff.scopeBoundary) {
397
+ for (const item of ensureArray(handoff.scopeBoundary)) {
336
398
  lines.push(`- ${item}`);
337
399
  }
338
400
  lines.push("");
339
401
  lines.push("ACCEPTANCE CRITERIA");
340
- for (const item of handoff.acceptanceCriteria) {
402
+ for (const item of ensureArray(handoff.acceptanceCriteria)) {
341
403
  lines.push(`[ ] ${item}`);
342
404
  }
343
405
  lines.push("");
344
406
  lines.push("SECURITY CONSIDERATIONS");
345
- lines.push(handoff.securityConsiderations);
407
+ lines.push(handoff.securityConsiderations ?? "");
408
+ const verificationFiles = ensureArray(handoff.verificationFiles);
409
+ if (verificationFiles.length > 0) {
410
+ lines.push("");
411
+ lines.push("PRE-BUILD VERIFICATION");
412
+ lines.push("Before implementing, read these files and check if the functionality already exists:");
413
+ for (const item of verificationFiles) {
414
+ lines.push(`- ${item}`);
415
+ }
416
+ lines.push('If >80% of the scope is already implemented, call build_execute with completed="yes" and note "already built" in surprises instead of re-implementing.');
417
+ }
346
418
  lines.push("");
347
419
  lines.push("FILES LIKELY TOUCHED");
348
- for (const item of handoff.filesLikelyTouched) {
420
+ for (const item of ensureArray(handoff.filesLikelyTouched)) {
349
421
  lines.push(`- ${item}`);
350
422
  }
351
423
  lines.push("");
352
424
  lines.push("EFFORT");
353
- lines.push(handoff.effort);
425
+ lines.push(handoff.effort ?? "M");
354
426
  return lines.join("\n");
355
427
  }
356
428
 
357
- // src/parsers/sprint-board.ts
429
+ // src/parsers/cycle-board.ts
358
430
  var YAML_MARKER = "<!-- PAPI-ADAPTER: parse the yaml block below -->";
359
431
  var YAML_START = "<!-- PAPI-YAML-START -->";
360
432
  var YAML_END = "<!-- PAPI-YAML-END -->";
361
- function toSprintTask(raw) {
433
+ function toCycleTask(raw) {
362
434
  return {
363
435
  uuid: raw.uuid || randomUUID3(),
364
436
  id: raw.id,
@@ -372,8 +444,8 @@ function toSprintTask(raw) {
372
444
  phase: raw.phase,
373
445
  owner: raw.owner,
374
446
  reviewed: raw.reviewed ?? false,
375
- sprint: raw.sprint != null ? raw.sprint : void 0,
376
- createdSprint: raw.created_sprint != null ? raw.created_sprint : void 0,
447
+ cycle: raw.cycle != null ? raw.cycle : void 0,
448
+ createdCycle: raw.created_sprint != null ? raw.created_sprint : void 0,
377
449
  createdAt: raw.created_at || void 0,
378
450
  why: raw.why || void 0,
379
451
  dependsOn: raw.depends_on || void 0,
@@ -381,13 +453,17 @@ function toSprintTask(raw) {
381
453
  stateHistory: raw.state_history?.length ? raw.state_history.map((e) => ({ status: e.status, timestamp: e.timestamp })) : void 0,
382
454
  closureReason: raw.closure_reason || void 0,
383
455
  buildHandoff: raw.build_handoff ? parseBuildHandoff(raw.build_handoff) ?? void 0 : void 0,
384
- buildReport: raw.build_report || void 0
456
+ buildReport: raw.build_report || void 0,
457
+ scopeClass: raw.scope_class === "brief" ? "brief" : "task",
458
+ assigneeId: raw.assignee_id || void 0,
459
+ claimSource: raw.claim_source === "pool" || raw.claim_source === "self_generated" ? raw.claim_source : void 0,
460
+ reviewerId: raw.reviewer_id || void 0
385
461
  };
386
462
  }
387
463
  function sanitizeDelimiters(value) {
388
464
  return value.replaceAll(YAML_END, "<!-- PAPI-YAML-END (sanitized) -->");
389
465
  }
390
- function fromSprintTask(task) {
466
+ function fromCycleTask(task) {
391
467
  const raw = {
392
468
  uuid: task.uuid,
393
469
  id: task.id,
@@ -403,8 +479,8 @@ function fromSprintTask(task) {
403
479
  depends_on: task.dependsOn ?? "",
404
480
  notes: task.notes ? sanitizeDelimiters(task.notes) : ""
405
481
  };
406
- if (task.sprint != null) raw.sprint = task.sprint;
407
- if (task.createdSprint != null) raw.created_sprint = task.createdSprint;
482
+ if (task.cycle != null) raw.cycle = task.cycle;
483
+ if (task.createdCycle != null) raw.created_sprint = task.createdCycle;
408
484
  if (task.createdAt) raw.created_at = task.createdAt;
409
485
  if (task.why) raw.why = task.why;
410
486
  if (task.stateHistory?.length) {
@@ -413,6 +489,10 @@ function fromSprintTask(task) {
413
489
  if (task.closureReason) raw.closure_reason = task.closureReason;
414
490
  if (task.buildHandoff) raw.build_handoff = sanitizeDelimiters(serializeBuildHandoff(task.buildHandoff));
415
491
  if (task.buildReport) raw.build_report = sanitizeDelimiters(task.buildReport);
492
+ if (task.scopeClass && task.scopeClass !== "task") raw.scope_class = task.scopeClass;
493
+ if (task.assigneeId) raw.assignee_id = task.assigneeId;
494
+ if (task.claimSource) raw.claim_source = task.claimSource;
495
+ if (task.reviewerId) raw.reviewer_id = task.reviewerId;
416
496
  return raw;
417
497
  }
418
498
  function mergeConflictHint(content) {
@@ -423,17 +503,17 @@ function mergeConflictHint(content) {
423
503
  }
424
504
  function extractYamlBlock(content) {
425
505
  const markerIdx = content.indexOf(YAML_MARKER);
426
- if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in SPRINT_BOARD.md");
506
+ if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLE_BOARD.md");
427
507
  const afterMarker = content.slice(markerIdx + YAML_MARKER.length);
428
508
  const startIdx = afterMarker.indexOf(YAML_START);
429
509
  if (startIdx !== -1) {
430
510
  const yamlStart = startIdx + YAML_START.length;
431
511
  const endIdx = afterMarker.indexOf(YAML_END, yamlStart);
432
- if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in SPRINT_BOARD.md");
512
+ if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLE_BOARD.md");
433
513
  return afterMarker.slice(yamlStart, endIdx);
434
514
  }
435
515
  const blockMatch = afterMarker.match(/```yaml\n([\s\S]*?)```/);
436
- if (!blockMatch) throw new Error("YAML block not found in SPRINT_BOARD.md");
516
+ if (!blockMatch) throw new Error("YAML block not found in CYCLE_BOARD.md");
437
517
  return blockMatch[1];
438
518
  }
439
519
  function parseBoard(content) {
@@ -446,27 +526,27 @@ function parseBoard(content) {
446
526
  const lineInfo = yamlErr.mark?.line != null ? ` (near line ${yamlErr.mark.line + 1} of YAML block)` : "";
447
527
  const hint = mergeConflictHint(yamlText);
448
528
  throw new Error(
449
- `YAML parse error in SPRINT_BOARD.md${lineInfo}. Check for syntax errors \u2014 unquoted special characters, bad indentation, or missing colons.${hint}`
529
+ `YAML parse error in CYCLE_BOARD.md${lineInfo}. Check for syntax errors \u2014 unquoted special characters, bad indentation, or missing colons.${hint}`
450
530
  );
451
531
  }
452
- return (data.tasks ?? []).map(toSprintTask);
532
+ return (data.tasks ?? []).map(toCycleTask);
453
533
  }
454
534
  function serializeBoard(tasks, content) {
455
- const raw = tasks.map(fromSprintTask);
535
+ const raw = tasks.map(fromCycleTask);
456
536
  const yamlStr = yaml.dump({ tasks: raw }, { lineWidth: 120, quotingType: '"' });
457
537
  const markerIdx = content.indexOf(YAML_MARKER);
458
- if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in SPRINT_BOARD.md");
538
+ if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLE_BOARD.md");
459
539
  const afterMarker = content.slice(markerIdx + YAML_MARKER.length);
460
540
  const htmlStartIdx = afterMarker.indexOf(YAML_START);
461
541
  if (htmlStartIdx !== -1) {
462
542
  const absStart = markerIdx + YAML_MARKER.length + htmlStartIdx;
463
543
  const endIdx = afterMarker.indexOf(YAML_END, htmlStartIdx);
464
- if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in SPRINT_BOARD.md");
544
+ if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLE_BOARD.md");
465
545
  const absEnd = markerIdx + YAML_MARKER.length + endIdx + YAML_END.length;
466
546
  return content.slice(0, absStart) + YAML_START + "\n" + yamlStr + YAML_END + content.slice(absEnd);
467
547
  }
468
548
  const blockMatch = afterMarker.match(/```yaml\n[\s\S]*?```/);
469
- if (!blockMatch) throw new Error("YAML block not found in SPRINT_BOARD.md");
549
+ if (!blockMatch) throw new Error("YAML block not found in CYCLE_BOARD.md");
470
550
  const blockStart = markerIdx + YAML_MARKER.length + afterMarker.indexOf(blockMatch[0]);
471
551
  const blockEnd = blockStart + blockMatch[0].length;
472
552
  return content.slice(0, blockStart) + YAML_START + "\n" + yamlStr + YAML_END + content.slice(blockEnd);
@@ -479,6 +559,7 @@ function filterTasks(tasks, options) {
479
559
  if (options.reviewed !== void 0 && task.reviewed !== options.reviewed) return false;
480
560
  if (options.module && task.module !== options.module) return false;
481
561
  if (options.epic && task.epic !== options.epic) return false;
562
+ if (options.assigneeId && task.assigneeId !== options.assigneeId) return false;
482
563
  return true;
483
564
  });
484
565
  }
@@ -508,9 +589,9 @@ function parseEffort(effortLine) {
508
589
  return match ? { actual: match[1], estimated: match[2] } : { actual: effortLine, estimated: "" };
509
590
  }
510
591
  function parseBuildReports(content) {
511
- const chunks = content.split(/^(?=### .+ — .+ — (?:Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Sprint|Session) \d+/));
592
+ const chunks = content.split(/^(?=### .+ — .+ — (?:Cycle|Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Cycle|Sprint|Session) \d+/));
512
593
  return chunks.map((block) => {
513
- const headingMatch = block.match(/^### (.+?) — (.+?) — (?:Sprint|Session) (\d+)/);
594
+ const headingMatch = block.match(/^### (.+?) — (.+?) — (?:Cycle|Sprint|Session) (\d+)/);
514
595
  if (!headingMatch) return null;
515
596
  const effortLine = parseField(block, "Actual Effort");
516
597
  const { actual, estimated } = parseEffort(effortLine);
@@ -533,7 +614,7 @@ function parseBuildReports(content) {
533
614
  taskId,
534
615
  taskName: headingMatch[1].trim(),
535
616
  date: headingMatch[2].trim(),
536
- sprint: parseInt(headingMatch[3], 10),
617
+ cycle: parseInt(headingMatch[3], 10),
537
618
  completed: completedRaw.startsWith("Yes") ? "Yes" : completedRaw.startsWith("No") ? "No" : "Partial",
538
619
  actualEffort,
539
620
  estimatedEffort,
@@ -549,7 +630,7 @@ function parseBuildReports(content) {
549
630
  }
550
631
  function serializeBuildReport(report) {
551
632
  const lines = [
552
- `### ${report.taskName} \u2014 ${report.date} \u2014 Sprint ${report.sprint}`
633
+ `### ${report.taskName} \u2014 ${report.date} \u2014 Cycle ${report.cycle}`
553
634
  ];
554
635
  if (report.uuid) lines.push(`- **UUID:** ${report.uuid}`);
555
636
  if (report.displayId) lines.push(`- **Display ID:** ${report.displayId}`);
@@ -569,14 +650,14 @@ function serializeBuildReport(report) {
569
650
  }
570
651
  return lines.join("\n");
571
652
  }
572
- function formatCompressedSummary(reports, sprintRange, aiSummary) {
653
+ function formatCompressedSummary(reports, cycleRange, aiSummary) {
573
654
  const dates = reports.map((r) => r.date).filter(Boolean);
574
655
  const dateRange = dates.length > 0 ? `${dates[dates.length - 1]} \u2013 ${dates[0]}` : "unknown";
575
656
  const completed = reports.filter((r) => r.completed === "Yes");
576
657
  const partial = reports.filter((r) => r.completed === "Partial");
577
658
  const failed = reports.filter((r) => r.completed === "No");
578
659
  const formatTaskList = (list) => list.map((r) => r.taskId !== "unknown" ? `${r.taskId} (${r.taskName})` : r.taskName).join(", ");
579
- const lines = [`### ${sprintRange} \u2014 Compressed Summary`];
660
+ const lines = [`### ${cycleRange} \u2014 Compressed Summary`];
580
661
  lines.push(`**Date range:** ${dateRange}`);
581
662
  lines.push(`**Reports:** ${reports.length}`);
582
663
  if (completed.length > 0) {
@@ -602,11 +683,11 @@ function formatCompressedSummary(reports, sprintRange, aiSummary) {
602
683
  return lines.join("\n");
603
684
  }
604
685
  function compressBuildReportsInContent(content, threshold, summary) {
605
- const chunks = content.split(/^(?=### .+ — .+ — (?:Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Sprint|Session) \d+/));
686
+ const chunks = content.split(/^(?=### .+ — .+ — (?:Cycle|Sprint|Session) \d+)/m).map((c) => c.trim()).filter((c) => c.match(/^### .+ — .+ — (?:Cycle|Sprint|Session) \d+/));
606
687
  const keep = [];
607
688
  const oldChunks = [];
608
689
  for (const block of chunks) {
609
- const match = block.match(/— (?:Sprint|Session) (\d+)/);
690
+ const match = block.match(/— (?:Cycle|Sprint|Session) (\d+)/);
610
691
  const num = match ? parseInt(match[1], 10) : 0;
611
692
  if (num >= threshold) {
612
693
  keep.push(block);
@@ -616,8 +697,8 @@ function compressBuildReportsInContent(content, threshold, summary) {
616
697
  }
617
698
  if (oldChunks.length === 0) return content;
618
699
  const oldReports = parseBuildReports(oldChunks.join("\n\n---\n\n"));
619
- const sprintRange = `Sprints 1\u2013${threshold - 1}`;
620
- const summaryBlock = formatCompressedSummary(oldReports, sprintRange, summary);
700
+ const cycleRange = `Cycles 1\u2013${threshold - 1}`;
701
+ const summaryBlock = formatCompressedSummary(oldReports, cycleRange, summary);
621
702
  const firstReportIdx = content.search(/^### /m);
622
703
  const header = firstReportIdx === -1 ? content : content.slice(0, firstReportIdx);
623
704
  const entries = [...keep, summaryBlock].join("\n\n---\n\n");
@@ -638,7 +719,7 @@ function mergeBuildReports(existing, incoming) {
638
719
  taskId: incoming.taskId,
639
720
  taskName: incoming.taskName,
640
721
  date: incoming.date,
641
- sprint: incoming.sprint,
722
+ cycle: incoming.cycle,
642
723
  completed: incoming.completed,
643
724
  actualEffort: incoming.actualEffort,
644
725
  estimatedEffort: incoming.estimatedEffort,
@@ -659,8 +740,12 @@ function replaceBuildReport(existing, replacement, content) {
659
740
  if (idx !== -1) {
660
741
  return content.slice(0, idx) + newSerialized + content.slice(idx + oldSerialized.length);
661
742
  }
662
- const headingPattern = `### ${existing.taskName} \u2014 ${existing.date} \u2014 Sprint ${existing.sprint}`;
663
- const headingIdx = content.indexOf(headingPattern);
743
+ const headingPattern = `### ${existing.taskName} \u2014 ${existing.date} \u2014 Cycle ${existing.cycle}`;
744
+ let headingIdx = content.indexOf(headingPattern);
745
+ if (headingIdx === -1) {
746
+ const legacyPattern = `### ${existing.taskName} \u2014 ${existing.date} \u2014 Sprint ${existing.cycle}`;
747
+ headingIdx = content.indexOf(legacyPattern);
748
+ }
664
749
  if (headingIdx === -1) throw new Error(`Could not find existing build report for ${existing.taskId}`);
665
750
  const afterHeading = content.slice(headingIdx);
666
751
  const nextSeparator = afterHeading.indexOf("\n\n---\n");
@@ -687,9 +772,9 @@ function prependBuildReport(report, content) {
687
772
  }
688
773
 
689
774
  // src/parsers/metrics.ts
690
- var TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model | Sprint | Context |";
775
+ var TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model | Cycle | Context |";
691
776
  var TABLE_SEPARATOR = "|-----------|------|---------------|--------------|---------------|----------|-------|--------|---------|";
692
- var PREV_TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model | Sprint |";
777
+ var PREV_TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model | Cycle |";
693
778
  var LEGACY_TABLE_HEADER = "| Timestamp | Tool | Duration (ms) | Input Tokens | Output Tokens | Cost ($) | Model |";
694
779
  var SECTION_HEADING = "## Tool Call Metrics";
695
780
  var FILE_TEMPLATE = `# PAPI Metrics
@@ -728,8 +813,8 @@ function parseToolMetrics(content) {
728
813
  const outputTokens = cells[4] !== "-" ? parseInt(cells[4].replace(/,/g, ""), 10) : void 0;
729
814
  const cost = cells[5] !== "-" ? parseFloat(cells[5]) : void 0;
730
815
  const model = cells[6] !== "-" ? cells[6] : void 0;
731
- const sprintRaw = cells.length >= 8 && cells[7] !== "-" ? parseInt(cells[7], 10) : void 0;
732
- const sprintNumber = sprintRaw !== void 0 && !isNaN(sprintRaw) ? sprintRaw : void 0;
816
+ const cycleRaw = cells.length >= 8 && cells[7] !== "-" ? parseInt(cells[7], 10) : void 0;
817
+ const cycleNumber = cycleRaw !== void 0 && !isNaN(cycleRaw) ? cycleRaw : void 0;
733
818
  const contextRaw = cells.length >= 9 && cells[8] !== "-" ? parseInt(cells[8].replace(/,/g, ""), 10) : void 0;
734
819
  const contextBytes = contextRaw !== void 0 && !isNaN(contextRaw) ? contextRaw : void 0;
735
820
  const utilisationRaw = cells.length >= 10 && cells[9] !== "-" ? parseFloat(cells[9]) : void 0;
@@ -742,7 +827,7 @@ function parseToolMetrics(content) {
742
827
  ...outputTokens !== void 0 && !isNaN(outputTokens) ? { outputTokens } : {},
743
828
  ...cost !== void 0 && !isNaN(cost) ? { estimatedCostUsd: cost } : {},
744
829
  ...model ? { model } : {},
745
- ...sprintNumber !== void 0 ? { sprintNumber } : {},
830
+ ...cycleNumber !== void 0 ? { cycleNumber } : {},
746
831
  ...contextBytes !== void 0 ? { contextBytes } : {},
747
832
  ...contextUtilisation !== void 0 ? { contextUtilisation } : {}
748
833
  });
@@ -757,10 +842,10 @@ function serializeToolMetric(metric) {
757
842
  const outputTokens = metric.outputTokens !== void 0 ? formatNumber(metric.outputTokens) : "-";
758
843
  const cost = metric.estimatedCostUsd !== void 0 ? metric.estimatedCostUsd.toFixed(4) : "-";
759
844
  const model = metric.model ?? "-";
760
- const sprint = metric.sprintNumber !== void 0 ? String(metric.sprintNumber) : "-";
845
+ const cycle = metric.cycleNumber !== void 0 ? String(metric.cycleNumber) : "-";
761
846
  const context = metric.contextBytes !== void 0 ? formatNumber(metric.contextBytes) : "-";
762
847
  const utilisation = metric.contextUtilisation !== void 0 ? metric.contextUtilisation.toFixed(2) : "-";
763
- return `| ${metric.timestamp} | ${metric.tool} | ${formatNumber(metric.durationMs)} | ${inputTokens} | ${outputTokens} | ${cost} | ${model} | ${sprint} | ${context} | ${utilisation} |`;
848
+ return `| ${metric.timestamp} | ${metric.tool} | ${formatNumber(metric.durationMs)} | ${inputTokens} | ${outputTokens} | ${cost} | ${model} | ${cycle} | ${context} | ${utilisation} |`;
764
849
  }
765
850
  function appendToolMetricToContent(metric, content) {
766
851
  if (!content.trim()) {
@@ -791,8 +876,8 @@ function appendToolMetricToContent(metric, content) {
791
876
  const after = content.slice(costIdx);
792
877
  return before + "\n" + serializeToolMetric(metric) + "\n\n" + after;
793
878
  }
794
- function aggregateCostSummary(metrics, sprintNumber) {
795
- const filtered = sprintNumber !== void 0 ? metrics.filter((m) => m.sprintNumber === sprintNumber) : metrics;
879
+ function aggregateCostSummary(metrics, cycleNumber) {
880
+ const filtered = cycleNumber !== void 0 ? metrics.filter((m) => m.cycleNumber === cycleNumber) : metrics;
796
881
  let totalCostUsd = 0;
797
882
  let totalInputTokens = 0;
798
883
  let totalOutputTokens = 0;
@@ -824,7 +909,6 @@ function aggregateCostSummary(metrics, sprintNumber) {
824
909
  };
825
910
  }
826
911
  var COST_SECTION_HEADING = "## Cost Summary";
827
- var COST_TABLE_HEADER = "| Sprint | Date | Total Cost ($) | Input Tokens | Output Tokens | Calls |";
828
912
  var COST_TABLE_SEPARATOR = "|--------|------|----------------|--------------|---------------|-------|";
829
913
  function parseCostSnapshots(content) {
830
914
  const lines = content.split("\n");
@@ -843,7 +927,7 @@ function parseCostSnapshots(content) {
843
927
  const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
844
928
  if (cells.length < 6) continue;
845
929
  snapshots.push({
846
- sprint: parseInt(cells[0], 10),
930
+ cycle: parseInt(cells[0], 10),
847
931
  date: cells[1],
848
932
  totalCostUsd: parseFloat(cells[2]),
849
933
  totalInputTokens: parseInt(cells[3].replace(/,/g, ""), 10),
@@ -853,34 +937,12 @@ function parseCostSnapshots(content) {
853
937
  }
854
938
  return snapshots;
855
939
  }
856
- function serializeCostSnapshot(snapshot) {
857
- return `| ${snapshot.sprint} | ${snapshot.date} | ${snapshot.totalCostUsd.toFixed(4)} | ${formatNumber(snapshot.totalInputTokens)} | ${formatNumber(snapshot.totalOutputTokens)} | ${formatNumber(snapshot.totalCalls)} |`;
858
- }
859
- function writeCostSnapshotToContent(snapshot, content) {
860
- if (!content.includes(COST_SECTION_HEADING)) {
861
- return content.trimEnd() + "\n\n" + COST_SECTION_HEADING + "\n\n" + COST_TABLE_HEADER + "\n" + COST_TABLE_SEPARATOR + "\n" + serializeCostSnapshot(snapshot) + "\n";
862
- }
863
- const lines = content.split("\n");
864
- const sprintPrefix = `| ${snapshot.sprint} |`;
865
- let replaced = false;
866
- for (let i = 0; i < lines.length; i++) {
867
- if (lines[i].startsWith(sprintPrefix)) {
868
- lines[i] = serializeCostSnapshot(snapshot);
869
- replaced = true;
870
- break;
871
- }
872
- }
873
- if (replaced) {
874
- return lines.join("\n");
875
- }
876
- return content.trimEnd() + "\n" + serializeCostSnapshot(snapshot) + "\n";
877
- }
878
940
 
879
- // src/parsers/sprint-metrics.ts
880
- var FILE_HEADING = "# Sprint Methodology Metrics";
881
- var ACCURACY_HEADER = "| Sprint | Reports | Match Rate | MAE | Bias |";
941
+ // src/parsers/cycle-metrics.ts
942
+ var FILE_HEADING = "# Cycle Methodology Metrics";
943
+ var ACCURACY_HEADER = "| Cycle | Reports | Match Rate | MAE | Bias |";
882
944
  var ACCURACY_SEPARATOR = "|--------|---------|------------|-----|------|";
883
- var VELOCITY_HEADER = "| Sprint | Completed | Partial | Failed | Effort Points |";
945
+ var VELOCITY_HEADER = "| Cycle | Completed | Partial | Failed | Effort Points |";
884
946
  var VELOCITY_SEPARATOR = "|--------|-----------|---------|--------|---------------|";
885
947
  var EFFORT_SCALE = {
886
948
  XS: 1,
@@ -893,21 +955,21 @@ function effortOrdinal(effort) {
893
955
  const normalized = effort.trim().toUpperCase();
894
956
  return EFFORT_SCALE[normalized];
895
957
  }
896
- function calculateSprintMetrics(reports, currentSprint, window = 5) {
958
+ function calculateCycleMetrics(reports, currentCycle, window = 5) {
897
959
  const recentReports = reports.filter(
898
- (r) => r.sprint > currentSprint - window && r.sprint <= currentSprint
960
+ (r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
899
961
  );
900
- const perSprint = /* @__PURE__ */ new Map();
962
+ const perCycle = /* @__PURE__ */ new Map();
901
963
  for (const r of recentReports) {
902
- const group = perSprint.get(r.sprint) ?? [];
964
+ const group = perCycle.get(r.cycle) ?? [];
903
965
  group.push(r);
904
- perSprint.set(r.sprint, group);
966
+ perCycle.set(r.cycle, group);
905
967
  }
906
968
  const accuracy = [];
907
969
  const velocity = [];
908
- const sortedSprints = [...perSprint.keys()].sort((a, b) => a - b);
909
- for (const sprint of sortedSprints) {
910
- const reps = perSprint.get(sprint);
970
+ const sortedCycles = [...perCycle.keys()].sort((a, b) => a - b);
971
+ for (const cycle of sortedCycles) {
972
+ const reps = perCycle.get(cycle);
911
973
  const deltas = [];
912
974
  for (const r of reps) {
913
975
  const actual = effortOrdinal(r.actualEffort);
@@ -918,7 +980,7 @@ function calculateSprintMetrics(reports, currentSprint, window = 5) {
918
980
  }
919
981
  if (deltas.length > 0) {
920
982
  accuracy.push({
921
- sprint,
983
+ cycle,
922
984
  reports: deltas.length,
923
985
  matchRate: Math.round(deltas.filter((d) => d === 0).length / deltas.length * 100),
924
986
  mae: Math.round(deltas.reduce((s, d) => s + Math.abs(d), 0) / deltas.length * 10) / 10,
@@ -926,7 +988,7 @@ function calculateSprintMetrics(reports, currentSprint, window = 5) {
926
988
  });
927
989
  }
928
990
  velocity.push({
929
- sprint,
991
+ cycle,
930
992
  completed: reps.filter((r) => r.completed === "Yes").length,
931
993
  partial: reps.filter((r) => r.completed === "Partial").length,
932
994
  failed: reps.filter((r) => r.completed === "No").length,
@@ -936,23 +998,23 @@ function calculateSprintMetrics(reports, currentSprint, window = 5) {
936
998
  return { accuracy, velocity };
937
999
  }
938
1000
  function serializeAccuracyRow(a) {
939
- return `| ${a.sprint} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
1001
+ return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
940
1002
  }
941
1003
  function serializeVelocityRow(v) {
942
- return `| ${v.sprint} | ${v.completed} | ${v.partial} | ${v.failed} | ${v.effortPoints} |`;
1004
+ return `| ${v.cycle} | ${v.completed} | ${v.partial} | ${v.failed} | ${v.effortPoints} |`;
943
1005
  }
944
1006
  function serializeSnapshot(snapshot) {
945
1007
  const lines = [];
946
- lines.push(`## Sprint ${snapshot.sprint} Snapshot \u2014 ${snapshot.date}`);
1008
+ lines.push(`## Cycle ${snapshot.cycle} Snapshot \u2014 ${snapshot.date}`);
947
1009
  lines.push("");
948
- lines.push("### Estimation Accuracy (last 5 sprints)");
1010
+ lines.push("### Estimation Accuracy (last 5 cycles)");
949
1011
  lines.push(ACCURACY_HEADER);
950
1012
  lines.push(ACCURACY_SEPARATOR);
951
1013
  for (const a of snapshot.accuracy) {
952
1014
  lines.push(serializeAccuracyRow(a));
953
1015
  }
954
1016
  lines.push("");
955
- lines.push("### Sprint Velocity");
1017
+ lines.push("### Cycle Velocity");
956
1018
  lines.push(VELOCITY_HEADER);
957
1019
  lines.push(VELOCITY_SEPARATOR);
958
1020
  for (const v of snapshot.velocity) {
@@ -965,10 +1027,13 @@ function appendSnapshotToContent(snapshot, content) {
965
1027
  if (!content.trim()) {
966
1028
  return FILE_HEADING + "\n\n" + block + "\n";
967
1029
  }
968
- const marker = `## Sprint ${snapshot.sprint} Snapshot`;
969
- const markerIdx = content.indexOf(marker);
1030
+ const marker = `## Cycle ${snapshot.cycle} Snapshot`;
1031
+ const legacyMarker = `## Sprint ${snapshot.cycle} Snapshot`;
1032
+ let markerIdx = content.indexOf(marker);
1033
+ if (markerIdx === -1) markerIdx = content.indexOf(legacyMarker);
970
1034
  if (markerIdx !== -1) {
971
- const nextSnapshotIdx = content.indexOf("\n## Sprint ", markerIdx + marker.length);
1035
+ let nextSnapshotIdx = content.indexOf("\n## Cycle ", markerIdx + marker.length);
1036
+ if (nextSnapshotIdx === -1) nextSnapshotIdx = content.indexOf("\n## Sprint ", markerIdx + marker.length);
972
1037
  const before = content.slice(0, markerIdx).trimEnd();
973
1038
  const after = nextSnapshotIdx !== -1 ? content.slice(nextSnapshotIdx) : "";
974
1039
  return before + "\n\n" + block + (after ? after : "\n");
@@ -978,12 +1043,12 @@ function appendSnapshotToContent(snapshot, content) {
978
1043
  function parseSnapshots(content) {
979
1044
  if (!content.trim()) return [];
980
1045
  const snapshots = [];
981
- const headerRegex = /^## Sprint (\d+) Snapshot — (\S+)/gm;
1046
+ const headerRegex = /^## (?:Cycle|Sprint) (\d+) Snapshot — (\S+)/gm;
982
1047
  let match;
983
1048
  const headers = [];
984
1049
  while ((match = headerRegex.exec(content)) !== null) {
985
1050
  headers.push({
986
- sprint: parseInt(match[1], 10),
1051
+ cycle: parseInt(match[1], 10),
987
1052
  date: match[2],
988
1053
  index: match.index
989
1054
  });
@@ -995,7 +1060,7 @@ function parseSnapshots(content) {
995
1060
  const accuracy = parseAccuracyTable(block);
996
1061
  const velocity = parseVelocityTable(block);
997
1062
  snapshots.push({
998
- sprint: headers[i].sprint,
1063
+ cycle: headers[i].cycle,
999
1064
  date: headers[i].date,
1000
1065
  accuracy,
1001
1066
  velocity
@@ -1011,22 +1076,22 @@ function isRealFinding(text) {
1011
1076
  const trimmed = text.trim();
1012
1077
  return trimmed.length > 0 && !NONE_PATTERN.test(trimmed);
1013
1078
  }
1014
- async function detectBuildPatterns(reports, currentSprint, window = 5, clusterer) {
1079
+ async function detectBuildPatterns(reports, currentCycle, window = 5, clusterer) {
1015
1080
  const recentReports = reports.filter(
1016
- (r) => r.sprint > currentSprint - window && r.sprint <= currentSprint
1081
+ (r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
1017
1082
  );
1018
1083
  let recurringSurprises;
1019
1084
  const realSurprises = recentReports.filter((r) => isRealFinding(r.surprises));
1020
1085
  if (clusterer && realSurprises.length >= 2) {
1021
1086
  const entries = realSurprises.map((r) => ({
1022
1087
  text: r.surprises.trim(),
1023
- source: String(r.sprint)
1088
+ source: String(r.cycle)
1024
1089
  }));
1025
1090
  const clusters = await clusterer(entries);
1026
1091
  recurringSurprises = clusters.filter((c) => c.entries.length >= 2).map((c) => ({
1027
1092
  text: c.theme,
1028
1093
  count: c.entries.length,
1029
- sprints: [...new Set(c.entries.map((e) => parseInt(e.source, 10)))].sort((a, b) => a - b)
1094
+ cycles: [...new Set(c.entries.map((e) => parseInt(e.source, 10)))].sort((a, b) => a - b)
1030
1095
  }));
1031
1096
  recurringSurprises.sort((a, b) => b.count - a.count);
1032
1097
  } else {
@@ -1036,9 +1101,9 @@ async function detectBuildPatterns(reports, currentSprint, window = 5, clusterer
1036
1101
  const existing = surpriseMap.get(key);
1037
1102
  if (existing) {
1038
1103
  existing.count += 1;
1039
- existing.sprints.add(r.sprint);
1104
+ existing.cycles.add(r.cycle);
1040
1105
  } else {
1041
- surpriseMap.set(key, { original: r.surprises.trim(), count: 1, sprints: /* @__PURE__ */ new Set([r.sprint]) });
1106
+ surpriseMap.set(key, { original: r.surprises.trim(), count: 1, cycles: /* @__PURE__ */ new Set([r.cycle]) });
1042
1107
  }
1043
1108
  }
1044
1109
  recurringSurprises = [];
@@ -1047,7 +1112,7 @@ async function detectBuildPatterns(reports, currentSprint, window = 5, clusterer
1047
1112
  recurringSurprises.push({
1048
1113
  text: entry.original,
1049
1114
  count: entry.count,
1050
- sprints: [...entry.sprints].sort((a, b) => a - b)
1115
+ cycles: [...entry.cycles].sort((a, b) => a - b)
1051
1116
  });
1052
1117
  }
1053
1118
  }
@@ -1117,7 +1182,7 @@ function parseAccuracyTable(block) {
1117
1182
  const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
1118
1183
  if (cells.length < 5) continue;
1119
1184
  rows.push({
1120
- sprint: parseInt(cells[0], 10),
1185
+ cycle: parseInt(cells[0], 10),
1121
1186
  reports: parseInt(cells[1], 10),
1122
1187
  matchRate: parseInt(cells[2].replace("%", ""), 10),
1123
1188
  mae: parseFloat(cells[3]),
@@ -1143,7 +1208,7 @@ function parseVelocityTable(block) {
1143
1208
  const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
1144
1209
  if (cells.length < 5) continue;
1145
1210
  rows.push({
1146
- sprint: parseInt(cells[0], 10),
1211
+ cycle: parseInt(cells[0], 10),
1147
1212
  completed: parseInt(cells[1], 10),
1148
1213
  partial: parseInt(cells[2], 10),
1149
1214
  failed: parseInt(cells[3], 10),
@@ -1178,12 +1243,12 @@ function parseReviews(content) {
1178
1243
  const verdictRaw = parseField2(block, "Verdict");
1179
1244
  if (!VALID_VERDICTS.has(verdictRaw)) return null;
1180
1245
  const verdict = verdictRaw;
1181
- const sprint = parseInt(parseField2(block, "Sprint"), 10);
1182
- if (isNaN(sprint)) return null;
1246
+ const cycle = parseInt(parseField2(block, "Cycle"), 10);
1247
+ if (isNaN(cycle)) return null;
1183
1248
  const comments = parseField2(block, "Comments");
1184
1249
  const uuidRaw = parseField2(block, "UUID");
1185
1250
  const displayIdRaw = parseField2(block, "Display ID");
1186
- const review = { uuid: uuidRaw ?? randomUUID5(), ...displayIdRaw ? { displayId: displayIdRaw } : {}, taskId, stage, reviewer, verdict, sprint, date, comments };
1251
+ const review = { uuid: uuidRaw ?? randomUUID5(), ...displayIdRaw ? { displayId: displayIdRaw } : {}, taskId, stage, reviewer, verdict, cycle, date, comments };
1187
1252
  const handoffRevRaw = parseField2(block, "Handoff Revision");
1188
1253
  if (handoffRevRaw) {
1189
1254
  const parsed = parseInt(handoffRevRaw, 10);
@@ -1209,11 +1274,22 @@ function serializeReview(review) {
1209
1274
  lines.push(
1210
1275
  `- **Reviewer:** ${review.reviewer}`,
1211
1276
  `- **Verdict:** ${review.verdict}`,
1212
- `- **Sprint:** ${review.sprint}`,
1277
+ `- **Cycle:** ${review.cycle}`,
1213
1278
  `- **Comments:** ${review.comments}`
1214
1279
  );
1215
1280
  if (review.handoffRevision !== void 0) lines.push(`- **Handoff Revision:** ${review.handoffRevision}`);
1216
1281
  if (review.buildCommitSha) lines.push(`- **Build Commit SHA:** ${review.buildCommitSha}`);
1282
+ if (review.autoReview) {
1283
+ lines.push("", `#### Auto-Review (${review.autoReview.verdict})`);
1284
+ lines.push(`> ${review.autoReview.summary}`);
1285
+ if (review.autoReview.findings.length > 0) {
1286
+ lines.push("");
1287
+ for (const f of review.autoReview.findings) {
1288
+ const loc = f.file ? f.line ? `${f.file}:${f.line}` : f.file : "";
1289
+ lines.push(`- \`${f.severity}\`${loc ? ` ${loc}` : ""}: ${f.message}`);
1290
+ }
1291
+ }
1292
+ }
1217
1293
  return lines.join("\n");
1218
1294
  }
1219
1295
  function prependReview(review, content) {
@@ -1320,126 +1396,15 @@ ${PHASES_END}`;
1320
1396
  return content.trimEnd() + "\n\n" + newSection + "\n";
1321
1397
  }
1322
1398
 
1323
- // src/parsers/features.ts
1324
- var VALID_STATUSES2 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
1325
- var FEATURES_START = "<!-- FEATURES:START -->";
1326
- var FEATURES_END = "<!-- FEATURES:END -->";
1327
- function parseFeatures(content) {
1328
- const startIdx = content.indexOf(FEATURES_START);
1329
- const endIdx = content.indexOf(FEATURES_END);
1330
- if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return [];
1331
- const section = content.slice(startIdx + FEATURES_START.length, endIdx);
1332
- const yamlMatch = section.match(/```yaml\s*\n([\s\S]*?)```/);
1333
- if (!yamlMatch) return [];
1334
- const yamlBody = yamlMatch[1];
1335
- const features = [];
1336
- const blocks = yamlBody.split(/^(?=\s*- id:)/m).filter((b) => b.trim());
1337
- for (const block of blocks) {
1338
- const feature = parseFeatureBlock(block);
1339
- if (feature) features.push(feature);
1340
- }
1341
- return features.sort((a, b) => a.roadmapPosition - b.roadmapPosition);
1342
- }
1343
- function parseYamlField2(block, field) {
1344
- const pattern = new RegExp(`^\\s*${field}:\\s*(.+)$`, "m");
1345
- const match = block.match(pattern);
1346
- if (!match) return "";
1347
- let value = match[1].trim();
1348
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1349
- value = value.slice(1, -1);
1350
- }
1351
- return value;
1352
- }
1353
- function parseOptionalYamlField(block, field) {
1354
- const value = parseYamlField2(block, field);
1355
- return value || void 0;
1356
- }
1357
- function parseFeatureBlock(block) {
1358
- const idMatch = block.match(/^\s*- id:\s*(.+)$/m);
1359
- if (!idMatch) return null;
1360
- let id = idMatch[1].trim();
1361
- if (id.startsWith('"') && id.endsWith('"') || id.startsWith("'") && id.endsWith("'")) {
1362
- id = id.slice(1, -1);
1363
- }
1364
- const displayId = parseYamlField2(block, "displayId");
1365
- const slug = parseYamlField2(block, "slug");
1366
- const name = parseYamlField2(block, "name");
1367
- const description = parseYamlField2(block, "description");
1368
- const statusRaw = parseYamlField2(block, "status");
1369
- const roadmapPositionRaw = parseYamlField2(block, "roadmapPosition");
1370
- const createdAt = parseYamlField2(block, "createdAt");
1371
- const createdSprintRaw = parseYamlField2(block, "createdSprint");
1372
- if (!displayId || !slug || !name || !statusRaw || !roadmapPositionRaw || !createdAt || !createdSprintRaw) return null;
1373
- const status = statusRaw;
1374
- if (!VALID_STATUSES2.has(status)) return null;
1375
- const roadmapPosition = parseInt(roadmapPositionRaw, 10);
1376
- if (isNaN(roadmapPosition)) return null;
1377
- const createdSprint = parseInt(createdSprintRaw, 10);
1378
- if (isNaN(createdSprint)) return null;
1379
- const completedAt = parseOptionalYamlField(block, "completedAt");
1380
- const commitDate = parseOptionalYamlField(block, "commitDate");
1381
- const completedSprintRaw = parseOptionalYamlField(block, "completedSprint");
1382
- const completedSprint = completedSprintRaw ? parseInt(completedSprintRaw, 10) : void 0;
1383
- return {
1384
- id,
1385
- displayId,
1386
- slug,
1387
- name,
1388
- description,
1389
- status,
1390
- roadmapPosition,
1391
- createdAt,
1392
- completedAt,
1393
- commitDate,
1394
- createdSprint,
1395
- completedSprint: completedSprint !== void 0 && !isNaN(completedSprint) ? completedSprint : void 0
1396
- };
1397
- }
1398
- function serializeFeatures(features) {
1399
- const sorted = [...features].sort((a, b) => a.roadmapPosition - b.roadmapPosition);
1400
- const yamlLines = ["features:"];
1401
- for (const f of sorted) {
1402
- yamlLines.push(` - id: "${f.id}"`);
1403
- yamlLines.push(` displayId: "${f.displayId}"`);
1404
- yamlLines.push(` slug: "${f.slug}"`);
1405
- yamlLines.push(` name: "${f.name}"`);
1406
- yamlLines.push(` description: "${f.description}"`);
1407
- yamlLines.push(` status: "${f.status}"`);
1408
- yamlLines.push(` roadmapPosition: ${f.roadmapPosition}`);
1409
- yamlLines.push(` createdAt: "${f.createdAt}"`);
1410
- if (f.completedAt) yamlLines.push(` completedAt: "${f.completedAt}"`);
1411
- if (f.commitDate) yamlLines.push(` commitDate: "${f.commitDate}"`);
1412
- yamlLines.push(` createdSprint: ${f.createdSprint}`);
1413
- if (f.completedSprint !== void 0) yamlLines.push(` completedSprint: ${f.completedSprint}`);
1414
- }
1415
- return yamlLines.join("\n");
1416
- }
1417
- function writeFeaturesToContent(features, content) {
1418
- const yaml4 = serializeFeatures(features);
1419
- const newSection = `${FEATURES_START}
1420
-
1421
- \`\`\`yaml
1422
- ${yaml4}
1423
- \`\`\`
1424
-
1425
- ${FEATURES_END}`;
1426
- const startIdx = content.indexOf(FEATURES_START);
1427
- const endIdx = content.indexOf(FEATURES_END);
1428
- if (startIdx !== -1 && endIdx !== -1) {
1429
- return content.slice(0, startIdx) + newSection + content.slice(endIdx + FEATURES_END.length);
1430
- }
1431
- return content.trimEnd() + "\n\n" + newSection + "\n";
1432
- }
1433
-
1434
- // src/parsers/sprints.ts
1399
+ // src/parsers/cycles.ts
1435
1400
  import yaml2 from "js-yaml";
1436
1401
  var YAML_MARKER2 = "<!-- PAPI-ADAPTER: parse the yaml block below -->";
1437
1402
  var YAML_START2 = "<!-- PAPI-YAML-START -->";
1438
1403
  var YAML_END2 = "<!-- PAPI-YAML-END -->";
1439
- var VALID_STATUSES3 = /* @__PURE__ */ new Set(["planning", "active", "complete"]);
1440
- function toSprint(raw) {
1441
- if (!VALID_STATUSES3.has(raw.status)) return null;
1442
- const sprint = {
1404
+ var VALID_STATUSES2 = /* @__PURE__ */ new Set(["planning", "active", "complete"]);
1405
+ function toCycle(raw) {
1406
+ if (!VALID_STATUSES2.has(raw.status)) return null;
1407
+ const cycle = {
1443
1408
  id: raw.id,
1444
1409
  number: raw.number,
1445
1410
  status: raw.status,
@@ -1448,71 +1413,73 @@ function toSprint(raw) {
1448
1413
  boardHealth: raw.board_health ?? "",
1449
1414
  taskIds: raw.task_ids ?? []
1450
1415
  };
1451
- if (raw.end_date) sprint.endDate = raw.end_date;
1452
- return sprint;
1416
+ if (raw.end_date) cycle.endDate = raw.end_date;
1417
+ if (raw.user_id) cycle.userId = raw.user_id;
1418
+ return cycle;
1453
1419
  }
1454
- function fromSprint(sprint) {
1420
+ function fromCycle(cycle) {
1455
1421
  const raw = {
1456
- id: sprint.id,
1457
- number: sprint.number,
1458
- status: sprint.status,
1459
- start_date: sprint.startDate,
1460
- goals: sprint.goals,
1461
- board_health: sprint.boardHealth,
1462
- task_ids: sprint.taskIds
1422
+ id: cycle.id,
1423
+ number: cycle.number,
1424
+ status: cycle.status,
1425
+ start_date: cycle.startDate,
1426
+ goals: cycle.goals,
1427
+ board_health: cycle.boardHealth,
1428
+ task_ids: cycle.taskIds
1463
1429
  };
1464
- if (sprint.endDate) raw.end_date = sprint.endDate;
1430
+ if (cycle.endDate) raw.end_date = cycle.endDate;
1431
+ if (cycle.userId) raw.user_id = cycle.userId;
1465
1432
  return raw;
1466
1433
  }
1467
1434
  function extractYamlBlock2(content) {
1468
1435
  const markerIdx = content.indexOf(YAML_MARKER2);
1469
- if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in SPRINTS.md");
1436
+ if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLES.md");
1470
1437
  const afterMarker = content.slice(markerIdx + YAML_MARKER2.length);
1471
1438
  const startIdx = afterMarker.indexOf(YAML_START2);
1472
- if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in SPRINTS.md");
1439
+ if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in CYCLES.md");
1473
1440
  const yamlStart = startIdx + YAML_START2.length;
1474
1441
  const endIdx = afterMarker.indexOf(YAML_END2, yamlStart);
1475
- if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in SPRINTS.md");
1442
+ if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLES.md");
1476
1443
  return afterMarker.slice(yamlStart, endIdx);
1477
1444
  }
1478
- function parseSprints(content) {
1445
+ function parseCycles(content) {
1479
1446
  if (!content.trim()) return [];
1480
1447
  const yamlText = extractYamlBlock2(content);
1481
1448
  const data = yaml2.load(yamlText);
1482
- return (data.sprints ?? []).map(toSprint).filter((s) => s !== null);
1449
+ return (data.cycles ?? []).map(toCycle).filter((s) => s !== null);
1483
1450
  }
1484
- function serializeSprints(sprints, content) {
1485
- const raw = sprints.map(fromSprint);
1486
- const yamlStr = yaml2.dump({ sprints: raw }, { lineWidth: 120, quotingType: '"' });
1451
+ function serializeCycles(cycles, content) {
1452
+ const raw = cycles.map(fromCycle);
1453
+ const yamlStr = yaml2.dump({ cycles: raw }, { lineWidth: 120, quotingType: '"' });
1487
1454
  const markerIdx = content.indexOf(YAML_MARKER2);
1488
- if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in SPRINTS.md");
1455
+ if (markerIdx === -1) throw new Error("PAPI-ADAPTER marker not found in CYCLES.md");
1489
1456
  const afterMarker = content.slice(markerIdx + YAML_MARKER2.length);
1490
1457
  const startIdx = afterMarker.indexOf(YAML_START2);
1491
- if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in SPRINTS.md");
1458
+ if (startIdx === -1) throw new Error("PAPI-YAML-START marker not found in CYCLES.md");
1492
1459
  const absStart = markerIdx + YAML_MARKER2.length + startIdx;
1493
1460
  const endIdx = afterMarker.indexOf(YAML_END2, startIdx);
1494
- if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in SPRINTS.md");
1461
+ if (endIdx === -1) throw new Error("PAPI-YAML-END marker not found in CYCLES.md");
1495
1462
  const absEnd = markerIdx + YAML_MARKER2.length + endIdx + YAML_END2.length;
1496
1463
  return content.slice(0, absStart) + YAML_START2 + "\n" + yamlStr + YAML_END2 + content.slice(absEnd);
1497
1464
  }
1498
- function serializeSprint(sprint) {
1499
- const raw = fromSprint(sprint);
1465
+ function serializeCycle(cycle) {
1466
+ const raw = fromCycle(cycle);
1500
1467
  return yaml2.dump(raw, { lineWidth: 120, quotingType: '"' }).trim();
1501
1468
  }
1502
- function prependSprint(sprint, content) {
1469
+ function prependCycle(cycle, content) {
1503
1470
  if (!content.trim()) {
1504
- const header = `# Sprints
1471
+ const header = `# Cycles
1505
1472
 
1506
1473
  <!-- PAPI-ADAPTER: parse the yaml block below -->
1507
1474
 
1508
1475
  `;
1509
- const raw = fromSprint(sprint);
1510
- const yamlStr = yaml2.dump({ sprints: [raw] }, { lineWidth: 120, quotingType: '"' });
1476
+ const raw = fromCycle(cycle);
1477
+ const yamlStr = yaml2.dump({ cycles: [raw] }, { lineWidth: 120, quotingType: '"' });
1511
1478
  return header + YAML_START2 + "\n" + yamlStr + YAML_END2 + "\n";
1512
1479
  }
1513
- const existing = parseSprints(content);
1514
- const merged = [sprint, ...existing];
1515
- return serializeSprints(merged, content);
1480
+ const existing = parseCycles(content);
1481
+ const merged = [cycle, ...existing];
1482
+ return serializeCycles(merged, content);
1516
1483
  }
1517
1484
 
1518
1485
  // src/parsers/registries.ts
@@ -1588,77 +1555,87 @@ var MdFileAdapter = class {
1588
1555
  await writeFile(this.path(file), content, "utf-8");
1589
1556
  }
1590
1557
  // --- Planning Log ---
1591
- /** Parse the full planning context into structured sections (reads from PLANNING_LOG.md + ACTIVE_DECISIONS.md + SPRINT_LOG.md). */
1558
+ /** Parse the full planning context into structured sections (reads from PLANNING_LOG.md + ACTIVE_DECISIONS.md + CYCLE_LOG.md). */
1592
1559
  async readPlanningLog() {
1593
- const [planningContent, activeDecisionsContent, sprintLogContent] = await Promise.all([
1560
+ const [planningContent, activeDecisionsContent, cycleLogContent] = await Promise.all([
1594
1561
  this.read("PLANNING_LOG.md"),
1595
1562
  this.readOptional("ACTIVE_DECISIONS.md"),
1596
1563
  this.readOptional("SPRINT_LOG.md")
1597
1564
  ]);
1598
- return parsePlanningLog(planningContent, activeDecisionsContent, sprintLogContent);
1565
+ return parsePlanningLog(planningContent, activeDecisionsContent, cycleLogContent);
1599
1566
  }
1600
- /** Read the Sprint Health table from PLANNING_LOG.md. */
1601
- async getSprintHealth() {
1602
- return parseSprintHealth(await this.read("PLANNING_LOG.md"));
1567
+ /** Read the Cycle Health table from PLANNING_LOG.md. */
1568
+ async getCycleHealth() {
1569
+ return parseCycleHealth(await this.read("PLANNING_LOG.md"));
1603
1570
  }
1604
- /** Read all Active Decisions from ACTIVE_DECISIONS.md. */
1605
- async getActiveDecisions() {
1571
+ /**
1572
+ * Read Active Decisions from ACTIVE_DECISIONS.md.
1573
+ *
1574
+ * Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
1575
+ * Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
1576
+ */
1577
+ async getActiveDecisions(options) {
1606
1578
  const content = await this.readOptional("ACTIVE_DECISIONS.md");
1607
1579
  if (!content) return [];
1608
- return parseActiveDecisions(content);
1580
+ const all = parseActiveDecisions(content);
1581
+ if (options?.includeRetired) return all;
1582
+ return all.filter(isLiveDecision);
1609
1583
  }
1610
- /** Read sprint log entries (newest first), optionally limited to {@link limit} entries. */
1611
- async getSprintLog(limit) {
1612
- return parseSprintLog(await this.read("SPRINT_LOG.md"), limit);
1584
+ /** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
1585
+ async getCycleLog(limit) {
1586
+ return parseCycleLog(await this.read("SPRINT_LOG.md"), limit);
1613
1587
  }
1614
- async getSprintLogSince(sprintNumber) {
1615
- const log = await this.getSprintLog();
1616
- return log.filter((entry) => entry.sprintNumber >= sprintNumber);
1588
+ async getCycleLogSince(cycleNumber) {
1589
+ const log = await this.getCycleLog();
1590
+ return log.filter((entry) => entry.cycleNumber >= cycleNumber);
1617
1591
  }
1618
- /** Merge partial updates into the Sprint Health table and write back. */
1619
- async setSprintHealth(updates) {
1592
+ /** Merge partial updates into the Cycle Health table and write back. */
1593
+ async setCycleHealth(updates) {
1620
1594
  const content = await this.read("PLANNING_LOG.md");
1621
- const current = parseSprintHealth(content);
1595
+ const current = parseCycleHealth(content);
1622
1596
  const updated = { ...current, ...updates };
1623
- await this.write("PLANNING_LOG.md", serializeSprintHealth(updated, content));
1624
- }
1625
- /** Prepend a new sprint log entry at the top of the Sprint Log section. */
1626
- async writeSprintLogEntry(entry) {
1627
- if (!entry.uuid) {
1628
- entry = { ...entry, uuid: randomUUID6() };
1629
- }
1597
+ await this.write("PLANNING_LOG.md", serializeCycleHealth(updated, content));
1598
+ }
1599
+ /** Prepend a new cycle log entry at the top of the Cycle Log section. */
1600
+ async writeCycleLogEntry(entry) {
1601
+ const patched = {
1602
+ ...entry,
1603
+ uuid: entry.uuid || randomUUID6(),
1604
+ date: entry.date ?? (/* @__PURE__ */ new Date()).toISOString()
1605
+ };
1630
1606
  const content = await this.read("SPRINT_LOG.md");
1631
- await this.write("SPRINT_LOG.md", prependSprintLogEntry(entry, content));
1607
+ await this.write("SPRINT_LOG.md", prependCycleLogEntry(patched, content));
1632
1608
  }
1633
- /** Write a strategy review — for md adapter, delegates to sprint log. */
1609
+ /** Write a strategy review — for md adapter, delegates to cycle log. */
1634
1610
  async writeStrategyReview(review) {
1635
- await this.writeSprintLogEntry({
1611
+ await this.writeCycleLogEntry({
1636
1612
  uuid: randomUUID6(),
1637
- sprintNumber: review.sprintNumber,
1613
+ cycleNumber: review.cycleNumber,
1638
1614
  title: review.title,
1639
1615
  content: review.content,
1640
1616
  notes: review.notes
1641
1617
  });
1642
1618
  }
1643
- /** Get the sprint number of the last strategy review. */
1644
- async getLastStrategyReviewSprint() {
1645
- const log = await this.getSprintLog();
1619
+ /** Get the cycle number of the last strategy review. */
1620
+ async getLastStrategyReviewCycle() {
1621
+ const log = await this.getCycleLog();
1646
1622
  const entry = log.find(
1647
1623
  (e) => /strategy.*review|strategic.*shift/i.test(e.title)
1648
1624
  );
1649
- return entry?.sprintNumber ?? 0;
1625
+ return entry?.cycleNumber ?? 0;
1650
1626
  }
1651
- /** Get strategy reviews — md adapter returns empty (reviews live in sprint log). */
1652
- async getStrategyReviews(_limit) {
1627
+ /** Get strategy reviews — md adapter returns empty (reviews live in cycle log). */
1628
+ async getStrategyReviews(_limit, _includeFullAnalysis) {
1653
1629
  return [];
1654
1630
  }
1655
1631
  /** Update or insert an Active Decision block by ID. */
1656
- async updateActiveDecision(id, body, sprintNumber) {
1632
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1633
+ async updateActiveDecision(id, body, cycleNumber, _action) {
1657
1634
  const content = await this.readOptional("ACTIVE_DECISIONS.md") || "## Active Decisions\n\n";
1658
- await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, sprintNumber));
1635
+ await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, cycleNumber));
1659
1636
  }
1660
- // --- Sprint Board ---
1661
- /** Query the sprint board, optionally filtering by status/priority/phase/etc. */
1637
+ // --- Cycle Board ---
1638
+ /** Query the cycle board, optionally filtering by status/priority/phase/etc. */
1662
1639
  async queryBoard(options) {
1663
1640
  const tasks = parseBoard(await this.read("SPRINT_BOARD.md"));
1664
1641
  return options ? filterTasks(tasks, options) : tasks;
@@ -1714,7 +1691,7 @@ var MdFileAdapter = class {
1714
1691
  const archivedTasks = archiveContent ? parseBoard(archiveContent) : [];
1715
1692
  await this.warnInvalidPhase(task.phase);
1716
1693
  await this.warnInvalidModule(task.module);
1717
- await this.warnInvalidEpic(task.epic);
1694
+ if (task.epic) await this.warnInvalidEpic(task.epic);
1718
1695
  if (task.dependsOn) {
1719
1696
  const allTaskIds = new Set([...tasks, ...archivedTasks].map((t) => t.id));
1720
1697
  const depIds = task.dependsOn.split(",").map((s) => s.trim()).filter(Boolean);
@@ -1783,6 +1760,68 @@ var MdFileAdapter = class {
1783
1760
  async updateTaskStatus(id, status) {
1784
1761
  return this.updateTask(id, { status });
1785
1762
  }
1763
+ /**
1764
+ * task-1763 (C293): atomic compare-and-swap task claim. First-claim-wins —
1765
+ * sets assigneeId only if the task is currently unclaimed. The markdown adapter
1766
+ * is single-process, so the read-check-write is trivially atomic here; the real
1767
+ * concurrency guarantee lives in the pg adapter's RETURNING CAS. Returns the
1768
+ * claimed task, or null if it was already claimed or does not exist.
1769
+ *
1770
+ * task-2071 (MU-3): the pooled-task invariant is `assigneeId == null && cycle
1771
+ * == null` — a task already pulled into someone's cycle is NOT in the pool and
1772
+ * cannot be claimed. Sets claimSource='pool' on success.
1773
+ */
1774
+ async claimTask(taskId, assigneeId) {
1775
+ const content = await this.read("SPRINT_BOARD.md");
1776
+ const tasks = parseBoard(content);
1777
+ const idx = tasks.findIndex((t) => t.id === taskId);
1778
+ if (idx === -1) return null;
1779
+ if (tasks[idx].assigneeId || tasks[idx].cycle != null) return null;
1780
+ tasks[idx] = { ...tasks[idx], assigneeId, claimSource: "pool" };
1781
+ await this.write("SPRINT_BOARD.md", serializeBoard(tasks, content));
1782
+ return tasks[idx];
1783
+ }
1784
+ /**
1785
+ * task-2071 (C293, MU-3): claimer-only release. Clears assigneeId + claimSource
1786
+ * only if the task is currently assigned to `assigneeId` and has not entered
1787
+ * review. Returns the unclaimed task, or null if the caller is not the claimer,
1788
+ * the task has progressed, or it does not exist.
1789
+ */
1790
+ async unclaimTask(taskId, assigneeId) {
1791
+ const content = await this.read("SPRINT_BOARD.md");
1792
+ const tasks = parseBoard(content);
1793
+ const idx = tasks.findIndex((t2) => t2.id === taskId);
1794
+ if (idx === -1) return null;
1795
+ const t = tasks[idx];
1796
+ if (t.assigneeId !== assigneeId) return null;
1797
+ if (t.status === "In Review" || t.status === "Done") return null;
1798
+ const next = { ...t };
1799
+ delete next.assigneeId;
1800
+ delete next.claimSource;
1801
+ tasks[idx] = next;
1802
+ await this.write("SPRINT_BOARD.md", serializeBoard(tasks, content));
1803
+ return tasks[idx];
1804
+ }
1805
+ /**
1806
+ * task-2072 (C293, MU-4): atomic review claim. Sets reviewerId only if the task
1807
+ * is In Review and not yet claimed for review (reviewerId == null). First-claim-
1808
+ * wins. Returns the claimed task, or null if already review-claimed / not In
1809
+ * Review / missing.
1810
+ */
1811
+ async claimReview(taskId, reviewerId) {
1812
+ const content = await this.read("SPRINT_BOARD.md");
1813
+ const tasks = parseBoard(content);
1814
+ const idx = tasks.findIndex((t2) => t2.id === taskId);
1815
+ if (idx === -1) return null;
1816
+ const t = tasks[idx];
1817
+ if (t.status !== "In Review") return null;
1818
+ if (t.reviewerId) return null;
1819
+ tasks[idx] = { ...t, reviewerId };
1820
+ await this.write("SPRINT_BOARD.md", serializeBoard(tasks, content));
1821
+ return tasks[idx];
1822
+ }
1823
+ async recordTransition(_taskId, _fromStatus, _toStatus, _changedBy) {
1824
+ }
1786
1825
  // --- Build Reports ---
1787
1826
  /** Insert a new build report at the top of BUILD_REPORTS.md. */
1788
1827
  async appendBuildReport(report) {
@@ -1810,10 +1849,15 @@ var MdFileAdapter = class {
1810
1849
  const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
1811
1850
  return reports.slice(0, count);
1812
1851
  }
1813
- /** Return all build reports from sprints >= {@link sprintNumber}. */
1814
- async getBuildReportsSince(sprintNumber) {
1852
+ /** Return the number of build reports for a specific task. */
1853
+ async getBuildReportCountForTask(taskId) {
1815
1854
  const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
1816
- return reports.filter((r) => r.sprint >= sprintNumber);
1855
+ return reports.filter((r) => r.taskId === taskId).length;
1856
+ }
1857
+ /** Return all build reports from cycles >= {@link cycleNumber}. */
1858
+ async getBuildReportsSince(cycleNumber) {
1859
+ const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
1860
+ return reports.filter((r) => r.cycle >= cycleNumber);
1817
1861
  }
1818
1862
  // --- Human Reviews ---
1819
1863
  /** Return recent human reviews from REVIEWS.md (newest first), optionally limited to {@link count}. */
@@ -1845,10 +1889,10 @@ var MdFileAdapter = class {
1845
1889
  await this.write("REVIEWS.md", prependReview(review, content));
1846
1890
  }
1847
1891
  // --- Compression ---
1848
- /** Compress old sprint log entries below {@link threshold} into a summary block. */
1849
- async compressSprintLog(threshold, summary) {
1892
+ /** Compress old cycle log entries below {@link threshold} into a summary block. */
1893
+ async compressCycleLog(threshold, summary) {
1850
1894
  const content = await this.read("SPRINT_LOG.md");
1851
- await this.write("SPRINT_LOG.md", compressSprintLogInContent(content, threshold, summary));
1895
+ await this.write("SPRINT_LOG.md", compressCycleLogInContent(content, threshold, summary));
1852
1896
  }
1853
1897
  /** Compress old build reports below {@link threshold} into a summary block. */
1854
1898
  async compressBuildReports(threshold, summary) {
@@ -1870,17 +1914,17 @@ var MdFileAdapter = class {
1870
1914
  const { buildHandoff, buildReport, ...rest } = task;
1871
1915
  return rest;
1872
1916
  }
1873
- /** Append tasks to ARCHIVE_SPRINT_BOARD.md, stripping heavy fields and deduplicating by ID. */
1917
+ /** Append tasks to ARCHIVE_CYCLE_BOARD.md, stripping heavy fields and deduplicating by ID. */
1874
1918
  async appendToArchive(tasks) {
1875
1919
  const existing = await this.readOptional("ARCHIVE_SPRINT_BOARD.md");
1876
- const archiveContent = existing || "# PAPI Sprint Board \u2014 Archive\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntasks: []\n<!-- PAPI-YAML-END -->\n";
1920
+ const archiveContent = existing || "# PAPI Cycle Board \u2014 Archive\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntasks: []\n<!-- PAPI-YAML-END -->\n";
1877
1921
  const existingArchived = parseBoard(archiveContent);
1878
1922
  const existingIds = new Set(existingArchived.map((t) => t.id));
1879
1923
  const newArchive = tasks.filter((t) => !existingIds.has(t.id)).map((t) => this.stripHeavyFields(t));
1880
1924
  const merged = [...existingArchived, ...newArchive];
1881
1925
  await this.write("ARCHIVE_SPRINT_BOARD.md", serializeBoard(merged, archiveContent));
1882
1926
  }
1883
- /** Archive tasks matching phases and/or statuses to ARCHIVE_SPRINT_BOARD.md and remove them from active board. */
1927
+ /** Archive tasks matching phases and/or statuses to ARCHIVE_CYCLE_BOARD.md and remove them from active board. */
1884
1928
  async archiveTasks(phases, statuses) {
1885
1929
  const content = await this.read("SPRINT_BOARD.md");
1886
1930
  const tasks = parseBoard(content);
@@ -1916,6 +1960,11 @@ var MdFileAdapter = class {
1916
1960
  async updateProductBrief(content) {
1917
1961
  await this.write("PRODUCT_BRIEF.md", content);
1918
1962
  }
1963
+ async readDiscoveryCanvas() {
1964
+ return {};
1965
+ }
1966
+ async updateDiscoveryCanvas(_canvas) {
1967
+ }
1919
1968
  /** Read all phases from PHASES.md (falls back to PRODUCT_BRIEF.md for migration). */
1920
1969
  async readPhases() {
1921
1970
  const phasesContent = await this.readOptional("PHASES.md");
@@ -1944,59 +1993,6 @@ ${PHASES_END2}`;
1944
1993
  } else {
1945
1994
  await this.write("PHASES.md", `# Phases
1946
1995
 
1947
- ${newSection}
1948
- `);
1949
- }
1950
- }
1951
- // --- Features ---
1952
- /** Read all features from FEATURES.md. */
1953
- async readFeatures() {
1954
- const content = await this.readOptional("FEATURES.md");
1955
- return content ? parseFeatures(content) : [];
1956
- }
1957
- /** Get a single feature by UUID. */
1958
- async getFeature(id) {
1959
- const features = await this.readFeatures();
1960
- return features.find((f) => f.id === id) ?? null;
1961
- }
1962
- /** Create a new feature and append it to FEATURES.md. */
1963
- async createFeature(feature) {
1964
- const features = await this.readFeatures();
1965
- const id = randomUUID6();
1966
- const newFeature = { id, ...feature };
1967
- features.push(newFeature);
1968
- await this.writeFeatures(features);
1969
- return newFeature;
1970
- }
1971
- /** Update an existing feature by UUID. */
1972
- async updateFeature(id, updates) {
1973
- const features = await this.readFeatures();
1974
- const idx = features.findIndex((f) => f.id === id);
1975
- if (idx === -1) throw new Error(`Feature not found: ${id}`);
1976
- features[idx] = { ...features[idx], ...updates };
1977
- await this.writeFeatures(features);
1978
- }
1979
- /** Write all features to FEATURES.md. */
1980
- async writeFeatures(features) {
1981
- const content = await this.readOptional("FEATURES.md");
1982
- const existing = content || "";
1983
- const yaml4 = serializeFeatures(features);
1984
- const FEATURES_START2 = "<!-- FEATURES:START -->";
1985
- const FEATURES_END2 = "<!-- FEATURES:END -->";
1986
- const newSection = `${FEATURES_START2}
1987
-
1988
- \`\`\`yaml
1989
- ${yaml4}
1990
- \`\`\`
1991
-
1992
- ${FEATURES_END2}`;
1993
- const startIdx = existing.indexOf(FEATURES_START2);
1994
- const endIdx = existing.indexOf(FEATURES_END2);
1995
- if (startIdx !== -1 && endIdx !== -1) {
1996
- await this.write("FEATURES.md", existing.slice(0, startIdx) + newSection + existing.slice(endIdx + FEATURES_END2.length));
1997
- } else {
1998
- await this.write("FEATURES.md", `# Features
1999
-
2000
1996
  ${newSection}
2001
1997
  `);
2002
1998
  }
@@ -2013,15 +2009,14 @@ ${newSection}
2013
2009
  if (!content) return [];
2014
2010
  return parseToolMetrics(content);
2015
2011
  }
2016
- /** Aggregate tool call metrics into a cost summary, optionally filtered by sprint. */
2017
- async getCostSummary(sprintNumber) {
2012
+ async hasToolMilestone(name) {
2018
2013
  const metrics = await this.readToolMetrics();
2019
- return aggregateCostSummary(metrics, sprintNumber);
2014
+ return metrics.some((m) => m.tool === name);
2020
2015
  }
2021
- /** Write a cost snapshot to the Cost Summary section of METRICS.md. */
2022
- async writeCostSnapshot(snapshot) {
2023
- const content = await this.readOptional("METRICS.md");
2024
- await this.write("METRICS.md", writeCostSnapshotToContent(snapshot, content));
2016
+ /** Aggregate tool call metrics into a cost summary, optionally filtered by cycle. */
2017
+ async getCostSummary(cycleNumber) {
2018
+ const metrics = await this.readToolMetrics();
2019
+ return aggregateCostSummary(metrics, cycleNumber);
2025
2020
  }
2026
2021
  /** Read all cost snapshots from the Cost Summary section of METRICS.md. */
2027
2022
  async getCostSnapshots() {
@@ -2029,29 +2024,29 @@ ${newSection}
2029
2024
  if (!content) return [];
2030
2025
  return parseCostSnapshots(content);
2031
2026
  }
2032
- // --- Sprint Methodology Metrics ---
2033
- /** Append a sprint metrics snapshot to SPRINT_METRICS.md. */
2034
- async appendSprintMetrics(snapshot) {
2027
+ // --- Cycle Methodology Metrics ---
2028
+ /** Append a cycle metrics snapshot to CYCLE_METRICS.md. */
2029
+ async appendCycleMetrics(snapshot) {
2035
2030
  const content = await this.readOptional("SPRINT_METRICS.md");
2036
2031
  await this.write("SPRINT_METRICS.md", appendSnapshotToContent(snapshot, content));
2037
2032
  }
2038
- /** Read all sprint metrics snapshots from SPRINT_METRICS.md. */
2039
- async readSprintMetrics() {
2033
+ /** Read all cycle metrics snapshots from CYCLE_METRICS.md. */
2034
+ async readCycleMetrics() {
2040
2035
  const content = await this.readOptional("SPRINT_METRICS.md");
2041
2036
  if (!content) return [];
2042
2037
  return parseSnapshots(content);
2043
2038
  }
2044
- // --- Sprints ---
2045
- /** Read all Sprint entities from SPRINTS.md (newest first). */
2046
- async readSprints() {
2047
- const content = await this.readOptional("SPRINTS.md");
2039
+ // --- Cycles ---
2040
+ /** Read all Cycle entities from CYCLES.md (newest first). */
2041
+ async readCycles() {
2042
+ const content = await this.readOptional("CYCLES.md");
2048
2043
  if (!content) return [];
2049
- return parseSprints(content);
2044
+ return parseCycles(content);
2050
2045
  }
2051
- /** Write a new Sprint entity to SPRINTS.md. */
2052
- async createSprint(sprint) {
2053
- const content = await this.readOptional("SPRINTS.md");
2054
- await this.write("SPRINTS.md", prependSprint(sprint, content));
2046
+ /** Write a new Cycle entity to CYCLES.md. */
2047
+ async createCycle(cycle) {
2048
+ const content = await this.readOptional("CYCLES.md");
2049
+ await this.write("CYCLES.md", prependCycle(cycle, content));
2055
2050
  }
2056
2051
  // --- Registries ---
2057
2052
  /** Read module and epic registries from REGISTRIES.md. */
@@ -2081,8 +2076,9 @@ ${newSection}
2081
2076
  ` type: ${full.type}`,
2082
2077
  ` status: ${full.status}`,
2083
2078
  ` content: ${JSON.stringify(full.content)}`,
2084
- ` created_sprint: ${full.createdSprint}`,
2085
- full.actionedSprint != null ? ` actioned_sprint: ${full.actionedSprint}` : null
2079
+ ` created_sprint: ${full.createdCycle}`,
2080
+ full.actionedCycle != null ? ` actioned_cycle: ${full.actionedCycle}` : null,
2081
+ full.target != null ? ` target: ${JSON.stringify(full.target)}` : null
2086
2082
  ].filter(Boolean).join("\n");
2087
2083
  if (!content) {
2088
2084
  await this.write("STRATEGY_RECOMMENDATIONS.md", `${header}${entry}
@@ -2115,7 +2111,7 @@ ${footer}`);
2115
2111
  const statusMatch = block.match(/status:\s+(.+)/);
2116
2112
  const contentMatch = block.match(/content:\s+(.+)/);
2117
2113
  const createdMatch = block.match(/created_sprint:\s+(\d+)/);
2118
- const actionedMatch = block.match(/actioned_sprint:\s+(\d+)/);
2114
+ const actionedMatch = block.match(/actioned_cycle:\s+(\d+)/);
2119
2115
  if (!idMatch || !typeMatch || !statusMatch || !contentMatch || !createdMatch) continue;
2120
2116
  const status = statusMatch[1].trim();
2121
2117
  if (status !== "pending") continue;
@@ -2131,29 +2127,125 @@ ${footer}`);
2131
2127
  type: typeMatch[1].trim(),
2132
2128
  status: "pending",
2133
2129
  content: parsedContent,
2134
- createdSprint: parseInt(createdMatch[1], 10),
2135
- actionedSprint: actionedMatch ? parseInt(actionedMatch[1], 10) : void 0
2130
+ createdCycle: parseInt(createdMatch[1], 10),
2131
+ actionedCycle: actionedMatch ? parseInt(actionedMatch[1], 10) : void 0
2136
2132
  });
2137
2133
  }
2138
2134
  return recs;
2139
2135
  }
2140
2136
  /** Mark a recommendation as actioned. */
2141
- async actionRecommendation(id, sprintNumber) {
2137
+ async actionRecommendation(id, cycleNumber) {
2142
2138
  const content = await this.readOptional("STRATEGY_RECOMMENDATIONS.md");
2143
2139
  if (!content) return;
2144
2140
  const idPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?)(\\s+status:\\s+)pending`);
2145
2141
  let updated = content.replace(idPattern, `$1$2actioned`);
2146
2142
  const entryPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?)(?=\\s+-\\s+id:|<!-- PAPI-YAML-END -->)`);
2147
2143
  const entryMatch = updated.match(entryPattern);
2148
- if (entryMatch && !entryMatch[0].includes("actioned_sprint:")) {
2149
- const sprintPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_sprint:\\s+\\d+)\\n`);
2150
- updated = updated.replace(sprintPattern, `$1
2151
- actioned_sprint: ${sprintNumber}
2144
+ if (entryMatch && !entryMatch[0].includes("actioned_cycle:")) {
2145
+ const cyclePattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_sprint:\\s+\\d+)\\n`);
2146
+ updated = updated.replace(cyclePattern, `$1
2147
+ actioned_cycle: ${cycleNumber}
2152
2148
  `);
2153
2149
  }
2154
2150
  await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
2155
2151
  }
2156
2152
  // -------------------------------------------------------------------------
2153
+ // Strategy Review Agenda (markdown persistence)
2154
+ // -------------------------------------------------------------------------
2155
+ async addAgendaTopic(input) {
2156
+ const id = randomUUID6();
2157
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2158
+ const full = {
2159
+ id,
2160
+ topic: input.topic,
2161
+ source: input.source,
2162
+ sourceCycle: input.sourceCycle,
2163
+ status: "pending",
2164
+ createdAt
2165
+ };
2166
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2167
+ const header = "# Strategy Review Agenda\n\n<!-- PAPI-ADAPTER: parse the yaml block below -->\n\n<!-- PAPI-YAML-START -->\ntopics:\n";
2168
+ const footer = "<!-- PAPI-YAML-END -->\n";
2169
+ const entry = [
2170
+ ` - id: ${full.id}`,
2171
+ ` topic: ${JSON.stringify(full.topic)}`,
2172
+ ` source: ${full.source}`,
2173
+ full.sourceCycle != null ? ` source_cycle: ${full.sourceCycle}` : null,
2174
+ ` status: ${full.status}`,
2175
+ ` created_at: ${full.createdAt}`
2176
+ ].filter(Boolean).join("\n");
2177
+ if (!content) {
2178
+ await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
2179
+ ${footer}`);
2180
+ } else {
2181
+ const insertPoint = content.indexOf("<!-- PAPI-YAML-END -->");
2182
+ if (insertPoint === -1) {
2183
+ await this.write("STRATEGY_REVIEW_AGENDA.md", `${header}${entry}
2184
+ ${footer}`);
2185
+ } else {
2186
+ const updated = content.slice(0, insertPoint) + entry + "\n" + content.slice(insertPoint);
2187
+ await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
2188
+ }
2189
+ }
2190
+ return full;
2191
+ }
2192
+ async getPendingAgendaTopics() {
2193
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2194
+ if (!content) return [];
2195
+ const yamlStart = content.indexOf("<!-- PAPI-YAML-START -->");
2196
+ const yamlEnd = content.indexOf("<!-- PAPI-YAML-END -->");
2197
+ if (yamlStart === -1 || yamlEnd === -1) return [];
2198
+ const yamlBlock = content.slice(yamlStart + "<!-- PAPI-YAML-START -->".length, yamlEnd).trim();
2199
+ const entries = yamlBlock.split(/(?=\s+-\s+id:)/);
2200
+ const topics = [];
2201
+ for (const block of entries) {
2202
+ const idMatch = block.match(/id:\s+(.+)/);
2203
+ const topicMatch = block.match(/topic:\s+(.+)/);
2204
+ const sourceMatch = block.match(/source:\s+(\S+)/);
2205
+ const statusMatch = block.match(/status:\s+(\S+)/);
2206
+ const createdMatch = block.match(/created_at:\s+(.+)/);
2207
+ const sourceCycleMatch = block.match(/source_cycle:\s+(\d+)/);
2208
+ if (!idMatch || !topicMatch || !sourceMatch || !statusMatch || !createdMatch) continue;
2209
+ if (statusMatch[1].trim() !== "pending") continue;
2210
+ let parsedTopic = topicMatch[1].trim();
2211
+ if (parsedTopic.startsWith('"') && parsedTopic.endsWith('"')) {
2212
+ try {
2213
+ parsedTopic = JSON.parse(parsedTopic);
2214
+ } catch {
2215
+ }
2216
+ }
2217
+ topics.push({
2218
+ id: idMatch[1].trim(),
2219
+ topic: parsedTopic,
2220
+ source: sourceMatch[1].trim(),
2221
+ sourceCycle: sourceCycleMatch ? parseInt(sourceCycleMatch[1], 10) : void 0,
2222
+ status: "pending",
2223
+ createdAt: createdMatch[1].trim()
2224
+ });
2225
+ }
2226
+ return topics;
2227
+ }
2228
+ async markAgendaTopicsAddressed(ids, cycleNumber) {
2229
+ if (ids.length === 0) return;
2230
+ const content = await this.readOptional("STRATEGY_REVIEW_AGENDA.md");
2231
+ if (!content) return;
2232
+ let updated = content;
2233
+ const addressedAt = (/* @__PURE__ */ new Date()).toISOString();
2234
+ for (const id of ids) {
2235
+ const statusPattern = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+status:\\s+)pending`);
2236
+ updated = updated.replace(statusPattern, `$1addressed`);
2237
+ const insertionAnchor = new RegExp(`(\\s+-\\s+id:\\s+${id}\\n(?:.*\\n)*?\\s+created_at:\\s+[^\\n]+)\\n`);
2238
+ const match = updated.match(insertionAnchor);
2239
+ if (match && !match[0].includes("addressed_at:")) {
2240
+ updated = updated.replace(insertionAnchor, `$1
2241
+ addressed_at: ${addressedAt}
2242
+ addressed_in_review: ${cycleNumber}
2243
+ `);
2244
+ }
2245
+ }
2246
+ await this.write("STRATEGY_REVIEW_AGENDA.md", updated);
2247
+ }
2248
+ // -------------------------------------------------------------------------
2157
2249
  // Decision Events & Scores (markdown persistence)
2158
2250
  // -------------------------------------------------------------------------
2159
2251
  async appendDecisionEvent(event) {
@@ -2162,11 +2254,13 @@ ${footer}`);
2162
2254
  const full = { ...event, id, createdAt };
2163
2255
  const content = await this.readOptional("DECISION_EVENTS.md");
2164
2256
  const header = "# Decision Events\n\n";
2165
- const entry = `## ${full.decisionId} | ${full.eventType} | Sprint ${full.sprint}
2257
+ const entry = `## ${full.decisionId} | ${full.eventType} | Cycle ${full.cycle}
2166
2258
  - **id:** ${full.id}
2167
2259
  - **source:** ${full.source}
2168
2260
  ` + (full.sourceRef ? `- **sourceRef:** ${full.sourceRef}
2169
2261
  ` : "") + (full.detail ? `- **detail:** ${full.detail}
2262
+ ` : "") + (full.evidenceRef ? `- **evidenceRef:** ${full.evidenceRef}
2263
+ ` : "") + (full.metricDelta ? `- **metricDelta:** ${JSON.stringify(full.metricDelta)}
2170
2264
  ` : "") + `- **createdAt:** ${full.createdAt}
2171
2265
 
2172
2266
  ---
@@ -2184,9 +2278,9 @@ ${footer}`);
2184
2278
  const filtered = all.filter((e) => e.decisionId === decisionId);
2185
2279
  return limit ? filtered.slice(0, limit) : filtered;
2186
2280
  }
2187
- async getDecisionEventsSince(sprint) {
2281
+ async getDecisionEventsSince(cycle) {
2188
2282
  const all = await this.parseDecisionEvents();
2189
- return all.filter((e) => e.sprint >= sprint);
2283
+ return all.filter((e) => e.cycle >= cycle);
2190
2284
  }
2191
2285
  async parseDecisionEvents() {
2192
2286
  const content = await this.readOptional("DECISION_EVENTS.md");
@@ -2194,22 +2288,34 @@ ${footer}`);
2194
2288
  const events = [];
2195
2289
  const blocks = content.split("---").filter((b) => b.trim());
2196
2290
  for (const block of blocks) {
2197
- const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+(\S+)\s+\|\s+Sprint\s+(\d+)/m);
2291
+ const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+(\S+)\s+\|\s+Cycle\s+(\d+)/m);
2198
2292
  if (!headingMatch) continue;
2199
2293
  const idMatch = block.match(/\*\*id:\*\*\s+(.+)/);
2200
2294
  const sourceMatch = block.match(/\*\*source:\*\*\s+(.+)/);
2201
2295
  const sourceRefMatch = block.match(/\*\*sourceRef:\*\*\s+(.+)/);
2202
2296
  const detailMatch = block.match(/\*\*detail:\*\*\s+(.+)/);
2297
+ const evidenceRefMatch = block.match(/\*\*evidenceRef:\*\*\s+(.+)/);
2298
+ const metricDeltaMatch = block.match(/\*\*metricDelta:\*\*\s+(.+)/);
2203
2299
  const createdAtMatch = block.match(/\*\*createdAt:\*\*\s+(.+)/);
2204
2300
  if (!idMatch || !sourceMatch || !createdAtMatch) continue;
2301
+ let metricDelta;
2302
+ if (metricDeltaMatch?.[1]) {
2303
+ try {
2304
+ metricDelta = JSON.parse(metricDeltaMatch[1].trim());
2305
+ } catch {
2306
+ metricDelta = null;
2307
+ }
2308
+ }
2205
2309
  events.push({
2206
2310
  id: idMatch[1].trim(),
2207
2311
  decisionId: headingMatch[1],
2208
2312
  eventType: headingMatch[2],
2209
- sprint: parseInt(headingMatch[3], 10),
2313
+ cycle: parseInt(headingMatch[3], 10),
2210
2314
  source: sourceMatch[1].trim(),
2211
2315
  sourceRef: sourceRefMatch?.[1]?.trim(),
2212
2316
  detail: detailMatch?.[1]?.trim(),
2317
+ evidenceRef: evidenceRefMatch?.[1]?.trim(),
2318
+ metricDelta,
2213
2319
  createdAt: createdAtMatch[1].trim()
2214
2320
  });
2215
2321
  }
@@ -2222,7 +2328,7 @@ ${footer}`);
2222
2328
  const full = { ...score, id, totalScore, createdAt };
2223
2329
  const content = await this.readOptional("DECISION_SCORES.md");
2224
2330
  const header = "# Decision Scores\n\n";
2225
- const entry = `## ${full.decisionId} | Sprint ${full.sprint}
2331
+ const entry = `## ${full.decisionId} | Cycle ${full.cycle}
2226
2332
  - **id:** ${full.id}
2227
2333
  - **effort:** ${full.effort}
2228
2334
  - **risk:** ${full.risk}
@@ -2252,7 +2358,7 @@ ${footer}`);
2252
2358
  const latest = /* @__PURE__ */ new Map();
2253
2359
  for (const s of all) {
2254
2360
  const existing = latest.get(s.decisionId);
2255
- if (!existing || s.sprint > existing.sprint) {
2361
+ if (!existing || s.cycle > existing.cycle) {
2256
2362
  latest.set(s.decisionId, s);
2257
2363
  }
2258
2364
  }
@@ -2264,7 +2370,7 @@ ${footer}`);
2264
2370
  const scores = [];
2265
2371
  const blocks = content.split("---").filter((b) => b.trim());
2266
2372
  for (const block of blocks) {
2267
- const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+Sprint\s+(\d+)/m);
2373
+ const headingMatch = block.match(/^##\s+(\S+)\s+\|\s+Cycle\s+(\d+)/m);
2268
2374
  if (!headingMatch) continue;
2269
2375
  const field = (name) => block.match(new RegExp(`\\*\\*${name}:\\*\\*\\s+(.+)`))?.[1]?.trim();
2270
2376
  const id = field("id");
@@ -2273,7 +2379,7 @@ ${footer}`);
2273
2379
  scores.push({
2274
2380
  id,
2275
2381
  decisionId: headingMatch[1],
2276
- sprint: parseInt(headingMatch[2], 10),
2382
+ cycle: parseInt(headingMatch[2], 10),
2277
2383
  effort: parseInt(field("effort") ?? "0", 10),
2278
2384
  risk: parseInt(field("risk") ?? "0", 10),
2279
2385
  reversibility: parseInt(field("reversibility") ?? "0", 10),
@@ -2289,9 +2395,26 @@ ${footer}`);
2289
2395
  // Entity reference tracking — no-op for md adapter (DB-only feature)
2290
2396
  async logEntityReferences(_refs) {
2291
2397
  }
2292
- async getDecisionUsage(_currentSprint) {
2398
+ async getDecisionUsage(_currentCycle) {
2293
2399
  return [];
2294
2400
  }
2401
+ // --- North Star ---
2402
+ async getCurrentNorthStar() {
2403
+ const content = await this.read("PLANNING_LOG.md");
2404
+ const ns = parseNorthStar(content);
2405
+ return ns || null;
2406
+ }
2407
+ async getNorthStarSetCycle() {
2408
+ return null;
2409
+ }
2410
+ async getNorthStarStaleness() {
2411
+ return null;
2412
+ }
2413
+ async upsertNorthStar(statement, _cycleNumber) {
2414
+ const content = await this.read("PLANNING_LOG.md");
2415
+ const updated = upsertNorthStarInContent(content, statement);
2416
+ await this.write("PLANNING_LOG.md", updated);
2417
+ }
2295
2418
  };
2296
2419
 
2297
2420
  // src/parsers/review-patterns.ts
@@ -2303,9 +2426,9 @@ function isRealComment(text) {
2303
2426
  const trimmed = text.trim();
2304
2427
  return trimmed.length > 0 && !NONE_PATTERN2.test(trimmed);
2305
2428
  }
2306
- async function detectReviewPatterns(reviews, currentSprint, window = 5, clusterer) {
2429
+ async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer) {
2307
2430
  const recentReviews = reviews.filter(
2308
- (r) => r.sprint > currentSprint - window && r.sprint <= currentSprint
2431
+ (r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
2309
2432
  );
2310
2433
  let recurringFeedback;
2311
2434
  const realComments = recentReviews.filter((r) => isRealComment(r.comments));
@@ -2367,7 +2490,7 @@ export {
2367
2490
  VALID_TRANSITIONS,
2368
2491
  aggregateCostSummary,
2369
2492
  appendToolMetricToContent,
2370
- calculateSprintMetrics,
2493
+ calculateCycleMetrics,
2371
2494
  detectBuildPatterns,
2372
2495
  detectReviewPatterns,
2373
2496
  hasBuildPatterns,
@@ -2375,24 +2498,21 @@ export {
2375
2498
  isValidStatus,
2376
2499
  isValidTransition,
2377
2500
  parseBuildHandoff,
2501
+ parseCycles,
2378
2502
  parseEffortSize,
2379
- parseFeatures,
2380
2503
  parsePhases,
2381
2504
  parseRegistries,
2382
2505
  parseReviews,
2383
- parseSprints,
2384
2506
  parseToolMetrics,
2507
+ prependCycle,
2385
2508
  prependReview,
2386
- prependSprint,
2387
2509
  serializeBuildHandoff,
2388
- serializeFeatures,
2510
+ serializeCycle,
2511
+ serializeCycles,
2389
2512
  serializePhases,
2390
2513
  serializeRegistries,
2391
2514
  serializeReview,
2392
- serializeSprint,
2393
- serializeSprints,
2394
2515
  serializeToolMetric,
2395
2516
  validateTransition,
2396
- writeFeaturesToContent,
2397
2517
  writePhasesToContent
2398
2518
  };