@openplaybooks/converge-studio 0.4.1 → 0.4.3
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.js +191 -18
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { mkdir, readdir, readFile, writeFile, rm, cp, rename } from 'fs/promises
|
|
|
5
5
|
import { existsSync } from 'fs';
|
|
6
6
|
import { join, resolve, dirname } from 'path';
|
|
7
7
|
import { parse, stringify } from 'yaml';
|
|
8
|
-
import { plan } from '@openplaybooks/converge-core';
|
|
8
|
+
import { plan, parseTaskMd } from '@openplaybooks/converge-core';
|
|
9
9
|
import { loadHumanReviewHandoffById, ensureHumanReviewHandoff, getHumanReviewHandoffRoute } from '@openplaybooks/converge-core/task/review';
|
|
10
10
|
import { loadPlaybookFromFolder } from '@openplaybooks/converge-core/playbook';
|
|
11
11
|
import { createServer } from 'http';
|
|
@@ -644,7 +644,9 @@ async function handleRequest(args) {
|
|
|
644
644
|
taskId: handoff.taskId
|
|
645
645
|
});
|
|
646
646
|
await appendHumanReview(projectDir, review);
|
|
647
|
-
await
|
|
647
|
+
await rm(getHumanReportArtifactPath(projectDir, handoff.playbook, handoff.taskId), { force: true }).catch(() => {
|
|
648
|
+
});
|
|
649
|
+
await loadOrCreateHumanReportArtifact(projectDir, handoff.playbook, handoff.taskId);
|
|
648
650
|
redirect(res, `/studio/handoff/${encodeURIComponent(handoff.id)}`);
|
|
649
651
|
return;
|
|
650
652
|
}
|
|
@@ -655,7 +657,9 @@ async function handleRequest(args) {
|
|
|
655
657
|
const body = await readForm(req);
|
|
656
658
|
const review = normalizeHumanReview(body, { playbook, taskId });
|
|
657
659
|
await appendHumanReview(projectDir, review);
|
|
658
|
-
await
|
|
660
|
+
await rm(getHumanReportArtifactPath(projectDir, playbook, taskId), { force: true }).catch(() => {
|
|
661
|
+
});
|
|
662
|
+
await loadOrCreateHumanReportArtifact(projectDir, playbook, taskId);
|
|
659
663
|
redirect(
|
|
660
664
|
res,
|
|
661
665
|
`/studio/handoff/${encodeURIComponent((await ensureHumanReviewHandoff(projectDir, playbook, taskId)).id)}`
|
|
@@ -674,6 +678,8 @@ async function handleRequest(args) {
|
|
|
674
678
|
sendHtml(res, 404, renderLayout("Not found", [panel("Not found", "Unknown human review page.")]));
|
|
675
679
|
return;
|
|
676
680
|
}
|
|
681
|
+
const reviews = await loadHumanReviews(projectDir, handoff.playbook, handoff.taskId);
|
|
682
|
+
const reviewPrompt = await loadTaskReviewPrompt(projectDir, handoff.playbook, handoff.taskId);
|
|
677
683
|
sendHtml(
|
|
678
684
|
res,
|
|
679
685
|
200,
|
|
@@ -681,7 +687,9 @@ async function handleRequest(args) {
|
|
|
681
687
|
playbook: handoff.playbook,
|
|
682
688
|
taskId: handoff.taskId,
|
|
683
689
|
reportContentHtml: report,
|
|
684
|
-
submitPath: path
|
|
690
|
+
submitPath: path,
|
|
691
|
+
reviews,
|
|
692
|
+
reviewPrompt
|
|
685
693
|
})
|
|
686
694
|
);
|
|
687
695
|
return;
|
|
@@ -705,6 +713,8 @@ async function handleRequest(args) {
|
|
|
705
713
|
sendHtml(res, 404, renderLayout("Not found", [panel("Not found", "Unknown human review page.")]));
|
|
706
714
|
return;
|
|
707
715
|
}
|
|
716
|
+
const reviews = await loadHumanReviews(projectDir, name, taskId);
|
|
717
|
+
const reviewPrompt = await loadTaskReviewPrompt(projectDir, name, taskId);
|
|
708
718
|
sendHtml(
|
|
709
719
|
res,
|
|
710
720
|
200,
|
|
@@ -712,7 +722,9 @@ async function handleRequest(args) {
|
|
|
712
722
|
playbook: name,
|
|
713
723
|
taskId,
|
|
714
724
|
reportContentHtml: report,
|
|
715
|
-
submitPath: path
|
|
725
|
+
submitPath: path,
|
|
726
|
+
reviews,
|
|
727
|
+
reviewPrompt
|
|
716
728
|
})
|
|
717
729
|
);
|
|
718
730
|
return;
|
|
@@ -1449,10 +1461,28 @@ async function buildRunView(projectDir, name) {
|
|
|
1449
1461
|
async function loadOrCreateHumanReportArtifact(projectDir, playbook, taskId) {
|
|
1450
1462
|
const playbookDir = join(projectDir, ".converge", "playbooks", playbook);
|
|
1451
1463
|
if (!existsSync(playbookDir)) return null;
|
|
1452
|
-
const
|
|
1453
|
-
if (existsSync(
|
|
1464
|
+
const cachedPath = getHumanReportArtifactPath(projectDir, playbook, taskId);
|
|
1465
|
+
if (existsSync(cachedPath)) {
|
|
1454
1466
|
await announceHumanReviewArtifact(projectDir, playbook, taskId);
|
|
1455
|
-
return await readFile(
|
|
1467
|
+
return await readFile(cachedPath, "utf8");
|
|
1468
|
+
}
|
|
1469
|
+
const taskMdPath = resolveTaskMdPath(projectDir, playbook, taskId);
|
|
1470
|
+
try {
|
|
1471
|
+
const parsed = await parseTaskMd(taskMdPath);
|
|
1472
|
+
if (parsed?.def.review?.artifact) {
|
|
1473
|
+
const reviewArtifactFile = join(projectDir, parsed.def.review.artifact);
|
|
1474
|
+
if (existsSync(reviewArtifactFile)) {
|
|
1475
|
+
const raw = await readFile(reviewArtifactFile, "utf8");
|
|
1476
|
+
const format = parsed.def.review.format ?? (reviewArtifactFile.endsWith(".html") ? "html" : "md");
|
|
1477
|
+
const html = format === "html" ? raw : renderMarkdownArtifact(raw);
|
|
1478
|
+
await mkdir(join(projectDir, ".converge", "inventory", playbook, "reports"), { recursive: true });
|
|
1479
|
+
await writeFile(cachedPath, html, "utf8");
|
|
1480
|
+
await announceHumanReviewArtifact(projectDir, playbook, taskId);
|
|
1481
|
+
return html;
|
|
1482
|
+
}
|
|
1483
|
+
return renderWaitingForArtifact(parsed.def.review.artifact);
|
|
1484
|
+
}
|
|
1485
|
+
} catch {
|
|
1456
1486
|
}
|
|
1457
1487
|
return await writeHumanReportArtifact(projectDir, playbook, taskId);
|
|
1458
1488
|
}
|
|
@@ -1470,6 +1500,31 @@ async function writeHumanReportArtifact(projectDir, playbook, taskId) {
|
|
|
1470
1500
|
await announceHumanReviewArtifact(projectDir, playbook, taskId);
|
|
1471
1501
|
return html;
|
|
1472
1502
|
}
|
|
1503
|
+
function resolveTaskMdPath(projectDir, playbook, taskId) {
|
|
1504
|
+
return join(projectDir, ".converge", "playbooks", playbook, "tasks", taskId, "TASK.md");
|
|
1505
|
+
}
|
|
1506
|
+
async function loadTaskReviewPrompt(projectDir, playbook, taskId) {
|
|
1507
|
+
try {
|
|
1508
|
+
const parsed = await parseTaskMd(resolveTaskMdPath(projectDir, playbook, taskId));
|
|
1509
|
+
return parsed?.def.review?.prompt;
|
|
1510
|
+
} catch {
|
|
1511
|
+
return void 0;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function renderMarkdownArtifact(markdown) {
|
|
1515
|
+
return `<div class="summary-block report-body">
|
|
1516
|
+
<div class="label">Artifact content</div>
|
|
1517
|
+
<pre style="white-space: pre-wrap; font-family: inherit; color: var(--text); line-height: 1.7; margin: 0;">${escapeHtml(markdown)}</pre>
|
|
1518
|
+
</div>`;
|
|
1519
|
+
}
|
|
1520
|
+
function renderWaitingForArtifact(artifactPath) {
|
|
1521
|
+
return `<div class="summary-block report-body">
|
|
1522
|
+
<div class="label">Artifact pending</div>
|
|
1523
|
+
<h3>Waiting for artifact</h3>
|
|
1524
|
+
<p class="lead">The review artifact at <code>${escapeHtml(artifactPath)}</code> has not been generated yet.</p>
|
|
1525
|
+
<p>The upstream task must complete and produce this file before the review can proceed.</p>
|
|
1526
|
+
</div>`;
|
|
1527
|
+
}
|
|
1473
1528
|
function getHumanReportArtifactPath(projectDir, playbook, taskId) {
|
|
1474
1529
|
return join(projectDir, ".converge", "inventory", playbook, "reports", `${taskId}.html`);
|
|
1475
1530
|
}
|
|
@@ -1494,6 +1549,21 @@ async function announceHumanReviewArtifact(projectDir, playbook, taskId) {
|
|
|
1494
1549
|
}
|
|
1495
1550
|
function renderHumanReviewPageHtml(args) {
|
|
1496
1551
|
const contentHtml = args.reportContentHtml;
|
|
1552
|
+
const reviews = args.reviews ?? [];
|
|
1553
|
+
const latestDecision = reviews.length > 0 ? reviews[reviews.length - 1].decision : void 0;
|
|
1554
|
+
const decisionBadge = latestDecision ? `<span class="decision-badge decision-${escapeHtml(latestDecision)}">${escapeHtml(humanDecisionLabel(latestDecision))}</span>` : `<span class="decision-badge decision-pending">Awaiting review</span>`;
|
|
1555
|
+
const promptHtml = args.reviewPrompt ? `<p class="lede">${escapeHtml(args.reviewPrompt)}</p>` : `<p class="lede">Review the report below. Accept to proceed, or leave feedback for revision.</p>`;
|
|
1556
|
+
const historyHtml = reviews.length > 0 ? `<div class="review-history">
|
|
1557
|
+
<h3 class="history-title">Review history</h3>
|
|
1558
|
+
${reviews.map((r, i) => `<div class="history-entry">
|
|
1559
|
+
<div class="history-meta">
|
|
1560
|
+
<span class="history-index">#${i + 1}</span>
|
|
1561
|
+
<span class="history-decision decision-${escapeHtml(r.decision)}">${escapeHtml(humanDecisionLabel(r.decision))}</span>
|
|
1562
|
+
<span class="history-time">${escapeHtml(formatHumanTimestamp(r.ts))}</span>
|
|
1563
|
+
</div>
|
|
1564
|
+
${r.feedback ? `<p class="history-feedback">${escapeHtml(r.feedback)}</p>` : ""}
|
|
1565
|
+
</div>`).join("")}
|
|
1566
|
+
</div>` : "";
|
|
1497
1567
|
return `<!doctype html>
|
|
1498
1568
|
<html lang="en">
|
|
1499
1569
|
<head>
|
|
@@ -1511,7 +1581,11 @@ function renderHumanReviewPageHtml(args) {
|
|
|
1511
1581
|
<div>
|
|
1512
1582
|
<div class="eyebrow">Human review report</div>
|
|
1513
1583
|
<h1>${escapeHtml(args.playbook)} / ${escapeHtml(args.taskId)}</h1>
|
|
1514
|
-
|
|
1584
|
+
${promptHtml}
|
|
1585
|
+
</div>
|
|
1586
|
+
<div class="hero-status">
|
|
1587
|
+
${decisionBadge}
|
|
1588
|
+
<span class="hero-meta">${reviews.length} review${reviews.length === 1 ? "" : "s"}</span>
|
|
1515
1589
|
</div>
|
|
1516
1590
|
</section>
|
|
1517
1591
|
|
|
@@ -1521,17 +1595,18 @@ function renderHumanReviewPageHtml(args) {
|
|
|
1521
1595
|
</article>
|
|
1522
1596
|
|
|
1523
1597
|
<aside class="sidebar">
|
|
1524
|
-
<h2 class="section-title">
|
|
1598
|
+
<h2 class="section-title">Decision</h2>
|
|
1525
1599
|
<form method="post" action="${escapeHtml(args.submitPath)}" class="form">
|
|
1526
1600
|
<label>
|
|
1527
|
-
<span>
|
|
1601
|
+
<span>Feedback note</span>
|
|
1528
1602
|
<textarea name="feedback" placeholder="What should be clarified, revised, or rejected before this moves forward?"></textarea>
|
|
1529
1603
|
</label>
|
|
1530
1604
|
<div class="feed-actions">
|
|
1531
|
-
<button type="submit" name="action" value="accept">Accept</button>
|
|
1532
|
-
<button type="submit" name="action" value="feedback">
|
|
1605
|
+
<button type="submit" name="action" value="accept" class="btn-accept">Accept & continue</button>
|
|
1606
|
+
<button type="submit" name="action" value="feedback" class="btn-revise">Request revision</button>
|
|
1533
1607
|
</div>
|
|
1534
1608
|
</form>
|
|
1609
|
+
${historyHtml}
|
|
1535
1610
|
</aside>
|
|
1536
1611
|
</section>
|
|
1537
1612
|
|
|
@@ -1713,17 +1788,115 @@ function renderStudioReviewStyles() {
|
|
|
1713
1788
|
font-weight: 700;
|
|
1714
1789
|
padding: 12px 16px;
|
|
1715
1790
|
}
|
|
1716
|
-
|
|
1791
|
+
.btn-accept {
|
|
1717
1792
|
background: linear-gradient(135deg, #38bdf8, #22c55e);
|
|
1718
1793
|
color: #04111b;
|
|
1719
1794
|
}
|
|
1720
|
-
|
|
1795
|
+
.btn-revise {
|
|
1721
1796
|
background: rgba(148, 163, 184, 0.12);
|
|
1722
1797
|
color: #e7eef8;
|
|
1723
1798
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
1724
1799
|
}
|
|
1800
|
+
.hero {
|
|
1801
|
+
display: grid;
|
|
1802
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1803
|
+
align-items: start;
|
|
1804
|
+
}
|
|
1805
|
+
.hero-status {
|
|
1806
|
+
display: grid;
|
|
1807
|
+
gap: 8px;
|
|
1808
|
+
align-content: start;
|
|
1809
|
+
text-align: right;
|
|
1810
|
+
}
|
|
1811
|
+
.hero-meta {
|
|
1812
|
+
color: var(--muted);
|
|
1813
|
+
font-size: 0.88rem;
|
|
1814
|
+
}
|
|
1815
|
+
.decision-badge {
|
|
1816
|
+
display: inline-flex;
|
|
1817
|
+
align-items: center;
|
|
1818
|
+
justify-content: center;
|
|
1819
|
+
padding: 7px 14px;
|
|
1820
|
+
border-radius: 999px;
|
|
1821
|
+
font-size: 0.82rem;
|
|
1822
|
+
font-weight: 700;
|
|
1823
|
+
text-transform: uppercase;
|
|
1824
|
+
letter-spacing: 0.08em;
|
|
1825
|
+
}
|
|
1826
|
+
.decision-approve {
|
|
1827
|
+
background: rgba(34, 197, 94, 0.16);
|
|
1828
|
+
color: #6ee7a0;
|
|
1829
|
+
border: 1px solid rgba(34, 197, 94, 0.24);
|
|
1830
|
+
}
|
|
1831
|
+
.decision-revise {
|
|
1832
|
+
background: rgba(245, 158, 11, 0.16);
|
|
1833
|
+
color: #fbbf44;
|
|
1834
|
+
border: 1px solid rgba(245, 158, 11, 0.24);
|
|
1835
|
+
}
|
|
1836
|
+
.decision-reject {
|
|
1837
|
+
background: rgba(251, 113, 133, 0.16);
|
|
1838
|
+
color: #ffb0bd;
|
|
1839
|
+
border: 1px solid rgba(251, 113, 133, 0.24);
|
|
1840
|
+
}
|
|
1841
|
+
.decision-pending {
|
|
1842
|
+
background: rgba(148, 163, 184, 0.12);
|
|
1843
|
+
color: #b7c5d9;
|
|
1844
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
1845
|
+
}
|
|
1846
|
+
.review-history {
|
|
1847
|
+
margin-top: 18px;
|
|
1848
|
+
display: grid;
|
|
1849
|
+
gap: 10px;
|
|
1850
|
+
}
|
|
1851
|
+
.history-title {
|
|
1852
|
+
margin: 0;
|
|
1853
|
+
font-size: 0.88rem;
|
|
1854
|
+
text-transform: uppercase;
|
|
1855
|
+
letter-spacing: 0.1em;
|
|
1856
|
+
color: #9cb5cd;
|
|
1857
|
+
font-weight: 700;
|
|
1858
|
+
}
|
|
1859
|
+
.history-entry {
|
|
1860
|
+
padding: 12px 14px;
|
|
1861
|
+
border-radius: 14px;
|
|
1862
|
+
background: rgba(2, 6, 23, 0.55);
|
|
1863
|
+
border: 1px solid rgba(148, 163, 184, 0.14);
|
|
1864
|
+
display: grid;
|
|
1865
|
+
gap: 8px;
|
|
1866
|
+
}
|
|
1867
|
+
.history-meta {
|
|
1868
|
+
display: flex;
|
|
1869
|
+
flex-wrap: wrap;
|
|
1870
|
+
gap: 8px;
|
|
1871
|
+
align-items: center;
|
|
1872
|
+
}
|
|
1873
|
+
.history-index {
|
|
1874
|
+
color: #dce7f7;
|
|
1875
|
+
font-weight: 700;
|
|
1876
|
+
font-size: 0.86rem;
|
|
1877
|
+
}
|
|
1878
|
+
.history-decision {
|
|
1879
|
+
display: inline-flex;
|
|
1880
|
+
padding: 3px 8px;
|
|
1881
|
+
border-radius: 999px;
|
|
1882
|
+
font-size: 0.75rem;
|
|
1883
|
+
font-weight: 700;
|
|
1884
|
+
text-transform: uppercase;
|
|
1885
|
+
letter-spacing: 0.06em;
|
|
1886
|
+
}
|
|
1887
|
+
.history-time {
|
|
1888
|
+
color: var(--muted);
|
|
1889
|
+
font-size: 0.82rem;
|
|
1890
|
+
}
|
|
1891
|
+
.history-feedback {
|
|
1892
|
+
margin: 0;
|
|
1893
|
+
color: #d7e2f1;
|
|
1894
|
+
font-size: 0.92rem;
|
|
1895
|
+
line-height: 1.6;
|
|
1896
|
+
}
|
|
1725
1897
|
@media (max-width: 900px) {
|
|
1726
|
-
.hero
|
|
1898
|
+
.hero { grid-template-columns: 1fr; }
|
|
1899
|
+
.hero-status { text-align: left; }
|
|
1727
1900
|
.layout { grid-template-columns: 1fr; }
|
|
1728
1901
|
.sidebar { position: static; }
|
|
1729
1902
|
}
|
|
@@ -1930,8 +2103,8 @@ function normalizeHumanReview(body, context) {
|
|
|
1930
2103
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1931
2104
|
playbook: context.playbook,
|
|
1932
2105
|
taskId: context.taskId,
|
|
1933
|
-
template: "
|
|
1934
|
-
reportTitle: body.reportTitle?.trim() ||
|
|
2106
|
+
template: "",
|
|
2107
|
+
reportTitle: body.reportTitle?.trim() || context.taskId,
|
|
1935
2108
|
summary: body.summary?.trim() || "",
|
|
1936
2109
|
decision: action === "accept" ? "approve" : action === "feedback" ? "revise" : normalizeDecision(body.decision),
|
|
1937
2110
|
feedback: body.feedback?.trim() || ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openplaybooks/converge-studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser studio for planning Converge playbooks",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"LICENSE"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
31
|
+
"@openplaybooks/converge-core": "^0.4.3",
|
|
32
|
+
"yaml": "^2.8.3"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^22.0.0",
|