@broject/bro 0.2.1 → 0.2.2
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/README.md +22 -1
- package/dist/{dist-CZDdATdS.js → dist-DxwSa6Hg.js} +13 -2
- package/dist/index.js +364 -90
- package/dist/plugin.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,7 +112,11 @@ run on defaults until they set up. Everything's optional:
|
|
|
112
112
|
{
|
|
113
113
|
"stores": ["jsonl", "beads"],
|
|
114
114
|
"personality": "terse",
|
|
115
|
-
"debt": {
|
|
115
|
+
"debt": {
|
|
116
|
+
"dir": ".agents/review-debt",
|
|
117
|
+
"sources": ["review-threads", "dependabot", "secret-scanning"],
|
|
118
|
+
"stale_days": 14
|
|
119
|
+
},
|
|
116
120
|
"connectors": { "reviews": "github", "tasks": "beads" },
|
|
117
121
|
"sdd": { "mode": "remind", "dir": "specs" }
|
|
118
122
|
}
|
|
@@ -141,6 +145,23 @@ The ledger dir is machine-local state too — bro adds it to
|
|
|
141
145
|
`.git/info/exclude` on first write so harvest evidence can't be committed
|
|
142
146
|
by accident.
|
|
143
147
|
|
|
148
|
+
`debt.sources` picks which collectors `bro debt collect` runs. The
|
|
149
|
+
default is `["review-threads"]` — the original merged-PR sweep. Opt-in
|
|
150
|
+
sources feed the same ledger and beads projection (`debt:<source>`
|
|
151
|
+
labels, `thread_id` is the source's stable key):
|
|
152
|
+
|
|
153
|
+
| Source | Feeds on |
|
|
154
|
+
| ------ | -------- |
|
|
155
|
+
| `review-threads` | Unresolved review threads on merged PRs (default) |
|
|
156
|
+
| `dependabot` | Open Dependabot alerts — skipped when an open `dependabot/*` PR already covers the dependency |
|
|
157
|
+
| `code-scanning` | Open code-scanning alerts (rule + file ref) |
|
|
158
|
+
| `secret-scanning` | Open secret-scanning alerts — always blocking priority |
|
|
159
|
+
| `stale-prs` | Open PRs idle > `debt.stale_days` days or failing checks (WIP drafts don't count) |
|
|
160
|
+
| `failed-ci` | Latest default-branch workflow run, if it failed |
|
|
161
|
+
|
|
162
|
+
Alert sources reconcile both ways: a finding that disappears upstream is
|
|
163
|
+
marked `done` in the ledger on the next collect.
|
|
164
|
+
|
|
144
165
|
## Labels bro manages
|
|
145
166
|
|
|
146
167
|
| Label | Meaning |
|
|
@@ -187,7 +187,14 @@ const PERSONALITIES = [
|
|
|
187
187
|
"mentor",
|
|
188
188
|
"sarcastic"
|
|
189
189
|
];
|
|
190
|
-
const debtSection = (raw) =>
|
|
190
|
+
const debtSection = (raw) => {
|
|
191
|
+
const obj = typeof raw === "object" && raw !== null ? raw : {};
|
|
192
|
+
return {
|
|
193
|
+
dir: typeof obj.dir === "string" && obj.dir.trim() !== "" ? obj.dir : DEFAULT_CONFIG.debt.dir,
|
|
194
|
+
sources: Array.isArray(obj.sources) && obj.sources.every((s) => typeof s === "string" && s.trim() !== "") && obj.sources.length > 0 ? obj.sources : DEFAULT_CONFIG.debt.sources,
|
|
195
|
+
stale_days: typeof obj.stale_days === "number" && Number.isFinite(obj.stale_days) && obj.stale_days > 0 ? obj.stale_days : DEFAULT_CONFIG.debt.stale_days
|
|
196
|
+
};
|
|
197
|
+
};
|
|
191
198
|
const syncSection = (raw) => {
|
|
192
199
|
const obj = typeof raw === "object" && raw !== null ? raw : {};
|
|
193
200
|
return {
|
|
@@ -273,7 +280,11 @@ const CORE_SECTIONS = {
|
|
|
273
280
|
const DEFAULT_CONFIG = {
|
|
274
281
|
stores: ["jsonl", "beads"],
|
|
275
282
|
personality: "terse",
|
|
276
|
-
debt: {
|
|
283
|
+
debt: {
|
|
284
|
+
dir: ".agents/review-debt",
|
|
285
|
+
sources: ["review-threads"],
|
|
286
|
+
stale_days: 14
|
|
287
|
+
},
|
|
277
288
|
sync: {
|
|
278
289
|
ref: "refs/bro/data",
|
|
279
290
|
remote: "origin",
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as parallelWorkLines, B as stackSection, C as ghJson, D as initBeadsStealth, E as gitTry, F as registerConnector, H as syncSection, I as reviewHost, L as sddSection, M as promptContextLines, N as readPlanDoc, O as loadConfig, P as refKind$1, R as sessionStartLines, S as gh, T as git, U as taskStore, V as stopGateContributions, W as verbMethod, _ as docTypeNamed, a as bd, b as evidenceKind$1, c as beadsSection, d as dataRefPull, f as dataRefPush, g as definePlugin, i as actSection, j as planKind, l as checkBeads, m as debtSection, n as PERSONALITIES, o as bdJson$1, p as dataRefRoot, r as STANDARD_VERBS, s as bdTry, t as DEFAULT_GLOBAL_BEADS_DIR, u as dataRefCommit, v as docVerbs, w as ghTry, x as facade, y as ensureAuth, z as sessionTaskClaims } from "./dist-
|
|
2
|
+
import { A as parallelWorkLines, B as stackSection, C as ghJson, D as initBeadsStealth, E as gitTry, F as registerConnector, H as syncSection, I as reviewHost, L as sddSection, M as promptContextLines, N as readPlanDoc, O as loadConfig, P as refKind$1, R as sessionStartLines, S as gh, T as git, U as taskStore, V as stopGateContributions, W as verbMethod, _ as docTypeNamed, a as bd, b as evidenceKind$1, c as beadsSection, d as dataRefPull, f as dataRefPush, g as definePlugin, i as actSection, j as planKind, l as checkBeads, m as debtSection, n as PERSONALITIES, o as bdJson$1, p as dataRefRoot, r as STANDARD_VERBS, s as bdTry, t as DEFAULT_GLOBAL_BEADS_DIR, u as dataRefCommit, v as docVerbs, w as ghTry, x as facade, y as ensureAuth, z as sessionTaskClaims } from "./dist-DxwSa6Hg.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -963,7 +963,7 @@ function sanitizeRunId(runId) {
|
|
|
963
963
|
function harvestFilename(opts) {
|
|
964
964
|
const d = new Date(opts.harvestedAt);
|
|
965
965
|
const pad = (n) => String(n).padStart(2, "0");
|
|
966
|
-
return `${`${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`}
|
|
966
|
+
return `${`${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`}-${opts.source ? `src-${sanitizeRunId(opts.source)}` : `pr-${opts.pr}`}-run-${sanitizeRunId(opts.runId)}.jsonl`;
|
|
967
967
|
}
|
|
968
968
|
/** Append-only harvest snapshot (one new file per PR per run — no merge conflicts). */
|
|
969
969
|
function writeHarvestFile(opts) {
|
|
@@ -972,7 +972,8 @@ function writeHarvestFile(opts) {
|
|
|
972
972
|
const path = join(dir, harvestFilename({
|
|
973
973
|
harvestedAt: opts.harvestedAt,
|
|
974
974
|
pr: opts.pr,
|
|
975
|
-
runId: opts.runId
|
|
975
|
+
runId: opts.runId,
|
|
976
|
+
source: opts.source
|
|
976
977
|
}));
|
|
977
978
|
mkdirSync(dir, { recursive: true });
|
|
978
979
|
ensureDebtDirExcluded(debtDir(opts.cwd));
|
|
@@ -1648,7 +1649,8 @@ function beadInput(rec) {
|
|
|
1648
1649
|
description: `${rec.body}\n\n---\nthread: ${rec.thread_url}\npr: ${rec.source_pr_url}`,
|
|
1649
1650
|
labels: [
|
|
1650
1651
|
"debt",
|
|
1651
|
-
`
|
|
1652
|
+
...rec.source ? [`debt:${rec.source}`] : [],
|
|
1653
|
+
...rec.source_pr > 0 ? [`pr:${rec.source_pr}`] : [],
|
|
1652
1654
|
`area:${rec.area}`,
|
|
1653
1655
|
`author:${rec.author}`,
|
|
1654
1656
|
`priority:${rec.priority}`
|
|
@@ -1817,6 +1819,202 @@ const debtConnector = {
|
|
|
1817
1819
|
}
|
|
1818
1820
|
} })
|
|
1819
1821
|
};
|
|
1822
|
+
const DEBT_SOURCES = [
|
|
1823
|
+
"dependabot",
|
|
1824
|
+
"code-scanning",
|
|
1825
|
+
"secret-scanning",
|
|
1826
|
+
"stale-prs",
|
|
1827
|
+
"failed-ci"
|
|
1828
|
+
];
|
|
1829
|
+
const ALL_SOURCES = ["review-threads", ...DEBT_SOURCES];
|
|
1830
|
+
function parseSources(raw) {
|
|
1831
|
+
if (!Array.isArray(raw)) return ["review-threads"];
|
|
1832
|
+
const valid = new Set(ALL_SOURCES);
|
|
1833
|
+
const out = raw.filter((s) => typeof s === "string" && valid.has(s));
|
|
1834
|
+
return out.length > 0 ? out : ["review-threads"];
|
|
1835
|
+
}
|
|
1836
|
+
/** `gh api --paginate --slurp` — all pages of a list endpoint as one flat
|
|
1837
|
+
* array. Truncating at page 1 would drop real findings and falsely
|
|
1838
|
+
* resolve ledger rows that still exist upstream. */
|
|
1839
|
+
function ghJsonAll(endpoint) {
|
|
1840
|
+
return ghJson([
|
|
1841
|
+
"api",
|
|
1842
|
+
"--paginate",
|
|
1843
|
+
"--slurp",
|
|
1844
|
+
endpoint
|
|
1845
|
+
]).flat();
|
|
1846
|
+
}
|
|
1847
|
+
function baseRecord(ctx, opts) {
|
|
1848
|
+
return {
|
|
1849
|
+
thread_id: opts.threadId,
|
|
1850
|
+
thread_url: opts.url,
|
|
1851
|
+
status: "open",
|
|
1852
|
+
priority: opts.priority,
|
|
1853
|
+
needs: "code_change",
|
|
1854
|
+
source_pr: 0,
|
|
1855
|
+
source_pr_url: "",
|
|
1856
|
+
source_pr_title: opts.source,
|
|
1857
|
+
merged_at: opts.createdAt,
|
|
1858
|
+
merged_sha: "",
|
|
1859
|
+
path: opts.path,
|
|
1860
|
+
line: null,
|
|
1861
|
+
author: opts.author,
|
|
1862
|
+
body: opts.body,
|
|
1863
|
+
body_preview: bodyPreview(opts.body),
|
|
1864
|
+
fingerprint: fingerprint({
|
|
1865
|
+
body: opts.body,
|
|
1866
|
+
path: opts.path
|
|
1867
|
+
}),
|
|
1868
|
+
area: deriveArea(opts.path),
|
|
1869
|
+
harvested_at: ctx.harvestedAt,
|
|
1870
|
+
harvest_run_id: ctx.runId,
|
|
1871
|
+
times_seen: 1,
|
|
1872
|
+
fix_pr: null,
|
|
1873
|
+
fixed_at: null,
|
|
1874
|
+
notes: null,
|
|
1875
|
+
source: opts.source
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
/** Open dependabot PRs — an alert with a fix PR in flight isn't debt, the
|
|
1879
|
+
* PR is the work item. Matched by head ref `dependabot/.../<pkg>-<ver>` —
|
|
1880
|
+
* the trailing dash is the version boundary so `ember` can't match
|
|
1881
|
+
* `ember-cli-1.0`. Dependabot refs drop the `@` from scoped packages.
|
|
1882
|
+
* Failure propagates: an empty list here would mark every covered alert
|
|
1883
|
+
* as debt, so the caller skips the whole source on error. */
|
|
1884
|
+
function dependabotOpenPrs(repo) {
|
|
1885
|
+
return ghJson([
|
|
1886
|
+
"pr",
|
|
1887
|
+
"list",
|
|
1888
|
+
"--repo",
|
|
1889
|
+
repo,
|
|
1890
|
+
"--author",
|
|
1891
|
+
"app/dependabot",
|
|
1892
|
+
"--state",
|
|
1893
|
+
"open",
|
|
1894
|
+
"--json",
|
|
1895
|
+
"number,headRefName,url",
|
|
1896
|
+
"--limit",
|
|
1897
|
+
"200"
|
|
1898
|
+
]);
|
|
1899
|
+
}
|
|
1900
|
+
function collectDependabot(ctx) {
|
|
1901
|
+
const alerts = ghJsonAll(`repos/${ctx.repo}/dependabot/alerts?state=open&per_page=100`);
|
|
1902
|
+
const prs = dependabotOpenPrs(ctx.repo);
|
|
1903
|
+
const out = [];
|
|
1904
|
+
for (const a of alerts) {
|
|
1905
|
+
const pkg = a.security_vulnerability?.package?.name?.replace(/^@/, "") ?? "";
|
|
1906
|
+
if (pkg ? prs.find((p) => p.headRefName.includes(`${pkg}-`)) : void 0) continue;
|
|
1907
|
+
const severity = a.security_vulnerability?.severity ?? "medium";
|
|
1908
|
+
const body = a.security_vulnerability?.summary ?? `Dependabot alert #${a.number} (${pkg || "unknown package"}, ${severity})`;
|
|
1909
|
+
out.push(baseRecord(ctx, {
|
|
1910
|
+
threadId: `dependabot:${a.number}`,
|
|
1911
|
+
url: a.html_url,
|
|
1912
|
+
priority: severity === "critical" || severity === "high" ? "blocking" : "scan",
|
|
1913
|
+
path: a.dependency?.manifest_path ?? "",
|
|
1914
|
+
author: "dependabot[bot]",
|
|
1915
|
+
body: `[${severity}] ${pkg}: ${body}`,
|
|
1916
|
+
createdAt: a.created_at,
|
|
1917
|
+
source: "dependabot"
|
|
1918
|
+
}));
|
|
1919
|
+
}
|
|
1920
|
+
return out;
|
|
1921
|
+
}
|
|
1922
|
+
function collectCodeScanning(ctx) {
|
|
1923
|
+
return ghJsonAll(`repos/${ctx.repo}/code-scanning/alerts?state=open&per_page=100`).map((a) => baseRecord(ctx, {
|
|
1924
|
+
threadId: `code-scanning:${a.number}`,
|
|
1925
|
+
url: a.html_url,
|
|
1926
|
+
priority: a.rule?.severity === "error" ? "blocking" : "scan",
|
|
1927
|
+
path: a.most_recent_instance?.location?.path ?? "",
|
|
1928
|
+
author: "github-code-scanning",
|
|
1929
|
+
body: `${a.rule?.id ?? "unknown rule"}: ${a.rule?.description ?? "code scanning alert"}`,
|
|
1930
|
+
createdAt: a.created_at,
|
|
1931
|
+
source: "code-scanning"
|
|
1932
|
+
}));
|
|
1933
|
+
}
|
|
1934
|
+
function collectSecretScanning(ctx) {
|
|
1935
|
+
return ghJsonAll(`repos/${ctx.repo}/secret-scanning/alerts?state=open&per_page=100`).map((a) => baseRecord(ctx, {
|
|
1936
|
+
threadId: `secret-scanning:${a.number}`,
|
|
1937
|
+
url: a.html_url,
|
|
1938
|
+
priority: "blocking",
|
|
1939
|
+
path: "",
|
|
1940
|
+
author: "github-secret-scanning",
|
|
1941
|
+
body: `Secret scanning alert: ${a.secret_type_display_name ?? a.secret_type ?? "unknown type"}`,
|
|
1942
|
+
createdAt: a.created_at,
|
|
1943
|
+
source: "secret-scanning"
|
|
1944
|
+
}));
|
|
1945
|
+
}
|
|
1946
|
+
/** Open PRs idle > staleDays or failing checks. Drafts are flagged only when
|
|
1947
|
+
* they fail checks — a quiet WIP draft is work in progress, not debt. */
|
|
1948
|
+
function collectStalePrs(ctx, staleDays) {
|
|
1949
|
+
const prs = ghJson([
|
|
1950
|
+
"pr",
|
|
1951
|
+
"list",
|
|
1952
|
+
"--repo",
|
|
1953
|
+
ctx.repo,
|
|
1954
|
+
"--state",
|
|
1955
|
+
"open",
|
|
1956
|
+
"--json",
|
|
1957
|
+
"number,title,url,updatedAt,isDraft,author,statusCheckRollup",
|
|
1958
|
+
"--limit",
|
|
1959
|
+
"200"
|
|
1960
|
+
]);
|
|
1961
|
+
const cutoff = Date.now() - staleDays * 24 * 60 * 60 * 1e3;
|
|
1962
|
+
const out = [];
|
|
1963
|
+
for (const pr of prs) {
|
|
1964
|
+
const failing = (pr.statusCheckRollup ?? []).some((c) => c.conclusion === "FAILURE");
|
|
1965
|
+
const updated = Date.parse(pr.updatedAt);
|
|
1966
|
+
if (Number.isNaN(updated)) continue;
|
|
1967
|
+
const idleDays = Math.floor((Date.now() - updated) / 864e5);
|
|
1968
|
+
const stale = updated < cutoff;
|
|
1969
|
+
if (pr.isDraft && !failing) continue;
|
|
1970
|
+
if (!stale && !failing) continue;
|
|
1971
|
+
const reasons = [stale ? `idle ${idleDays}d` : null, failing ? "failing checks" : null].filter(Boolean);
|
|
1972
|
+
out.push(baseRecord(ctx, {
|
|
1973
|
+
threadId: `stale-pr:${pr.number}`,
|
|
1974
|
+
url: pr.url,
|
|
1975
|
+
priority: failing ? "blocking" : "nit",
|
|
1976
|
+
path: "",
|
|
1977
|
+
author: pr.author?.login ?? "unknown",
|
|
1978
|
+
body: `PR #${pr.number} "${pr.title}" — ${reasons.join(", ")}`,
|
|
1979
|
+
createdAt: pr.updatedAt,
|
|
1980
|
+
source: "stale-prs"
|
|
1981
|
+
}));
|
|
1982
|
+
}
|
|
1983
|
+
return out;
|
|
1984
|
+
}
|
|
1985
|
+
/** Latest completed run on the default branch — a single stable row
|
|
1986
|
+
* (`failed-ci:<branch>`) instead of one row per failed run. */
|
|
1987
|
+
function collectFailedCi(ctx) {
|
|
1988
|
+
const branch = ghJson(["api", `repos/${ctx.repo}`]).default_branch ?? "main";
|
|
1989
|
+
const latest = ghJson(["api", `repos/${ctx.repo}/actions/runs?branch=${encodeURIComponent(branch)}&status=completed&per_page=1`]).workflow_runs?.[0];
|
|
1990
|
+
if (latest?.conclusion !== "failure") return [];
|
|
1991
|
+
return [baseRecord(ctx, {
|
|
1992
|
+
threadId: `failed-ci:${branch}`,
|
|
1993
|
+
url: latest.html_url,
|
|
1994
|
+
priority: "blocking",
|
|
1995
|
+
path: "",
|
|
1996
|
+
author: "ci",
|
|
1997
|
+
body: `Default-branch CI failing: "${latest.name ?? "workflow"}" on ${branch} (run ${latest.id})`,
|
|
1998
|
+
createdAt: latest.created_at,
|
|
1999
|
+
source: "failed-ci"
|
|
2000
|
+
})];
|
|
2001
|
+
}
|
|
2002
|
+
const COLLECTORS = {
|
|
2003
|
+
dependabot: (ctx) => collectDependabot(ctx),
|
|
2004
|
+
"code-scanning": (ctx) => collectCodeScanning(ctx),
|
|
2005
|
+
"secret-scanning": (ctx) => collectSecretScanning(ctx),
|
|
2006
|
+
"stale-prs": (ctx, staleDays) => collectStalePrs(ctx, staleDays),
|
|
2007
|
+
"failed-ci": (ctx) => collectFailedCi(ctx)
|
|
2008
|
+
};
|
|
2009
|
+
/**
|
|
2010
|
+
* Alerts/CI are server-side truth: an open ledger row for a source whose
|
|
2011
|
+
* finding is absent from the fresh fetch resolved upstream — return its
|
|
2012
|
+
* thread_ids so collect can overlay them `done`.
|
|
2013
|
+
*/
|
|
2014
|
+
function resolvedThreadIds(existing, source, fresh) {
|
|
2015
|
+
const live = new Set(fresh.map((r) => r.thread_id));
|
|
2016
|
+
return existing.filter((r) => r.source === source && r.status === "open" && !live.has(r.thread_id)).map((r) => r.thread_id);
|
|
2017
|
+
}
|
|
1820
2018
|
|
|
1821
2019
|
//#endregion
|
|
1822
2020
|
//#region ../drill/dist/index.js
|
|
@@ -5759,6 +5957,9 @@ Commands:
|
|
|
5759
5957
|
Filters: --pr-ids, --merged-since, --merged-until,
|
|
5760
5958
|
--last, --pr-author, --labels, --thread-author
|
|
5761
5959
|
Flags: --dry-run, --list-only, --reharvest, --no-label
|
|
5960
|
+
Sources: debt.sources config — default review-threads;
|
|
5961
|
+
opt in: ${DEBT_SOURCES.join(", ")}
|
|
5962
|
+
debt.stale_days tunes stale-prs (default 14)
|
|
5762
5963
|
status Ledger summary + unprocessed merged PR count
|
|
5763
5964
|
prs [--limit N] [--all] Unprocessed merged PRs (--all: full matrix)
|
|
5764
5965
|
list [filters] Ledger rows (--status, --area, --author,
|
|
@@ -5856,102 +6057,51 @@ async function cmdCollect(argv) {
|
|
|
5856
6057
|
const rev = reviewHost(void 0, loadBroConfig().connectors);
|
|
5857
6058
|
const args = parseCollectArgs(rev, argv);
|
|
5858
6059
|
ensureAuth("reviews", { dir: process.cwd() }, { prefer: loadBroConfig().connectors });
|
|
5859
|
-
const
|
|
5860
|
-
const
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
const stale = processed.filter((pr) => {
|
|
5869
|
-
if (prDebtState(pr.labels) === "skipped") return false;
|
|
5870
|
-
const at = processedAt.get(pr.number);
|
|
5871
|
-
return at === void 0 || at > futureBound || pr.updatedAt !== null && pr.updatedAt > at;
|
|
5872
|
-
});
|
|
5873
|
-
const targets = args.reharvest ? matched.filter((pr) => prDebtState(pr.labels) !== "skipped") : [...pending, ...stale];
|
|
5874
|
-
console.error(`debt collect: ${matched.length} merged PR(s) matched, ${processed.length - stale.length} already processed (debt:* label)` + (stale.length > 0 ? `, ${stale.length} stale (post-scan activity)` : "") + `, scanning ${targets.length}`);
|
|
5875
|
-
if (args.listOnly) {
|
|
5876
|
-
for (const pr of targets) console.log(`${pr.number}\t${pr.mergedAt}\t${pr.author}\t${prDebtState(pr.labels) ?? "none"}`);
|
|
5877
|
-
return;
|
|
5878
|
-
}
|
|
5879
|
-
if (targets.length === 0) return;
|
|
5880
|
-
const labelingEnabled = !args.noLabel && args.threadAuthor === null;
|
|
5881
|
-
if (args.threadAuthor !== null && !args.noLabel) console.error("debt collect: --thread-author is a partial scan — labels disabled");
|
|
5882
|
-
if (!args.dryRun && labelingEnabled) ensureDebtLabels(rev, args.repo);
|
|
5883
|
-
let totalRows = 0;
|
|
5884
|
-
let labeled = 0;
|
|
5885
|
-
for (const pr of targets) {
|
|
5886
|
-
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5887
|
-
let result;
|
|
6060
|
+
const debtCfg = loadBroConfig().debt;
|
|
6061
|
+
const knownSources = new Set(ALL_SOURCES);
|
|
6062
|
+
const sources = new Set(parseSources(debtCfg.sources));
|
|
6063
|
+
for (const s of debtCfg.sources.filter((x) => !knownSources.has(x))) console.error(`debt collect: unknown source "${s}" — ignored`);
|
|
6064
|
+
if (sources.has("review-threads")) await collectReviewThreads(rev, args);
|
|
6065
|
+
for (const source of DEBT_SOURCES) {
|
|
6066
|
+
if (!sources.has(source)) continue;
|
|
6067
|
+
const harvestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6068
|
+
let records = [];
|
|
5888
6069
|
try {
|
|
5889
|
-
|
|
6070
|
+
records = COLLECTORS[source]({
|
|
5890
6071
|
repo: args.repo,
|
|
5891
|
-
pr: pr.number,
|
|
5892
6072
|
runId: args.runId,
|
|
5893
|
-
|
|
5894
|
-
});
|
|
6073
|
+
harvestedAt
|
|
6074
|
+
}, debtCfg.stale_days);
|
|
5895
6075
|
} catch (err) {
|
|
5896
|
-
console.error(`
|
|
6076
|
+
console.error(`debt: source ${source} failed — ${err instanceof Error ? err.message : err}`);
|
|
5897
6077
|
continue;
|
|
5898
6078
|
}
|
|
5899
|
-
console.error(`debt:
|
|
6079
|
+
console.error(`debt: ${source} — ${records.length} finding(s)`);
|
|
5900
6080
|
if (args.dryRun) {
|
|
5901
|
-
for (const row of
|
|
6081
|
+
for (const row of records) console.log(JSON.stringify(row));
|
|
5902
6082
|
continue;
|
|
5903
6083
|
}
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
return {
|
|
5922
|
-
thread_id: r.thread_id,
|
|
5923
|
-
status: "open",
|
|
5924
|
-
fix_pr: null,
|
|
5925
|
-
fixed_at: null,
|
|
5926
|
-
notes: prev?.notes ? `${prev.notes} | reopened by reharvest` : "reopened by reharvest"
|
|
5927
|
-
};
|
|
5928
|
-
}));
|
|
5929
|
-
console.error(`debt: reopened ${reopen.length} terminal row(s) — still unresolved`);
|
|
5930
|
-
}
|
|
5931
|
-
}
|
|
5932
|
-
if (labelingEnabled) {
|
|
5933
|
-
if (prDebtState(rev.labels({
|
|
5934
|
-
repo: args.repo,
|
|
5935
|
-
pr: pr.number
|
|
5936
|
-
})) !== "skipped") {
|
|
5937
|
-
applyCollectLabel(rev, {
|
|
5938
|
-
repo: args.repo,
|
|
5939
|
-
pr: pr.number,
|
|
5940
|
-
state
|
|
5941
|
-
});
|
|
5942
|
-
labeled += 1;
|
|
5943
|
-
}
|
|
5944
|
-
const cursor = rev.prUpdatedAt({
|
|
5945
|
-
repo: args.repo,
|
|
5946
|
-
pr: pr.number
|
|
5947
|
-
}) ?? pr.updatedAt ?? scannedAt;
|
|
5948
|
-
markProcessedAt([pr.number], cursor);
|
|
6084
|
+
if (records.length > 0) writeHarvestFile({
|
|
6085
|
+
pr: 0,
|
|
6086
|
+
source,
|
|
6087
|
+
runId: args.runId,
|
|
6088
|
+
harvestedAt,
|
|
6089
|
+
records
|
|
6090
|
+
});
|
|
6091
|
+
const gone = resolvedThreadIds(readDebtRecords(), source, records);
|
|
6092
|
+
if (gone.length > 0) {
|
|
6093
|
+
upsertLedgerOverlays(gone.map((thread_id) => ({
|
|
6094
|
+
thread_id,
|
|
6095
|
+
status: "done",
|
|
6096
|
+
fix_pr: null,
|
|
6097
|
+
fixed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6098
|
+
notes: "resolved upstream — no longer reported"
|
|
6099
|
+
})));
|
|
6100
|
+
console.error(`debt: ${source} — resolved ${gone.length} row(s) no longer reported`);
|
|
5949
6101
|
}
|
|
5950
6102
|
}
|
|
5951
6103
|
if (!args.dryRun) {
|
|
5952
|
-
if (
|
|
5953
|
-
const labeledMsg = labelingEnabled ? `labeled ${labeled} PR(s)` : "labels disabled";
|
|
5954
|
-
console.error(`debt collect: wrote ${totalRows} row(s), ${labeledMsg}`);
|
|
6104
|
+
if (readDebtRecords().length > 0) writeSummary(buildSummary(readDebtRecords()));
|
|
5955
6105
|
if (loadBroConfig().stores.includes("beads")) try {
|
|
5956
6106
|
const res = syncDebtToBeads(readDebtRecords());
|
|
5957
6107
|
console.error(`debt sync: ${res.created} created, ${res.closed} closed, ${res.reopened} reopened, ${res.linked} linked`);
|
|
@@ -5962,6 +6112,128 @@ async function cmdCollect(argv) {
|
|
|
5962
6112
|
}
|
|
5963
6113
|
}
|
|
5964
6114
|
}
|
|
6115
|
+
function selectTargets(matched, args) {
|
|
6116
|
+
const { pending, processed } = partitionByProcessed(matched);
|
|
6117
|
+
const processedAt = readProcessedAt();
|
|
6118
|
+
const futureBound = new Date(Date.now() + 300 * 1e3).toISOString();
|
|
6119
|
+
const stale = processed.filter((pr) => {
|
|
6120
|
+
if (prDebtState(pr.labels) === "skipped") return false;
|
|
6121
|
+
const at = processedAt.get(pr.number);
|
|
6122
|
+
return at === void 0 || at > futureBound || pr.updatedAt !== null && pr.updatedAt > at;
|
|
6123
|
+
});
|
|
6124
|
+
return {
|
|
6125
|
+
targets: args.reharvest ? matched.filter((pr) => prDebtState(pr.labels) !== "skipped") : [...pending, ...stale],
|
|
6126
|
+
staleCount: stale.length,
|
|
6127
|
+
processedCount: processed.length
|
|
6128
|
+
};
|
|
6129
|
+
}
|
|
6130
|
+
/** A reharvested thread that was marked done/wontfix is unresolved again
|
|
6131
|
+
* — the terminal overlay must not shadow the fresh open evidence. */
|
|
6132
|
+
function reopenTerminalRows(rows) {
|
|
6133
|
+
const overlays = readLedgerOverlays();
|
|
6134
|
+
const reopen = rows.filter((r) => {
|
|
6135
|
+
const s = overlays.get(r.thread_id)?.status;
|
|
6136
|
+
return s === "done" || s === "wontfix";
|
|
6137
|
+
});
|
|
6138
|
+
if (reopen.length === 0) return;
|
|
6139
|
+
upsertLedgerOverlays(reopen.map((r) => {
|
|
6140
|
+
const prev = overlays.get(r.thread_id);
|
|
6141
|
+
return {
|
|
6142
|
+
thread_id: r.thread_id,
|
|
6143
|
+
status: "open",
|
|
6144
|
+
fix_pr: null,
|
|
6145
|
+
fixed_at: null,
|
|
6146
|
+
notes: prev?.notes ? `${prev.notes} | reopened by reharvest` : "reopened by reharvest"
|
|
6147
|
+
};
|
|
6148
|
+
}));
|
|
6149
|
+
console.error(`debt: reopened ${reopen.length} terminal row(s) — still unresolved`);
|
|
6150
|
+
}
|
|
6151
|
+
async function harvestPr(rev, args, pr, labelingEnabled) {
|
|
6152
|
+
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6153
|
+
let result;
|
|
6154
|
+
try {
|
|
6155
|
+
result = await collectPr(rev, {
|
|
6156
|
+
repo: args.repo,
|
|
6157
|
+
pr: pr.number,
|
|
6158
|
+
runId: args.runId,
|
|
6159
|
+
threadAuthor: args.threadAuthor
|
|
6160
|
+
});
|
|
6161
|
+
} catch (err) {
|
|
6162
|
+
console.error(`warning: PR ${rev.prLink(args.repo, pr.number)} skipped — ${err instanceof Error ? err.message : err}`);
|
|
6163
|
+
return {
|
|
6164
|
+
rows: 0,
|
|
6165
|
+
labeled: 0
|
|
6166
|
+
};
|
|
6167
|
+
}
|
|
6168
|
+
console.error(`debt: PR ${rev.prLink(args.repo, pr.number)} — ${result.incoming.length} thread(s)`);
|
|
6169
|
+
if (args.dryRun) {
|
|
6170
|
+
for (const row of result.incoming) console.log(JSON.stringify(row));
|
|
6171
|
+
return {
|
|
6172
|
+
rows: 0,
|
|
6173
|
+
labeled: 0
|
|
6174
|
+
};
|
|
6175
|
+
}
|
|
6176
|
+
if (result.incoming.length > 0) {
|
|
6177
|
+
writeHarvestFile({
|
|
6178
|
+
pr: result.pr,
|
|
6179
|
+
runId: args.runId,
|
|
6180
|
+
harvestedAt: result.incoming[0].harvested_at,
|
|
6181
|
+
records: result.incoming
|
|
6182
|
+
});
|
|
6183
|
+
reopenTerminalRows(result.incoming);
|
|
6184
|
+
}
|
|
6185
|
+
let labeled = 0;
|
|
6186
|
+
if (labelingEnabled) {
|
|
6187
|
+
if (prDebtState(rev.labels({
|
|
6188
|
+
repo: args.repo,
|
|
6189
|
+
pr: pr.number
|
|
6190
|
+
})) !== "skipped") {
|
|
6191
|
+
const state = result.incoming.length > 0 ? "collected" : "clean";
|
|
6192
|
+
applyCollectLabel(rev, {
|
|
6193
|
+
repo: args.repo,
|
|
6194
|
+
pr: pr.number,
|
|
6195
|
+
state
|
|
6196
|
+
});
|
|
6197
|
+
labeled = 1;
|
|
6198
|
+
}
|
|
6199
|
+
const cursor = rev.prUpdatedAt({
|
|
6200
|
+
repo: args.repo,
|
|
6201
|
+
pr: pr.number
|
|
6202
|
+
}) ?? pr.updatedAt ?? scannedAt;
|
|
6203
|
+
markProcessedAt([pr.number], cursor);
|
|
6204
|
+
}
|
|
6205
|
+
return {
|
|
6206
|
+
rows: result.incoming.length,
|
|
6207
|
+
labeled
|
|
6208
|
+
};
|
|
6209
|
+
}
|
|
6210
|
+
async function collectReviewThreads(rev, args) {
|
|
6211
|
+
const listLimit = Math.max(args.filters.lastN ?? 0, 100);
|
|
6212
|
+
const matched = resolveHarvestPrs(rev, {
|
|
6213
|
+
repo: args.repo,
|
|
6214
|
+
filters: args.filters,
|
|
6215
|
+
listLimit
|
|
6216
|
+
});
|
|
6217
|
+
const { targets, staleCount, processedCount } = selectTargets(matched, args);
|
|
6218
|
+
console.error(`debt collect: ${matched.length} merged PR(s) matched, ${processedCount - staleCount} already processed (debt:* label)` + (staleCount > 0 ? `, ${staleCount} stale (post-scan activity)` : "") + `, scanning ${targets.length}`);
|
|
6219
|
+
if (args.listOnly) {
|
|
6220
|
+
for (const pr of targets) console.log(`${pr.number}\t${pr.mergedAt}\t${pr.author}\t${prDebtState(pr.labels) ?? "none"}`);
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6223
|
+
if (targets.length === 0) return;
|
|
6224
|
+
const labelingEnabled = !args.noLabel && args.threadAuthor === null;
|
|
6225
|
+
if (args.threadAuthor !== null && !args.noLabel) console.error("debt collect: --thread-author is a partial scan — labels disabled");
|
|
6226
|
+
if (!args.dryRun && labelingEnabled) ensureDebtLabels(rev, args.repo);
|
|
6227
|
+
let totalRows = 0;
|
|
6228
|
+
let labeled = 0;
|
|
6229
|
+
for (const pr of targets) {
|
|
6230
|
+
const res = await harvestPr(rev, args, pr, labelingEnabled);
|
|
6231
|
+
totalRows += res.rows;
|
|
6232
|
+
labeled += res.labeled;
|
|
6233
|
+
}
|
|
6234
|
+
const labeledMsg = labelingEnabled ? `labeled ${labeled} PR(s)` : "labels disabled";
|
|
6235
|
+
console.error(`debt collect: wrote ${totalRows} row(s), ${labeledMsg}`);
|
|
6236
|
+
}
|
|
5965
6237
|
function cmdStatus$1() {
|
|
5966
6238
|
const records = readDebtRecords();
|
|
5967
6239
|
const summary = buildSummary(records);
|
|
@@ -6022,6 +6294,7 @@ function rowMatchesFilters(row, filters) {
|
|
|
6022
6294
|
if (filters.author !== null && row.author !== filters.author) return false;
|
|
6023
6295
|
if (filters.priority !== null && row.priority !== filters.priority) return false;
|
|
6024
6296
|
if (filters.pr !== null && row.source_pr !== filters.pr) return false;
|
|
6297
|
+
if (filters.source !== null && (row.source ?? "review-threads") !== filters.source) return false;
|
|
6025
6298
|
return true;
|
|
6026
6299
|
}
|
|
6027
6300
|
function cmdList$2(argv) {
|
|
@@ -6030,7 +6303,8 @@ function cmdList$2(argv) {
|
|
|
6030
6303
|
area: null,
|
|
6031
6304
|
author: null,
|
|
6032
6305
|
priority: null,
|
|
6033
|
-
pr: null
|
|
6306
|
+
pr: null,
|
|
6307
|
+
source: null
|
|
6034
6308
|
};
|
|
6035
6309
|
let limit = 50;
|
|
6036
6310
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -7025,7 +7299,7 @@ const SKILL_FILES = {
|
|
|
7025
7299
|
"convoy/agents/openai.yaml": "interface:\n display_name: \"Bro Convoy\"\n short_description: \"Agent-internal molecule execution — DAG scheduling over beads via the bro CLI\"\n brand_color: \"#6B4FBB\"\n default_prompt: \"Use $convoy to get started.\"\npolicy:\n allow_implicit_invocation: false\n",
|
|
7026
7300
|
"convoy/SKILL.md": "---\nname: convoy\ndescription: \"Use when the user invokes /convoy or asks to run a beads workflow — a molecule's DAG of steps executed inside the agent. Thin wrapper over the bro CLI: `bro convoy next` is the scheduler as code; `bro convoy done` advances it. Requires `bro` (npx -y @broject/bro@0) and bd.\"\n---\n\n# /convoy (bro)\n\n**All mechanics live in the `bro` CLI.** This skill is policy only — do not\nreimplement scheduling; `bro convoy next` computes it from beads state.\n\nPrereq: `bro` on PATH or `npx -y @broject/bro@0` (major-pinned). Requires\n`bd` and an initialized `.beads/`.\n\n## What a convoy is\n\nA **convoy** is a beads **molecule** — the persistent instantiation of a\nformula (`bd mol pour`). Steps are child issues; `blocks` edges are the DAG.\nAll state lives in beads: a convoy survives session restarts, compaction,\nand parallel agents. Gas City is **not** required — bro is the executor.\n\n## The loop\n\n```text\nbro convoy pour <formula> [--var K=V]… → prints the molecule root id (once per run)\nbro convoy status [mol] → DAG: ✓ done / ▸ ready / · blocked\nbro convoy next [mol] → JSON: what to do now\nbro convoy claim <step-id> → atomic claim — do this before starting\nbro convoy done <step-id> --result \"…\" → closes the step, emits the new next\n```\n\nRepeat `next` → `claim` → work → `done` until `next` reports\n`\"state\": \"complete\"`. Every command takes the molecule id (positional or\n`--mol`) — pass it whenever more than one convoy is open; no-arg\nresolution only works with exactly one.\n\n`next` states:\n\n- `step` — execute `step` (id, title, description = the work instructions),\n then `bro convoy done <step.id> --result \"<what you did>\"`.\n- `gate` — a human step is next. **Ask the user** — do not execute it\n yourself and do not mark it done on their behalf without their answer.\n Their approval is the result; then `done` it with their decision.\n- `blocked` — open steps exist but none are ready (a dependency cycle,\n or remaining steps all waiting on other open work). Run\n `bro convoy status` and report.\n- `complete` — every step done. Close the molecule root:\n `bd close <mol-id> --reason \"convoy complete\"`.\n\n`next` also reports `gates[]` — every ready human gate, even while an\nagent step runs. Surface them to the user promptly; a ready gate means\nthe convoy is waiting on a human somewhere. `inProgress[]` lists steps\nalready claimed — including yours, once you claim. Steps you didn't claim\nthat appear here belong to another agent; pick a different ready step. And `inputs[]` — the `--result` each closed direct\ndependency was completed with. That is the handoff: read it before\nstarting the step.\n\nWith several open molecules, pass `--mol <id>` explicitly — no-arg\nresolution errors on ambiguity rather than guessing.\n\n## Policy\n\n- **Never skip `next`.** Do not pick steps by eyeballing titles — the DAG\n decides order. Independent ready steps appear together in `ready[]`;\n parallel execution across agent sessions is allowed, sequential `step`\n selection is the safe default.\n- **`--result` is the handoff.** Future steps read it — write what the\n next step needs (paths, PR urls, verdicts), not a diary entry.\n- **Gates are not optional.** A `human` step exists to stop the machine;\n treating it as an agent step defeats the formula.\n- **Claim before you start.** `bro convoy claim <step>` is an atomic\n lease (assignee + in_progress, refused if another agent holds it). With\n parallel agents in the same repo, an unclaimed step is shared work.\n- **Resume is free.** After compaction or a fresh session, `bro convoy\n status` reconstructs everything — there is no in-memory state to lose.\n- **Failures stay visible.** If a step cannot be completed, leave it open\n and tell the user — do not force-close a step you didn't do.\n",
|
|
7027
7301
|
"debt/agents/openai.yaml": "interface:\n display_name: \"Bro Debt\"\n short_description: \"Review-debt pipeline via the bro CLI — collect + status\"\n brand_color: \"#B60205\"\n default_prompt: \"Use $debt to get started.\"\npolicy:\n allow_implicit_invocation: false\n",
|
|
7028
|
-
"debt/SKILL.md": "---\nname: debt\ndescription: \"Use when the user invokes /debt or asks about review debt on merged PRs. Thin wrapper over the bro CLI — all mechanics live in `bro debt *` commands; this skill carries policy only. Requires `bro` (npx -y @broject/bro@0) and gh.\"\n---\n\n# /debt (bro)\n\n**All mechanics live in the `bro` CLI.** This skill is policy only — do not\nreimplement what `bro debt` already does.\n\nPrereq: `bro` on PATH or `npx -y @broject/bro@0` (major-pinned). Requires\n`gh` auth; `bd` is required only with beads enabled (the default) —\n`\"stores\": [\"jsonl\"]` in bro.config.json opts out and drops the `bd`\nrequirement.\n\n## Commands\n\n| Command | What it does |\n| ------- | ------------ |\n| `bro debt collect [filters]` | Scan merged PRs **without** a `debt:*` label → collect unresolved threads → write `harvests/*.jsonl` → label `debt:collected` / `debt:clean` |\n| `bro debt status` | Ledger summary + unprocessed merged-PR count |\n| `bro debt prs` | Merged PRs still unprocessed — the work queue (`--all`: full matrix) |\n| `bro debt list` | Ledger rows (`--status`, `--area`, `--author`, `--priority`, `--pr`) |\n| `bro debt mark <pr> <state>` | `collected` / `clean` / `skipped` / `none` — manual override |\n| `bro debt set <status> --thread-id ID` | Ledger status: `claimed` / `done --fix-pr N` / `wontfix` / `duplicate` — feeds `bro debt sync` |\n| `bro debt sync` | Project the ledger into beads — `bd ready -l debt` becomes the work queue |\n\n## Policy\n\n- **`debt:*` labels are the PR-level source of truth** for \"processed\".\n `skipped` is a human opt-out and always wins over machine labels.\n- **beads is a default store.** A normal collect auto-runs\n `bd init --stealth --skip-agents --skip-hooks` when `.beads` is missing\n (skipped by `--dry-run`, `--list-only`, and an empty target list) and\n keeps the ledger dir out of git via `.git/info/exclude`.\n `\"stores\": [\"jsonl\"]` in bro.config.json is the opt-out.\n- **Label after the file lands.** In CI pipelines run\n `collect --no-label`, land `harvests/*.jsonl` (`git add -f` — the ledger\n dir is locally excluded), then `bro debt mark`.\n- **Collect, don't fix.** Triage → backlog, fixes → the fix loop. `bro debt` never edits product code or resolves threads on source PRs.\n- `--reharvest` bypasses the label skip for one run.\n",
|
|
7302
|
+
"debt/SKILL.md": "---\nname: debt\ndescription: \"Use when the user invokes /debt or asks about review debt on merged PRs. Thin wrapper over the bro CLI — all mechanics live in `bro debt *` commands; this skill carries policy only. Requires `bro` (npx -y @broject/bro@0) and gh.\"\n---\n\n# /debt (bro)\n\n**All mechanics live in the `bro` CLI.** This skill is policy only — do not\nreimplement what `bro debt` already does.\n\nPrereq: `bro` on PATH or `npx -y @broject/bro@0` (major-pinned). Requires\n`gh` auth; `bd` is required only with beads enabled (the default) —\n`\"stores\": [\"jsonl\"]` in bro.config.json opts out and drops the `bd`\nrequirement.\n\n## Commands\n\n| Command | What it does |\n| ------- | ------------ |\n| `bro debt collect [filters]` | Scan merged PRs **without** a `debt:*` label → collect unresolved threads → write `harvests/*.jsonl` → label `debt:collected` / `debt:clean`. With `debt.sources` in bro.config.json it also runs opt-in collectors (`dependabot`, `code-scanning`, `secret-scanning`, `stale-prs`, `failed-ci`) — same ledger, `debt:<source>` labels on beads, `debt.stale_days` tunes stale-prs |\n| `bro debt status` | Ledger summary + unprocessed merged-PR count |\n| `bro debt prs` | Merged PRs still unprocessed — the work queue (`--all`: full matrix) |\n| `bro debt list` | Ledger rows (`--status`, `--area`, `--author`, `--priority`, `--pr`) |\n| `bro debt mark <pr> <state>` | `collected` / `clean` / `skipped` / `none` — manual override |\n| `bro debt set <status> --thread-id ID` | Ledger status: `claimed` / `done --fix-pr N` / `wontfix` / `duplicate` — feeds `bro debt sync` |\n| `bro debt sync` | Project the ledger into beads — `bd ready -l debt` becomes the work queue |\n\n## Policy\n\n- **`debt:*` labels are the PR-level source of truth** for \"processed\".\n `skipped` is a human opt-out and always wins over machine labels.\n- **beads is a default store.** A normal collect auto-runs\n `bd init --stealth --skip-agents --skip-hooks` when `.beads` is missing\n (skipped by `--dry-run`, `--list-only`, and an empty target list) and\n keeps the ledger dir out of git via `.git/info/exclude`.\n `\"stores\": [\"jsonl\"]` in bro.config.json is the opt-out.\n- **Label after the file lands.** In CI pipelines run\n `collect --no-label`, land `harvests/*.jsonl` (`git add -f` — the ledger\n dir is locally excluded), then `bro debt mark`.\n- **Collect, don't fix.** Triage → backlog, fixes → the fix loop. `bro debt` never edits product code or resolves threads on source PRs.\n- `--reharvest` bypasses the label skip for one run.\n",
|
|
7029
7303
|
"docs/SKILL.md": "---\nname: docs\ndescription: \"Use when reading or mutating bro documents directly — tasks (beads) and stores — through the verb-first `bro <verb> <noun|ref>` surface. Also covers the user-level global task store. Thin wrapper — mechanics live in the CLI.\"\n---\n\n# /docs (bro)\n\n**All mechanics live in the `bro` CLI.** This skill is policy only.\n\nbro's document surface is noun-first, gh-style: `bro <noun> <verb>\n[ref…] [--flags]`. Doc types are plugin-registered (`task`, `store`);\na type's adapter methods are the verb registry — there is no verb\nlist to consult. Verbs live inside their noun's namespace, so plugin\ndoc types may define any custom verbs freely.\n\nShorthand: task verbs also work verb-first — `bro list`, `bro show\n<id>`, `bro close <id>` mean `bro task …`. A bare ref infers the type\nfrom its prefix; a bare noun (`bro task`, `bro store`) means `list`.\n\nThe doc namespace is guarded, not declared: reserved words = plugin\ncommand names ∪ doc nouns/aliases ∪ shorthand verbs. A plugin taking\na reserved name, or a doc type colliding on noun/alias/idPrefix, is\nskipped with a warning — everything else is usable.\n\nTask verbs resolve through the `tasks` connector facade — beads is the\nbuilt-in provider; `connectors.tasks` in `bro.config.json` (or a plugin\nshipping its own connector via `BroPlugin.connectors`) retargets them.\n\n| Command | What it does |\n| ------- | ------------ |\n| `bro task list [--status=open]` / `bro list` | List tasks — flags pass through to the store |\n| `bro task show <id>` / `bro show <id>` | One doc; the ref infers the type |\n| `bro task new \"title\" [--type=bug]` / `bro new task \"title\"` | Create — returns the doc |\n| `bro task set <id> --status=blocked` / `bro set <id> …` | Update fields |\n| `bro task close <id> [--reason=…]` / `bro rm <id>` | Close / delete a task |\n| `bro task exec [--global] -- <bd args>` | Raw `bd` against the store — escape hatch, not contract |\n| `bro store list` | Store inventory — path + prefix + health |\n| `bro store show --global` | Details of the resolved global dir |\n| `bro store init --global [--prefix=P]` | Create + validate the global store |\n| `bro store path --global` | Print the resolved global store dir |\n| `bro next --global` | Schedule from the global queue — same pipeline, cwd = the store |\n\n## Scopes\n\n`--global` is a scope flag, not a command: the same verbs hit the\nuser-level store (`~/.local/share/bro/beads` by default — `beads.global`\nin `bro.config.json` or `$BRO_GLOBAL_BEADS` relocate it, env wins).\nStores never mix: `bro list`, `bro next`, `bro loop`, and hooks read the\ncheckout's `.beads` only; global work is opt-in at every call.\n\nThe global store is local-only by default — no sync remote. For a\ncross-machine queue set the store's own Dolt remote inside it\n(`bd dolt remote` in the store dir) — bro does not manage it.\n\n## Policy\n\n- **Project work goes to project beads.** Global beads are claimed only\n through `bro next --global` or `--global` doc verbs.\n- **Global is for user-level work** — cross-repo tasks, personal backlog,\n anything that must not ride the project's issue tracker or land in its\n PRs. If in doubt, file in the project.\n- **Prefer verbs over `exec`.** `bro task exec -- …` exists for bd features\n the doc layer doesn't model yet; workflow output should come through\n typed verbs so ref inference and rendering keep working.\n",
|
|
7030
7304
|
"drill/agents/openai.yaml": "interface:\n display_name: \"Bro Drill\"\n short_description: \"Scoped descent over beads — drill frames with mandatory result+prevention memos\"\n brand_color: \"#B60205\"\n default_prompt: \"Use $drill to start a scoped drill frame.\"\npolicy:\n allow_implicit_invocation: false\n",
|
|
7031
7305
|
"drill/SKILL.md": "---\nname: drill\ndescription: \"Use when the user invokes /drill or /unwind, or when a task needs scoped descent — narrower investigation frames with mandatory result+prevention memos. Thin wrapper over the bro CLI: drill frames are beads; bd owns storage, `bro drill` owns the invariants. Requires `bro` (npx -y @broject/bro@0) and bd.\"\n---\n\n# /drill (bro)\n\n**All mechanics live in the `bro` CLI over `bd`.** This skill is policy\nonly. A drill frame is a bead labeled `drill`; nesting uses bd's native\n`--parent` hierarchy. There is no `.drills/` directory tree and no\nclaim.json — **beads IS the memory system**.\n\nPrereq: `bro` on PATH or `npx -y @broject/bro@0`, `bd init` done.\n\n## Commands\n\n| Command | What it does |\n| ------- | ------------ |\n| `bro drill down <title> [--under ID] [--ephemeral]` | New child frame under the current leaf (or a root). `--ephemeral` = wisp, no audit trail |\n| `bro drill up --result T [--prevent T]… [--evidence R]…` | Close the current frame. `--result` is **mandatory** (CLI-enforced). `--prevent` is policy-mandatory when the drill found an error, gap, or reusable lesson — each item spawns a `prevention`-labeled task linked `discovered-from` the frame. `--evidence` records a provenance ref (sha/PR) |\n| `bro unwind …` | Alias for `drill up` — collapse a solved frame into its parent |\n| `bro drill current` | The active leaf frame (deepest open path) |\n| `bro drill tree` | All drill hierarchies (● open / ○ closed) |\n| `bro drill list` | Open frames |\n| `bro drill distill ID` | `bd mol distill` — turn a successful drill tree into a reusable proto/formula |\n\n## Policy\n\n- **Narrow → investigate → ascend → prevent.** Descend only into a\n strictly narrower problem. A frame that returns nothing teaches\n nothing — the CLI refuses `drill up` without `--result`.\n- **Found a root cause or a reusable lesson? `--prevent` is the policy,\n not a suggestion.** The CLI can't judge whether a lesson exists — that\n call is yours; when it does, each item becomes follow-up work in beads,\n not a footnote.\n- **Isolation.** A child frame gets only task-relevant context; the\n curated memo lands as a `bd note` on the closed child (prevention items\n become beads linked `discovered-from` it) — never the raw transcript.\n- **Can't ascend past open children** — bd blocks the close; ascend\n bottom-up.\n- **Drill → distill → formula** is the learning loop: a drill tree that\n solved a problem well can be distilled into a proto and re-poured next\n time (`bd mol pour`/`bd mol wisp`).\n- Lifecycle is auditable for persistent frames: `claim` on down,\n `handoff` on up, evidence refs as provenance events\n (`bd provenance log <id>`). `--ephemeral` wisps skip all provenance —\n by design they leave no audit trail.\n",
|
package/dist/plugin.js
CHANGED