@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +768 -16
  2. package/dist/index.js +305 -46
  3. 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
- feedback: 3
26
+ discovery: 1,
27
+ // Non-code brief types for non-technical Owners (AD-12)
28
+ "design-brief": 1,
29
+ "research-brief": 1,
30
+ "marketing-brief": 1,
31
+ "ops-brief": 1
23
32
  };
24
33
 
25
34
  // src/parsers/planning-log.ts
@@ -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 serializeBuildHandoff(handoff) {
359
+ function ensureArray(value) {
360
+ if (Array.isArray(value)) return value;
361
+ if (typeof value === "string") {
362
+ try {
363
+ const parsed = JSON.parse(value);
364
+ if (Array.isArray(parsed)) return parsed;
365
+ } catch {
366
+ }
367
+ return value.trim() ? [value] : [];
368
+ }
369
+ return [];
370
+ }
371
+ function serializeBuildHandoff(raw) {
372
+ let handoff;
373
+ if (typeof raw === "string") {
374
+ try {
375
+ handoff = JSON.parse(raw);
376
+ } catch {
377
+ return raw;
378
+ }
379
+ } else {
380
+ handoff = raw;
381
+ }
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
- /** Read all Active Decisions from ACTIVE_DECISIONS.md. */
1514
- async getActiveDecisions() {
1571
+ /**
1572
+ * Read Active Decisions from ACTIVE_DECISIONS.md.
1573
+ *
1574
+ * Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
1575
+ * Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
1576
+ */
1577
+ async getActiveDecisions(options) {
1515
1578
  const content = await this.readOptional("ACTIVE_DECISIONS.md");
1516
1579
  if (!content) return [];
1517
- return parseActiveDecisions(content);
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
- if (!entry.uuid) {
1537
- entry = { ...entry, uuid: randomUUID6() };
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(entry, content));
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
- async updateActiveDecision(id, body, cycleNumber) {
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.1.1-alpha",
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-alpha",
41
+ "@papi-ai/shared": "^0.1.0",
36
42
  "js-yaml": "^4.1.0"
37
43
  },
38
44
  "devDependencies": {