@papi-ai/adapter-md 0.1.1-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/dist/index.d.ts +768 -16
- package/dist/index.js +305 -46
- package/package.json +8 -2
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
|
|
@@ -199,6 +208,30 @@ ${after}`;
|
|
|
199
208
|
function parseNorthStar(content) {
|
|
200
209
|
return extractSection(content, "North Star").replace(/^## North Star\s*/m, "").trim();
|
|
201
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
|
+
}
|
|
202
235
|
function parseDeferred(content) {
|
|
203
236
|
const section = extractSection(content, "Deferred / Parking Lot");
|
|
204
237
|
return section.split("\n").filter((line) => line.match(/^-\s+/)).map((line) => line.replace(/^-\s+/, "").trim());
|
|
@@ -250,6 +283,7 @@ var SECTION_HEADERS = [
|
|
|
250
283
|
"SCOPE BOUNDARY (DO NOT DO THIS)",
|
|
251
284
|
"ACCEPTANCE CRITERIA",
|
|
252
285
|
"SECURITY CONSIDERATIONS",
|
|
286
|
+
"PRE-BUILD VERIFICATION",
|
|
253
287
|
"FILES LIKELY TOUCHED",
|
|
254
288
|
"EFFORT"
|
|
255
289
|
];
|
|
@@ -280,6 +314,9 @@ function splitSections(text) {
|
|
|
280
314
|
function parseBulletList(text) {
|
|
281
315
|
return text.split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
|
|
282
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
|
+
}
|
|
283
320
|
function parseChecklist(text) {
|
|
284
321
|
return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
|
|
285
322
|
}
|
|
@@ -314,11 +351,34 @@ function parseBuildHandoff(markdown) {
|
|
|
314
351
|
scopeBoundary: parseBulletList(sections.get("SCOPE BOUNDARY (DO NOT DO THIS)") ?? ""),
|
|
315
352
|
acceptanceCriteria: parseChecklist(sections.get("ACCEPTANCE CRITERIA") ?? ""),
|
|
316
353
|
securityConsiderations: (sections.get("SECURITY CONSIDERATIONS") ?? "").trim(),
|
|
354
|
+
verificationFiles: parseBulletsOnly(sections.get("PRE-BUILD VERIFICATION") ?? ""),
|
|
317
355
|
filesLikelyTouched: parseBulletList(sections.get("FILES LIKELY TOUCHED") ?? ""),
|
|
318
356
|
effort
|
|
319
357
|
};
|
|
320
358
|
}
|
|
321
|
-
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
|
+
}
|
|
322
382
|
const lines = [];
|
|
323
383
|
lines.push(`BUILD HANDOFF \u2014 ${handoff.taskId}`);
|
|
324
384
|
if (handoff.uuid) lines.push(`UUID: ${handoff.uuid}`);
|
|
@@ -329,30 +389,40 @@ function serializeBuildHandoff(handoff) {
|
|
|
329
389
|
lines.push(`Why now: ${handoff.whyNow}`);
|
|
330
390
|
lines.push("");
|
|
331
391
|
lines.push("SCOPE (DO THIS)");
|
|
332
|
-
for (const item of handoff.scope) {
|
|
392
|
+
for (const item of ensureArray(handoff.scope)) {
|
|
333
393
|
lines.push(`- ${item}`);
|
|
334
394
|
}
|
|
335
395
|
lines.push("");
|
|
336
396
|
lines.push("SCOPE BOUNDARY (DO NOT DO THIS)");
|
|
337
|
-
for (const item of handoff.scopeBoundary) {
|
|
397
|
+
for (const item of ensureArray(handoff.scopeBoundary)) {
|
|
338
398
|
lines.push(`- ${item}`);
|
|
339
399
|
}
|
|
340
400
|
lines.push("");
|
|
341
401
|
lines.push("ACCEPTANCE CRITERIA");
|
|
342
|
-
for (const item of handoff.acceptanceCriteria) {
|
|
402
|
+
for (const item of ensureArray(handoff.acceptanceCriteria)) {
|
|
343
403
|
lines.push(`[ ] ${item}`);
|
|
344
404
|
}
|
|
345
405
|
lines.push("");
|
|
346
406
|
lines.push("SECURITY CONSIDERATIONS");
|
|
347
|
-
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
|
+
}
|
|
348
418
|
lines.push("");
|
|
349
419
|
lines.push("FILES LIKELY TOUCHED");
|
|
350
|
-
for (const item of handoff.filesLikelyTouched) {
|
|
420
|
+
for (const item of ensureArray(handoff.filesLikelyTouched)) {
|
|
351
421
|
lines.push(`- ${item}`);
|
|
352
422
|
}
|
|
353
423
|
lines.push("");
|
|
354
424
|
lines.push("EFFORT");
|
|
355
|
-
lines.push(handoff.effort);
|
|
425
|
+
lines.push(handoff.effort ?? "M");
|
|
356
426
|
return lines.join("\n");
|
|
357
427
|
}
|
|
358
428
|
|
|
@@ -383,7 +453,11 @@ function toCycleTask(raw) {
|
|
|
383
453
|
stateHistory: raw.state_history?.length ? raw.state_history.map((e) => ({ status: e.status, timestamp: e.timestamp })) : void 0,
|
|
384
454
|
closureReason: raw.closure_reason || void 0,
|
|
385
455
|
buildHandoff: raw.build_handoff ? parseBuildHandoff(raw.build_handoff) ?? void 0 : void 0,
|
|
386
|
-
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
|
|
387
461
|
};
|
|
388
462
|
}
|
|
389
463
|
function sanitizeDelimiters(value) {
|
|
@@ -415,6 +489,10 @@ function fromCycleTask(task) {
|
|
|
415
489
|
if (task.closureReason) raw.closure_reason = task.closureReason;
|
|
416
490
|
if (task.buildHandoff) raw.build_handoff = sanitizeDelimiters(serializeBuildHandoff(task.buildHandoff));
|
|
417
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;
|
|
418
496
|
return raw;
|
|
419
497
|
}
|
|
420
498
|
function mergeConflictHint(content) {
|
|
@@ -481,6 +559,7 @@ function filterTasks(tasks, options) {
|
|
|
481
559
|
if (options.reviewed !== void 0 && task.reviewed !== options.reviewed) return false;
|
|
482
560
|
if (options.module && task.module !== options.module) return false;
|
|
483
561
|
if (options.epic && task.epic !== options.epic) return false;
|
|
562
|
+
if (options.assigneeId && task.assigneeId !== options.assigneeId) return false;
|
|
484
563
|
return true;
|
|
485
564
|
});
|
|
486
565
|
}
|
|
@@ -830,7 +909,6 @@ function aggregateCostSummary(metrics, cycleNumber) {
|
|
|
830
909
|
};
|
|
831
910
|
}
|
|
832
911
|
var COST_SECTION_HEADING = "## Cost Summary";
|
|
833
|
-
var COST_TABLE_HEADER = "| Cycle | Date | Total Cost ($) | Input Tokens | Output Tokens | Calls |";
|
|
834
912
|
var COST_TABLE_SEPARATOR = "|--------|------|----------------|--------------|---------------|-------|";
|
|
835
913
|
function parseCostSnapshots(content) {
|
|
836
914
|
const lines = content.split("\n");
|
|
@@ -859,28 +937,6 @@ function parseCostSnapshots(content) {
|
|
|
859
937
|
}
|
|
860
938
|
return snapshots;
|
|
861
939
|
}
|
|
862
|
-
function serializeCostSnapshot(snapshot) {
|
|
863
|
-
return `| ${snapshot.cycle} | ${snapshot.date} | ${snapshot.totalCostUsd.toFixed(4)} | ${formatNumber(snapshot.totalInputTokens)} | ${formatNumber(snapshot.totalOutputTokens)} | ${formatNumber(snapshot.totalCalls)} |`;
|
|
864
|
-
}
|
|
865
|
-
function writeCostSnapshotToContent(snapshot, content) {
|
|
866
|
-
if (!content.includes(COST_SECTION_HEADING)) {
|
|
867
|
-
return content.trimEnd() + "\n\n" + COST_SECTION_HEADING + "\n\n" + COST_TABLE_HEADER + "\n" + COST_TABLE_SEPARATOR + "\n" + serializeCostSnapshot(snapshot) + "\n";
|
|
868
|
-
}
|
|
869
|
-
const lines = content.split("\n");
|
|
870
|
-
const cyclePrefix = `| ${snapshot.cycle} |`;
|
|
871
|
-
let replaced = false;
|
|
872
|
-
for (let i = 0; i < lines.length; i++) {
|
|
873
|
-
if (lines[i].startsWith(cyclePrefix)) {
|
|
874
|
-
lines[i] = serializeCostSnapshot(snapshot);
|
|
875
|
-
replaced = true;
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
if (replaced) {
|
|
880
|
-
return lines.join("\n");
|
|
881
|
-
}
|
|
882
|
-
return content.trimEnd() + "\n" + serializeCostSnapshot(snapshot) + "\n";
|
|
883
|
-
}
|
|
884
940
|
|
|
885
941
|
// src/parsers/cycle-metrics.ts
|
|
886
942
|
var FILE_HEADING = "# Cycle Methodology Metrics";
|
|
@@ -1358,6 +1414,7 @@ function toCycle(raw) {
|
|
|
1358
1414
|
taskIds: raw.task_ids ?? []
|
|
1359
1415
|
};
|
|
1360
1416
|
if (raw.end_date) cycle.endDate = raw.end_date;
|
|
1417
|
+
if (raw.user_id) cycle.userId = raw.user_id;
|
|
1361
1418
|
return cycle;
|
|
1362
1419
|
}
|
|
1363
1420
|
function fromCycle(cycle) {
|
|
@@ -1371,6 +1428,7 @@ function fromCycle(cycle) {
|
|
|
1371
1428
|
task_ids: cycle.taskIds
|
|
1372
1429
|
};
|
|
1373
1430
|
if (cycle.endDate) raw.end_date = cycle.endDate;
|
|
1431
|
+
if (cycle.userId) raw.user_id = cycle.userId;
|
|
1374
1432
|
return raw;
|
|
1375
1433
|
}
|
|
1376
1434
|
function extractYamlBlock2(content) {
|
|
@@ -1510,11 +1568,18 @@ var MdFileAdapter = class {
|
|
|
1510
1568
|
async getCycleHealth() {
|
|
1511
1569
|
return parseCycleHealth(await this.read("PLANNING_LOG.md"));
|
|
1512
1570
|
}
|
|
1513
|
-
/**
|
|
1514
|
-
|
|
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) {
|
|
1515
1578
|
const content = await this.readOptional("ACTIVE_DECISIONS.md");
|
|
1516
1579
|
if (!content) return [];
|
|
1517
|
-
|
|
1580
|
+
const all = parseActiveDecisions(content);
|
|
1581
|
+
if (options?.includeRetired) return all;
|
|
1582
|
+
return all.filter(isLiveDecision);
|
|
1518
1583
|
}
|
|
1519
1584
|
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
1520
1585
|
async getCycleLog(limit) {
|
|
@@ -1533,11 +1598,13 @@ var MdFileAdapter = class {
|
|
|
1533
1598
|
}
|
|
1534
1599
|
/** Prepend a new cycle log entry at the top of the Cycle Log section. */
|
|
1535
1600
|
async writeCycleLogEntry(entry) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1601
|
+
const patched = {
|
|
1602
|
+
...entry,
|
|
1603
|
+
uuid: entry.uuid || randomUUID6(),
|
|
1604
|
+
date: entry.date ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1605
|
+
};
|
|
1539
1606
|
const content = await this.read("SPRINT_LOG.md");
|
|
1540
|
-
await this.write("SPRINT_LOG.md", prependCycleLogEntry(
|
|
1607
|
+
await this.write("SPRINT_LOG.md", prependCycleLogEntry(patched, content));
|
|
1541
1608
|
}
|
|
1542
1609
|
/** Write a strategy review — for md adapter, delegates to cycle log. */
|
|
1543
1610
|
async writeStrategyReview(review) {
|
|
@@ -1562,7 +1629,8 @@ var MdFileAdapter = class {
|
|
|
1562
1629
|
return [];
|
|
1563
1630
|
}
|
|
1564
1631
|
/** Update or insert an Active Decision block by ID. */
|
|
1565
|
-
|
|
1632
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1633
|
+
async updateActiveDecision(id, body, cycleNumber, _action) {
|
|
1566
1634
|
const content = await this.readOptional("ACTIVE_DECISIONS.md") || "## Active Decisions\n\n";
|
|
1567
1635
|
await this.write("ACTIVE_DECISIONS.md", updateActiveDecisionInContent(id, body, content, cycleNumber));
|
|
1568
1636
|
}
|
|
@@ -1692,6 +1760,66 @@ var MdFileAdapter = class {
|
|
|
1692
1760
|
async updateTaskStatus(id, status) {
|
|
1693
1761
|
return this.updateTask(id, { status });
|
|
1694
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
|
+
}
|
|
1695
1823
|
async recordTransition(_taskId, _fromStatus, _toStatus, _changedBy) {
|
|
1696
1824
|
}
|
|
1697
1825
|
// --- Build Reports ---
|
|
@@ -1721,6 +1849,11 @@ var MdFileAdapter = class {
|
|
|
1721
1849
|
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
1722
1850
|
return reports.slice(0, count);
|
|
1723
1851
|
}
|
|
1852
|
+
/** Return the number of build reports for a specific task. */
|
|
1853
|
+
async getBuildReportCountForTask(taskId) {
|
|
1854
|
+
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
1855
|
+
return reports.filter((r) => r.taskId === taskId).length;
|
|
1856
|
+
}
|
|
1724
1857
|
/** Return all build reports from cycles >= {@link cycleNumber}. */
|
|
1725
1858
|
async getBuildReportsSince(cycleNumber) {
|
|
1726
1859
|
const reports = parseBuildReports(await this.read("BUILD_REPORTS.md"));
|
|
@@ -1876,16 +2009,15 @@ ${newSection}
|
|
|
1876
2009
|
if (!content) return [];
|
|
1877
2010
|
return parseToolMetrics(content);
|
|
1878
2011
|
}
|
|
2012
|
+
async hasToolMilestone(name) {
|
|
2013
|
+
const metrics = await this.readToolMetrics();
|
|
2014
|
+
return metrics.some((m) => m.tool === name);
|
|
2015
|
+
}
|
|
1879
2016
|
/** Aggregate tool call metrics into a cost summary, optionally filtered by cycle. */
|
|
1880
2017
|
async getCostSummary(cycleNumber) {
|
|
1881
2018
|
const metrics = await this.readToolMetrics();
|
|
1882
2019
|
return aggregateCostSummary(metrics, cycleNumber);
|
|
1883
2020
|
}
|
|
1884
|
-
/** Write a cost snapshot to the Cost Summary section of METRICS.md. */
|
|
1885
|
-
async writeCostSnapshot(snapshot) {
|
|
1886
|
-
const content = await this.readOptional("METRICS.md");
|
|
1887
|
-
await this.write("METRICS.md", writeCostSnapshotToContent(snapshot, content));
|
|
1888
|
-
}
|
|
1889
2021
|
/** Read all cost snapshots from the Cost Summary section of METRICS.md. */
|
|
1890
2022
|
async getCostSnapshots() {
|
|
1891
2023
|
const content = await this.readOptional("METRICS.md");
|
|
@@ -2018,6 +2150,102 @@ ${footer}`);
|
|
|
2018
2150
|
await this.write("STRATEGY_RECOMMENDATIONS.md", updated);
|
|
2019
2151
|
}
|
|
2020
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
|
+
// -------------------------------------------------------------------------
|
|
2021
2249
|
// Decision Events & Scores (markdown persistence)
|
|
2022
2250
|
// -------------------------------------------------------------------------
|
|
2023
2251
|
async appendDecisionEvent(event) {
|
|
@@ -2031,6 +2259,8 @@ ${footer}`);
|
|
|
2031
2259
|
- **source:** ${full.source}
|
|
2032
2260
|
` + (full.sourceRef ? `- **sourceRef:** ${full.sourceRef}
|
|
2033
2261
|
` : "") + (full.detail ? `- **detail:** ${full.detail}
|
|
2262
|
+
` : "") + (full.evidenceRef ? `- **evidenceRef:** ${full.evidenceRef}
|
|
2263
|
+
` : "") + (full.metricDelta ? `- **metricDelta:** ${JSON.stringify(full.metricDelta)}
|
|
2034
2264
|
` : "") + `- **createdAt:** ${full.createdAt}
|
|
2035
2265
|
|
|
2036
2266
|
---
|
|
@@ -2064,8 +2294,18 @@ ${footer}`);
|
|
|
2064
2294
|
const sourceMatch = block.match(/\*\*source:\*\*\s+(.+)/);
|
|
2065
2295
|
const sourceRefMatch = block.match(/\*\*sourceRef:\*\*\s+(.+)/);
|
|
2066
2296
|
const detailMatch = block.match(/\*\*detail:\*\*\s+(.+)/);
|
|
2297
|
+
const evidenceRefMatch = block.match(/\*\*evidenceRef:\*\*\s+(.+)/);
|
|
2298
|
+
const metricDeltaMatch = block.match(/\*\*metricDelta:\*\*\s+(.+)/);
|
|
2067
2299
|
const createdAtMatch = block.match(/\*\*createdAt:\*\*\s+(.+)/);
|
|
2068
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
|
+
}
|
|
2069
2309
|
events.push({
|
|
2070
2310
|
id: idMatch[1].trim(),
|
|
2071
2311
|
decisionId: headingMatch[1],
|
|
@@ -2074,6 +2314,8 @@ ${footer}`);
|
|
|
2074
2314
|
source: sourceMatch[1].trim(),
|
|
2075
2315
|
sourceRef: sourceRefMatch?.[1]?.trim(),
|
|
2076
2316
|
detail: detailMatch?.[1]?.trim(),
|
|
2317
|
+
evidenceRef: evidenceRefMatch?.[1]?.trim(),
|
|
2318
|
+
metricDelta,
|
|
2077
2319
|
createdAt: createdAtMatch[1].trim()
|
|
2078
2320
|
});
|
|
2079
2321
|
}
|
|
@@ -2156,6 +2398,23 @@ ${footer}`);
|
|
|
2156
2398
|
async getDecisionUsage(_currentCycle) {
|
|
2157
2399
|
return [];
|
|
2158
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
|
+
}
|
|
2159
2418
|
};
|
|
2160
2419
|
|
|
2161
2420
|
// src/parsers/review-patterns.ts
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papi-ai/adapter-md",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "PAPI markdown file storage adapter — reads and writes .papi/ project files",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
9
15
|
"files": [
|
|
10
16
|
"dist",
|
|
11
17
|
"README.md"
|
|
@@ -32,7 +38,7 @@
|
|
|
32
38
|
"node": ">=18.0.0"
|
|
33
39
|
},
|
|
34
40
|
"dependencies": {
|
|
35
|
-
"@papi-ai/shared": "^0.1.0
|
|
41
|
+
"@papi-ai/shared": "^0.1.0",
|
|
36
42
|
"js-yaml": "^4.1.0"
|
|
37
43
|
},
|
|
38
44
|
"devDependencies": {
|