@nanocollective/roster 0.1.0-alpha.3 → 0.1.0-alpha.5
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/cli.js +856 -169
- package/docs/README.md +10 -5
- package/docs/agents.md +320 -9
- package/docs/commands.md +5 -6
- package/docs/concepts.md +27 -9
- package/docs/cost.md +3 -2
- package/docs/doctor-codes.md +13 -4
- package/docs/export.md +2 -1
- package/docs/extending.md +11 -2
- package/docs/getting-started.md +89 -84
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/docs/manual-steps.md +36 -13
- package/docs/memory.md +9 -6
- package/docs/org-yaml.md +37 -9
- package/docs/portal.md +197 -31
- package/docs/prompts.md +50 -11
- package/docs/security.md +19 -7
- package/docs/session-workflow.md +8 -10
- package/docs/staff-yaml.md +3 -5
- package/docs/troubleshooting.md +17 -14
- package/docs/writing-a-charter.md +18 -17
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +1 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +10 -4
- package/templates/brain/staff.yaml +0 -1
- package/templates/ops/.github/workflows/session.yaml +9 -26
- package/templates/ops/agents.mjs +121 -6
- package/templates/ops/compose.mjs +61 -4
- package/templates/ops/org/operating.md +0 -6
- package/templates/ops/prompts/_identity.md +8 -1
- package/templates/ops/prompts/mention.md +16 -2
- package/templates/portal/css/base.css +116 -8
- package/templates/portal/css/brain.css +8 -1
- package/templates/portal/css/diff.css +6 -2
- package/templates/portal/css/health.css +21 -2
- package/templates/portal/css/inbox.css +105 -5
- package/templates/portal/css/layout.css +37 -4
- package/templates/portal/css/markdown.css +23 -3
- package/templates/portal/css/setup.css +11 -6
- package/templates/portal/index.html +12 -2
- package/templates/portal/js/api.js +33 -0
- package/templates/portal/js/app.js +68 -8
- package/templates/portal/js/dialog.js +47 -4
- package/templates/portal/js/dom.js +25 -0
- package/templates/portal/js/icons.js +8 -1
- package/templates/portal/js/lightbox.js +273 -0
- package/templates/portal/js/md.js +23 -6
- package/templates/portal/js/mention.js +264 -0
- package/templates/portal/js/refresh.js +136 -6
- package/templates/portal/js/state.js +53 -8
- package/templates/portal/js/views/checklist.js +20 -7
- package/templates/portal/js/views/docs.js +94 -4
- package/templates/portal/js/views/files.js +58 -14
- package/templates/portal/js/views/health.js +163 -35
- package/templates/portal/js/views/inbox.js +934 -98
- package/templates/portal/js/views/memory.js +16 -1
- package/templates/portal/js/views/org.js +142 -62
- package/templates/portal/js/views/prompt.js +50 -63
- package/templates/portal/js/views/staff.js +62 -2
- package/templates/portal/js/yaml.js +134 -0
- package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +0 -50
- package/templates/ops/prompts/pr-mention.md +0 -57
package/dist/cli.js
CHANGED
|
@@ -244,6 +244,11 @@ function opsTemplateDir() {
|
|
|
244
244
|
// src/lib/render.ts
|
|
245
245
|
var NOTE = "%%TOKENS%%";
|
|
246
246
|
var TOKEN = /%%([A-Z_]+)%%/g;
|
|
247
|
+
function toolsOf(org) {
|
|
248
|
+
const list = org?.defaults?.allowed_tools;
|
|
249
|
+
const asked = Array.isArray(list) ? list.join(",") : typeof list === "string" ? list : "";
|
|
250
|
+
return asked.replace(/\s+/g, "") || "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch";
|
|
251
|
+
}
|
|
247
252
|
function orgTokens(org) {
|
|
248
253
|
return {
|
|
249
254
|
ORG: org.org,
|
|
@@ -251,7 +256,11 @@ function orgTokens(org) {
|
|
|
251
256
|
OPS_REPO: org.opsRepo,
|
|
252
257
|
OPS_REPO_DIR: org.opsDirName,
|
|
253
258
|
HUMAN: org.human,
|
|
254
|
-
HUMAN_MARKER: org.humanMarker
|
|
259
|
+
HUMAN_MARKER: org.humanMarker,
|
|
260
|
+
/* A JSON array, spelled into a GitHub expression as `fromJSON('%%HUMAN_LOGINS%%')`. One
|
|
261
|
+
login is still a one-element list, so the generated gate has one shape rather than two. */
|
|
262
|
+
HUMAN_LOGINS: JSON.stringify((org.humanLogins ?? [org.human]).filter(Boolean)),
|
|
263
|
+
ALLOWED_TOOLS: org.allowedTools
|
|
255
264
|
};
|
|
256
265
|
}
|
|
257
266
|
function tokensFor(org, s) {
|
|
@@ -269,7 +278,6 @@ function tokensFor(org, s) {
|
|
|
269
278
|
MODEL: s.model,
|
|
270
279
|
TIMEOUT: String(s.timeout),
|
|
271
280
|
MENTION_TIMEOUT: String(s.mentionTimeout),
|
|
272
|
-
PR_MENTION_TIMEOUT: String(s.prMentionTimeout),
|
|
273
281
|
SECRET_PREFIX: s.secretPrefix,
|
|
274
282
|
PUBLIC_SECRET_PREFIX: s.publicSecretPrefix,
|
|
275
283
|
APP: s.app,
|
|
@@ -354,11 +362,11 @@ function specFromManifest(m, dir) {
|
|
|
354
362
|
worksIn: (m.works_in ?? []).map((w) => String(w?.repo)).filter(Boolean),
|
|
355
363
|
schedule: String(m.schedule ?? ""),
|
|
356
364
|
model: String(m.model ?? ""),
|
|
357
|
-
timeout: Number(m.timeout_minutes ??
|
|
358
|
-
// Absent from the manifests written before these were separate fields.
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
timeout: Number(m.timeout_minutes ?? 90),
|
|
366
|
+
// Absent from the manifests written before these were separate fields. 90 either way: a
|
|
367
|
+
// mention that ends in a build needs a session's room, and the 30 these once fell back to
|
|
368
|
+
// killed four runs mid-gate at a ceiling nobody had chosen.
|
|
369
|
+
mentionTimeout: Number(m.mention_timeout_minutes ?? 90),
|
|
362
370
|
secretPrefix: String(priv.secret_prefix ?? String(m.handle ?? dir).toUpperCase()),
|
|
363
371
|
publicSecretPrefix: String(pub.secret_prefix ?? "BOT"),
|
|
364
372
|
app: String(priv.app ?? ""),
|
|
@@ -622,11 +630,37 @@ ${text.trimEnd()}
|
|
|
622
630
|
${bars}`;
|
|
623
631
|
}
|
|
624
632
|
|
|
633
|
+
// src/lib/humans.ts
|
|
634
|
+
function readHumans(org) {
|
|
635
|
+
const listed = Array.isArray(org?.humans) ? org.humans : org?.humans ? [org.humans] : [];
|
|
636
|
+
const raw = [...listed, org?.human].filter(Boolean);
|
|
637
|
+
const out = [];
|
|
638
|
+
for (const entry of raw) {
|
|
639
|
+
const github = String(entry.github ?? entry.login ?? "").trim();
|
|
640
|
+
const name = String(entry.name ?? github).trim();
|
|
641
|
+
if (!github && !name) continue;
|
|
642
|
+
if (github && out.some((h) => h.github.toLowerCase() === github.toLowerCase())) continue;
|
|
643
|
+
out.push({
|
|
644
|
+
github,
|
|
645
|
+
name: name || github,
|
|
646
|
+
marker: String(entry.marker ?? "").trim() || defaultMarker(name || github),
|
|
647
|
+
role: entry.role === void 0 || entry.role === null ? void 0 : String(entry.role)
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
function humanLogins(org) {
|
|
653
|
+
return readHumans(org).map((h) => h.github).filter(Boolean);
|
|
654
|
+
}
|
|
655
|
+
function defaultMarker(name) {
|
|
656
|
+
return String(name).trim().split(/[\s-]+/)[0].toLowerCase().replace(/[^a-z0-9]/g, "") || "human";
|
|
657
|
+
}
|
|
658
|
+
|
|
625
659
|
// src/lib/prompt.ts
|
|
626
660
|
import { execFileSync } from "child_process";
|
|
627
661
|
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync } from "fs";
|
|
628
662
|
import { dirname as dirname3, join as join6, relative as relative2, resolve as resolve2 } from "path";
|
|
629
|
-
var KINDS = ["daily", "mention"
|
|
663
|
+
var KINDS = ["daily", "mention"];
|
|
630
664
|
function promptView(ws, compose, staffHandle, brainDir, kind) {
|
|
631
665
|
const had = process.env.ROSTER_CONTEXT;
|
|
632
666
|
if (kind !== "daily" && !had) {
|
|
@@ -774,12 +808,15 @@ function validateOrgYaml(text, parseYaml) {
|
|
|
774
808
|
return `org.yaml needs a "${key}", and every prompt is composed against it`;
|
|
775
809
|
}
|
|
776
810
|
}
|
|
777
|
-
for (const key of ["staff", "repos"]) {
|
|
811
|
+
for (const key of ["staff", "repos", "humans"]) {
|
|
778
812
|
if (doc[key] !== void 0 && !Array.isArray(doc[key])) return `"${key}" has to be a list`;
|
|
779
813
|
}
|
|
780
814
|
for (const s of doc.staff ?? []) {
|
|
781
815
|
if (!s?.handle) return "every staff entry needs a handle";
|
|
782
816
|
}
|
|
817
|
+
for (const h of doc.humans ?? []) {
|
|
818
|
+
if (!h?.github) return "every entry in humans needs a github login";
|
|
819
|
+
}
|
|
783
820
|
return null;
|
|
784
821
|
}
|
|
785
822
|
|
|
@@ -814,7 +851,7 @@ roster brief <kind> [handle]
|
|
|
814
851
|
these briefs by \`roster init\` and \`roster hire\`. There is nothing in them that is
|
|
815
852
|
specific to any agent.
|
|
816
853
|
|
|
817
|
-
--kind <k> for amend: daily | mention
|
|
854
|
+
--kind <k> for amend: daily | mention (default: daily)
|
|
818
855
|
--want <text> for amend: what you want changed
|
|
819
856
|
--ops <dir> ops repo directory (default: found by walking up)
|
|
820
857
|
`;
|
|
@@ -876,14 +913,19 @@ function staffTokens(ws, org, handle, parseYaml) {
|
|
|
876
913
|
return tokensFor(spec(org, ws), specFromManifest(manifest, dir));
|
|
877
914
|
}
|
|
878
915
|
function spec(org, ws) {
|
|
879
|
-
const
|
|
916
|
+
const humans = readHumans(org);
|
|
917
|
+
const first = humans[0];
|
|
880
918
|
return {
|
|
881
919
|
org: org.org,
|
|
882
920
|
name: org.name,
|
|
883
921
|
opsRepo: `${org.org}/${ws.opsName}`,
|
|
884
922
|
opsDirName: ws.opsName,
|
|
885
|
-
|
|
886
|
-
|
|
923
|
+
/* A brief is prose addressed to one person, so it names the first. The rest are in the
|
|
924
|
+
manifest and in the gate; a brief that said "Will and Sam" would read as a committee. */
|
|
925
|
+
human: first?.name ?? first?.github ?? "the human",
|
|
926
|
+
humanMarker: first?.marker ?? "human",
|
|
927
|
+
humanLogins: humans.map((h) => h.github).filter(Boolean),
|
|
928
|
+
allowedTools: toolsOf(org)
|
|
887
929
|
};
|
|
888
930
|
}
|
|
889
931
|
function renderBrief(text, tokens, where) {
|
|
@@ -921,7 +963,7 @@ function parseFlags2(argv) {
|
|
|
921
963
|
}
|
|
922
964
|
|
|
923
965
|
// src/commands/doctor.ts
|
|
924
|
-
import { existsSync as existsSync10, readdirSync as
|
|
966
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
|
|
925
967
|
import { join as join11 } from "path";
|
|
926
968
|
|
|
927
969
|
// src/lib/memory.ts
|
|
@@ -1124,7 +1166,15 @@ function looksUnwritten(body) {
|
|
|
1124
1166
|
|
|
1125
1167
|
// src/commands/upgrade.ts
|
|
1126
1168
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1127
|
-
import {
|
|
1169
|
+
import {
|
|
1170
|
+
existsSync as existsSync9,
|
|
1171
|
+
mkdirSync,
|
|
1172
|
+
mkdtempSync as mkdtempSync2,
|
|
1173
|
+
readdirSync as readdirSync4,
|
|
1174
|
+
readFileSync as readFileSync8,
|
|
1175
|
+
rmSync as rmSync2,
|
|
1176
|
+
writeFileSync as writeFileSync3
|
|
1177
|
+
} from "fs";
|
|
1128
1178
|
import { tmpdir as tmpdir2 } from "os";
|
|
1129
1179
|
import { dirname as dirname4, join as join10 } from "path";
|
|
1130
1180
|
|
|
@@ -1454,8 +1504,10 @@ function planBrains(ws, parseYaml) {
|
|
|
1454
1504
|
name: org.name,
|
|
1455
1505
|
opsRepo: `${org.org}/${ws.opsName}`,
|
|
1456
1506
|
opsDirName: ws.opsName,
|
|
1457
|
-
human: org
|
|
1458
|
-
humanMarker: org
|
|
1507
|
+
human: readHumans(org)[0]?.github ?? "",
|
|
1508
|
+
humanMarker: readHumans(org)[0]?.marker ?? "human",
|
|
1509
|
+
humanLogins: humanLogins(org),
|
|
1510
|
+
allowedTools: toolsOf(org)
|
|
1459
1511
|
};
|
|
1460
1512
|
const out = [];
|
|
1461
1513
|
for (const entry of org.staff ?? []) {
|
|
@@ -1494,6 +1546,12 @@ function planBrains(ws, parseYaml) {
|
|
|
1494
1546
|
have === text ? { rel, kind, verdict: "same" } : { rel, kind, verdict: "regenerate", next: text, diff: unified(have, text) }
|
|
1495
1547
|
);
|
|
1496
1548
|
}
|
|
1549
|
+
for (const rel of callerFiles(root)) {
|
|
1550
|
+
if (want.has(rel)) continue;
|
|
1551
|
+
const text = readFileSync8(join10(root, rel), "utf8");
|
|
1552
|
+
if (!text.includes(`${orgSpec2.opsRepo}/.github/workflows/session.yaml`)) continue;
|
|
1553
|
+
base2.files.push({ rel, kind: "generated", verdict: "obsolete" });
|
|
1554
|
+
}
|
|
1497
1555
|
out.push(base2);
|
|
1498
1556
|
}
|
|
1499
1557
|
return out;
|
|
@@ -1518,6 +1576,11 @@ function unified(before, after) {
|
|
|
1518
1576
|
rmSync2(dir, { recursive: true, force: true });
|
|
1519
1577
|
}
|
|
1520
1578
|
}
|
|
1579
|
+
function callerFiles(root) {
|
|
1580
|
+
const dir = join10(root, ".github", "workflows");
|
|
1581
|
+
if (!existsSync9(dir)) return [];
|
|
1582
|
+
return readdirSync4(dir).filter((n) => n.endsWith(".yaml") || n.endsWith(".yml")).map((n) => `.github/workflows/${n}`);
|
|
1583
|
+
}
|
|
1521
1584
|
function reportBrains(brains, opts) {
|
|
1522
1585
|
for (const b of brains) {
|
|
1523
1586
|
const interesting = b.files.filter((f) => f.verdict !== "same");
|
|
@@ -1541,6 +1604,13 @@ function reportBrains(brains, opts) {
|
|
|
1541
1604
|
`);
|
|
1542
1605
|
continue;
|
|
1543
1606
|
}
|
|
1607
|
+
if (f.verdict === "obsolete") {
|
|
1608
|
+
process.stdout.write(
|
|
1609
|
+
` - ${f.rel} the framework no longer generates this; will be removed
|
|
1610
|
+
`
|
|
1611
|
+
);
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1544
1614
|
process.stdout.write(` \u2191 ${f.rel} differs from the template
|
|
1545
1615
|
`);
|
|
1546
1616
|
if (f.diff) {
|
|
@@ -1555,6 +1625,11 @@ function applyBrains(brains) {
|
|
|
1555
1625
|
let wrote = 0;
|
|
1556
1626
|
for (const b of brains) {
|
|
1557
1627
|
for (const f of b.files) {
|
|
1628
|
+
if (f.verdict === "obsolete") {
|
|
1629
|
+
rmSync2(join10(b.root, f.rel), { force: true });
|
|
1630
|
+
wrote++;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1558
1633
|
if (f.next === void 0) continue;
|
|
1559
1634
|
const dest = join10(b.root, f.rel);
|
|
1560
1635
|
mkdirSync(dirname4(dest), { recursive: true });
|
|
@@ -1602,11 +1677,12 @@ async function collect(opts) {
|
|
|
1602
1677
|
const composer = await loadComposer(ws.opsDir);
|
|
1603
1678
|
const org = readOrg(ws.opsDir, composer.parseYaml);
|
|
1604
1679
|
const staff = (org.staff ?? []).filter((s) => !opts.only || s.handle === opts.only);
|
|
1605
|
-
if (!staff.length) return null;
|
|
1680
|
+
if (opts.only && !staff.length) return null;
|
|
1606
1681
|
const findings = [];
|
|
1607
1682
|
const online = !opts.offline && await gate(findings);
|
|
1608
1683
|
findings.push(...checkWorkspace(ws, org));
|
|
1609
1684
|
findings.push(...checkBusiness(ws));
|
|
1685
|
+
findings.push(...await checkAgent(ws, org));
|
|
1610
1686
|
if (online) findings.push(...await checkOrgOnline(ws, org));
|
|
1611
1687
|
const perStaff = await Promise.all(staff.map((s) => checkStaff(ws, org, s, composer, online)));
|
|
1612
1688
|
for (const f of perStaff) findings.push(...f);
|
|
@@ -1659,13 +1735,22 @@ function checkWorkspace(ws, org) {
|
|
|
1659
1735
|
id: "org.yaml",
|
|
1660
1736
|
title: `org.yaml names ${org.repos?.length ?? 0} repos and ${org.staff?.length ?? 0} staff`
|
|
1661
1737
|
});
|
|
1662
|
-
|
|
1738
|
+
const humans = readHumans(org);
|
|
1739
|
+
if (!humans.some((h) => h.github)) {
|
|
1663
1740
|
out.push({
|
|
1664
1741
|
scope,
|
|
1665
1742
|
level: "fail",
|
|
1666
1743
|
id: "human",
|
|
1667
|
-
title: "org.yaml
|
|
1668
|
-
fix: "The mention callers gate on
|
|
1744
|
+
title: "org.yaml names no human with a github login",
|
|
1745
|
+
fix: "The mention callers gate on these logins. Without one nothing can wake an agent. Write `human: { github: you }`, or a `humans:` list for more than one."
|
|
1746
|
+
});
|
|
1747
|
+
} else if (humans.some((h) => !h.github)) {
|
|
1748
|
+
out.push({
|
|
1749
|
+
scope,
|
|
1750
|
+
level: "warn",
|
|
1751
|
+
id: "human.login",
|
|
1752
|
+
title: `${humans.filter((h) => !h.github).map((h) => h.name).join(", ")} has no github login`,
|
|
1753
|
+
fix: "Give them one, or the mention gate silently ignores everything they write."
|
|
1669
1754
|
});
|
|
1670
1755
|
}
|
|
1671
1756
|
try {
|
|
@@ -1714,6 +1799,57 @@ function checkWorkspace(ws, org) {
|
|
|
1714
1799
|
}
|
|
1715
1800
|
return out;
|
|
1716
1801
|
}
|
|
1802
|
+
async function checkAgent(ws, org) {
|
|
1803
|
+
const scope = "workspace";
|
|
1804
|
+
let agent;
|
|
1805
|
+
try {
|
|
1806
|
+
const { resolveAgent } = await import(`file://${join11(ws.opsDir, "agents.mjs")}`);
|
|
1807
|
+
agent = resolveAgent(org, {});
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
return [
|
|
1810
|
+
{
|
|
1811
|
+
scope,
|
|
1812
|
+
level: "fail",
|
|
1813
|
+
id: "agent",
|
|
1814
|
+
title: `no runner resolves: ${firstLine(err)}`,
|
|
1815
|
+
fix: "Nothing can run until org.yaml names an agent this tenant's agents.mjs knows."
|
|
1816
|
+
}
|
|
1817
|
+
];
|
|
1818
|
+
}
|
|
1819
|
+
const out = [{ scope, level: "ok", id: "agent", title: `runs on ${agent.id}` }];
|
|
1820
|
+
const orgFile = join11(ws.opsDir, "org.yaml");
|
|
1821
|
+
if (existsSync10(orgFile) && /FILL IN/.test(readFileSync9(orgFile, "utf8"))) {
|
|
1822
|
+
out.push({
|
|
1823
|
+
scope,
|
|
1824
|
+
level: "fail",
|
|
1825
|
+
id: "agent.config",
|
|
1826
|
+
title: "org.yaml still has a blank in it",
|
|
1827
|
+
fix: "Fill in the FILL IN. A run cannot pick a model from a placeholder."
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
if (!agent.config) return out;
|
|
1831
|
+
const path = join11(ws.opsDir, agent.config.path);
|
|
1832
|
+
if (!existsSync10(path)) {
|
|
1833
|
+
out.push({
|
|
1834
|
+
scope,
|
|
1835
|
+
level: "fail",
|
|
1836
|
+
id: "agent.config",
|
|
1837
|
+
title: `${agent.id} needs ${agent.config.path} and there is none`,
|
|
1838
|
+
fix: "roster init writes it for a new tenant. See docs/agents.md for what goes in it."
|
|
1839
|
+
});
|
|
1840
|
+
return out;
|
|
1841
|
+
}
|
|
1842
|
+
if (/FILL IN/.test(readFileSync9(path, "utf8"))) {
|
|
1843
|
+
out.push({
|
|
1844
|
+
scope,
|
|
1845
|
+
level: "fail",
|
|
1846
|
+
id: "agent.config",
|
|
1847
|
+
title: `${agent.config.path} still has a blank in it`,
|
|
1848
|
+
fix: `Fill in the FILL IN. ${agent.id} cannot pick a model without it.`
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
return out;
|
|
1852
|
+
}
|
|
1717
1853
|
function checkBusiness(ws) {
|
|
1718
1854
|
const path = join11(ws.opsDir, "org", "business.md");
|
|
1719
1855
|
if (!existsSync10(path)) {
|
|
@@ -1847,11 +1983,10 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1847
1983
|
const CONTEXT = JSON.stringify({
|
|
1848
1984
|
issue_number: "1",
|
|
1849
1985
|
comment_id: "1",
|
|
1850
|
-
pr_number: "1",
|
|
1851
1986
|
repo: `${org.org}/example`
|
|
1852
1987
|
});
|
|
1853
1988
|
const before = process.env.ROSTER_CONTEXT;
|
|
1854
|
-
for (const kind of ["daily", "mention"
|
|
1989
|
+
for (const kind of ["daily", "mention"]) {
|
|
1855
1990
|
try {
|
|
1856
1991
|
if (kind === "daily") delete process.env.ROSTER_CONTEXT;
|
|
1857
1992
|
else process.env.ROSTER_CONTEXT = CONTEXT;
|
|
@@ -1879,16 +2014,16 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1879
2014
|
scope,
|
|
1880
2015
|
level: "ok",
|
|
1881
2016
|
id: "compose",
|
|
1882
|
-
title: "prompts compose for daily
|
|
2017
|
+
title: "prompts compose for daily and mention"
|
|
1883
2018
|
});
|
|
1884
2019
|
}
|
|
1885
|
-
const callers = readCallers(root);
|
|
1886
|
-
if (callers.length !==
|
|
2020
|
+
const callers = readCallers(root, entry.handle);
|
|
2021
|
+
if (callers.length !== 2) {
|
|
1887
2022
|
out.push({
|
|
1888
2023
|
scope,
|
|
1889
2024
|
level: callers.length ? "warn" : "fail",
|
|
1890
2025
|
id: "callers",
|
|
1891
|
-
title: `${callers.length} caller workflow${callers.length === 1 ? "" : "s"}, expected
|
|
2026
|
+
title: `${callers.length} caller workflow${callers.length === 1 ? "" : "s"}, expected 2 (daily, mention)`,
|
|
1892
2027
|
fix: "A missing caller is a route that silently never fires. Compare against roster's templates/brain."
|
|
1893
2028
|
});
|
|
1894
2029
|
}
|
|
@@ -1926,8 +2061,13 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1926
2061
|
});
|
|
1927
2062
|
}
|
|
1928
2063
|
}
|
|
1929
|
-
if (callers.length ===
|
|
1930
|
-
out.push({
|
|
2064
|
+
if (callers.length === 2 && !out.some((f) => f.id.startsWith("callers"))) {
|
|
2065
|
+
out.push({
|
|
2066
|
+
scope,
|
|
2067
|
+
level: "ok",
|
|
2068
|
+
id: "callers",
|
|
2069
|
+
title: `2 callers, both pointing at ${opsRepo}`
|
|
2070
|
+
});
|
|
1931
2071
|
}
|
|
1932
2072
|
const missing = (manifest.surfaces ?? []).map((s) => s.path).filter((p) => p && !existsSync10(join11(root, p)));
|
|
1933
2073
|
if (missing.length) {
|
|
@@ -1946,7 +2086,7 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1946
2086
|
}
|
|
1947
2087
|
function timeoutOf(callerText) {
|
|
1948
2088
|
const m = /timeout_minutes:\s*(\d+)/.exec(callerText);
|
|
1949
|
-
return m ? Number(m[1]) :
|
|
2089
|
+
return m ? Number(m[1]) : 90;
|
|
1950
2090
|
}
|
|
1951
2091
|
function runMinutes(run6) {
|
|
1952
2092
|
return (new Date(run6.updatedAt).getTime() - new Date(run6.createdAt).getTime()) / 6e4;
|
|
@@ -1971,10 +2111,11 @@ function inferredCeiling(cancelled) {
|
|
|
1971
2111
|
}
|
|
1972
2112
|
return most >= 2 ? best : null;
|
|
1973
2113
|
}
|
|
1974
|
-
function readCallers(root) {
|
|
2114
|
+
function readCallers(root, handle) {
|
|
1975
2115
|
const dir = join11(root, ".github", "workflows");
|
|
1976
2116
|
if (!existsSync10(dir)) return [];
|
|
1977
|
-
|
|
2117
|
+
const mine = new RegExp(`^${handle}-(daily|mention)\\.ya?ml$`);
|
|
2118
|
+
return readdirSync5(dir).filter((f) => mine.test(f)).sort().map((name) => ({ name, text: readFileSync9(join11(dir, name), "utf8") }));
|
|
1978
2119
|
}
|
|
1979
2120
|
async function checkOrgOnline(ws, org) {
|
|
1980
2121
|
const out = [];
|
|
@@ -2165,6 +2306,9 @@ async function checkStaffOnline(scope, manifest, callers) {
|
|
|
2165
2306
|
}
|
|
2166
2307
|
);
|
|
2167
2308
|
}
|
|
2309
|
+
const proven = runs.some(
|
|
2310
|
+
({ res }) => (res.data ?? []).some((r) => r.status === "completed" && r.conclusion === "success")
|
|
2311
|
+
);
|
|
2168
2312
|
for (const { name, res } of runs) {
|
|
2169
2313
|
if (!res.ok) {
|
|
2170
2314
|
out.push({
|
|
@@ -2189,13 +2333,20 @@ async function checkStaffOnline(scope, manifest, callers) {
|
|
|
2189
2333
|
continue;
|
|
2190
2334
|
}
|
|
2191
2335
|
if (!real.length) {
|
|
2192
|
-
out.push(
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2336
|
+
out.push(
|
|
2337
|
+
proven ? {
|
|
2338
|
+
scope,
|
|
2339
|
+
level: "ok",
|
|
2340
|
+
id: "runs",
|
|
2341
|
+
title: `${name}: ${all.length} recent triggers, all gated out, which is its normal state`
|
|
2342
|
+
} : {
|
|
2343
|
+
scope,
|
|
2344
|
+
level: "warn",
|
|
2345
|
+
id: "runs",
|
|
2346
|
+
title: `${name}: ${all.length} recent triggers, all gated out before doing anything`,
|
|
2347
|
+
fix: "Nothing in this repo has exercised the app grant. A skipped run proves only the trigger, so trigger one workflow here by hand before trusting any of them."
|
|
2348
|
+
}
|
|
2349
|
+
);
|
|
2199
2350
|
continue;
|
|
2200
2351
|
}
|
|
2201
2352
|
const timeout = timeoutOf(callers.find((c) => c.name === name)?.text ?? "");
|
|
@@ -2208,7 +2359,15 @@ async function checkStaffOnline(scope, manifest, callers) {
|
|
|
2208
2359
|
const cancelled = real.filter((r) => !killed(r) && r.conclusion === "cancelled");
|
|
2209
2360
|
const ok = real.filter((r) => r.conclusion === "success");
|
|
2210
2361
|
const ceiling = timedOut.length ? Math.round(runMinutes(timedOut[0])) : timeout;
|
|
2211
|
-
|
|
2362
|
+
const provedSince = ceiling !== timeout && ok.some((r) => r.createdAt > timedOut[0].createdAt);
|
|
2363
|
+
if (timedOut.length && provedSince) {
|
|
2364
|
+
out.push({
|
|
2365
|
+
scope,
|
|
2366
|
+
level: "ok",
|
|
2367
|
+
id: "runs.timeout",
|
|
2368
|
+
title: `${name}: the ${ceiling}m ceiling that killed ${timedOut.length} of the last ${real.length} was raised to ${timeout}m, and a run has finished since`
|
|
2369
|
+
});
|
|
2370
|
+
} else if (timedOut.length) {
|
|
2212
2371
|
out.push({
|
|
2213
2372
|
scope,
|
|
2214
2373
|
level: "fail",
|
|
@@ -2301,7 +2460,7 @@ import { writeFileSync as writeFileSync4 } from "fs";
|
|
|
2301
2460
|
|
|
2302
2461
|
// src/lib/export.ts
|
|
2303
2462
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2304
|
-
import { existsSync as existsSync11, readdirSync as
|
|
2463
|
+
import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
2305
2464
|
import { join as join12, relative as relative3 } from "path";
|
|
2306
2465
|
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "out", ".DS_Store"]);
|
|
2307
2466
|
function buildExport(ws, org, parseYaml, opts = {}) {
|
|
@@ -2344,11 +2503,13 @@ function buildExport(ws, org, parseYaml, opts = {}) {
|
|
|
2344
2503
|
s.soloBots = s.bots.filter((b) => times.get(b) === 1);
|
|
2345
2504
|
s.sharedBots = s.bots.filter((b) => (times.get(b) ?? 0) > 1);
|
|
2346
2505
|
}
|
|
2506
|
+
const humans = readHumans(org);
|
|
2347
2507
|
return {
|
|
2348
2508
|
org: org.org,
|
|
2349
2509
|
name: org.name,
|
|
2350
2510
|
opsName: ws.opsName,
|
|
2351
|
-
human: org.human ?? {},
|
|
2511
|
+
human: humans[0] ?? org.human ?? {},
|
|
2512
|
+
humans,
|
|
2352
2513
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2353
2514
|
staff
|
|
2354
2515
|
};
|
|
@@ -2369,7 +2530,7 @@ function readRig(root, manifest, commits) {
|
|
|
2369
2530
|
const wfDir = join12(root, ".github", "workflows");
|
|
2370
2531
|
const declared = manifest.surfaces ?? [];
|
|
2371
2532
|
return {
|
|
2372
|
-
workflows: existsSync11(wfDir) ?
|
|
2533
|
+
workflows: existsSync11(wfDir) ? readdirSync6(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [],
|
|
2373
2534
|
hasCharter: existsSync11(join12(root, "CHARTER.md")),
|
|
2374
2535
|
hasManifest: existsSync11(join12(root, "staff.yaml")),
|
|
2375
2536
|
missingSurfaces: declared.filter((s) => s?.path && !existsSync11(join12(root, s.path))).map((s) => s.path),
|
|
@@ -2398,7 +2559,7 @@ function readSurfaces(root, declared) {
|
|
|
2398
2559
|
}));
|
|
2399
2560
|
}
|
|
2400
2561
|
function walk2(dir, root, out = []) {
|
|
2401
|
-
for (const name of
|
|
2562
|
+
for (const name of readdirSync6(dir)) {
|
|
2402
2563
|
if (SKIP.has(name)) continue;
|
|
2403
2564
|
const full = join12(dir, name);
|
|
2404
2565
|
const st = statSync3(full);
|
|
@@ -2839,7 +3000,7 @@ import { dirname as dirname5, join as join14 } from "path";
|
|
|
2839
3000
|
var hireHelp = `
|
|
2840
3001
|
roster hire <handle> [--name "Chief Financial Officer"] [--apply]
|
|
2841
3002
|
|
|
2842
|
-
Scaffold a new staff member: their brain repo, the
|
|
3003
|
+
Scaffold a new staff member: their brain repo, the two caller workflows, a manifest, a
|
|
2843
3004
|
memory index, a charter stub, labels, a pinned status issue, and the peer wiring in both
|
|
2844
3005
|
directions.
|
|
2845
3006
|
|
|
@@ -2856,7 +3017,6 @@ roster hire <handle> [--name "Chief Financial Officer"] [--apply]
|
|
|
2856
3017
|
--model <id> defaults to org.yaml's
|
|
2857
3018
|
--timeout <n> daily run ceiling, minutes
|
|
2858
3019
|
--mention-timeout <n> mention run ceiling, minutes
|
|
2859
|
-
--pr-timeout <n> PR-amendment run ceiling, minutes
|
|
2860
3020
|
--secret-prefix <X> secrets are <X>_APP_ID and <X>_APP_PRIVATE_KEY. Defaults to HANDLE.
|
|
2861
3021
|
--app <slug> this staff member's GitHub App. Defaults to the pattern its peers use.
|
|
2862
3022
|
--public-app <slug> the shared public identity. Defaults to whatever the peers use.
|
|
@@ -2937,9 +3097,11 @@ function buildPlan(ws, org, handle, opts, parseYaml) {
|
|
|
2937
3097
|
worksIn: (org.repos ?? []).filter((r) => r.role === "product").map((r) => `${org.org}/${r.name}`),
|
|
2938
3098
|
schedule,
|
|
2939
3099
|
model,
|
|
2940
|
-
timeout: opts.timeout ?? org.defaults?.timeout_minutes ??
|
|
2941
|
-
|
|
2942
|
-
|
|
3100
|
+
timeout: opts.timeout ?? org.defaults?.timeout_minutes ?? 90,
|
|
3101
|
+
// A mention that ends in a build needs a session's room: the product repo's gate alone
|
|
3102
|
+
// can outlast a short ceiling, and a hire that has to discover that costs a run. An org
|
|
3103
|
+
// that wants them split says so; absent that, one number governs both.
|
|
3104
|
+
mentionTimeout: opts.mentionTimeout ?? org.defaults?.mention_timeout_minutes ?? org.defaults?.timeout_minutes ?? 90,
|
|
2943
3105
|
secretPrefix: opts.secretPrefix ?? handle.toUpperCase(),
|
|
2944
3106
|
publicSecretPrefix: publicIdentity?.secret_prefix ?? "BOT",
|
|
2945
3107
|
app: app ?? `${handle}`,
|
|
@@ -2956,16 +3118,24 @@ function buildPlan(ws, org, handle, opts, parseYaml) {
|
|
|
2956
3118
|
"status_issue is 0 until --apply opens the pinned issue; prompts will not compose before then"
|
|
2957
3119
|
);
|
|
2958
3120
|
}
|
|
3121
|
+
const humans = readHumans(org);
|
|
2959
3122
|
const orgSpec2 = {
|
|
2960
3123
|
org: org.org,
|
|
2961
3124
|
name: org.name,
|
|
2962
3125
|
opsRepo: `${org.org}/${ws.opsName}`,
|
|
2963
3126
|
opsDirName: ws.opsName,
|
|
2964
|
-
human:
|
|
2965
|
-
humanMarker:
|
|
3127
|
+
human: humans[0]?.github ?? "",
|
|
3128
|
+
humanMarker: humans[0]?.marker ?? "human",
|
|
3129
|
+
humanLogins: humans.map((h) => h.github).filter(Boolean),
|
|
3130
|
+
allowedTools: toolsOf(org)
|
|
2966
3131
|
};
|
|
2967
3132
|
if (!orgSpec2.human)
|
|
2968
3133
|
warnings.push("org.yaml has no human.github, so the mention gate will never match");
|
|
3134
|
+
if (humans.length > 1) {
|
|
3135
|
+
warnings.push(
|
|
3136
|
+
`the mention gate accepts ${humans.map((h) => h.github).join(", ")} \u2014 everyone in org.yaml`
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
2969
3139
|
const tokens = tokensFor(orgSpec2, staff);
|
|
2970
3140
|
const files = renderTree(brainTemplateDir(), tokens);
|
|
2971
3141
|
for (const [rel, text] of briefCommands(["charter"], tokens)) files.set(rel, text);
|
|
@@ -3301,7 +3471,6 @@ function parseFlags6(argv) {
|
|
|
3301
3471
|
else if (flag === "--model") out.model = value;
|
|
3302
3472
|
else if (flag === "--timeout") out.timeout = Number(value);
|
|
3303
3473
|
else if (flag === "--mention-timeout") out.mentionTimeout = Number(value);
|
|
3304
|
-
else if (flag === "--pr-timeout") out.prMentionTimeout = Number(value);
|
|
3305
3474
|
else if (flag === "--secret-prefix") out.secretPrefix = value;
|
|
3306
3475
|
else if (flag === "--app") out.app = value;
|
|
3307
3476
|
else if (flag === "--public-app") out.publicApp = value;
|
|
@@ -3362,7 +3531,7 @@ async function initCommand(argv) {
|
|
|
3362
3531
|
`);
|
|
3363
3532
|
return 2;
|
|
3364
3533
|
}
|
|
3365
|
-
const files = initFiles({ org: opts.org, name, human, marker, opsName, agent: opts.agent });
|
|
3534
|
+
const files = await initFiles({ org: opts.org, name, human, marker, opsName, agent: opts.agent });
|
|
3366
3535
|
process.stdout.write(`
|
|
3367
3536
|
roster init \u2014 ${name} (${opts.org})
|
|
3368
3537
|
|
|
@@ -3438,12 +3607,18 @@ async function initCommand(argv) {
|
|
|
3438
3607
|
`);
|
|
3439
3608
|
return 0;
|
|
3440
3609
|
}
|
|
3441
|
-
function initFiles(o) {
|
|
3610
|
+
async function initFiles(o) {
|
|
3442
3611
|
const files = /* @__PURE__ */ new Map();
|
|
3443
3612
|
for (const rel of templateFiles(opsTemplateDir())) {
|
|
3444
3613
|
files.set(rel, readFileSync13(join15(opsTemplateDir(), rel), "utf8"));
|
|
3445
3614
|
}
|
|
3446
|
-
|
|
3615
|
+
const agent = o.agent ?? "claude-code-action";
|
|
3616
|
+
const preset = await agentPreset(agent);
|
|
3617
|
+
const model = preset.model || '"FILL IN: a model the provider below serves"';
|
|
3618
|
+
files.set("org.yaml", orgYaml({ ...o, agent, model }));
|
|
3619
|
+
if (preset.config) {
|
|
3620
|
+
files.set(preset.config.path, JSON.stringify(preset.config.contents, null, 2) + "\n");
|
|
3621
|
+
}
|
|
3447
3622
|
files.set("org/business.md", businessStub(o.name, o.org));
|
|
3448
3623
|
for (const [rel, text] of briefCommands(["discover", "voice"], {
|
|
3449
3624
|
ORG: o.org,
|
|
@@ -3458,6 +3633,18 @@ function initFiles(o) {
|
|
|
3458
3633
|
files.set(".roster-version", stamp2() + "\n");
|
|
3459
3634
|
return files;
|
|
3460
3635
|
}
|
|
3636
|
+
async function agentPreset(id) {
|
|
3637
|
+
const path = join15(opsTemplateDir(), "agents.mjs");
|
|
3638
|
+
const { PRESETS } = await import(`file://${path}`);
|
|
3639
|
+
const preset = PRESETS[id];
|
|
3640
|
+
if (!preset) {
|
|
3641
|
+
throw new Error(
|
|
3642
|
+
`unknown agent "${id}". Known: ${Object.keys(PRESETS).join(", ")}.
|
|
3643
|
+
Anything else works by writing install, run and token_env into org.yaml by hand.`
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
return preset;
|
|
3647
|
+
}
|
|
3461
3648
|
function orgYaml(o) {
|
|
3462
3649
|
return `# The org manifest. Read at the top of every composed prompt.
|
|
3463
3650
|
# Kept deliberately simple: compose.mjs parses a small, strict YAML subset, and a manifest
|
|
@@ -3467,6 +3654,11 @@ org: ${o.org}
|
|
|
3467
3654
|
name: ${o.name}
|
|
3468
3655
|
ops_dir: ${o.opsName}
|
|
3469
3656
|
|
|
3657
|
+
# Who the staff answer to. More than one person? Use a "humans:" list instead \u2014 the mention
|
|
3658
|
+
# gate accepts every login in it, and the first is the one the prompts address:
|
|
3659
|
+
# humans:
|
|
3660
|
+
# - { name: ${o.human}, github: ${o.human}, marker: ${o.marker}, role: founder }
|
|
3661
|
+
# - { name: Sam, github: sam, marker: sam, role: operations }
|
|
3470
3662
|
human:
|
|
3471
3663
|
name: ${o.human}
|
|
3472
3664
|
github: ${o.human}
|
|
@@ -3481,11 +3673,18 @@ experiment_private: true
|
|
|
3481
3673
|
# works by writing the three fields out longhand. See agents.mjs.
|
|
3482
3674
|
agent:
|
|
3483
3675
|
id: ${o.agent}
|
|
3676
|
+
# How much it may do without stopping to ask: full, workspace, or read-only. One word here,
|
|
3677
|
+
# translated into each agent's own vocabulary by agents.mjs \u2014 a tool list for Claude, a
|
|
3678
|
+
# sandbox and an approval policy for Codex, a development mode for nanocoder.
|
|
3679
|
+
permissions: full
|
|
3680
|
+
# Anything else, in that agent's own words, passed through untranslated:
|
|
3681
|
+
# options:
|
|
3682
|
+
# provider: openrouter
|
|
3484
3683
|
|
|
3485
3684
|
defaults:
|
|
3486
|
-
model:
|
|
3487
|
-
timeout_minutes:
|
|
3488
|
-
|
|
3685
|
+
model: ${o.model}
|
|
3686
|
+
timeout_minutes: 90
|
|
3687
|
+
mention_timeout_minutes: 90
|
|
3489
3688
|
|
|
3490
3689
|
# Every staff member, and where their brain lands in the runner checkout.
|
|
3491
3690
|
# Written by \`roster hire\`.
|
|
@@ -3659,16 +3858,81 @@ function parseFlags8(argv) {
|
|
|
3659
3858
|
}
|
|
3660
3859
|
|
|
3661
3860
|
// src/commands/portal.ts
|
|
3662
|
-
import { execFileSync as
|
|
3861
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
3663
3862
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3664
|
-
import { existsSync as
|
|
3863
|
+
import { createReadStream, existsSync as existsSync22, readdirSync as readdirSync9, readFileSync as readFileSync19, statSync as statSync5 } from "fs";
|
|
3665
3864
|
import { createServer as createServer2 } from "http";
|
|
3666
|
-
import { extname as
|
|
3865
|
+
import { extname as extname3, join as join24, resolve as resolve5 } from "path";
|
|
3667
3866
|
|
|
3668
3867
|
// src/lib/act.ts
|
|
3669
3868
|
import { execFile as execFile3 } from "child_process";
|
|
3670
3869
|
import { promisify as promisify3 } from "util";
|
|
3870
|
+
|
|
3871
|
+
// src/lib/ask.ts
|
|
3872
|
+
var MAX_PATCH_LINES = 40;
|
|
3873
|
+
function askTitle(req) {
|
|
3874
|
+
const name = req.pr.repo.split("/")[1] ?? req.pr.repo;
|
|
3875
|
+
const head2 = `${name}#${req.pr.number} \u2014 `;
|
|
3876
|
+
const room = 120 - head2.length;
|
|
3877
|
+
const title = req.pr.title.trim() || "a pull request";
|
|
3878
|
+
return head2 + (title.length > room ? `${title.slice(0, room - 1).trimEnd()}\u2026` : title);
|
|
3879
|
+
}
|
|
3880
|
+
function askBody(req) {
|
|
3881
|
+
const { staff, pr, anchor } = req;
|
|
3882
|
+
const said = req.body.trim();
|
|
3883
|
+
const isPr = pr.kind !== "issue";
|
|
3884
|
+
const it = isPr ? "pull request" : "issue";
|
|
3885
|
+
const out = [];
|
|
3886
|
+
out.push(stripLeadingMention(said, staff.mention) ? `${staff.mention} ${said}` : said);
|
|
3887
|
+
out.push("", "---", "");
|
|
3888
|
+
out.push(`**This is about ${pr.repo}#${pr.number} \u2014 "${pr.title.trim()}".**`);
|
|
3889
|
+
out.push("", pr.url);
|
|
3890
|
+
if (pr.head && pr.base) out.push("", `Branch \`${pr.head}\` \u2192 \`${pr.base}\`.`);
|
|
3891
|
+
if (anchor?.path) {
|
|
3892
|
+
out.push("", `They were looking at \`${anchor.path}\`.`);
|
|
3893
|
+
const hunk = clip(anchor.patch ?? "");
|
|
3894
|
+
if (hunk) out.push("", fence2(hunk, "diff"));
|
|
3895
|
+
else if (anchor.patch) out.push("", "The diff for it is on the pull request.");
|
|
3896
|
+
}
|
|
3897
|
+
out.push(
|
|
3898
|
+
"",
|
|
3899
|
+
`**Answer on the ${it}, not here.** That is where it will be read${isPr ? ", and it is where the diff is" : ""}:`,
|
|
3900
|
+
"",
|
|
3901
|
+
fence2(`gh issue comment ${pr.number} --repo ${pr.repo} --body "\u2026"`, ""),
|
|
3902
|
+
"",
|
|
3903
|
+
...isPr ? [
|
|
3904
|
+
pr.head ? `Push any change to \`${pr.head}\`. Close this issue once the reply is up.` : "Push any change to the pull request's branch. Close this issue once the reply is up."
|
|
3905
|
+
] : ["Close this issue once the reply is up."]
|
|
3906
|
+
);
|
|
3907
|
+
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}
|
|
3908
|
+
`;
|
|
3909
|
+
}
|
|
3910
|
+
function stripLeadingMention(said, mention) {
|
|
3911
|
+
return !new RegExp(`(^|\\s)${escapeRe(mention)}(\\s|$)`).test(said);
|
|
3912
|
+
}
|
|
3913
|
+
function escapeRe(s) {
|
|
3914
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3915
|
+
}
|
|
3916
|
+
function clip(patch) {
|
|
3917
|
+
const lines = patch.replace(/\s+$/, "").split("\n");
|
|
3918
|
+
if (!lines[0]) return "";
|
|
3919
|
+
if (lines.length <= MAX_PATCH_LINES) return lines.join("\n");
|
|
3920
|
+
return [
|
|
3921
|
+
...lines.slice(0, MAX_PATCH_LINES),
|
|
3922
|
+
`\u2026 ${lines.length - MAX_PATCH_LINES} more lines, on the pull request`
|
|
3923
|
+
].join("\n");
|
|
3924
|
+
}
|
|
3925
|
+
function fence2(text, lang) {
|
|
3926
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
3927
|
+
const ticks = "`".repeat(Math.max(3, longest + 1));
|
|
3928
|
+
return `${ticks}${lang}
|
|
3929
|
+
${text}
|
|
3930
|
+
${ticks}`;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
// src/lib/act.ts
|
|
3671
3934
|
var run3 = promisify3(execFile3);
|
|
3935
|
+
var MERGE_FLAG = { squash: "--squash", merge: "--merge", rebase: "--rebase" };
|
|
3672
3936
|
async function act(req) {
|
|
3673
3937
|
const { action, repo } = req;
|
|
3674
3938
|
if (action === "create") {
|
|
@@ -3679,6 +3943,37 @@ async function act(req) {
|
|
|
3679
3943
|
const { stdout } = await run3("gh", args, { encoding: "utf8" });
|
|
3680
3944
|
return { ok: true, action, url: stdout.trim().split("\n").pop() };
|
|
3681
3945
|
}
|
|
3946
|
+
if (action === "ask") {
|
|
3947
|
+
const ask = req.ask;
|
|
3948
|
+
if (!ask) throw new Error("an ask needs to say who it is for and what it is about");
|
|
3949
|
+
if (!ask.staff?.brain) throw new Error(`${ask.staff?.name ?? "they"} has no brain repo`);
|
|
3950
|
+
if (repo !== ask.staff.brain)
|
|
3951
|
+
throw new Error(`an ask goes to ${ask.staff.brain}, not to ${repo}`);
|
|
3952
|
+
if (!ask.body?.trim()) throw new Error("an empty ask is not an ask");
|
|
3953
|
+
const title = (req.title ?? "").trim() || askTitle(ask);
|
|
3954
|
+
const args = ["issue", "create", "--repo", repo, "--title", title, "--body-file", "-"];
|
|
3955
|
+
for (const l of req.labels ?? []) args.push("--label", l);
|
|
3956
|
+
const { stdout } = await execWithStdin(args, askBody(ask));
|
|
3957
|
+
const url = stdout.trim().split("\n").pop();
|
|
3958
|
+
if (!req.alsoOnPr) return { ok: true, action, url };
|
|
3959
|
+
const note = `${ask.body.trim()}
|
|
3960
|
+
|
|
3961
|
+
\u2014 asked ${ask.staff.name} (${ask.staff.mention}): ${url ?? ask.staff.brain}`;
|
|
3962
|
+
try {
|
|
3963
|
+
const posted = await execWithStdin(
|
|
3964
|
+
["issue", "comment", String(ask.pr.number), "--repo", ask.pr.repo, "--body-file", "-"],
|
|
3965
|
+
note
|
|
3966
|
+
);
|
|
3967
|
+
return { ok: true, action, url, prUrl: posted.stdout.trim().split("\n").pop() };
|
|
3968
|
+
} catch (err) {
|
|
3969
|
+
return {
|
|
3970
|
+
ok: true,
|
|
3971
|
+
action,
|
|
3972
|
+
url,
|
|
3973
|
+
warning: `${ask.staff.name} was asked, but the copy on ${ask.pr.repo}#${ask.pr.number} did not go up: ${String(err?.message ?? err).split("\n")[0]}`
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3682
3977
|
const number = req.number;
|
|
3683
3978
|
if (!Number.isInteger(number) || number <= 0)
|
|
3684
3979
|
throw new Error("a valid issue number is required");
|
|
@@ -3703,8 +3998,37 @@ async function act(req) {
|
|
|
3703
3998
|
await run3("gh", ["issue", "reopen", n, "--repo", repo], { encoding: "utf8" });
|
|
3704
3999
|
return { ok: true, action };
|
|
3705
4000
|
}
|
|
4001
|
+
if (action === "merge") {
|
|
4002
|
+
const how = req.mergeMethod ?? await mergeMethodFor(repo);
|
|
4003
|
+
const flag = MERGE_FLAG[how];
|
|
4004
|
+
if (!flag) throw new Error(`unknown merge method "${req.mergeMethod}"`);
|
|
4005
|
+
const args = ["pr", "merge", n, "--repo", repo, flag];
|
|
4006
|
+
if (req.body?.trim() && how !== "rebase") args.push("--body", req.body.trim());
|
|
4007
|
+
await run3("gh", args, { encoding: "utf8" });
|
|
4008
|
+
return { ok: true, action, mergedBy: how };
|
|
4009
|
+
}
|
|
3706
4010
|
throw new Error(`unknown action "${action}"`);
|
|
3707
4011
|
}
|
|
4012
|
+
async function mergeMethodFor(repo) {
|
|
4013
|
+
try {
|
|
4014
|
+
const { stdout } = await run3(
|
|
4015
|
+
"gh",
|
|
4016
|
+
[
|
|
4017
|
+
"api",
|
|
4018
|
+
`repos/${repo}`,
|
|
4019
|
+
"--jq",
|
|
4020
|
+
"[.allow_squash_merge,.allow_merge_commit,.allow_rebase_merge]"
|
|
4021
|
+
],
|
|
4022
|
+
{ encoding: "utf8" }
|
|
4023
|
+
);
|
|
4024
|
+
const [squash, commit, rebase] = JSON.parse(stdout);
|
|
4025
|
+
if (squash) return "squash";
|
|
4026
|
+
if (commit) return "merge";
|
|
4027
|
+
if (rebase) return "rebase";
|
|
4028
|
+
} catch {
|
|
4029
|
+
}
|
|
4030
|
+
return "squash";
|
|
4031
|
+
}
|
|
3708
4032
|
function execWithStdin(args, input) {
|
|
3709
4033
|
return new Promise((resolve6, reject) => {
|
|
3710
4034
|
const child = execFile3(
|
|
@@ -3717,26 +4041,144 @@ function execWithStdin(args, input) {
|
|
|
3717
4041
|
});
|
|
3718
4042
|
}
|
|
3719
4043
|
|
|
4044
|
+
// src/lib/attach.ts
|
|
4045
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
4046
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
4047
|
+
import { basename, extname, join as join17 } from "path";
|
|
4048
|
+
var MAX_UPLOAD = 25 * 1024 * 1024;
|
|
4049
|
+
var ATTACH_DIR = "attachments";
|
|
4050
|
+
function attach(opts) {
|
|
4051
|
+
const { repoDir, repo, data, today } = opts;
|
|
4052
|
+
if (!existsSync15(join17(repoDir, ".git"))) {
|
|
4053
|
+
throw new Error(`${repo} is not checked out here, so there is nowhere to put the file`);
|
|
4054
|
+
}
|
|
4055
|
+
if (!data.length) throw new Error("that file is empty");
|
|
4056
|
+
if (data.length > MAX_UPLOAD) {
|
|
4057
|
+
throw new Error(`${basename(opts.name)} is larger than ${MAX_UPLOAD / 1024 / 1024}MB`);
|
|
4058
|
+
}
|
|
4059
|
+
const rel = free(repoDir, `${ATTACH_DIR}/${today}-${safeName(opts.name)}`);
|
|
4060
|
+
const full = join17(repoDir, rel);
|
|
4061
|
+
mkdirSync4(join17(repoDir, ATTACH_DIR), { recursive: true });
|
|
4062
|
+
writeFileSync7(full, data);
|
|
4063
|
+
const git4 = (args) => execFileSync7("git", ["-C", repoDir, ...args], {
|
|
4064
|
+
encoding: "utf8",
|
|
4065
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4066
|
+
});
|
|
4067
|
+
try {
|
|
4068
|
+
git4(["add", "--", rel]);
|
|
4069
|
+
git4(["commit", "-m", `portal: attach ${rel}`, "--", rel]);
|
|
4070
|
+
} catch (err) {
|
|
4071
|
+
throw new Error(`commit failed: ${short2(err)}`);
|
|
4072
|
+
}
|
|
4073
|
+
const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
4074
|
+
const url = `https://github.com/${repo}/blob/${branch}/${rel.split("/").map(encodeURIComponent).join("/")}`;
|
|
4075
|
+
try {
|
|
4076
|
+
git4(["push"]);
|
|
4077
|
+
} catch (err) {
|
|
4078
|
+
return { path: rel, url, bytes: data.length, pushed: false, note: short2(err) };
|
|
4079
|
+
}
|
|
4080
|
+
return { path: rel, url, bytes: data.length, pushed: true };
|
|
4081
|
+
}
|
|
4082
|
+
function safeName(raw) {
|
|
4083
|
+
const name = basename(String(raw ?? "")).replace(/^\.+/, "");
|
|
4084
|
+
const ext = extname(name).toLowerCase().slice(0, 12);
|
|
4085
|
+
const stem = name.slice(0, name.length - extname(name).length).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "file";
|
|
4086
|
+
return stem + ext.replace(/[^a-z0-9.]/g, "");
|
|
4087
|
+
}
|
|
4088
|
+
function free(repoDir, rel) {
|
|
4089
|
+
if (!existsSync15(join17(repoDir, rel))) return rel;
|
|
4090
|
+
const ext = extname(rel);
|
|
4091
|
+
const stem = rel.slice(0, rel.length - ext.length);
|
|
4092
|
+
for (let n = 2; n < 500; n++) {
|
|
4093
|
+
const next = `${stem}-${n}${ext}`;
|
|
4094
|
+
if (!existsSync15(join17(repoDir, next))) return next;
|
|
4095
|
+
}
|
|
4096
|
+
throw new Error("too many files by that name today");
|
|
4097
|
+
}
|
|
4098
|
+
function short2(e) {
|
|
4099
|
+
const msg = e instanceof Error ? e.stderr?.toString() || e.message : String(e);
|
|
4100
|
+
const line = msg.split("\n").find((l) => l.trim()) ?? msg;
|
|
4101
|
+
return line.length > 200 ? `${line.slice(0, 199)}\u2026` : line;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
3720
4104
|
// src/lib/docs.ts
|
|
3721
|
-
import { existsSync as
|
|
3722
|
-
import { dirname as dirname7, join as
|
|
4105
|
+
import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync14 } from "fs";
|
|
4106
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
3723
4107
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3724
4108
|
function docsDir() {
|
|
3725
4109
|
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
3726
|
-
const candidates = [
|
|
3727
|
-
for (const path of candidates) if (
|
|
4110
|
+
const candidates = [join18(here, "..", "..", "docs"), join18(here, "..", "docs")];
|
|
4111
|
+
for (const path of candidates) if (existsSync16(path)) return path;
|
|
3728
4112
|
throw new Error(`docs not found. Looked in:
|
|
3729
4113
|
${candidates.join("\n ")}`);
|
|
3730
4114
|
}
|
|
3731
4115
|
function docPages() {
|
|
3732
4116
|
const dir = docsDir();
|
|
3733
|
-
const all =
|
|
3734
|
-
const index = readFileSync14(
|
|
4117
|
+
const all = readdirSync7(dir).filter((f) => f.endsWith(".md"));
|
|
4118
|
+
const index = readFileSync14(join18(dir, "README.md"), "utf8");
|
|
3735
4119
|
const ordered = [...index.matchAll(/\[([^\]]+)\]\(([\w.-]+\.md)\)/g)].map((m) => ({ title: m[1], file: m[2] })).filter((d, i, xs) => all.includes(d.file) && xs.findIndex((x) => x.file === d.file) === i);
|
|
3736
4120
|
const listed = new Set(ordered.map((d) => d.file));
|
|
3737
|
-
const rest = all.filter((f) => f !== "README.md" && !listed.has(f)).map((file) => ({ file, title: titleOf(
|
|
4121
|
+
const rest = all.filter((f) => f !== "README.md" && !listed.has(f)).map((file) => ({ file, title: titleOf(join18(dir, file)) }));
|
|
3738
4122
|
return [{ file: "README.md", title: "Overview" }, ...ordered, ...rest];
|
|
3739
4123
|
}
|
|
4124
|
+
var ASSET_TYPES = {
|
|
4125
|
+
".png": "image/png",
|
|
4126
|
+
".jpg": "image/jpeg",
|
|
4127
|
+
".jpeg": "image/jpeg",
|
|
4128
|
+
".gif": "image/gif",
|
|
4129
|
+
".svg": "image/svg+xml",
|
|
4130
|
+
".webp": "image/webp"
|
|
4131
|
+
};
|
|
4132
|
+
function docAsset(name) {
|
|
4133
|
+
const dir = join18(docsDir(), "images");
|
|
4134
|
+
if (!existsSync16(dir)) return null;
|
|
4135
|
+
const wanted = String(name ?? "").replace(/^images\//, "");
|
|
4136
|
+
const found = readdirSync7(dir).find((f) => f === wanted);
|
|
4137
|
+
if (!found) return null;
|
|
4138
|
+
const type = ASSET_TYPES[found.slice(found.lastIndexOf(".")).toLowerCase()];
|
|
4139
|
+
return type ? { path: join18(dir, found), type } : null;
|
|
4140
|
+
}
|
|
4141
|
+
function searchDocs(query2, snippets = 3) {
|
|
4142
|
+
const terms = String(query2 ?? "").toLowerCase().split(/\s+/).map((t) => t.trim()).filter(Boolean);
|
|
4143
|
+
if (!terms.length) return [];
|
|
4144
|
+
const dir = docsDir();
|
|
4145
|
+
const out = [];
|
|
4146
|
+
for (const page2 of docPages()) {
|
|
4147
|
+
const text = readFileSync14(join18(dir, page2.file), "utf8").replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
4148
|
+
const haystack = (page2.title + "\n" + text).toLowerCase();
|
|
4149
|
+
if (!terms.every((t) => haystack.includes(t))) continue;
|
|
4150
|
+
const title = page2.title.toLowerCase();
|
|
4151
|
+
const matches = [];
|
|
4152
|
+
let body = 0;
|
|
4153
|
+
text.split("\n").forEach((raw, i) => {
|
|
4154
|
+
const line = raw.trim();
|
|
4155
|
+
if (!line) return;
|
|
4156
|
+
const hits = terms.filter((t) => line.toLowerCase().includes(t)).length;
|
|
4157
|
+
if (!hits) return;
|
|
4158
|
+
body += hits === terms.length ? 2 : 1;
|
|
4159
|
+
if (line.startsWith("#")) body += 8;
|
|
4160
|
+
if (matches.length < snippets) {
|
|
4161
|
+
matches.push({ line: i + 1, text: snippet(line, terms) });
|
|
4162
|
+
}
|
|
4163
|
+
});
|
|
4164
|
+
out.push({
|
|
4165
|
+
...page2,
|
|
4166
|
+
score: terms.filter((t) => title.includes(t)).length * 100 + Math.min(body, 60),
|
|
4167
|
+
matches
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return out.sort((a, b) => b.score - a.score);
|
|
4171
|
+
}
|
|
4172
|
+
function snippet(line, terms, width = 150) {
|
|
4173
|
+
const clean = line.replace(/^#+\s*/, "").replace(/^[-*|]\s*/, "");
|
|
4174
|
+
if (clean.length <= width) return clean;
|
|
4175
|
+
const at = Math.min(
|
|
4176
|
+
...terms.map((t) => clean.toLowerCase().indexOf(t)).filter((n) => n >= 0),
|
|
4177
|
+
clean.length
|
|
4178
|
+
);
|
|
4179
|
+
const from = Math.max(0, at - 40);
|
|
4180
|
+
return (from ? "\u2026" : "") + clean.slice(from, from + width).trim() + "\u2026";
|
|
4181
|
+
}
|
|
3740
4182
|
function titleOf(path) {
|
|
3741
4183
|
const first = readFileSync14(path, "utf8").split("\n").find((l) => l.startsWith("# "));
|
|
3742
4184
|
return first ? first.slice(2).trim() : path.split("/").pop();
|
|
@@ -3746,9 +4188,15 @@ function titleOf(path) {
|
|
|
3746
4188
|
import { execFile as execFile4 } from "child_process";
|
|
3747
4189
|
import { promisify as promisify4 } from "util";
|
|
3748
4190
|
var run4 = promisify4(execFile4);
|
|
4191
|
+
var REACTIONS = `
|
|
4192
|
+
reactionGroups {
|
|
4193
|
+
content
|
|
4194
|
+
reactors(first:6) { totalCount nodes { ... on User { login } ... on Bot { login } } }
|
|
4195
|
+
}
|
|
4196
|
+
`;
|
|
3749
4197
|
var TIMELINE_COMMON = `
|
|
3750
4198
|
__typename
|
|
3751
|
-
... on IssueComment { author { login } createdAt body url }
|
|
4199
|
+
... on IssueComment { author { login } createdAt body url ${REACTIONS} }
|
|
3752
4200
|
... on CrossReferencedEvent {
|
|
3753
4201
|
actor { login } createdAt
|
|
3754
4202
|
source {
|
|
@@ -3804,6 +4252,7 @@ query($owner:String!, $name:String!) {
|
|
|
3804
4252
|
nodes {
|
|
3805
4253
|
number title body url state createdAt updatedAt
|
|
3806
4254
|
author { login }
|
|
4255
|
+
${REACTIONS}
|
|
3807
4256
|
labels(first:12) { nodes { name } }
|
|
3808
4257
|
assignees(first:8) { nodes { login } }
|
|
3809
4258
|
timelineItems(last:80, itemTypes:${ISSUE_TYPES}) { nodes { ${TIMELINE_COMMON} } }
|
|
@@ -3811,8 +4260,9 @@ query($owner:String!, $name:String!) {
|
|
|
3811
4260
|
}
|
|
3812
4261
|
pullRequests(states:OPEN, first:60, orderBy:{field:UPDATED_AT, direction:DESC}) {
|
|
3813
4262
|
nodes {
|
|
3814
|
-
number title body url state createdAt updatedAt isDraft
|
|
4263
|
+
number title body url state createdAt updatedAt isDraft mergeable
|
|
3815
4264
|
author { login }
|
|
4265
|
+
${REACTIONS}
|
|
3816
4266
|
labels(first:12) { nodes { name } }
|
|
3817
4267
|
assignees(first:8) { nodes { login } }
|
|
3818
4268
|
commits(last:1) { nodes { commit { statusCheckRollup { state } } } }
|
|
@@ -3827,6 +4277,7 @@ query($owner:String!, $name:String!) {
|
|
|
3827
4277
|
nodes {
|
|
3828
4278
|
number title body url state createdAt updatedAt
|
|
3829
4279
|
author { login }
|
|
4280
|
+
${REACTIONS}
|
|
3830
4281
|
labels(first:12) { nodes { name } }
|
|
3831
4282
|
assignees(first:8) { nodes { login } }
|
|
3832
4283
|
timelineItems(last:${CLOSED_TIMELINE}, itemTypes:${ISSUE_TYPES}) {
|
|
@@ -3840,6 +4291,7 @@ query($owner:String!, $name:String!) {
|
|
|
3840
4291
|
nodes {
|
|
3841
4292
|
number title body url state createdAt updatedAt isDraft
|
|
3842
4293
|
author { login }
|
|
4294
|
+
${REACTIONS}
|
|
3843
4295
|
labels(first:12) { nodes { name } }
|
|
3844
4296
|
assignees(first:8) { nodes { login } }
|
|
3845
4297
|
timelineItems(last:${CLOSED_TIMELINE}, itemTypes:${PR_TYPES}) {
|
|
@@ -3863,6 +4315,7 @@ async function fetchInbox(repos) {
|
|
|
3863
4315
|
const item = shape(n, full, r.role, "pr");
|
|
3864
4316
|
item.draft = n.isDraft;
|
|
3865
4317
|
item.checks = rollup(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state);
|
|
4318
|
+
item.mergeable = n.mergeable;
|
|
3866
4319
|
items.push(item);
|
|
3867
4320
|
}
|
|
3868
4321
|
const cutoff = Date.now() - CLOSED_DAYS * 864e5;
|
|
@@ -3876,7 +4329,7 @@ async function fetchInbox(repos) {
|
|
|
3876
4329
|
items.push(item);
|
|
3877
4330
|
}
|
|
3878
4331
|
} catch (e) {
|
|
3879
|
-
errors.push(`${full}: ${
|
|
4332
|
+
errors.push(`${full}: ${short3(e)}`);
|
|
3880
4333
|
}
|
|
3881
4334
|
})
|
|
3882
4335
|
);
|
|
@@ -3908,15 +4361,30 @@ function shape(n, repo, role, kind) {
|
|
|
3908
4361
|
url: n.url,
|
|
3909
4362
|
state: n.state ?? "OPEN",
|
|
3910
4363
|
comments: events.filter((e) => e.type === "comment").map((e) => ({ author: e.actor, createdAt: e.createdAt, body: e.body ?? "" })),
|
|
3911
|
-
events
|
|
4364
|
+
events,
|
|
4365
|
+
reactions: reactions(n)
|
|
3912
4366
|
};
|
|
3913
4367
|
}
|
|
4368
|
+
function reactions(n) {
|
|
4369
|
+
return (n.reactionGroups ?? []).filter((g) => (g.reactors?.totalCount ?? 0) > 0).map((g) => ({
|
|
4370
|
+
content: g.content,
|
|
4371
|
+
count: g.reactors.totalCount,
|
|
4372
|
+
by: (g.reactors.nodes ?? []).map((u) => u?.login).filter(Boolean)
|
|
4373
|
+
}));
|
|
4374
|
+
}
|
|
3914
4375
|
function event(e) {
|
|
3915
4376
|
const who = e.actor?.login ?? e.author?.login ?? "";
|
|
3916
4377
|
const at = e.createdAt;
|
|
3917
4378
|
switch (e.__typename) {
|
|
3918
4379
|
case "IssueComment":
|
|
3919
|
-
return {
|
|
4380
|
+
return {
|
|
4381
|
+
type: "comment",
|
|
4382
|
+
actor: who,
|
|
4383
|
+
createdAt: at,
|
|
4384
|
+
body: e.body ?? "",
|
|
4385
|
+
url: e.url,
|
|
4386
|
+
reactions: reactions(e)
|
|
4387
|
+
};
|
|
3920
4388
|
case "CrossReferencedEvent": {
|
|
3921
4389
|
const s = e.source;
|
|
3922
4390
|
if (!s?.number) return null;
|
|
@@ -4021,7 +4489,7 @@ function rollup(state) {
|
|
|
4021
4489
|
if (/FAILURE|ERROR/i.test(state)) return "failing";
|
|
4022
4490
|
return "pending";
|
|
4023
4491
|
}
|
|
4024
|
-
function
|
|
4492
|
+
function short3(e) {
|
|
4025
4493
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4026
4494
|
const line = msg.split("\n").find((l) => l.trim() && !/^\s*$/.test(l)) ?? msg;
|
|
4027
4495
|
return line.length > 200 ? line.slice(0, 199) + "\u2026" : line;
|
|
@@ -4116,8 +4584,8 @@ function ensureNewline(text) {
|
|
|
4116
4584
|
}
|
|
4117
4585
|
|
|
4118
4586
|
// src/lib/pastebrief.ts
|
|
4119
|
-
import { existsSync as
|
|
4120
|
-
import { join as
|
|
4587
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
4588
|
+
import { join as join19 } from "path";
|
|
4121
4589
|
var WRITES = {
|
|
4122
4590
|
discover: (c) => [`${c.opsName}/org/business.md`],
|
|
4123
4591
|
voice: (c) => [`${c.opsName}/org/voice.md`],
|
|
@@ -4208,13 +4676,13 @@ ${END}`).join("\n\n");
|
|
|
4208
4676
|
function fileBlock(ws, rel, note) {
|
|
4209
4677
|
const body = read2(ws, rel);
|
|
4210
4678
|
if (!body.trim()) return [`### \`${rel}\``, "", `*Empty or absent (${note}).*`, ""];
|
|
4211
|
-
return [`### \`${rel}\``, "", `*${note}*`, "",
|
|
4679
|
+
return [`### \`${rel}\``, "", `*${note}*`, "", fence3(body), ""];
|
|
4212
4680
|
}
|
|
4213
4681
|
function read2(ws, rel) {
|
|
4214
|
-
const full =
|
|
4215
|
-
return
|
|
4682
|
+
const full = join19(ws.root, rel);
|
|
4683
|
+
return existsSync17(full) ? readFileSync15(full, "utf8") : "";
|
|
4216
4684
|
}
|
|
4217
|
-
function
|
|
4685
|
+
function fence3(text) {
|
|
4218
4686
|
const longest = (text.match(/`{3,}/g) ?? []).reduce((n, m) => Math.max(n, m.length), 2);
|
|
4219
4687
|
const bars = "`".repeat(longest + 1);
|
|
4220
4688
|
return `${bars}
|
|
@@ -4222,15 +4690,15 @@ ${text.trimEnd()}
|
|
|
4222
4690
|
${bars}`;
|
|
4223
4691
|
}
|
|
4224
4692
|
function briefTemplate(kind) {
|
|
4225
|
-
const path =
|
|
4226
|
-
if (!
|
|
4693
|
+
const path = join19(briefTemplateDir(), `${kind}.md`);
|
|
4694
|
+
if (!existsSync17(path)) throw new Error(`no brief template for "${kind}"`);
|
|
4227
4695
|
return readFileSync15(path, "utf8");
|
|
4228
4696
|
}
|
|
4229
4697
|
|
|
4230
4698
|
// src/lib/setup.ts
|
|
4231
4699
|
import { execFile as execFile5 } from "child_process";
|
|
4232
|
-
import { existsSync as
|
|
4233
|
-
import { join as
|
|
4700
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync16 } from "fs";
|
|
4701
|
+
import { join as join20 } from "path";
|
|
4234
4702
|
var AGENTS = [
|
|
4235
4703
|
{
|
|
4236
4704
|
id: "claude-code-action",
|
|
@@ -4294,14 +4762,14 @@ async function setupStatus(startedIn) {
|
|
|
4294
4762
|
};
|
|
4295
4763
|
}
|
|
4296
4764
|
function readOrgName(opsDir) {
|
|
4297
|
-
const path =
|
|
4298
|
-
if (!
|
|
4765
|
+
const path = join20(opsDir, "org.yaml");
|
|
4766
|
+
if (!existsSync18(path)) return void 0;
|
|
4299
4767
|
const m = /^org:\s*(\S+)\s*$/m.exec(readFileSync16(path, "utf8"));
|
|
4300
4768
|
return m?.[1];
|
|
4301
4769
|
}
|
|
4302
4770
|
function readRepoNames(opsDir) {
|
|
4303
|
-
const path =
|
|
4304
|
-
if (!
|
|
4771
|
+
const path = join20(opsDir, "org.yaml");
|
|
4772
|
+
if (!existsSync18(path)) return [];
|
|
4305
4773
|
const names = [];
|
|
4306
4774
|
let inside = false;
|
|
4307
4775
|
for (const line of readFileSync16(path, "utf8").split("\n")) {
|
|
@@ -4317,29 +4785,29 @@ function readRepoNames(opsDir) {
|
|
|
4317
4785
|
return names;
|
|
4318
4786
|
}
|
|
4319
4787
|
async function loadFrameworkComposer() {
|
|
4320
|
-
const path =
|
|
4788
|
+
const path = join20(templatesRoot(), "ops", "compose.mjs");
|
|
4321
4789
|
return await import(`file://${path}`);
|
|
4322
4790
|
}
|
|
4323
4791
|
async function joinTenant(root, org) {
|
|
4324
|
-
|
|
4792
|
+
mkdirSync5(root, { recursive: true });
|
|
4325
4793
|
const cloned = [];
|
|
4326
|
-
const opsDir =
|
|
4327
|
-
if (!
|
|
4794
|
+
const opsDir = join20(root, "roster-ops");
|
|
4795
|
+
if (!existsSync18(opsDir)) {
|
|
4328
4796
|
await clone(`https://github.com/${org}/roster-ops.git`, opsDir);
|
|
4329
4797
|
cloned.push("roster-ops");
|
|
4330
4798
|
}
|
|
4331
|
-
if (!
|
|
4799
|
+
if (!existsSync18(join20(opsDir, "org.yaml"))) {
|
|
4332
4800
|
throw new Error(`${org}/roster-ops has no org.yaml, so it is not a roster tenant`);
|
|
4333
4801
|
}
|
|
4334
|
-
const { parseYaml } = await import(`file://${
|
|
4802
|
+
const { parseYaml } = await import(`file://${join20(opsDir, "compose.mjs")}`).then(
|
|
4335
4803
|
(m) => m
|
|
4336
4804
|
);
|
|
4337
|
-
const orgFile = parseYaml(readFileSync16(
|
|
4805
|
+
const orgFile = parseYaml(readFileSync16(join20(opsDir, "org.yaml"), "utf8"), "org.yaml");
|
|
4338
4806
|
for (const person of orgFile.staff ?? []) {
|
|
4339
4807
|
const dir = person.dir ?? person.handle;
|
|
4340
|
-
if (
|
|
4808
|
+
if (existsSync18(join20(root, dir))) continue;
|
|
4341
4809
|
try {
|
|
4342
|
-
await clone(`https://github.com/${org}/${dir}.git`,
|
|
4810
|
+
await clone(`https://github.com/${org}/${dir}.git`, join20(root, dir));
|
|
4343
4811
|
cloned.push(dir);
|
|
4344
4812
|
} catch {
|
|
4345
4813
|
}
|
|
@@ -4354,16 +4822,16 @@ function clone(url, into) {
|
|
|
4354
4822
|
|
|
4355
4823
|
// src/lib/sync.ts
|
|
4356
4824
|
import { execFile as execFile6 } from "child_process";
|
|
4357
|
-
import { existsSync as
|
|
4358
|
-
import { join as
|
|
4825
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4826
|
+
import { join as join21 } from "path";
|
|
4359
4827
|
import { promisify as promisify5 } from "util";
|
|
4360
4828
|
var run5 = promisify5(execFile6);
|
|
4361
4829
|
async function syncRepos(root, dirs) {
|
|
4362
|
-
return Promise.all(dirs.map((dir) => syncOne(
|
|
4830
|
+
return Promise.all(dirs.map((dir) => syncOne(join21(root, dir), dir)));
|
|
4363
4831
|
}
|
|
4364
4832
|
async function syncOne(path, dir) {
|
|
4365
4833
|
const base2 = { dir, behind: 0, ahead: 0, dirty: false, pulled: false };
|
|
4366
|
-
if (!
|
|
4834
|
+
if (!existsSync19(join21(path, ".git"))) return { ...base2, skipped: "no-remote" };
|
|
4367
4835
|
const git4 = async (...args) => (await run5("git", ["-C", path, ...args], { encoding: "utf8" })).stdout.trim();
|
|
4368
4836
|
try {
|
|
4369
4837
|
const remotes = await git4("remote");
|
|
@@ -4384,35 +4852,37 @@ async function syncOne(path, dir) {
|
|
|
4384
4852
|
if (dirty) return { ...base2, ahead, behind, dirty, skipped: "dirty" };
|
|
4385
4853
|
if (ahead > 0) return { ...base2, ahead, behind, dirty, skipped: "diverged" };
|
|
4386
4854
|
await git4("pull", "--ff-only", "--quiet");
|
|
4387
|
-
return { ...base2, ahead, behind, dirty, pulled: true };
|
|
4855
|
+
return { ...base2, ahead, behind: 0, dirty, pulled: true };
|
|
4388
4856
|
} catch (err) {
|
|
4389
|
-
return { ...base2, error:
|
|
4857
|
+
return { ...base2, error: reason(err) };
|
|
4390
4858
|
}
|
|
4391
4859
|
}
|
|
4392
|
-
function
|
|
4393
|
-
const
|
|
4394
|
-
const
|
|
4860
|
+
function reason(e) {
|
|
4861
|
+
const err = e;
|
|
4862
|
+
const text = String(err?.stderr ?? "").trim() || String(err?.message ?? e);
|
|
4863
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4864
|
+
const line = lines.find((l) => !/^Command failed:/i.test(l)) ?? lines[0] ?? String(e);
|
|
4395
4865
|
return line.length > 180 ? line.slice(0, 179) + "\u2026" : line;
|
|
4396
4866
|
}
|
|
4397
4867
|
|
|
4398
4868
|
// src/portal/assets.ts
|
|
4399
|
-
import { existsSync as
|
|
4400
|
-
import { dirname as dirname8, extname, join as
|
|
4869
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17, statSync as statSync4 } from "fs";
|
|
4870
|
+
import { dirname as dirname8, extname as extname2, join as join22, resolve as resolve4 } from "path";
|
|
4401
4871
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4402
4872
|
function portalDir() {
|
|
4403
4873
|
const here = dirname8(fileURLToPath3(import.meta.url));
|
|
4404
4874
|
const candidates = [
|
|
4405
|
-
|
|
4875
|
+
join22(here, "..", "..", "templates", "portal"),
|
|
4406
4876
|
// src/portal → package root
|
|
4407
|
-
|
|
4877
|
+
join22(here, "..", "templates", "portal")
|
|
4408
4878
|
// dist → package root
|
|
4409
4879
|
];
|
|
4410
|
-
for (const path of candidates) if (
|
|
4880
|
+
for (const path of candidates) if (existsSync20(join22(path, "index.html"))) return path;
|
|
4411
4881
|
throw new Error(`portal UI not found. Looked in:
|
|
4412
4882
|
${candidates.join("\n ")}`);
|
|
4413
4883
|
}
|
|
4414
4884
|
function portalIndex() {
|
|
4415
|
-
return readFileSync17(
|
|
4885
|
+
return readFileSync17(join22(portalDir(), "index.html"), "utf8");
|
|
4416
4886
|
}
|
|
4417
4887
|
var ASSET_MIME = {
|
|
4418
4888
|
".css": "text/css; charset=utf-8",
|
|
@@ -4426,15 +4896,15 @@ function portalAsset(rel) {
|
|
|
4426
4896
|
const root = portalDir();
|
|
4427
4897
|
const full = resolve4(root, rel);
|
|
4428
4898
|
if (!full.startsWith(resolve4(root) + "/")) return null;
|
|
4429
|
-
const type = ASSET_MIME[
|
|
4899
|
+
const type = ASSET_MIME[extname2(full).toLowerCase()];
|
|
4430
4900
|
if (!type) return null;
|
|
4431
|
-
if (!
|
|
4901
|
+
if (!existsSync20(full) || statSync4(full).isDirectory()) return null;
|
|
4432
4902
|
return { body: readFileSync17(full), type };
|
|
4433
4903
|
}
|
|
4434
4904
|
|
|
4435
4905
|
// src/commands/retire.ts
|
|
4436
|
-
import { existsSync as
|
|
4437
|
-
import { join as
|
|
4906
|
+
import { existsSync as existsSync21, readdirSync as readdirSync8, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
4907
|
+
import { join as join23 } from "path";
|
|
4438
4908
|
var retireHelp = `
|
|
4439
4909
|
roster retire <handle>
|
|
4440
4910
|
|
|
@@ -4485,23 +4955,23 @@ async function retireCommand(argv) {
|
|
|
4485
4955
|
function buildRetirePlan(ws, org, handle, parseYaml) {
|
|
4486
4956
|
const entry = (org.staff ?? []).find((s) => s.handle === handle);
|
|
4487
4957
|
const dir = entry.dir ?? entry.handle;
|
|
4488
|
-
const root =
|
|
4958
|
+
const root = join23(ws.root, dir);
|
|
4489
4959
|
const warnings = [];
|
|
4490
4960
|
const manifest = readManifest3(root, parseYaml);
|
|
4491
4961
|
const brain = manifest?.brain;
|
|
4492
|
-
if (!
|
|
4493
|
-
const wfDir =
|
|
4494
|
-
const workflows =
|
|
4962
|
+
if (!existsSync21(root)) warnings.push(`${dir}/ is not checked out, so its files cannot be read`);
|
|
4963
|
+
const wfDir = join23(root, ".github", "workflows");
|
|
4964
|
+
const workflows = existsSync21(wfDir) ? readdirSync8(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [];
|
|
4495
4965
|
if (!workflows.length) warnings.push("no workflows found, so nothing to disable");
|
|
4496
4966
|
const peers = [];
|
|
4497
4967
|
for (const other of org.staff ?? []) {
|
|
4498
4968
|
if (other.handle === handle) continue;
|
|
4499
4969
|
const otherDir = other.dir ?? other.handle;
|
|
4500
|
-
const path =
|
|
4501
|
-
if (!
|
|
4970
|
+
const path = join23(ws.root, otherDir, "staff.yaml");
|
|
4971
|
+
if (!existsSync21(path)) continue;
|
|
4502
4972
|
const text = readFileSync18(path, "utf8");
|
|
4503
4973
|
if (!new RegExp(`handle: ${handle}[,\\s}]`).test(text)) continue;
|
|
4504
|
-
const theirs = readManifest3(
|
|
4974
|
+
const theirs = readManifest3(join23(ws.root, otherDir), parseYaml);
|
|
4505
4975
|
peers.push({
|
|
4506
4976
|
handle: other.handle,
|
|
4507
4977
|
dir: otherDir,
|
|
@@ -4509,7 +4979,7 @@ function buildRetirePlan(ws, org, handle, parseYaml) {
|
|
|
4509
4979
|
label: `from-${handle}`
|
|
4510
4980
|
});
|
|
4511
4981
|
}
|
|
4512
|
-
const facts = countFacts(
|
|
4982
|
+
const facts = countFacts(join23(root, "memory", "INDEX.md"));
|
|
4513
4983
|
return {
|
|
4514
4984
|
handle,
|
|
4515
4985
|
name: entry.name ?? handle,
|
|
@@ -4567,9 +5037,9 @@ async function applyRetirePlan(ws, plan2) {
|
|
|
4567
5037
|
removeFromOrgYaml(ws, plan2);
|
|
4568
5038
|
process.stdout.write(" removed from org.yaml\n");
|
|
4569
5039
|
for (const p of plan2.peers) {
|
|
4570
|
-
const path =
|
|
5040
|
+
const path = join23(ws.root, p.dir, "staff.yaml");
|
|
4571
5041
|
const text = readFileSync18(path, "utf8");
|
|
4572
|
-
|
|
5042
|
+
writeFileSync8(path, removePeerLine(text, plan2.handle));
|
|
4573
5043
|
process.stdout.write(` removed from ${p.dir}/staff.yaml
|
|
4574
5044
|
`);
|
|
4575
5045
|
if (!p.brain) continue;
|
|
@@ -4595,9 +5065,9 @@ async function applyRetirePlan(ws, plan2) {
|
|
|
4595
5065
|
return failed ? 1 : 0;
|
|
4596
5066
|
}
|
|
4597
5067
|
function removeFromOrgYaml(ws, plan2) {
|
|
4598
|
-
const path =
|
|
5068
|
+
const path = join23(ws.opsDir, "org.yaml");
|
|
4599
5069
|
const text = readFileSync18(path, "utf8");
|
|
4600
|
-
|
|
5070
|
+
writeFileSync8(path, removeOrgLines(text, plan2.handle, plan2.dir));
|
|
4601
5071
|
}
|
|
4602
5072
|
function removeOrgLines(text, handle, dir) {
|
|
4603
5073
|
return text.split("\n").filter((line) => {
|
|
@@ -4615,8 +5085,8 @@ function removePeerLine(text, handle) {
|
|
|
4615
5085
|
}).join("\n");
|
|
4616
5086
|
}
|
|
4617
5087
|
function readManifest3(root, parseYaml) {
|
|
4618
|
-
const path =
|
|
4619
|
-
if (!
|
|
5088
|
+
const path = join23(root, "staff.yaml");
|
|
5089
|
+
if (!existsSync21(path)) return null;
|
|
4620
5090
|
try {
|
|
4621
5091
|
return parseYaml(readFileSync18(path, "utf8"), "staff.yaml");
|
|
4622
5092
|
} catch {
|
|
@@ -4624,7 +5094,7 @@ function readManifest3(root, parseYaml) {
|
|
|
4624
5094
|
}
|
|
4625
5095
|
}
|
|
4626
5096
|
function countFacts(indexPath) {
|
|
4627
|
-
if (!
|
|
5097
|
+
if (!existsSync21(indexPath)) return 0;
|
|
4628
5098
|
return (readFileSync18(indexPath, "utf8").match(/^- \*\*`/gm) ?? []).length;
|
|
4629
5099
|
}
|
|
4630
5100
|
function parseFlags9(argv) {
|
|
@@ -4682,8 +5152,20 @@ var MIME = {
|
|
|
4682
5152
|
".svg": "image/svg+xml",
|
|
4683
5153
|
".webp": "image/webp",
|
|
4684
5154
|
".ico": "image/x-icon",
|
|
4685
|
-
".pdf": "application/pdf"
|
|
5155
|
+
".pdf": "application/pdf",
|
|
5156
|
+
// A brain holds recordings as often as it holds screenshots, and a video served as
|
|
5157
|
+
// application/octet-stream is a download prompt rather than something you can watch.
|
|
5158
|
+
".mp4": "video/mp4",
|
|
5159
|
+
".m4v": "video/mp4",
|
|
5160
|
+
".webm": "video/webm",
|
|
5161
|
+
".mov": "video/quicktime",
|
|
5162
|
+
".ogv": "video/ogg",
|
|
5163
|
+
".mp3": "audio/mpeg",
|
|
5164
|
+
".m4a": "audio/mp4",
|
|
5165
|
+
".wav": "audio/wav",
|
|
5166
|
+
".oga": "audio/ogg"
|
|
4686
5167
|
};
|
|
5168
|
+
var SEEKABLE = /^(video|audio)\//;
|
|
4687
5169
|
var pendingApps = /* @__PURE__ */ new Map();
|
|
4688
5170
|
var appResults = /* @__PURE__ */ new Map();
|
|
4689
5171
|
async function portalCommand(argv) {
|
|
@@ -4704,6 +5186,8 @@ async function portalCommand(argv) {
|
|
|
4704
5186
|
};
|
|
4705
5187
|
let cache = null;
|
|
4706
5188
|
const TTL = 45e3;
|
|
5189
|
+
const labelCache = /* @__PURE__ */ new Map();
|
|
5190
|
+
const LABEL_TTL = 3e5;
|
|
4707
5191
|
const brainDirs = () => (readOrg(ws.opsDir, parseYaml).staff ?? []).map((s) => s.dir ?? s.handle);
|
|
4708
5192
|
const knownRepos = () => {
|
|
4709
5193
|
const org = readOrg(ws.opsDir, parseYaml);
|
|
@@ -4741,7 +5225,7 @@ async function portalCommand(argv) {
|
|
|
4741
5225
|
}
|
|
4742
5226
|
const dir = entry.dir ?? entry.handle;
|
|
4743
5227
|
const spec2 = specFromManifest(
|
|
4744
|
-
parseYaml(readFileSync19(
|
|
5228
|
+
parseYaml(readFileSync19(join24(ws.root, dir, "staff.yaml"), "utf8"), "staff.yaml"),
|
|
4745
5229
|
dir
|
|
4746
5230
|
);
|
|
4747
5231
|
const isPublic = payload.scope === "public";
|
|
@@ -4845,7 +5329,7 @@ async function portalCommand(argv) {
|
|
|
4845
5329
|
res.end(JSON.stringify({ error: "a repo name and a known role are needed" }));
|
|
4846
5330
|
return;
|
|
4847
5331
|
}
|
|
4848
|
-
const path =
|
|
5332
|
+
const path = join24(ws.opsDir, "org.yaml");
|
|
4849
5333
|
const text = readFileSync19(path, "utf8");
|
|
4850
5334
|
if (new RegExp(`name: ${name}[,\\s}]`).test(text)) {
|
|
4851
5335
|
json(res, { ok: true, note: "already there" });
|
|
@@ -4883,16 +5367,16 @@ async function portalCommand(argv) {
|
|
|
4883
5367
|
if (route === "plan") {
|
|
4884
5368
|
json(res, {
|
|
4885
5369
|
org,
|
|
4886
|
-
dir:
|
|
5370
|
+
dir: join24(startedIn, "roster-ops"),
|
|
4887
5371
|
files: [
|
|
4888
|
-
...initFiles({
|
|
5372
|
+
...(await initFiles({
|
|
4889
5373
|
org,
|
|
4890
5374
|
name: String(params.name ?? org),
|
|
4891
5375
|
human: String(params.human ?? ""),
|
|
4892
5376
|
marker: String(params.marker ?? "human"),
|
|
4893
5377
|
opsName: "roster-ops",
|
|
4894
5378
|
agent: String(params.agent ?? "claude-code-action")
|
|
4895
|
-
}).keys()
|
|
5379
|
+
})).keys()
|
|
4896
5380
|
].sort()
|
|
4897
5381
|
});
|
|
4898
5382
|
return;
|
|
@@ -4914,11 +5398,11 @@ async function portalCommand(argv) {
|
|
|
4914
5398
|
}
|
|
4915
5399
|
res.writeHead(404).end("not found");
|
|
4916
5400
|
};
|
|
4917
|
-
const body = (req) => new Promise((resolve6, reject) => {
|
|
5401
|
+
const body = (req, limit = 1e6) => new Promise((resolve6, reject) => {
|
|
4918
5402
|
let buf = "";
|
|
4919
5403
|
req.on("data", (c) => {
|
|
4920
5404
|
buf += c;
|
|
4921
|
-
if (buf.length >
|
|
5405
|
+
if (buf.length > limit) reject(new Error("body too large"));
|
|
4922
5406
|
});
|
|
4923
5407
|
req.on("end", () => resolve6(buf));
|
|
4924
5408
|
req.on("error", reject);
|
|
@@ -4949,6 +5433,10 @@ async function portalCommand(argv) {
|
|
|
4949
5433
|
res.end(JSON.stringify(docPages()));
|
|
4950
5434
|
return;
|
|
4951
5435
|
}
|
|
5436
|
+
if (url.pathname === "/api/docsearch") {
|
|
5437
|
+
json(res, { hits: searchDocs(url.searchParams.get("q") ?? "") });
|
|
5438
|
+
return;
|
|
5439
|
+
}
|
|
4952
5440
|
if (url.pathname === "/api/doc") {
|
|
4953
5441
|
const page2 = url.searchParams.get("page") ?? "";
|
|
4954
5442
|
if (!docPages().some((d) => d.file === page2)) {
|
|
@@ -4956,7 +5444,20 @@ async function portalCommand(argv) {
|
|
|
4956
5444
|
return;
|
|
4957
5445
|
}
|
|
4958
5446
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
4959
|
-
res.end(readFileSync19(
|
|
5447
|
+
res.end(readFileSync19(join24(docsDir(), page2), "utf8"));
|
|
5448
|
+
return;
|
|
5449
|
+
}
|
|
5450
|
+
if (url.pathname === "/api/docasset") {
|
|
5451
|
+
const asset = docAsset(url.searchParams.get("path") ?? "");
|
|
5452
|
+
if (!asset) {
|
|
5453
|
+
res.writeHead(404).end("not found");
|
|
5454
|
+
return;
|
|
5455
|
+
}
|
|
5456
|
+
res.writeHead(200, {
|
|
5457
|
+
"content-type": asset.type,
|
|
5458
|
+
"cache-control": "public, max-age=86400"
|
|
5459
|
+
});
|
|
5460
|
+
res.end(readFileSync19(asset.path));
|
|
4960
5461
|
return;
|
|
4961
5462
|
}
|
|
4962
5463
|
if (url.pathname === "/setup/app/start") {
|
|
@@ -5041,6 +5542,23 @@ async function portalCommand(argv) {
|
|
|
5041
5542
|
res.end(JSON.stringify(data));
|
|
5042
5543
|
return;
|
|
5043
5544
|
}
|
|
5545
|
+
if (url.pathname === "/api/orglayer") {
|
|
5546
|
+
const listMd = (dir, prefix) => existsSync22(join24(w.opsDir, dir)) ? readdirSync9(join24(w.opsDir, dir)).filter((f) => f.endsWith(".md")).sort().map((f) => prefix + f) : [];
|
|
5547
|
+
const files = ["org.yaml", ...listMd("org", "org/"), ...listMd("prompts", "prompts/")].filter((rel) => isWritable(w, `${w.opsName}/${rel}`, brainDirs())).map((rel) => {
|
|
5548
|
+
const full = join24(w.opsDir, rel);
|
|
5549
|
+
const text = readFileSync19(full, "utf8");
|
|
5550
|
+
return {
|
|
5551
|
+
path: rel,
|
|
5552
|
+
bytes: statSync5(full).size,
|
|
5553
|
+
group: rel.startsWith("prompts/") ? "prompts" : "org",
|
|
5554
|
+
/* The first heading, for anything the page has no description of. Markdown
|
|
5555
|
+
only: `# ` opens a comment in YAML, and the first line of org.yaml is one. */
|
|
5556
|
+
title: rel.endsWith(".md") ? text.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() ?? "" : ""
|
|
5557
|
+
};
|
|
5558
|
+
});
|
|
5559
|
+
json(res, { files, opsName: w.opsName });
|
|
5560
|
+
return;
|
|
5561
|
+
}
|
|
5044
5562
|
if (url.pathname === "/api/prompt") {
|
|
5045
5563
|
const handle = url.searchParams.get("staff") ?? "";
|
|
5046
5564
|
const kind = url.searchParams.get("kind") ?? "daily";
|
|
@@ -5050,7 +5568,7 @@ async function portalCommand(argv) {
|
|
|
5050
5568
|
res.writeHead(400).end("bad request");
|
|
5051
5569
|
return;
|
|
5052
5570
|
}
|
|
5053
|
-
const brainDir =
|
|
5571
|
+
const brainDir = join24(w.root, entry.dir ?? entry.handle);
|
|
5054
5572
|
try {
|
|
5055
5573
|
const view = promptView(w, compose, handle, brainDir, kind);
|
|
5056
5574
|
view.problems = auditPrompt(w, view, workspaceRoots(org));
|
|
@@ -5168,6 +5686,34 @@ async function portalCommand(argv) {
|
|
|
5168
5686
|
});
|
|
5169
5687
|
return;
|
|
5170
5688
|
}
|
|
5689
|
+
if (url.pathname === "/api/promptaudit") {
|
|
5690
|
+
const handle = url.searchParams.get("staff") ?? "";
|
|
5691
|
+
const org = readOrg(w.opsDir, parseYaml);
|
|
5692
|
+
const entry = (org.staff ?? []).find((s) => s.handle === handle);
|
|
5693
|
+
if (!entry) {
|
|
5694
|
+
res.writeHead(400).end("bad request");
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5697
|
+
const brainDir = join24(w.root, entry.dir ?? entry.handle);
|
|
5698
|
+
const roots = workspaceRoots(org);
|
|
5699
|
+
const found = /* @__PURE__ */ new Map();
|
|
5700
|
+
const errors = [];
|
|
5701
|
+
for (const kind of KINDS) {
|
|
5702
|
+
try {
|
|
5703
|
+
const view = promptView(w, compose, handle, brainDir, kind);
|
|
5704
|
+
for (const p of auditPrompt(w, view, roots)) {
|
|
5705
|
+
const key = `${p.id}|${p.path ?? ""}|${p.title}`;
|
|
5706
|
+
const seen = found.get(key);
|
|
5707
|
+
if (seen) seen.kinds.push(kind);
|
|
5708
|
+
else found.set(key, { ...p, kind, kinds: [kind] });
|
|
5709
|
+
}
|
|
5710
|
+
} catch (err) {
|
|
5711
|
+
errors.push({ kind, error: String(err.message) });
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
json(res, { staff: handle, problems: [...found.values()], errors });
|
|
5715
|
+
return;
|
|
5716
|
+
}
|
|
5171
5717
|
if (url.pathname === "/api/amend") {
|
|
5172
5718
|
const handle = url.searchParams.get("staff") ?? "";
|
|
5173
5719
|
const kind = url.searchParams.get("kind") ?? "daily";
|
|
@@ -5179,9 +5725,9 @@ async function portalCommand(argv) {
|
|
|
5179
5725
|
return;
|
|
5180
5726
|
}
|
|
5181
5727
|
const dir = entry.dir ?? entry.handle;
|
|
5182
|
-
const view = promptView(w, compose, handle,
|
|
5183
|
-
const manifestPath =
|
|
5184
|
-
const tokens =
|
|
5728
|
+
const view = promptView(w, compose, handle, join24(w.root, dir), kind);
|
|
5729
|
+
const manifestPath = join24(w.root, dir, "staff.yaml");
|
|
5730
|
+
const tokens = existsSync22(manifestPath) ? tokensFor(
|
|
5185
5731
|
orgSpec(org, w),
|
|
5186
5732
|
specFromManifest(
|
|
5187
5733
|
parseYaml(readFileSync19(manifestPath, "utf8"), "staff.yaml"),
|
|
@@ -5242,6 +5788,10 @@ async function portalCommand(argv) {
|
|
|
5242
5788
|
const known = (org.repos ?? []).map((r) => `${org.org}/${r.name}`);
|
|
5243
5789
|
if (!known.includes(payload.repo))
|
|
5244
5790
|
throw new Error(`${payload.repo} is not a repo in org.yaml`);
|
|
5791
|
+
if (payload.action === "ask") {
|
|
5792
|
+
const prRepo = payload.ask?.pr?.repo ?? "";
|
|
5793
|
+
if (!known.includes(prRepo)) throw new Error(`${prRepo} is not a repo in org.yaml`);
|
|
5794
|
+
}
|
|
5245
5795
|
const result = await act(payload);
|
|
5246
5796
|
cache = null;
|
|
5247
5797
|
res.writeHead(200, {
|
|
@@ -5255,11 +5805,38 @@ async function portalCommand(argv) {
|
|
|
5255
5805
|
});
|
|
5256
5806
|
return;
|
|
5257
5807
|
}
|
|
5808
|
+
if (url.pathname === "/api/upload") {
|
|
5809
|
+
if (!writeAllowed(req)) {
|
|
5810
|
+
refuseWrite(res);
|
|
5811
|
+
return;
|
|
5812
|
+
}
|
|
5813
|
+
body(req, Math.ceil(MAX_UPLOAD * 1.4)).then((raw) => {
|
|
5814
|
+
const payload = JSON.parse(raw || "{}");
|
|
5815
|
+
const repo = payload.repo ?? "";
|
|
5816
|
+
const known = knownRepos().find((r) => `${r.owner}/${r.name}` === repo);
|
|
5817
|
+
if (!known) throw new Error(`${repo} is not a repo in org.yaml`);
|
|
5818
|
+
if (!payload.name) throw new Error("a file needs a name");
|
|
5819
|
+
json(
|
|
5820
|
+
res,
|
|
5821
|
+
attach({
|
|
5822
|
+
repoDir: join24(w.root, known.name),
|
|
5823
|
+
repo,
|
|
5824
|
+
name: payload.name,
|
|
5825
|
+
data: Buffer.from(payload.data ?? "", "base64"),
|
|
5826
|
+
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
5827
|
+
})
|
|
5828
|
+
);
|
|
5829
|
+
}).catch((err) => {
|
|
5830
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
5831
|
+
res.end(JSON.stringify({ error: String(err?.message ?? err) }));
|
|
5832
|
+
});
|
|
5833
|
+
return;
|
|
5834
|
+
}
|
|
5258
5835
|
if (url.pathname === "/api/sync") {
|
|
5259
5836
|
const org = readOrg(w.opsDir, parseYaml);
|
|
5260
5837
|
const dirs = [w.opsName, ...(org.staff ?? []).map((s) => s.dir ?? s.handle)];
|
|
5261
5838
|
syncRepos(w.root, dirs).then((results) => {
|
|
5262
|
-
cache = null;
|
|
5839
|
+
if (results.some((r) => r.pulled)) cache = null;
|
|
5263
5840
|
res.writeHead(200, {
|
|
5264
5841
|
"content-type": "application/json; charset=utf-8",
|
|
5265
5842
|
"cache-control": "no-store"
|
|
@@ -5271,6 +5848,38 @@ async function portalCommand(argv) {
|
|
|
5271
5848
|
});
|
|
5272
5849
|
return;
|
|
5273
5850
|
}
|
|
5851
|
+
if (url.pathname === "/api/repos") {
|
|
5852
|
+
json(res, { repos: knownRepos() });
|
|
5853
|
+
return;
|
|
5854
|
+
}
|
|
5855
|
+
if (url.pathname === "/api/labels") {
|
|
5856
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
5857
|
+
if (!knownRepos().some((r) => `${r.owner}/${r.name}` === repo)) {
|
|
5858
|
+
res.writeHead(400).end("bad request");
|
|
5859
|
+
return;
|
|
5860
|
+
}
|
|
5861
|
+
const hit = labelCache.get(repo);
|
|
5862
|
+
if (hit && Date.now() - hit.at < LABEL_TTL) {
|
|
5863
|
+
json(res, { labels: hit.labels });
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
api(
|
|
5867
|
+
`repos/${repo}/labels?per_page=100`
|
|
5868
|
+
).then((result) => {
|
|
5869
|
+
if (!result.ok) {
|
|
5870
|
+
json(res, { labels: [], error: result.error });
|
|
5871
|
+
return;
|
|
5872
|
+
}
|
|
5873
|
+
const labels = (result.data ?? []).map((l) => ({
|
|
5874
|
+
name: l.name,
|
|
5875
|
+
color: l.color,
|
|
5876
|
+
description: l.description ?? ""
|
|
5877
|
+
}));
|
|
5878
|
+
labelCache.set(repo, { at: Date.now(), labels });
|
|
5879
|
+
json(res, { labels });
|
|
5880
|
+
}).catch((err) => json(res, { labels: [], error: String(err?.message ?? err) }));
|
|
5881
|
+
return;
|
|
5882
|
+
}
|
|
5274
5883
|
if (url.pathname === "/api/inbox") {
|
|
5275
5884
|
const fresh = url.searchParams.get("refresh") === "1";
|
|
5276
5885
|
if (!fresh && cache && Date.now() - cache.at < TTL) {
|
|
@@ -5296,6 +5905,55 @@ async function portalCommand(argv) {
|
|
|
5296
5905
|
});
|
|
5297
5906
|
return;
|
|
5298
5907
|
}
|
|
5908
|
+
if (url.pathname === "/api/pr") {
|
|
5909
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
5910
|
+
const number = Number(url.searchParams.get("number"));
|
|
5911
|
+
const allowed = knownRepos().some((r) => `${r.owner}/${r.name}` === repo);
|
|
5912
|
+
if (!allowed || !Number.isInteger(number) || number <= 0) {
|
|
5913
|
+
res.writeHead(400).end("bad request");
|
|
5914
|
+
return;
|
|
5915
|
+
}
|
|
5916
|
+
Promise.all([
|
|
5917
|
+
api(`repos/${repo}/pulls/${number}`),
|
|
5918
|
+
api(`repos/${repo}/pulls/${number}/commits?per_page=100`),
|
|
5919
|
+
api(`repos/${repo}/pulls/${number}/files?per_page=100`)
|
|
5920
|
+
]).then(([pr, commits, files]) => {
|
|
5921
|
+
if (!pr.ok) {
|
|
5922
|
+
json(res, { error: pr.error });
|
|
5923
|
+
return;
|
|
5924
|
+
}
|
|
5925
|
+
json(res, {
|
|
5926
|
+
base: pr.data.base?.ref,
|
|
5927
|
+
head: pr.data.head?.ref,
|
|
5928
|
+
draft: !!pr.data.draft,
|
|
5929
|
+
/* GitHub computes mergeability in the background, so the first read of a fresh
|
|
5930
|
+
PR can honestly answer "I do not know yet". Passed through as null rather
|
|
5931
|
+
than flattened to false, which would read as a conflict. */
|
|
5932
|
+
mergeable: pr.data.mergeable,
|
|
5933
|
+
mergeState: pr.data.mergeable_state,
|
|
5934
|
+
additions: pr.data.additions,
|
|
5935
|
+
deletions: pr.data.deletions,
|
|
5936
|
+
changedFiles: pr.data.changed_files,
|
|
5937
|
+
commits: (commits.data ?? []).map((c) => ({
|
|
5938
|
+
sha: String(c.sha).slice(0, 7),
|
|
5939
|
+
subject: String(c.commit?.message ?? "").split("\n")[0],
|
|
5940
|
+
author: c.author?.login ?? c.commit?.author?.name ?? "",
|
|
5941
|
+
date: c.commit?.author?.date,
|
|
5942
|
+
url: c.html_url
|
|
5943
|
+
})),
|
|
5944
|
+
files: (files.data ?? []).map((f) => ({
|
|
5945
|
+
path: f.filename,
|
|
5946
|
+
status: f.status,
|
|
5947
|
+
additions: f.additions,
|
|
5948
|
+
deletions: f.deletions,
|
|
5949
|
+
// Absent on binaries and on files too large for the API to patch.
|
|
5950
|
+
patch: f.patch ?? ""
|
|
5951
|
+
})),
|
|
5952
|
+
errors: [commits, files].filter((r) => !r.ok).map((r) => r.error)
|
|
5953
|
+
});
|
|
5954
|
+
}).catch((err) => json(res, { error: String(err?.message ?? err) }));
|
|
5955
|
+
return;
|
|
5956
|
+
}
|
|
5299
5957
|
if (url.pathname === "/api/thread") {
|
|
5300
5958
|
const repo = url.searchParams.get("repo") ?? "";
|
|
5301
5959
|
const number = Number(url.searchParams.get("number"));
|
|
@@ -5328,9 +5986,9 @@ async function portalCommand(argv) {
|
|
|
5328
5986
|
res.writeHead(400).end("bad request");
|
|
5329
5987
|
return;
|
|
5330
5988
|
}
|
|
5331
|
-
const out =
|
|
5989
|
+
const out = execFileSync8(
|
|
5332
5990
|
"git",
|
|
5333
|
-
["-C",
|
|
5991
|
+
["-C", join24(w.root, dir), "show", "--format=%an%x1f%aI%x1f%s", sha, "--", path],
|
|
5334
5992
|
{
|
|
5335
5993
|
encoding: "utf8",
|
|
5336
5994
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -5343,15 +6001,42 @@ async function portalCommand(argv) {
|
|
|
5343
6001
|
if (url.pathname === "/api/file") {
|
|
5344
6002
|
const rel = url.searchParams.get("path") ?? "";
|
|
5345
6003
|
const full = resolve5(w.root, rel);
|
|
5346
|
-
if (!full.startsWith(resolve5(w.root) + "/") || !
|
|
6004
|
+
if (!full.startsWith(resolve5(w.root) + "/") || !existsSync22(full) || statSync5(full).isDirectory()) {
|
|
5347
6005
|
res.writeHead(404).end("not found");
|
|
5348
6006
|
return;
|
|
5349
6007
|
}
|
|
5350
|
-
const ext =
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
6008
|
+
const ext = extname3(full).toLowerCase();
|
|
6009
|
+
const type = MIME[ext] ?? "application/octet-stream";
|
|
6010
|
+
if (SEEKABLE.test(type)) {
|
|
6011
|
+
const size2 = statSync5(full).size;
|
|
6012
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
|
|
6013
|
+
if (range) {
|
|
6014
|
+
const start = range[1] ? Number(range[1]) : 0;
|
|
6015
|
+
const end = range[2] ? Math.min(Number(range[2]), size2 - 1) : size2 - 1;
|
|
6016
|
+
if (!(start <= end && start < size2)) {
|
|
6017
|
+
res.writeHead(416, { "content-range": `bytes */${size2}` }).end();
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
res.writeHead(206, {
|
|
6021
|
+
"content-type": type,
|
|
6022
|
+
"content-length": end - start + 1,
|
|
6023
|
+
"content-range": `bytes ${start}-${end}/${size2}`,
|
|
6024
|
+
"accept-ranges": "bytes",
|
|
6025
|
+
"cache-control": "no-store"
|
|
6026
|
+
});
|
|
6027
|
+
createReadStream(full, { start, end }).pipe(res);
|
|
6028
|
+
return;
|
|
6029
|
+
}
|
|
6030
|
+
res.writeHead(200, {
|
|
6031
|
+
"content-type": type,
|
|
6032
|
+
"content-length": size2,
|
|
6033
|
+
"accept-ranges": "bytes",
|
|
6034
|
+
"cache-control": "no-store"
|
|
6035
|
+
});
|
|
6036
|
+
createReadStream(full).pipe(res);
|
|
6037
|
+
return;
|
|
6038
|
+
}
|
|
6039
|
+
res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
|
|
5355
6040
|
res.end(readFileSync19(full));
|
|
5356
6041
|
return;
|
|
5357
6042
|
}
|
|
@@ -5403,7 +6088,7 @@ function buildPasteBrief(ws, parseYaml, kind, handle) {
|
|
|
5403
6088
|
throw new Error(`unknown staff handle "${handle}". org.yaml knows: ${known}`);
|
|
5404
6089
|
}
|
|
5405
6090
|
dir = entry.dir ?? entry.handle;
|
|
5406
|
-
const manifestPath =
|
|
6091
|
+
const manifestPath = join24(ws.root, dir, "staff.yaml");
|
|
5407
6092
|
tokens = tokensFor(
|
|
5408
6093
|
orgSpec(org, ws),
|
|
5409
6094
|
specFromManifest(parseYaml(readFileSync19(manifestPath, "utf8"), "staff.yaml"), dir)
|
|
@@ -5437,7 +6122,6 @@ function hireFlags(params) {
|
|
|
5437
6122
|
model: params.get("model") ?? void 0,
|
|
5438
6123
|
timeout: num("timeout"),
|
|
5439
6124
|
mentionTimeout: num("mentionTimeout"),
|
|
5440
|
-
prMentionTimeout: num("prMentionTimeout"),
|
|
5441
6125
|
secretPrefix: params.get("secretPrefix") ?? void 0,
|
|
5442
6126
|
app: params.get("app") ?? void 0,
|
|
5443
6127
|
publicApp: params.get("publicApp") ?? void 0,
|
|
@@ -5463,14 +6147,17 @@ function capture() {
|
|
|
5463
6147
|
};
|
|
5464
6148
|
}
|
|
5465
6149
|
function orgSpec(org, w) {
|
|
5466
|
-
const
|
|
6150
|
+
const humans = readHumans(org);
|
|
6151
|
+
const first = humans[0];
|
|
5467
6152
|
return {
|
|
5468
6153
|
org: org.org,
|
|
5469
6154
|
name: org.name,
|
|
5470
6155
|
opsRepo: `${org.org}/${w.opsName}`,
|
|
5471
6156
|
opsDirName: w.opsName,
|
|
5472
|
-
human:
|
|
5473
|
-
humanMarker:
|
|
6157
|
+
human: first?.name ?? first?.github ?? "the human",
|
|
6158
|
+
humanMarker: first?.marker ?? "human",
|
|
6159
|
+
humanLogins: humans.map((h) => h.github).filter(Boolean),
|
|
6160
|
+
allowedTools: toolsOf(org)
|
|
5474
6161
|
};
|
|
5475
6162
|
}
|
|
5476
6163
|
function workspaceRoots(org) {
|
|
@@ -5495,10 +6182,10 @@ function parseFlags10(argv) {
|
|
|
5495
6182
|
}
|
|
5496
6183
|
|
|
5497
6184
|
// src/commands/prompt.ts
|
|
5498
|
-
import { execFileSync as
|
|
5499
|
-
import { mkdtempSync as mkdtempSync3, writeFileSync as
|
|
6185
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
6186
|
+
import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync9 } from "fs";
|
|
5500
6187
|
import { tmpdir as tmpdir3 } from "os";
|
|
5501
|
-
import { join as
|
|
6188
|
+
import { join as join25 } from "path";
|
|
5502
6189
|
|
|
5503
6190
|
// src/lib/livePrompt.ts
|
|
5504
6191
|
import { readFileSync as readFileSync20 } from "fs";
|
|
@@ -5529,7 +6216,7 @@ roster prompt <handle> [options]
|
|
|
5529
6216
|
agent is sent \u2014 the CLI imports the tenant's own compose.mjs, so there is no second
|
|
5530
6217
|
implementation to drift.
|
|
5531
6218
|
|
|
5532
|
-
--kind <k> daily | mention
|
|
6219
|
+
--kind <k> daily | mention (default: daily)
|
|
5533
6220
|
--diff <file> diff the composed prompt against the prompt: block in a workflow file
|
|
5534
6221
|
--stat with --diff, print a summary instead of the full diff
|
|
5535
6222
|
--ops <dir> ops repo directory (default: found by walking up)
|
|
@@ -5559,12 +6246,12 @@ async function promptCommand(argv) {
|
|
|
5559
6246
|
process.stdout.write(composed);
|
|
5560
6247
|
return 0;
|
|
5561
6248
|
}
|
|
5562
|
-
const live = extractLivePrompt(
|
|
5563
|
-
const dir = mkdtempSync3(
|
|
5564
|
-
const a =
|
|
5565
|
-
const b =
|
|
5566
|
-
|
|
5567
|
-
|
|
6249
|
+
const live = extractLivePrompt(join25(ws.root, opts.diff));
|
|
6250
|
+
const dir = mkdtempSync3(join25(tmpdir3(), "roster-diff-"));
|
|
6251
|
+
const a = join25(dir, "live.md");
|
|
6252
|
+
const b = join25(dir, "composed.md");
|
|
6253
|
+
writeFileSync9(a, normalise(live));
|
|
6254
|
+
writeFileSync9(b, normalise(composed));
|
|
5568
6255
|
const entry = org.staff?.find((s) => s.handle === handle);
|
|
5569
6256
|
process.stdout.write(`
|
|
5570
6257
|
${entry?.name ?? handle} \xB7 ${kind}
|
|
@@ -5579,7 +6266,7 @@ async function promptCommand(argv) {
|
|
|
5579
6266
|
else args.push("--unified=2");
|
|
5580
6267
|
args.push(a, b);
|
|
5581
6268
|
try {
|
|
5582
|
-
|
|
6269
|
+
execFileSync9("git", args, { stdio: "inherit" });
|
|
5583
6270
|
process.stdout.write(" identical\n\n");
|
|
5584
6271
|
return 0;
|
|
5585
6272
|
} catch {
|