@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.
- package/LICENSE +98 -0
- package/dist/index.d.ts +1127 -194
- package/dist/index.js +577 -457
- 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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
63
|
-
"
|
|
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
|
|
71
|
-
"
|
|
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
|
|
90
|
-
const
|
|
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
|
-
|
|
101
|
-
|
|
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(
|
|
120
|
+
function buildTemporalMeta(createdCycle, modifiedCycle, uuid) {
|
|
110
121
|
const parts = [];
|
|
111
|
-
if (
|
|
112
|
-
if (
|
|
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
|
|
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,
|
|
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 ?
|
|
144
|
+
const existingCreated = existingMatch ? extractCreatedCycle(existingMatch[0]) : void 0;
|
|
134
145
|
const existingUuid = existingMatch ? extractUuid(existingMatch[0]) : void 0;
|
|
135
|
-
const meta2 =
|
|
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 =
|
|
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
|
|
150
|
-
const section = extractSectionCompat(content, "
|
|
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
|
|
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(/\*\*
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
-
**
|
|
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
|
|
205
|
-
const section = extractSectionCompat(content, "
|
|
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 = `###
|
|
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 = `##
|
|
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,
|
|
264
|
+
function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
|
|
230
265
|
return {
|
|
231
|
-
|
|
266
|
+
cycleHealth: parseCycleHealth(content),
|
|
232
267
|
northStar: parseNorthStar(content),
|
|
233
268
|
activeDecisions: parseActiveDecisions(activeDecisionsContent ?? content),
|
|
234
269
|
deferred: parseDeferred(content),
|
|
235
|
-
|
|
270
|
+
cycleLog: cycleLogContent ? parseCycleLog(cycleLogContent) : []
|
|
236
271
|
};
|
|
237
272
|
}
|
|
238
273
|
|
|
239
|
-
// src/parsers/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(`
|
|
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/
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
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
|
|
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.
|
|
407
|
-
if (task.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
532
|
+
return (data.tasks ?? []).map(toCycleTask);
|
|
453
533
|
}
|
|
454
534
|
function serializeBoard(tasks, content) {
|
|
455
|
-
const raw = tasks.map(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 = [`### ${
|
|
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
|
|
620
|
-
const summaryBlock = formatCompressedSummary(oldReports,
|
|
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
|
-
|
|
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
|
|
663
|
-
|
|
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 |
|
|
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 |
|
|
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
|
|
732
|
-
const
|
|
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
|
-
...
|
|
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
|
|
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} | ${
|
|
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,
|
|
795
|
-
const filtered =
|
|
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
|
-
|
|
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/
|
|
880
|
-
var FILE_HEADING = "#
|
|
881
|
-
var ACCURACY_HEADER = "|
|
|
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 = "|
|
|
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
|
|
958
|
+
function calculateCycleMetrics(reports, currentCycle, window = 5) {
|
|
897
959
|
const recentReports = reports.filter(
|
|
898
|
-
(r) => r.
|
|
960
|
+
(r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
|
|
899
961
|
);
|
|
900
|
-
const
|
|
962
|
+
const perCycle = /* @__PURE__ */ new Map();
|
|
901
963
|
for (const r of recentReports) {
|
|
902
|
-
const group =
|
|
964
|
+
const group = perCycle.get(r.cycle) ?? [];
|
|
903
965
|
group.push(r);
|
|
904
|
-
|
|
966
|
+
perCycle.set(r.cycle, group);
|
|
905
967
|
}
|
|
906
968
|
const accuracy = [];
|
|
907
969
|
const velocity = [];
|
|
908
|
-
const
|
|
909
|
-
for (const
|
|
910
|
-
const reps =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(`##
|
|
1008
|
+
lines.push(`## Cycle ${snapshot.cycle} Snapshot \u2014 ${snapshot.date}`);
|
|
947
1009
|
lines.push("");
|
|
948
|
-
lines.push("### Estimation Accuracy (last 5
|
|
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("###
|
|
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 = `##
|
|
969
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1079
|
+
async function detectBuildPatterns(reports, currentCycle, window = 5, clusterer) {
|
|
1015
1080
|
const recentReports = reports.filter(
|
|
1016
|
-
(r) => r.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1104
|
+
existing.cycles.add(r.cycle);
|
|
1040
1105
|
} else {
|
|
1041
|
-
surpriseMap.set(key, { original: r.surprises.trim(), count: 1,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1182
|
-
if (isNaN(
|
|
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,
|
|
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
|
-
`- **
|
|
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/
|
|
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
|
|
1440
|
-
function
|
|
1441
|
-
if (!
|
|
1442
|
-
const
|
|
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)
|
|
1452
|
-
|
|
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
|
|
1420
|
+
function fromCycle(cycle) {
|
|
1455
1421
|
const raw = {
|
|
1456
|
-
id:
|
|
1457
|
-
number:
|
|
1458
|
-
status:
|
|
1459
|
-
start_date:
|
|
1460
|
-
goals:
|
|
1461
|
-
board_health:
|
|
1462
|
-
task_ids:
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1449
|
+
return (data.cycles ?? []).map(toCycle).filter((s) => s !== null);
|
|
1483
1450
|
}
|
|
1484
|
-
function
|
|
1485
|
-
const raw =
|
|
1486
|
-
const yamlStr = yaml2.dump({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1499
|
-
const raw =
|
|
1465
|
+
function serializeCycle(cycle) {
|
|
1466
|
+
const raw = fromCycle(cycle);
|
|
1500
1467
|
return yaml2.dump(raw, { lineWidth: 120, quotingType: '"' }).trim();
|
|
1501
1468
|
}
|
|
1502
|
-
function
|
|
1469
|
+
function prependCycle(cycle, content) {
|
|
1503
1470
|
if (!content.trim()) {
|
|
1504
|
-
const header = `#
|
|
1471
|
+
const header = `# Cycles
|
|
1505
1472
|
|
|
1506
1473
|
<!-- PAPI-ADAPTER: parse the yaml block below -->
|
|
1507
1474
|
|
|
1508
1475
|
`;
|
|
1509
|
-
const raw =
|
|
1510
|
-
const yamlStr = yaml2.dump({
|
|
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 =
|
|
1514
|
-
const merged = [
|
|
1515
|
-
return
|
|
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 +
|
|
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,
|
|
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,
|
|
1565
|
+
return parsePlanningLog(planningContent, activeDecisionsContent, cycleLogContent);
|
|
1599
1566
|
}
|
|
1600
|
-
/** Read the
|
|
1601
|
-
async
|
|
1602
|
-
return
|
|
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
|
-
/**
|
|
1605
|
-
|
|
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
|
-
|
|
1580
|
+
const all = parseActiveDecisions(content);
|
|
1581
|
+
if (options?.includeRetired) return all;
|
|
1582
|
+
return all.filter(isLiveDecision);
|
|
1609
1583
|
}
|
|
1610
|
-
/** Read
|
|
1611
|
-
async
|
|
1612
|
-
return
|
|
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
|
|
1615
|
-
const log = await this.
|
|
1616
|
-
return log.filter((entry) => entry.
|
|
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
|
|
1619
|
-
async
|
|
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 =
|
|
1595
|
+
const current = parseCycleHealth(content);
|
|
1622
1596
|
const updated = { ...current, ...updates };
|
|
1623
|
-
await this.write("PLANNING_LOG.md",
|
|
1624
|
-
}
|
|
1625
|
-
/** Prepend a new
|
|
1626
|
-
async
|
|
1627
|
-
|
|
1628
|
-
|
|
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",
|
|
1607
|
+
await this.write("SPRINT_LOG.md", prependCycleLogEntry(patched, content));
|
|
1632
1608
|
}
|
|
1633
|
-
/** Write a strategy review — for md adapter, delegates to
|
|
1609
|
+
/** Write a strategy review — for md adapter, delegates to cycle log. */
|
|
1634
1610
|
async writeStrategyReview(review) {
|
|
1635
|
-
await this.
|
|
1611
|
+
await this.writeCycleLogEntry({
|
|
1636
1612
|
uuid: randomUUID6(),
|
|
1637
|
-
|
|
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
|
|
1644
|
-
async
|
|
1645
|
-
const log = await this.
|
|
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?.
|
|
1625
|
+
return entry?.cycleNumber ?? 0;
|
|
1650
1626
|
}
|
|
1651
|
-
/** Get strategy reviews — md adapter returns empty (reviews live in
|
|
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
|
-
|
|
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,
|
|
1635
|
+
await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, cycleNumber));
|
|
1659
1636
|
}
|
|
1660
|
-
// ---
|
|
1661
|
-
/** Query the
|
|
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
|
|
1814
|
-
async
|
|
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.
|
|
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
|
|
1849
|
-
async
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2017
|
-
async getCostSummary(sprintNumber) {
|
|
2012
|
+
async hasToolMilestone(name) {
|
|
2018
2013
|
const metrics = await this.readToolMetrics();
|
|
2019
|
-
return
|
|
2014
|
+
return metrics.some((m) => m.tool === name);
|
|
2020
2015
|
}
|
|
2021
|
-
/**
|
|
2022
|
-
async
|
|
2023
|
-
const
|
|
2024
|
-
|
|
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
|
-
// ---
|
|
2033
|
-
/** Append a
|
|
2034
|
-
async
|
|
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
|
|
2039
|
-
async
|
|
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
|
-
// ---
|
|
2045
|
-
/** Read all
|
|
2046
|
-
async
|
|
2047
|
-
const content = await this.readOptional("
|
|
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
|
|
2044
|
+
return parseCycles(content);
|
|
2050
2045
|
}
|
|
2051
|
-
/** Write a new
|
|
2052
|
-
async
|
|
2053
|
-
const content = await this.readOptional("
|
|
2054
|
-
await this.write("
|
|
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.
|
|
2085
|
-
full.
|
|
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(/
|
|
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
|
-
|
|
2135
|
-
|
|
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,
|
|
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("
|
|
2149
|
-
const
|
|
2150
|
-
updated = updated.replace(
|
|
2151
|
-
|
|
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} |
|
|
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(
|
|
2281
|
+
async getDecisionEventsSince(cycle) {
|
|
2188
2282
|
const all = await this.parseDecisionEvents();
|
|
2189
|
-
return all.filter((e) => e.
|
|
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+
|
|
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
|
-
|
|
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} |
|
|
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.
|
|
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+
|
|
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
|
-
|
|
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(
|
|
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,
|
|
2429
|
+
async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer) {
|
|
2307
2430
|
const recentReviews = reviews.filter(
|
|
2308
|
-
(r) => r.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|