@open-tomato/rafa 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/bundled/skills/dev-planner/SKILL.md +20 -0
- package/dist/cli.js +2986 -1656
- package/dist/effort/store/index.js +1 -1
- package/dist/{index-am92b8qp.js → index-ae290g0m.js} +2 -1
- package/dist/{index-pzpk59ma.js → index-e30zazs6.js} +1247 -567
- package/dist/{index-kv51t295.js → index-gzr2gtmh.js} +3153 -2559
- package/dist/index.js +2 -2
- package/dist/plan-prompt.md +7 -1
- package/dist/templates/epic-guard.yml +50 -0
- package/dist/templates/spec.md +4 -0
- package/package.json +2 -2
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
gitRemoteUrl,
|
|
32
32
|
interruptClaudeSessions,
|
|
33
33
|
mergeFrontmatter,
|
|
34
|
+
moduleOfLabels,
|
|
34
35
|
normalizeRemote,
|
|
35
36
|
parseInstinct,
|
|
36
37
|
planFilePath,
|
|
@@ -42,11 +43,12 @@ import {
|
|
|
42
43
|
scanLayout,
|
|
43
44
|
selectEffortStore,
|
|
44
45
|
sqliteStorePath,
|
|
46
|
+
typeOfLabels,
|
|
45
47
|
withSqliteStore,
|
|
46
48
|
writeFrontmatter,
|
|
47
49
|
writeInstinct,
|
|
48
50
|
writeSqliteStore
|
|
49
|
-
} from "./index-
|
|
51
|
+
} from "./index-ae290g0m.js";
|
|
50
52
|
import {
|
|
51
53
|
CHANGE_LEVELS,
|
|
52
54
|
CONFIG_DEFAULTS,
|
|
@@ -5129,7 +5131,7 @@ async function report(args, repoRoot) {
|
|
|
5129
5131
|
|
|
5130
5132
|
// package.json
|
|
5131
5133
|
var name = "@open-tomato/rafa";
|
|
5132
|
-
var version = "0.
|
|
5134
|
+
var version = "0.22.0";
|
|
5133
5135
|
|
|
5134
5136
|
// src/modules/manifest.ts
|
|
5135
5137
|
var MANIFEST_VERSION = 1;
|
|
@@ -5672,6 +5674,690 @@ var planListCommand = {
|
|
|
5672
5674
|
};
|
|
5673
5675
|
var list_default = Object.freeze(planListCommand);
|
|
5674
5676
|
|
|
5677
|
+
// src/board/blocked.ts
|
|
5678
|
+
var SPEC_BLOCKED_LABEL = "spec:blocked";
|
|
5679
|
+
var BLOCKED_BY_FIELD = "Blocked by";
|
|
5680
|
+
var FENCE_LINE = /^ {0,3}(?:```|~~~)/u;
|
|
5681
|
+
var FIELD_LINE = /^ {0,3}(?:[-*+]\s+)?[*_]{0,2}blocked\s+by[*_]{0,2}\s*:(.*)$/iu;
|
|
5682
|
+
var ISSUE_ID = /#(\d+)/gu;
|
|
5683
|
+
var FOREIGN_ID = /[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+#\d+/gu;
|
|
5684
|
+
var REMEDY = `name them as "${BLOCKED_BY_FIELD}: #24 #26", or take the ${SPEC_BLOCKED_LABEL} label off`;
|
|
5685
|
+
function hasSpecBlockedLabel(labels) {
|
|
5686
|
+
return labels.some((label) => label.trim().toLowerCase() === SPEC_BLOCKED_LABEL);
|
|
5687
|
+
}
|
|
5688
|
+
function findFieldLine(body) {
|
|
5689
|
+
const lines = body.split(`
|
|
5690
|
+
`).map((line) => line.replace(/\r$/u, ""));
|
|
5691
|
+
let fenced = false;
|
|
5692
|
+
for (const [index, line] of lines.entries()) {
|
|
5693
|
+
if (FENCE_LINE.test(line)) {
|
|
5694
|
+
fenced = !fenced;
|
|
5695
|
+
continue;
|
|
5696
|
+
}
|
|
5697
|
+
if (fenced)
|
|
5698
|
+
continue;
|
|
5699
|
+
const found = FIELD_LINE.exec(line);
|
|
5700
|
+
if (found === null)
|
|
5701
|
+
continue;
|
|
5702
|
+
return { number: index + 1, text: (found[1] ?? "").trim() };
|
|
5703
|
+
}
|
|
5704
|
+
return null;
|
|
5705
|
+
}
|
|
5706
|
+
function idsIn(text) {
|
|
5707
|
+
const found = [];
|
|
5708
|
+
for (const match of text.matchAll(ISSUE_ID)) {
|
|
5709
|
+
const id = Number(match[1]);
|
|
5710
|
+
if (!Number.isSafeInteger(id) || id < 1 || found.includes(id))
|
|
5711
|
+
continue;
|
|
5712
|
+
found.push(id);
|
|
5713
|
+
}
|
|
5714
|
+
return found;
|
|
5715
|
+
}
|
|
5716
|
+
function foreignIn(text) {
|
|
5717
|
+
const found = [];
|
|
5718
|
+
for (const [token] of text.matchAll(FOREIGN_ID)) {
|
|
5719
|
+
if (!found.includes(token))
|
|
5720
|
+
found.push(token);
|
|
5721
|
+
}
|
|
5722
|
+
return found;
|
|
5723
|
+
}
|
|
5724
|
+
var NAMED_NOTHING = { ids: [], foreign: [] };
|
|
5725
|
+
function reading(kind, issue3, found, named, unknown) {
|
|
5726
|
+
return {
|
|
5727
|
+
kind,
|
|
5728
|
+
issue: issue3,
|
|
5729
|
+
line: found?.number ?? null,
|
|
5730
|
+
text: found?.text ?? null,
|
|
5731
|
+
blockers: named.ids,
|
|
5732
|
+
foreign: named.foreign,
|
|
5733
|
+
unknown
|
|
5734
|
+
};
|
|
5735
|
+
}
|
|
5736
|
+
function readBlockedBy(issue3, body, known) {
|
|
5737
|
+
const found = findFieldLine(body);
|
|
5738
|
+
if (found === null)
|
|
5739
|
+
return reading("no-line", issue3, null, NAMED_NOTHING, []);
|
|
5740
|
+
const foreign = foreignIn(found.text);
|
|
5741
|
+
const ids = idsIn(found.text.replace(FOREIGN_ID, " "));
|
|
5742
|
+
const named = { ids, foreign };
|
|
5743
|
+
if (ids.length === 0)
|
|
5744
|
+
return reading("no-ids", issue3, found, named, []);
|
|
5745
|
+
const unknown = known === undefined ? [] : ids.filter((id) => id !== issue3 && !known.has(id));
|
|
5746
|
+
if (ids.includes(issue3))
|
|
5747
|
+
return reading("self-reference", issue3, found, named, unknown);
|
|
5748
|
+
if (unknown.length > 0)
|
|
5749
|
+
return reading("unknown-issue", issue3, found, named, unknown);
|
|
5750
|
+
return reading("blocked", issue3, found, named, unknown);
|
|
5751
|
+
}
|
|
5752
|
+
function nameIds(ids) {
|
|
5753
|
+
return ids.map((id) => `#${String(id)}`).join(" ");
|
|
5754
|
+
}
|
|
5755
|
+
function faultClause(read) {
|
|
5756
|
+
const at = `on line ${String(read.line ?? 0)}`;
|
|
5757
|
+
if (read.kind === "no-line") {
|
|
5758
|
+
return `is labelled ${SPEC_BLOCKED_LABEL} and its body carries no "${BLOCKED_BY_FIELD}:" line`;
|
|
5759
|
+
}
|
|
5760
|
+
if (read.kind === "no-ids" && read.foreign.length > 0) {
|
|
5761
|
+
return `has a "${BLOCKED_BY_FIELD}:" line ${at} naming only issues on other repositories: ${read.foreign.join(" ")}`;
|
|
5762
|
+
}
|
|
5763
|
+
if (read.kind === "no-ids") {
|
|
5764
|
+
return `has a "${BLOCKED_BY_FIELD}:" line ${at} naming no issue: "${read.text ?? ""}"`;
|
|
5765
|
+
}
|
|
5766
|
+
if (read.kind === "self-reference") {
|
|
5767
|
+
return `has a "${BLOCKED_BY_FIELD}:" line ${at} naming itself`;
|
|
5768
|
+
}
|
|
5769
|
+
return `has a "${BLOCKED_BY_FIELD}:" line ${at} naming ${nameIds(read.unknown)}, which the board has no issue for`;
|
|
5770
|
+
}
|
|
5771
|
+
function blockedFaultMessage(read) {
|
|
5772
|
+
if (read.kind === "blocked") {
|
|
5773
|
+
throw new TypeError(`board blocked: #${String(read.issue)} names ${nameIds(read.blockers)} and has no fault to report`);
|
|
5774
|
+
}
|
|
5775
|
+
return `#${String(read.issue)} ${faultClause(read)}; ${REMEDY}`;
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
// src/board/naming.ts
|
|
5779
|
+
import path from "path";
|
|
5780
|
+
var ID_PREFIX = "rafa";
|
|
5781
|
+
var BRANCH_PREFIX = "feat";
|
|
5782
|
+
var SPEC_EXTENSION = ".md";
|
|
5783
|
+
var NOTES_SUFFIX = "-notes";
|
|
5784
|
+
var MAX_SLUG_WORDS = 4;
|
|
5785
|
+
var MIN_SLUG_WORDS = 2;
|
|
5786
|
+
var FALLBACK_SLUG = "untitled";
|
|
5787
|
+
var STOP_WORDS = new Set([
|
|
5788
|
+
"a",
|
|
5789
|
+
"an",
|
|
5790
|
+
"and",
|
|
5791
|
+
"as",
|
|
5792
|
+
"at",
|
|
5793
|
+
"be",
|
|
5794
|
+
"by",
|
|
5795
|
+
"for",
|
|
5796
|
+
"from",
|
|
5797
|
+
"in",
|
|
5798
|
+
"into",
|
|
5799
|
+
"is",
|
|
5800
|
+
"it",
|
|
5801
|
+
"its",
|
|
5802
|
+
"of",
|
|
5803
|
+
"on",
|
|
5804
|
+
"or",
|
|
5805
|
+
"that",
|
|
5806
|
+
"the",
|
|
5807
|
+
"this",
|
|
5808
|
+
"to",
|
|
5809
|
+
"with"
|
|
5810
|
+
]);
|
|
5811
|
+
var CLAUSE_BREAK = /[,;:([\u2013\u2014]|\s-\s/u;
|
|
5812
|
+
var LEADING_ID = /^\s*#?rafa[-\s]?\d+\s*[-:.\u2013\u2014]?\s*/iu;
|
|
5813
|
+
var NON_WORD = /[^a-z0-9]+/gu;
|
|
5814
|
+
function requireIssueNumber(issue3) {
|
|
5815
|
+
if (!Number.isInteger(issue3) || issue3 <= 0) {
|
|
5816
|
+
throw new RangeError(`An issue number must be a positive integer, not ${String(issue3)}.`);
|
|
5817
|
+
}
|
|
5818
|
+
return issue3;
|
|
5819
|
+
}
|
|
5820
|
+
function wordsOf(title) {
|
|
5821
|
+
return title.toLowerCase().replace(NON_WORD, " ").split(" ").filter((word) => word !== "");
|
|
5822
|
+
}
|
|
5823
|
+
function withoutStopWords(words) {
|
|
5824
|
+
const kept = words.filter((word) => !STOP_WORDS.has(word));
|
|
5825
|
+
return kept.length > 0 ? kept : words;
|
|
5826
|
+
}
|
|
5827
|
+
function slugFromTitle(title) {
|
|
5828
|
+
const withoutId = title.replace(LEADING_ID, "");
|
|
5829
|
+
const [firstClause = ""] = withoutId.split(CLAUSE_BREAK);
|
|
5830
|
+
const whole = withoutStopWords(wordsOf(withoutId));
|
|
5831
|
+
const clause = withoutStopWords(wordsOf(firstClause));
|
|
5832
|
+
const chosen = clause.length >= MIN_SLUG_WORDS ? clause : whole;
|
|
5833
|
+
if (chosen.length === 0)
|
|
5834
|
+
return FALLBACK_SLUG;
|
|
5835
|
+
return chosen.slice(0, MAX_SLUG_WORDS).join("-");
|
|
5836
|
+
}
|
|
5837
|
+
function boardId(issue3) {
|
|
5838
|
+
return `${ID_PREFIX}-${requireIssueNumber(issue3)}`;
|
|
5839
|
+
}
|
|
5840
|
+
function planStub(issue3, title) {
|
|
5841
|
+
return `${boardId(issue3)}-${slugFromTitle(title)}`;
|
|
5842
|
+
}
|
|
5843
|
+
function specFileName(issue3, title) {
|
|
5844
|
+
return `${planStub(issue3, title)}${SPEC_EXTENSION}`;
|
|
5845
|
+
}
|
|
5846
|
+
function specPath(specsDir, issue3, title) {
|
|
5847
|
+
return path.join(specsDir, specFileName(issue3, title));
|
|
5848
|
+
}
|
|
5849
|
+
function notesFileName(issue3) {
|
|
5850
|
+
return `${boardId(issue3)}${NOTES_SUFFIX}${SPEC_EXTENSION}`;
|
|
5851
|
+
}
|
|
5852
|
+
function notesPath(specsDir, issue3) {
|
|
5853
|
+
return path.join(specsDir, notesFileName(issue3));
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
// src/board/roadmap.ts
|
|
5857
|
+
var PREFIX = "board roadmap";
|
|
5858
|
+
var ROADMAP_TITLE = "Roadmap";
|
|
5859
|
+
var ROADMAP_SETTING = "roadmap.issue";
|
|
5860
|
+
var ROADMAP_REFUSAL_EXIT = 2;
|
|
5861
|
+
var DEFAULT_REMOTE = "origin";
|
|
5862
|
+
var OPEN_PULL_REQUEST_LIMIT = 100;
|
|
5863
|
+
var PR_LIST_FIELDS = "number,headRefName,body";
|
|
5864
|
+
var ISSUE_LIST_FIELDS = "number,title";
|
|
5865
|
+
var TASK_LINE = /^\s*[-*+]\s+\[([ xX])\]\s+#(\d+)\s*(.*)$/u;
|
|
5866
|
+
var FENCE = /^\s*(?:`{3,}|~{3,})/u;
|
|
5867
|
+
var WHY_SEPARATOR = /^[-:,.\u2013\u2014]\s*/u;
|
|
5868
|
+
var CLOSING = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\b\s*:?\s*#(\d+)\b/giu;
|
|
5869
|
+
function whyOf(rest) {
|
|
5870
|
+
return rest.trim().replace(WHY_SEPARATOR, "").trim();
|
|
5871
|
+
}
|
|
5872
|
+
function parseRoadmapBody(body) {
|
|
5873
|
+
let lines = [];
|
|
5874
|
+
let fenced = false;
|
|
5875
|
+
body.split(/\r\n?|\n/u).forEach((text, index) => {
|
|
5876
|
+
if (FENCE.test(text)) {
|
|
5877
|
+
fenced = !fenced;
|
|
5878
|
+
return;
|
|
5879
|
+
}
|
|
5880
|
+
if (fenced)
|
|
5881
|
+
return;
|
|
5882
|
+
const found = TASK_LINE.exec(text);
|
|
5883
|
+
if (found === null)
|
|
5884
|
+
return;
|
|
5885
|
+
const [, box = "", digits = "", rest = ""] = found;
|
|
5886
|
+
const issue3 = Number.parseInt(digits, 10);
|
|
5887
|
+
if (!Number.isSafeInteger(issue3) || issue3 < 1)
|
|
5888
|
+
return;
|
|
5889
|
+
lines = [...lines, {
|
|
5890
|
+
issue: issue3,
|
|
5891
|
+
ticked: box.trim() !== "",
|
|
5892
|
+
why: whyOf(rest),
|
|
5893
|
+
lineNumber: index + 1
|
|
5894
|
+
}];
|
|
5895
|
+
});
|
|
5896
|
+
return Object.freeze(lines);
|
|
5897
|
+
}
|
|
5898
|
+
function detailOf(result, command) {
|
|
5899
|
+
const written = result.stderr.trim() || result.stdout.trim();
|
|
5900
|
+
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
5901
|
+
}
|
|
5902
|
+
function parseRows(stdout, command) {
|
|
5903
|
+
let payload;
|
|
5904
|
+
try {
|
|
5905
|
+
payload = JSON.parse(stdout);
|
|
5906
|
+
} catch (error) {
|
|
5907
|
+
throw new Error(`${PREFIX}: ${command} wrote output that is not JSON: ${messageOf(error)}`, { cause: error });
|
|
5908
|
+
}
|
|
5909
|
+
if (!Array.isArray(payload)) {
|
|
5910
|
+
throw new Error(`${PREFIX}: ${command} answered ${describeValue(payload)}, expected a list`);
|
|
5911
|
+
}
|
|
5912
|
+
return payload;
|
|
5913
|
+
}
|
|
5914
|
+
function rowNumber(row, key, command, where) {
|
|
5915
|
+
const value = isMapping(row) ? row[key] : null;
|
|
5916
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
5917
|
+
throw new Error(`${PREFIX}: ${command} answered ${where}.${key} as ${describeValue(value)}, expected a positive whole number`);
|
|
5918
|
+
}
|
|
5919
|
+
return value;
|
|
5920
|
+
}
|
|
5921
|
+
function rowText(row, key, command, where) {
|
|
5922
|
+
const value = isMapping(row) ? row[key] : null;
|
|
5923
|
+
if (typeof value !== "string") {
|
|
5924
|
+
throw new Error(`${PREFIX}: ${command} answered ${where}.${key} as ${describeValue(value)}, expected a string`);
|
|
5925
|
+
}
|
|
5926
|
+
return value;
|
|
5927
|
+
}
|
|
5928
|
+
function createGhRoadmapSearch(options) {
|
|
5929
|
+
const { gh } = options;
|
|
5930
|
+
const search = `${ROADMAP_TITLE} in:title`;
|
|
5931
|
+
const args = ["issue", "list", "--state", "open", "--search", search, "--json", ISSUE_LIST_FIELDS];
|
|
5932
|
+
const command = `gh issue list --state open --search "${search}" --json ${ISSUE_LIST_FIELDS}`;
|
|
5933
|
+
return async () => {
|
|
5934
|
+
const result = await gh(args);
|
|
5935
|
+
if (!result.ok)
|
|
5936
|
+
throw new Error(`${PREFIX}: ${detailOf(result, command)}`);
|
|
5937
|
+
return Object.freeze(parseRows(result.stdout, command).map((row, index) => ({
|
|
5938
|
+
number: rowNumber(row, "number", command, `issue ${String(index)}`),
|
|
5939
|
+
title: rowText(row, "title", command, `issue ${String(index)}`)
|
|
5940
|
+
})));
|
|
5941
|
+
};
|
|
5942
|
+
}
|
|
5943
|
+
function noRoadmapMessage() {
|
|
5944
|
+
return `no open issue is titled ${ROADMAP_TITLE}, so there is no roadmap to read;` + ` run rafa init --board to open one, or set ${ROADMAP_SETTING} to the issue that holds the order`;
|
|
5945
|
+
}
|
|
5946
|
+
function severalRoadmapsMessage(numbers) {
|
|
5947
|
+
const named = numbers.map((number) => `#${String(number)}`).join(", ");
|
|
5948
|
+
return `${String(numbers.length)} open issues are titled ${ROADMAP_TITLE} (${named}),` + ` so which one holds the order is not this command to guess; set ${ROADMAP_SETTING} to it`;
|
|
5949
|
+
}
|
|
5950
|
+
async function resolveRoadmapIssue(options) {
|
|
5951
|
+
const { configured, search } = options;
|
|
5952
|
+
if (configured !== null)
|
|
5953
|
+
return configured;
|
|
5954
|
+
const wanted = ROADMAP_TITLE.toLowerCase();
|
|
5955
|
+
const titled = (await search()).filter((issue3) => issue3.title.trim().toLowerCase() === wanted);
|
|
5956
|
+
if (titled.length === 0)
|
|
5957
|
+
throw new CommandExit(ROADMAP_REFUSAL_EXIT, noRoadmapMessage());
|
|
5958
|
+
if (titled.length > 1) {
|
|
5959
|
+
throw new CommandExit(ROADMAP_REFUSAL_EXIT, severalRoadmapsMessage(titled.map((issue3) => issue3.number)));
|
|
5960
|
+
}
|
|
5961
|
+
return titled[0]?.number ?? 0;
|
|
5962
|
+
}
|
|
5963
|
+
function createGhOpenPullRequests(options) {
|
|
5964
|
+
const { gh } = options;
|
|
5965
|
+
const limit = String(OPEN_PULL_REQUEST_LIMIT);
|
|
5966
|
+
const args = ["pr", "list", "--state", "open", "--json", PR_LIST_FIELDS, "--limit", limit];
|
|
5967
|
+
const command = `gh pr list --state open --json ${PR_LIST_FIELDS} --limit ${limit}`;
|
|
5968
|
+
return async () => {
|
|
5969
|
+
const result = await gh(args);
|
|
5970
|
+
if (!result.ok)
|
|
5971
|
+
throw new Error(`${PREFIX}: ${detailOf(result, command)}`);
|
|
5972
|
+
return Object.freeze(parseRows(result.stdout, command).map((row, index) => ({
|
|
5973
|
+
number: rowNumber(row, "number", command, `pull request ${String(index)}`),
|
|
5974
|
+
headRefName: rowText(row, "headRefName", command, `pull request ${String(index)}`),
|
|
5975
|
+
body: rowText(row, "body", command, `pull request ${String(index)}`)
|
|
5976
|
+
})));
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
function closedIssuesIn(body) {
|
|
5980
|
+
return Object.freeze([...body.matchAll(CLOSING)].map((found) => Number.parseInt(found[1] ?? "", 10)).filter((issue3) => Number.isSafeInteger(issue3) && issue3 > 0));
|
|
5981
|
+
}
|
|
5982
|
+
function branchClaims(ref, issue3) {
|
|
5983
|
+
const claim = `${BRANCH_PREFIX}/${boardId(issue3)}`;
|
|
5984
|
+
return new RegExp(`(?:^|/)${claim}(?:-|$)`, "u").test(ref);
|
|
5985
|
+
}
|
|
5986
|
+
function outputLines(stdout) {
|
|
5987
|
+
return stdout.split(`
|
|
5988
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
5989
|
+
}
|
|
5990
|
+
function scanClaimBranches(git, remote = DEFAULT_REMOTE) {
|
|
5991
|
+
const local = git(["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes"]);
|
|
5992
|
+
const pushed = git(["ls-remote", "--heads", remote]);
|
|
5993
|
+
const localRefs = local.ok ? outputLines(local.stdout) : [];
|
|
5994
|
+
const pushedRefs = pushed.ok ? outputLines(pushed.stdout).map((line) => line.split("\t").at(-1) ?? "") : [];
|
|
5995
|
+
const problems = [
|
|
5996
|
+
local.ok ? "" : `the branches of this checkout could not be read, so a branch held only here was not seen: ${detailOf(local, "git for-each-ref")}`,
|
|
5997
|
+
pushed.ok ? "" : `the branches on ${remote} could not be read, so a branch pushed but not fetched was not seen: ${detailOf(pushed, `git ls-remote --heads ${remote}`)}`
|
|
5998
|
+
].filter((problem) => problem !== "");
|
|
5999
|
+
return Object.freeze({
|
|
6000
|
+
refs: Object.freeze([...localRefs, ...pushedRefs].filter((ref) => ref !== "")),
|
|
6001
|
+
problems: Object.freeze(problems)
|
|
6002
|
+
});
|
|
6003
|
+
}
|
|
6004
|
+
function createRoadmapReadings(options) {
|
|
6005
|
+
const { issues, branches, pullRequests } = options;
|
|
6006
|
+
let open = null;
|
|
6007
|
+
return Object.freeze({
|
|
6008
|
+
isClosed: async (issue3) => (await issues(issue3)).state === "CLOSED",
|
|
6009
|
+
branchFor: (issue3) => branches.refs.find((ref) => branchClaims(ref, issue3)) ?? null,
|
|
6010
|
+
pullRequestFor: async (issue3) => {
|
|
6011
|
+
open = open ?? pullRequests();
|
|
6012
|
+
const closing = (await open).find((pull) => closedIssuesIn(pull.body).includes(issue3));
|
|
6013
|
+
return closing?.number ?? null;
|
|
6014
|
+
}
|
|
6015
|
+
});
|
|
6016
|
+
}
|
|
6017
|
+
function skipSentence(skip) {
|
|
6018
|
+
const id = `#${String(skip.line.issue)}`;
|
|
6019
|
+
if (skip.reason === "ticked")
|
|
6020
|
+
return `${id} done: ticked on the roadmap`;
|
|
6021
|
+
if (skip.reason === "closed")
|
|
6022
|
+
return `${id} done: the issue is closed`;
|
|
6023
|
+
return skip.reason === "branch" ? `${id} taken: branch ${skip.detail} exists` : `${id} taken: PR ${skip.detail} open`;
|
|
6024
|
+
}
|
|
6025
|
+
async function readRoadmapSkip(line, readings) {
|
|
6026
|
+
if (line.ticked)
|
|
6027
|
+
return { line, reason: "ticked", detail: "" };
|
|
6028
|
+
if (await readings.isClosed(line.issue))
|
|
6029
|
+
return { line, reason: "closed", detail: "" };
|
|
6030
|
+
const branch = readings.branchFor(line.issue);
|
|
6031
|
+
if (branch !== null)
|
|
6032
|
+
return { line, reason: "branch", detail: branch };
|
|
6033
|
+
const pull = await readings.pullRequestFor(line.issue);
|
|
6034
|
+
return pull === null ? null : { line, reason: "pull-request", detail: `#${String(pull)}` };
|
|
6035
|
+
}
|
|
6036
|
+
async function pickNextRoadmapLine(lines, readings) {
|
|
6037
|
+
let skipped2 = [];
|
|
6038
|
+
for (const line of lines) {
|
|
6039
|
+
const skip = await readRoadmapSkip(line, readings);
|
|
6040
|
+
if (skip === null)
|
|
6041
|
+
return Object.freeze({ line, skipped: Object.freeze(skipped2) });
|
|
6042
|
+
skipped2 = [...skipped2, skip];
|
|
6043
|
+
}
|
|
6044
|
+
return Object.freeze({ line: null, skipped: Object.freeze(skipped2) });
|
|
6045
|
+
}
|
|
6046
|
+
function exhaustedMessage(roadmap, skipped2) {
|
|
6047
|
+
const id = `issue #${String(roadmap)}`;
|
|
6048
|
+
return skipped2.length === 0 ? `the roadmap, ${id}, carries no "- [ ] #<n>" line; add the next spec to its task list` : `every line of the roadmap, ${id}, is done or taken (${String(skipped2.length)} of them);` + " open the next spec issue and add it to the task list";
|
|
6049
|
+
}
|
|
6050
|
+
|
|
6051
|
+
// src/board/epic-body.ts
|
|
6052
|
+
var ESTIMATE_FIELD = "Estimate";
|
|
6053
|
+
var DATE_FIELD = "Date";
|
|
6054
|
+
var OWNS_FIELD = "Owns";
|
|
6055
|
+
var DATE_SHAPE = "YYYY-MM-DD";
|
|
6056
|
+
var FENCE2 = /^\s*(?:`{3,}|~{3,})/u;
|
|
6057
|
+
var HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/u;
|
|
6058
|
+
var CRITERIA_TITLE = /^acceptance\s+criteria\s*:?$/iu;
|
|
6059
|
+
var ISO_DAY = /^(\d{4})-(\d{2})-(\d{2})$/u;
|
|
6060
|
+
var OWNS_SEPARATOR = /[\s,]+/u;
|
|
6061
|
+
function bodyLines(body) {
|
|
6062
|
+
let fenced = false;
|
|
6063
|
+
return body.split(/\r\n?|\n/u).map((text) => {
|
|
6064
|
+
if (FENCE2.test(text)) {
|
|
6065
|
+
fenced = !fenced;
|
|
6066
|
+
return { text, fenced: true };
|
|
6067
|
+
}
|
|
6068
|
+
return { text, fenced };
|
|
6069
|
+
});
|
|
6070
|
+
}
|
|
6071
|
+
function fieldPattern(name2) {
|
|
6072
|
+
return new RegExp(`^ {0,3}(?:[-*+]\\s+)?[*_]{0,2}${name2}[*_]{0,2}\\s*:[*_]{0,2}(.*)$`, "iu");
|
|
6073
|
+
}
|
|
6074
|
+
function findField(lines, name2) {
|
|
6075
|
+
const pattern = fieldPattern(name2);
|
|
6076
|
+
for (const [index, line] of lines.entries()) {
|
|
6077
|
+
if (line.fenced)
|
|
6078
|
+
continue;
|
|
6079
|
+
const found = pattern.exec(line.text);
|
|
6080
|
+
if (found !== null)
|
|
6081
|
+
return { number: index + 1, text: (found[1] ?? "").trim() };
|
|
6082
|
+
}
|
|
6083
|
+
return null;
|
|
6084
|
+
}
|
|
6085
|
+
function headingLevel(line) {
|
|
6086
|
+
if (line.fenced)
|
|
6087
|
+
return 0;
|
|
6088
|
+
return HEADING.exec(line.text)?.[1]?.length ?? 0;
|
|
6089
|
+
}
|
|
6090
|
+
function findCriteria(lines) {
|
|
6091
|
+
const start = lines.findIndex((line) => headingLevel(line) > 0 && CRITERIA_TITLE.test(HEADING.exec(line.text)?.[2] ?? ""));
|
|
6092
|
+
const opening = lines[start];
|
|
6093
|
+
if (opening === undefined)
|
|
6094
|
+
return null;
|
|
6095
|
+
const level = headingLevel(opening);
|
|
6096
|
+
const after = lines.slice(start + 1);
|
|
6097
|
+
const end = after.findIndex((line) => {
|
|
6098
|
+
const found = headingLevel(line);
|
|
6099
|
+
return found > 0 && found <= level;
|
|
6100
|
+
});
|
|
6101
|
+
const section = end === -1 ? after : after.slice(0, end);
|
|
6102
|
+
return {
|
|
6103
|
+
number: start + 1,
|
|
6104
|
+
text: section.map((line) => line.text).join(`
|
|
6105
|
+
`).trim()
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
function isCalendarDay(text) {
|
|
6109
|
+
const found = ISO_DAY.exec(text);
|
|
6110
|
+
if (found === null)
|
|
6111
|
+
return false;
|
|
6112
|
+
const [, year = "", month = "", day = ""] = found;
|
|
6113
|
+
const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
6114
|
+
return date.toISOString().slice(0, DATE_SHAPE.length) === text;
|
|
6115
|
+
}
|
|
6116
|
+
function ownedFolders(text) {
|
|
6117
|
+
const named = text.replaceAll("`", " ").split(OWNS_SEPARATOR).filter((folder) => folder !== "");
|
|
6118
|
+
return [...new Set(named)];
|
|
6119
|
+
}
|
|
6120
|
+
function absent(kind, found) {
|
|
6121
|
+
return { kind, line: found?.number ?? null, text: found?.text ?? null };
|
|
6122
|
+
}
|
|
6123
|
+
function readEpicBody(body) {
|
|
6124
|
+
const lines = bodyLines(body);
|
|
6125
|
+
const criteria = findCriteria(lines);
|
|
6126
|
+
const estimate = findField(lines, ESTIMATE_FIELD);
|
|
6127
|
+
const date = findField(lines, DATE_FIELD);
|
|
6128
|
+
const owns = findField(lines, OWNS_FIELD);
|
|
6129
|
+
const hasCriteria = criteria !== null && criteria.text !== "";
|
|
6130
|
+
const hasEstimate = estimate !== null && estimate.text !== "";
|
|
6131
|
+
const isDated = date !== null && isCalendarDay(date.text);
|
|
6132
|
+
const problems = [
|
|
6133
|
+
hasCriteria ? null : absent("no-criteria", criteria),
|
|
6134
|
+
hasEstimate ? null : absent("no-estimate", estimate),
|
|
6135
|
+
date === null || isDated ? null : { kind: "malformed-date", line: date.number, text: date.text }
|
|
6136
|
+
];
|
|
6137
|
+
return Object.freeze({
|
|
6138
|
+
criteria: hasCriteria ? criteria.text : null,
|
|
6139
|
+
estimate: hasEstimate ? estimate.text : null,
|
|
6140
|
+
date: isDated ? date.text : null,
|
|
6141
|
+
owns: Object.freeze(owns === null ? [] : ownedFolders(owns.text)),
|
|
6142
|
+
lines: parseRoadmapBody(body),
|
|
6143
|
+
problems: Object.freeze(problems.filter((problem) => problem !== null))
|
|
6144
|
+
});
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
// src/board/epics.ts
|
|
6148
|
+
var EPIC_LABEL_PREFIX = "epic:";
|
|
6149
|
+
var NOT_PLANNED_REASON = "NOT_PLANNED";
|
|
6150
|
+
var NO_REASON = "the board listing failed and gave no reason";
|
|
6151
|
+
var NO_PROGRESS = Object.freeze({ done: 0, total: 0, notPlanned: 0 });
|
|
6152
|
+
function epicSlugsOf(labels) {
|
|
6153
|
+
const slugs = labels.filter((label) => label.startsWith(EPIC_LABEL_PREFIX)).map((label) => label.slice(EPIC_LABEL_PREFIX.length));
|
|
6154
|
+
return Object.freeze([...new Set(slugs)]);
|
|
6155
|
+
}
|
|
6156
|
+
function isNotPlanned(issue3) {
|
|
6157
|
+
return issue3.state === "CLOSED" && issue3.stateReason === NOT_PLANNED_REASON;
|
|
6158
|
+
}
|
|
6159
|
+
function groupByEpicLabel(issues) {
|
|
6160
|
+
const groups = new Map;
|
|
6161
|
+
const sorted = [...issues].sort((left, right) => left.number - right.number);
|
|
6162
|
+
for (const issue3 of sorted) {
|
|
6163
|
+
if (issue3.type === "epic")
|
|
6164
|
+
continue;
|
|
6165
|
+
for (const slug of epicSlugsOf(issue3.labels)) {
|
|
6166
|
+
groups.set(slug, [...groups.get(slug) ?? [], issue3]);
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
return groups;
|
|
6170
|
+
}
|
|
6171
|
+
function progressOf(members) {
|
|
6172
|
+
const counted = members.filter((member) => !isNotPlanned(member));
|
|
6173
|
+
return Object.freeze({
|
|
6174
|
+
done: counted.filter((member) => member.state === "CLOSED").length,
|
|
6175
|
+
total: counted.length,
|
|
6176
|
+
notPlanned: members.length - counted.length
|
|
6177
|
+
});
|
|
6178
|
+
}
|
|
6179
|
+
function countedStarted(members, claims) {
|
|
6180
|
+
return members.some((member) => !isNotPlanned(member) && (member.state === "CLOSED" || claims.has(member.number)));
|
|
6181
|
+
}
|
|
6182
|
+
function computedState(members, progress, claims) {
|
|
6183
|
+
if (members.length === 0)
|
|
6184
|
+
return "empty";
|
|
6185
|
+
if (progress.total > 0 && progress.done === progress.total)
|
|
6186
|
+
return "done";
|
|
6187
|
+
return countedStarted(members, claims) ? "in-progress" : "backlog";
|
|
6188
|
+
}
|
|
6189
|
+
function storedState(epic, members, claims) {
|
|
6190
|
+
if (epic.state === "OPEN")
|
|
6191
|
+
return "open";
|
|
6192
|
+
if (!isNotPlanned(epic))
|
|
6193
|
+
return "closed";
|
|
6194
|
+
const started = countedStarted(members, claims) || members.some((member) => claims.has(member.number));
|
|
6195
|
+
return started ? "cancelled" : "discarded";
|
|
6196
|
+
}
|
|
6197
|
+
function disagreementOf(number, state, stored) {
|
|
6198
|
+
const id = `epic #${String(number)}`;
|
|
6199
|
+
if (state === "done" && stored === "open")
|
|
6200
|
+
return `done, but ${id} is still open`;
|
|
6201
|
+
if (state !== "done" && state !== "unknown" && stored === "closed")
|
|
6202
|
+
return `${state}, but ${id} is closed`;
|
|
6203
|
+
return null;
|
|
6204
|
+
}
|
|
6205
|
+
function localDay(date) {
|
|
6206
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
6207
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
6208
|
+
return `${String(date.getFullYear())}-${month}-${day}`;
|
|
6209
|
+
}
|
|
6210
|
+
function ownersOf(blocker, view) {
|
|
6211
|
+
if (blocker.type === "epic")
|
|
6212
|
+
return [blocker.number];
|
|
6213
|
+
return epicSlugsOf(blocker.labels).flatMap((slug) => view.epicsBySlug.get(slug) ?? []);
|
|
6214
|
+
}
|
|
6215
|
+
function blockedByOf(self, members, view) {
|
|
6216
|
+
const owners = members.filter((member) => member.state === "OPEN").map((member) => readBlockedBy(member.number, member.body, view.known)).filter((reading2) => reading2.kind === "blocked").flatMap((reading2) => reading2.blockers).map((blocker) => view.byNumber.get(blocker)).filter((blocker) => blocker?.state === "OPEN").flatMap((blocker) => ownersOf(blocker, view)).filter((owner) => owner !== self);
|
|
6217
|
+
return Object.freeze([...new Set(owners)].sort((left, right) => left - right));
|
|
6218
|
+
}
|
|
6219
|
+
function epicOf(epic, view) {
|
|
6220
|
+
const slug = epicSlugsOf(epic.labels)[0] ?? null;
|
|
6221
|
+
const members = slug === null ? [] : view.groups.get(slug) ?? [];
|
|
6222
|
+
const body = readEpicBody(epic.body);
|
|
6223
|
+
const progress = progressOf(members);
|
|
6224
|
+
const state = computedState(members, progress, view.claims);
|
|
6225
|
+
const stored = storedState(epic, members, view.claims);
|
|
6226
|
+
return Object.freeze({
|
|
6227
|
+
number: epic.number,
|
|
6228
|
+
title: epic.title,
|
|
6229
|
+
slug,
|
|
6230
|
+
body,
|
|
6231
|
+
members: Object.freeze([...members]),
|
|
6232
|
+
state,
|
|
6233
|
+
reason: null,
|
|
6234
|
+
progress,
|
|
6235
|
+
blockedBy: blockedByOf(epic.number, members, view),
|
|
6236
|
+
late: body.date !== null && body.date < view.today && state !== "done",
|
|
6237
|
+
stored,
|
|
6238
|
+
disagreement: disagreementOf(epic.number, state, stored)
|
|
6239
|
+
});
|
|
6240
|
+
}
|
|
6241
|
+
function readEpics(input) {
|
|
6242
|
+
if (input.issues === null) {
|
|
6243
|
+
return Object.freeze({ epics: Object.freeze([]), unknown: input.reason ?? NO_REASON });
|
|
6244
|
+
}
|
|
6245
|
+
const epicIssues = input.issues.filter((issue3) => issue3.type === "epic").sort((left, right) => left.number - right.number);
|
|
6246
|
+
const epicsBySlug = new Map;
|
|
6247
|
+
for (const epic of epicIssues) {
|
|
6248
|
+
const slug = epicSlugsOf(epic.labels)[0];
|
|
6249
|
+
if (slug !== undefined)
|
|
6250
|
+
epicsBySlug.set(slug, [...epicsBySlug.get(slug) ?? [], epic.number]);
|
|
6251
|
+
}
|
|
6252
|
+
const view = {
|
|
6253
|
+
groups: groupByEpicLabel(input.issues),
|
|
6254
|
+
byNumber: new Map(input.issues.map((issue3) => [issue3.number, issue3])),
|
|
6255
|
+
epicsBySlug,
|
|
6256
|
+
known: new Set(input.issues.map((issue3) => issue3.number)),
|
|
6257
|
+
claims: input.claims,
|
|
6258
|
+
today: localDay(input.today)
|
|
6259
|
+
};
|
|
6260
|
+
return Object.freeze({
|
|
6261
|
+
epics: Object.freeze(epicIssues.map((epic) => epicOf(epic, view))),
|
|
6262
|
+
unknown: null
|
|
6263
|
+
});
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
// src/board/epic-problems.ts
|
|
6267
|
+
var HORIZON_LABEL_PREFIX = "horizon:";
|
|
6268
|
+
var EPIC_PROBLEM_KINDS = Object.freeze([
|
|
6269
|
+
"several-epic-labels",
|
|
6270
|
+
"orphan-label",
|
|
6271
|
+
"horizon",
|
|
6272
|
+
"unlabelled-checklist",
|
|
6273
|
+
"unlisted-member"
|
|
6274
|
+
]);
|
|
6275
|
+
function byNumber(issues) {
|
|
6276
|
+
return [...issues].sort((left, right) => left.number - right.number);
|
|
6277
|
+
}
|
|
6278
|
+
function severalEpicLabels(issues) {
|
|
6279
|
+
return issues.flatMap((issue3) => {
|
|
6280
|
+
const slugs = epicSlugsOf(issue3.labels);
|
|
6281
|
+
const [slug] = slugs;
|
|
6282
|
+
return slug === undefined || slugs.length < 2 ? [] : [{ kind: "several-epic-labels", issue: issue3.number, slug, slugs }];
|
|
6283
|
+
});
|
|
6284
|
+
}
|
|
6285
|
+
function orphanLabels(groups, owned) {
|
|
6286
|
+
return [...groups.entries()].filter(([slug]) => !owned.has(slug)).flatMap(([slug, carriers]) => carriers.map((carrier) => ({
|
|
6287
|
+
kind: "orphan-label",
|
|
6288
|
+
issue: carrier.number,
|
|
6289
|
+
slug
|
|
6290
|
+
})));
|
|
6291
|
+
}
|
|
6292
|
+
function horizons(epics) {
|
|
6293
|
+
return epics.flatMap((epic) => {
|
|
6294
|
+
const found = epic.labels.filter((label) => label.startsWith(HORIZON_LABEL_PREFIX));
|
|
6295
|
+
return found.length === 1 ? [] : [{
|
|
6296
|
+
kind: "horizon",
|
|
6297
|
+
issue: epic.number,
|
|
6298
|
+
slug: epicSlugsOf(epic.labels)[0] ?? null,
|
|
6299
|
+
horizons: Object.freeze(found)
|
|
6300
|
+
}];
|
|
6301
|
+
});
|
|
6302
|
+
}
|
|
6303
|
+
function orderProblems(epic, slug, listed, members) {
|
|
6304
|
+
const lines = parseRoadmapBody(epic.body);
|
|
6305
|
+
const named = new Set(lines.map((line) => line.issue));
|
|
6306
|
+
const seen = new Set;
|
|
6307
|
+
const unlabelled = lines.flatMap((line) => {
|
|
6308
|
+
const spec = listed.get(line.issue);
|
|
6309
|
+
if (seen.has(line.issue) || spec === undefined || spec.type === "epic")
|
|
6310
|
+
return [];
|
|
6311
|
+
seen.add(line.issue);
|
|
6312
|
+
return epicSlugsOf(spec.labels).includes(slug) ? [] : [{ kind: "unlabelled-checklist", issue: line.issue, slug, epic: epic.number, lineNumber: line.lineNumber }];
|
|
6313
|
+
});
|
|
6314
|
+
const unlisted = members.filter((member) => member.type !== "bug" && !named.has(member.number)).map((member) => ({ kind: "unlisted-member", issue: member.number, slug, epic: epic.number }));
|
|
6315
|
+
return [...unlabelled, ...unlisted];
|
|
6316
|
+
}
|
|
6317
|
+
function compareProblems(left, right) {
|
|
6318
|
+
const kind = EPIC_PROBLEM_KINDS.indexOf(left.kind) - EPIC_PROBLEM_KINDS.indexOf(right.kind);
|
|
6319
|
+
if (kind !== 0)
|
|
6320
|
+
return kind;
|
|
6321
|
+
if (left.issue !== right.issue)
|
|
6322
|
+
return left.issue - right.issue;
|
|
6323
|
+
return (left.slug ?? "").localeCompare(right.slug ?? "");
|
|
6324
|
+
}
|
|
6325
|
+
function readEpicProblems(issues) {
|
|
6326
|
+
const sorted = byNumber(issues);
|
|
6327
|
+
const epics = sorted.filter((issue3) => issue3.type === "epic");
|
|
6328
|
+
const groups = groupByEpicLabel(sorted);
|
|
6329
|
+
const listed = new Map(sorted.map((issue3) => [issue3.number, issue3]));
|
|
6330
|
+
const owned = new Set(epics.flatMap((epic) => epicSlugsOf(epic.labels).slice(0, 1)));
|
|
6331
|
+
const order = epics.flatMap((epic) => {
|
|
6332
|
+
const slug = epicSlugsOf(epic.labels)[0];
|
|
6333
|
+
return slug === undefined ? [] : orderProblems(epic, slug, listed, groups.get(slug) ?? []);
|
|
6334
|
+
});
|
|
6335
|
+
const problems = [
|
|
6336
|
+
...severalEpicLabels(sorted),
|
|
6337
|
+
...orphanLabels(groups, owned),
|
|
6338
|
+
...horizons(epics),
|
|
6339
|
+
...order
|
|
6340
|
+
].sort(compareProblems);
|
|
6341
|
+
return Object.freeze(problems);
|
|
6342
|
+
}
|
|
6343
|
+
function epicProblemMessage(problem) {
|
|
6344
|
+
const id = `#${String(problem.issue)}`;
|
|
6345
|
+
switch (problem.kind) {
|
|
6346
|
+
case "several-epic-labels": {
|
|
6347
|
+
const labels = problem.slugs.map((slug) => `${EPIC_LABEL_PREFIX}${slug}`).join(", ");
|
|
6348
|
+
return `${id} carries ${String(problem.slugs.length)} epic labels (${labels}); an issue belongs to one epic, so remove all but one`;
|
|
6349
|
+
}
|
|
6350
|
+
case "orphan-label":
|
|
6351
|
+
return `${id} carries ${EPIC_LABEL_PREFIX}${problem.slug}, which no type:epic issue carries; fix the slug or open the epic`;
|
|
6352
|
+
case "horizon":
|
|
6353
|
+
return problem.horizons.length === 0 ? `epic ${id} carries no ${HORIZON_LABEL_PREFIX} label; add one of horizon:now, horizon:next or horizon:later` : `epic ${id} carries ${String(problem.horizons.length)} horizon labels (${problem.horizons.join(", ")}); keep one`;
|
|
6354
|
+
case "unlabelled-checklist":
|
|
6355
|
+
return `${id} is on epic #${String(problem.epic)}'s checklist on line ${String(problem.lineNumber)}` + ` but does not carry ${EPIC_LABEL_PREFIX}${problem.slug}; label it or take it off the checklist`;
|
|
6356
|
+
case "unlisted-member":
|
|
6357
|
+
return `${id} carries ${EPIC_LABEL_PREFIX}${problem.slug} but is not on epic #${String(problem.epic)}'s checklist;` + " it is walked last, by issue number, until it is added";
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
|
|
5675
6361
|
// src/board/gate.ts
|
|
5676
6362
|
import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync11, renameSync as renameSync2, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
5677
6363
|
import { basename as basename5, dirname as dirname7, join as join10, resolve as resolve6 } from "path";
|
|
@@ -5704,7 +6390,7 @@ var LIST_HEADINGS = [
|
|
|
5704
6390
|
var PREAMBLE_HEADING = "the opening";
|
|
5705
6391
|
var MARKER_PREFIX = "rafa:";
|
|
5706
6392
|
var HEADING_LINE = /^ {0,3}(#{1,6})\s+(.*)$/u;
|
|
5707
|
-
var
|
|
6393
|
+
var FENCE_LINE2 = /^ {0,3}(?:```|~~~)/u;
|
|
5708
6394
|
var LIST_ITEM = /^ {0,3}(?:[-*+]|\d+[.)])\s+\S/u;
|
|
5709
6395
|
var CODE_SPAN2 = /`[^`]*`/gu;
|
|
5710
6396
|
var CODE_TICK = "`";
|
|
@@ -5717,7 +6403,7 @@ var PLACEHOLDER_SHAPES = [
|
|
|
5717
6403
|
{ token: "TODO", pattern: /\bTODO\b/u },
|
|
5718
6404
|
{ token: "???", pattern: /\?{3,}/u }
|
|
5719
6405
|
];
|
|
5720
|
-
var
|
|
6406
|
+
var REMEDY2 = "fill each gap in the spec and rerun";
|
|
5721
6407
|
function codeSpanEnd(text, limit) {
|
|
5722
6408
|
const open = text.indexOf(CODE_TICK);
|
|
5723
6409
|
if (open === -1 || open > limit)
|
|
@@ -5783,7 +6469,7 @@ function readLines2(body) {
|
|
|
5783
6469
|
let fenced = false;
|
|
5784
6470
|
raw.forEach((text, index) => {
|
|
5785
6471
|
const number = index + 1;
|
|
5786
|
-
const isFence =
|
|
6472
|
+
const isFence = FENCE_LINE2.test(text);
|
|
5787
6473
|
if (fenced || !carried && isFence) {
|
|
5788
6474
|
fenced = !fenced || !isFence;
|
|
5789
6475
|
lines.push(fencedLine(number, text));
|
|
@@ -5914,7 +6600,7 @@ function readinessRefusalMessage(source, gaps) {
|
|
|
5914
6600
|
throw new TypeError(`board readiness: ${source} carries no gap, and has no refusal to name`);
|
|
5915
6601
|
}
|
|
5916
6602
|
const named = gaps.map(describeGap).join(", ");
|
|
5917
|
-
return `${source} is not ready to plan from: ${named}; ${
|
|
6603
|
+
return `${source} is not ready to plan from: ${named}; ${REMEDY2}`;
|
|
5918
6604
|
}
|
|
5919
6605
|
function requireCompleteSpec(source, body) {
|
|
5920
6606
|
const gaps = findReadinessGaps(body);
|
|
@@ -5927,13 +6613,13 @@ function requireCompleteSpec(source, body) {
|
|
|
5927
6613
|
var REPO_PATH = "{owner}/{repo}";
|
|
5928
6614
|
var TRUSTED_PERMISSIONS = ["admin", "maintain", "write"];
|
|
5929
6615
|
var TRUST_REFUSAL_EXIT = 2;
|
|
5930
|
-
var
|
|
6616
|
+
var REMEDY3 = Object.freeze({
|
|
5931
6617
|
issue: "a member must open the spec",
|
|
5932
6618
|
"pull request": "a member must open the pull request"
|
|
5933
6619
|
});
|
|
5934
6620
|
var UNREPORTED = "the lookup wrote nothing";
|
|
5935
|
-
function failureDetail(
|
|
5936
|
-
const detail =
|
|
6621
|
+
function failureDetail(reading2) {
|
|
6622
|
+
const detail = reading2.permission?.detail ?? "";
|
|
5937
6623
|
return detail === "" ? UNREPORTED : detail;
|
|
5938
6624
|
}
|
|
5939
6625
|
function isTrustedPermission(value) {
|
|
@@ -5945,7 +6631,7 @@ function stringOrNull(value) {
|
|
|
5945
6631
|
function lookupFailed(login, detail) {
|
|
5946
6632
|
return { login, permission: null, roleName: null, detail };
|
|
5947
6633
|
}
|
|
5948
|
-
function
|
|
6634
|
+
function detailOf2(result, command) {
|
|
5949
6635
|
const written = result.stderr.trim() || result.stdout.trim();
|
|
5950
6636
|
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
5951
6637
|
}
|
|
@@ -5955,11 +6641,11 @@ function createGhPermissions(options) {
|
|
|
5955
6641
|
if (!isGitHubLogin(login)) {
|
|
5956
6642
|
return lookupFailed(login, `${describeValue(login)} is not a GitHub login, so no permission was read`);
|
|
5957
6643
|
}
|
|
5958
|
-
const
|
|
5959
|
-
const command = `gh api ${
|
|
5960
|
-
const result = await gh(["api",
|
|
6644
|
+
const path2 = `repos/${REPO_PATH}/collaborators/${login}/permission`;
|
|
6645
|
+
const command = `gh api ${path2}`;
|
|
6646
|
+
const result = await gh(["api", path2]);
|
|
5961
6647
|
if (!result.ok)
|
|
5962
|
-
return lookupFailed(login,
|
|
6648
|
+
return lookupFailed(login, detailOf2(result, command));
|
|
5963
6649
|
let payload;
|
|
5964
6650
|
try {
|
|
5965
6651
|
payload = JSON.parse(result.stdout);
|
|
@@ -6019,25 +6705,25 @@ function readBoardTrust(trust, login) {
|
|
|
6019
6705
|
});
|
|
6020
6706
|
}
|
|
6021
6707
|
async function requireTrustedBoardAuthor(item, trust, login) {
|
|
6022
|
-
const
|
|
6023
|
-
requireTrustedAuthor({ kind: item.kind, number: item.number, repo: trust.repo },
|
|
6024
|
-
return
|
|
6708
|
+
const reading2 = await readBoardTrust(trust, login);
|
|
6709
|
+
requireTrustedAuthor({ kind: item.kind, number: item.number, repo: trust.repo }, reading2);
|
|
6710
|
+
return reading2;
|
|
6025
6711
|
}
|
|
6026
|
-
function trustRefusalClause(repo,
|
|
6027
|
-
if (
|
|
6028
|
-
throw new TypeError(`board trust: ${
|
|
6712
|
+
function trustRefusalClause(repo, reading2) {
|
|
6713
|
+
if (reading2.trusted) {
|
|
6714
|
+
throw new TypeError(`board trust: ${reading2.login} is trusted, and has no refusal to name`);
|
|
6029
6715
|
}
|
|
6030
|
-
return
|
|
6716
|
+
return reading2.refusal === "lookup-failed" ? `whose write access to ${repo} could not be read (${failureDetail(reading2)})` : `who has no write access to ${repo}`;
|
|
6031
6717
|
}
|
|
6032
|
-
function trustRefusalMessage(item,
|
|
6033
|
-
const because = trustRefusalClause(item.repo,
|
|
6034
|
-
const opened = `${item.kind} #${String(item.number)} was opened by ${
|
|
6035
|
-
return `${opened}, ${because}; ${
|
|
6718
|
+
function trustRefusalMessage(item, reading2) {
|
|
6719
|
+
const because = trustRefusalClause(item.repo, reading2);
|
|
6720
|
+
const opened = `${item.kind} #${String(item.number)} was opened by ${reading2.login}`;
|
|
6721
|
+
return `${opened}, ${because}; ${REMEDY3[item.kind]}`;
|
|
6036
6722
|
}
|
|
6037
|
-
function requireTrustedAuthor(item,
|
|
6038
|
-
if (
|
|
6723
|
+
function requireTrustedAuthor(item, reading2) {
|
|
6724
|
+
if (reading2.trusted)
|
|
6039
6725
|
return;
|
|
6040
|
-
throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item,
|
|
6726
|
+
throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading2));
|
|
6041
6727
|
}
|
|
6042
6728
|
|
|
6043
6729
|
// src/board/review-comment.ts
|
|
@@ -6080,21 +6766,21 @@ function assumedReviewCommentBody(gaps) {
|
|
|
6080
6766
|
function specReviewComments(comments) {
|
|
6081
6767
|
return [...comments].reverse().filter((comment) => comment.body.includes(SPEC_REVIEW_MARKER));
|
|
6082
6768
|
}
|
|
6083
|
-
function ignoredReviewCommentMessage(comment,
|
|
6084
|
-
const who = `was written by ${
|
|
6769
|
+
function ignoredReviewCommentMessage(comment, reading2, repo) {
|
|
6770
|
+
const who = `was written by ${reading2.login}, ${trustRefusalClause(repo, reading2)}`;
|
|
6085
6771
|
return `the ${SPEC_REVIEW_COMMENT_NAME} comment ${comment.id} ${who};` + " it was left alone and the gaps went in a comment of their own";
|
|
6086
6772
|
}
|
|
6087
6773
|
async function readTrustedSpecReviewComment(comments, trust) {
|
|
6088
6774
|
const trustOf = memoisedTrust(trust);
|
|
6089
6775
|
const ignored = [];
|
|
6090
6776
|
for (const comment of specReviewComments(comments)) {
|
|
6091
|
-
const
|
|
6092
|
-
if (
|
|
6777
|
+
const reading2 = await trustOf(comment.author);
|
|
6778
|
+
if (reading2.trusted)
|
|
6093
6779
|
return { comment, ignored };
|
|
6094
6780
|
ignored.push({
|
|
6095
6781
|
id: comment.id,
|
|
6096
6782
|
author: comment.author,
|
|
6097
|
-
reason: ignoredReviewCommentMessage(comment,
|
|
6783
|
+
reason: ignoredReviewCommentMessage(comment, reading2, trust.repo)
|
|
6098
6784
|
});
|
|
6099
6785
|
}
|
|
6100
6786
|
return { comment: null, ignored };
|
|
@@ -6235,50 +6921,50 @@ function describeGap2(gap) {
|
|
|
6235
6921
|
function remedyFor(issue3) {
|
|
6236
6922
|
return issue3 === null ? " Close the gaps in the spec, then plan from it again." : ` Close the gaps, label the issue ${SPEC_READY_LABEL} again, and plan from it again.`;
|
|
6237
6923
|
}
|
|
6238
|
-
function specNotReadyMessage(source,
|
|
6924
|
+
function specNotReadyMessage(source, reading2, gaps, issue3 = null) {
|
|
6239
6925
|
return [
|
|
6240
|
-
`\u274C ${source} is not ready to plan from: ${
|
|
6926
|
+
`\u274C ${source} is not ready to plan from: ${reading2}`,
|
|
6241
6927
|
...gaps.map(describeGap2),
|
|
6242
6928
|
remedyFor(issue3)
|
|
6243
6929
|
].join(`
|
|
6244
6930
|
`);
|
|
6245
6931
|
}
|
|
6246
|
-
function unreadReviewWarning(
|
|
6247
|
-
return `${
|
|
6932
|
+
function unreadReviewWarning(reading2, planPath) {
|
|
6933
|
+
return `${reading2}; ${planPath} reads as written, so the plan stands unreviewed`;
|
|
6248
6934
|
}
|
|
6249
|
-
function assumedReviewWarning(
|
|
6250
|
-
return `${
|
|
6935
|
+
function assumedReviewWarning(reading2, planPath) {
|
|
6936
|
+
return `${reading2}, none of them blocking; ${planPath} is written under the assumptions the review named`;
|
|
6251
6937
|
}
|
|
6252
|
-
function unreadReviewMessage(source,
|
|
6938
|
+
function unreadReviewMessage(source, reading2, planIssues) {
|
|
6253
6939
|
return [
|
|
6254
|
-
`\u274C ${source}: no plan stands \u2014 ${
|
|
6940
|
+
`\u274C ${source}: no plan stands \u2014 ${reading2}, and the plan the session wrote does not read as written:`,
|
|
6255
6941
|
...planIssues.map((line) => ` ${line}`),
|
|
6256
6942
|
" Plan from the spec again."
|
|
6257
6943
|
].join(`
|
|
6258
6944
|
`);
|
|
6259
6945
|
}
|
|
6260
|
-
function isWrittenFile(repoRoot,
|
|
6261
|
-
const full = resolve6(repoRoot,
|
|
6946
|
+
function isWrittenFile(repoRoot, path2) {
|
|
6947
|
+
const full = resolve6(repoRoot, path2);
|
|
6262
6948
|
return existsSync13(full) && statSync5(full).isFile();
|
|
6263
6949
|
}
|
|
6264
6950
|
var NOT_READY_REASON = "the planner judged the spec not ready, so no plan stands.";
|
|
6265
6951
|
var UNREAD_REASON = "no review came back and the plan does not read as written, so no plan stands.";
|
|
6266
6952
|
var REJECTED_DIR = "rejected";
|
|
6267
|
-
function rejectedPath(
|
|
6268
|
-
const parent = dirname7(
|
|
6269
|
-
return parent === "." ? join10(REJECTED_DIR, basename5(
|
|
6953
|
+
function rejectedPath(path2) {
|
|
6954
|
+
const parent = dirname7(path2);
|
|
6955
|
+
return parent === "." ? join10(REJECTED_DIR, basename5(path2)) : join10(parent, REJECTED_DIR, basename5(path2));
|
|
6270
6956
|
}
|
|
6271
6957
|
function moveWritten(options, output, reason) {
|
|
6272
|
-
for (const
|
|
6273
|
-
if (!isWrittenFile(options.repoRoot,
|
|
6958
|
+
for (const path2 of [options.planPath, options.prerequisitesPath]) {
|
|
6959
|
+
if (!isWrittenFile(options.repoRoot, path2))
|
|
6274
6960
|
continue;
|
|
6275
|
-
const destination = rejectedPath(
|
|
6961
|
+
const destination = rejectedPath(path2);
|
|
6276
6962
|
try {
|
|
6277
6963
|
mkdirSync3(resolve6(options.repoRoot, dirname7(destination)), { recursive: true });
|
|
6278
|
-
renameSync2(resolve6(options.repoRoot,
|
|
6279
|
-
output.info(`\uD83D\uDDC3 Moved ${
|
|
6964
|
+
renameSync2(resolve6(options.repoRoot, path2), resolve6(options.repoRoot, destination));
|
|
6965
|
+
output.info(`\uD83D\uDDC3 Moved ${path2} to ${destination}: ${reason}`);
|
|
6280
6966
|
} catch (error) {
|
|
6281
|
-
output.warn(`${
|
|
6967
|
+
output.warn(`${path2} was written against a refused review and could not be moved: ${messageOf(error)}`);
|
|
6282
6968
|
}
|
|
6283
6969
|
}
|
|
6284
6970
|
}
|
|
@@ -6390,37 +7076,37 @@ async function enforceSpecReview(options) {
|
|
|
6390
7076
|
}
|
|
6391
7077
|
|
|
6392
7078
|
// src/board/issue-board.ts
|
|
6393
|
-
var
|
|
7079
|
+
var PREFIX2 = "board issue";
|
|
6394
7080
|
var REPO_PATH2 = "{owner}/{repo}";
|
|
6395
7081
|
var COMMENT_ID = /^[1-9]\d*$/;
|
|
6396
|
-
function
|
|
7082
|
+
function detailOf3(result, command) {
|
|
6397
7083
|
const written = result.stderr.trim() || result.stdout.trim();
|
|
6398
7084
|
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
6399
7085
|
}
|
|
6400
7086
|
function failure(command, result) {
|
|
6401
|
-
return new Error(`${
|
|
7087
|
+
return new Error(`${PREFIX2}: ${detailOf3(result, command)}`);
|
|
6402
7088
|
}
|
|
6403
7089
|
function issueNumber(value, member) {
|
|
6404
7090
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
6405
|
-
throw new TypeError(`${
|
|
7091
|
+
throw new TypeError(`${PREFIX2}: ${member} refused issue number ${describeValue(value)}, expected a positive whole number`);
|
|
6406
7092
|
}
|
|
6407
7093
|
return String(value);
|
|
6408
7094
|
}
|
|
6409
7095
|
function commentId(value) {
|
|
6410
7096
|
if (typeof value !== "string" || !COMMENT_ID.test(value)) {
|
|
6411
|
-
throw new TypeError(`${
|
|
7097
|
+
throw new TypeError(`${PREFIX2}: editComment refused comment id ${describeValue(value)},` + " expected a positive whole number with no leading zero");
|
|
6412
7098
|
}
|
|
6413
7099
|
return value;
|
|
6414
7100
|
}
|
|
6415
7101
|
function commentBody2(value, member) {
|
|
6416
7102
|
if (typeof value !== "string") {
|
|
6417
|
-
throw new TypeError(`${
|
|
7103
|
+
throw new TypeError(`${PREFIX2}: ${member} refused a body ${describeValue(value)}, expected a string`);
|
|
6418
7104
|
}
|
|
6419
7105
|
return value;
|
|
6420
7106
|
}
|
|
6421
7107
|
function labelName(value, member, what) {
|
|
6422
7108
|
if (typeof value !== "string" || value === "" || value.startsWith("-")) {
|
|
6423
|
-
throw new TypeError(`${
|
|
7109
|
+
throw new TypeError(`${PREFIX2}: ${member} refused ${what} ${describeValue(value)},` + " expected a non-empty name that does not open with a hyphen");
|
|
6424
7110
|
}
|
|
6425
7111
|
return value;
|
|
6426
7112
|
}
|
|
@@ -6428,20 +7114,20 @@ function parseJson(stdout, command) {
|
|
|
6428
7114
|
try {
|
|
6429
7115
|
return JSON.parse(stdout);
|
|
6430
7116
|
} catch (error) {
|
|
6431
|
-
throw new Error(`${
|
|
7117
|
+
throw new Error(`${PREFIX2}: ${command} wrote output that is not JSON: ${messageOf(error)}`, { cause: error });
|
|
6432
7118
|
}
|
|
6433
7119
|
}
|
|
6434
7120
|
function readComment(value, command, where) {
|
|
6435
7121
|
if (!isMapping(value)) {
|
|
6436
|
-
throw new Error(`${
|
|
7122
|
+
throw new Error(`${PREFIX2}: ${command} answered ${where} as ${describeValue(value)}, expected a mapping`);
|
|
6437
7123
|
}
|
|
6438
7124
|
const id = value["id"];
|
|
6439
7125
|
if (typeof id !== "number" || !Number.isSafeInteger(id) || id < 1) {
|
|
6440
|
-
throw new Error(`${
|
|
7126
|
+
throw new Error(`${PREFIX2}: ${command} answered ${where}.id as ${describeValue(id)}, expected a positive whole number`);
|
|
6441
7127
|
}
|
|
6442
7128
|
const body = value["body"];
|
|
6443
7129
|
if (typeof body !== "string") {
|
|
6444
|
-
throw new Error(`${
|
|
7130
|
+
throw new Error(`${PREFIX2}: ${command} answered ${where}.body as ${describeValue(body)}, expected a string`);
|
|
6445
7131
|
}
|
|
6446
7132
|
const user = value["user"];
|
|
6447
7133
|
const login = isMapping(user) ? user["login"] : null;
|
|
@@ -6462,26 +7148,26 @@ function createGhIssueBoard(options) {
|
|
|
6462
7148
|
const commentsPath = (number) => `repos/${REPO_PATH2}/issues/${number}/comments`;
|
|
6463
7149
|
const board = {
|
|
6464
7150
|
comments: async (issue3) => {
|
|
6465
|
-
const
|
|
6466
|
-
const command = `gh api ${
|
|
6467
|
-
const rows = parseJson(await succeed(["api",
|
|
7151
|
+
const path2 = commentsPath(issueNumber(issue3, "comments"));
|
|
7152
|
+
const command = `gh api ${path2}`;
|
|
7153
|
+
const rows = parseJson(await succeed(["api", path2], command), command);
|
|
6468
7154
|
if (!Array.isArray(rows)) {
|
|
6469
|
-
throw new Error(`${
|
|
7155
|
+
throw new Error(`${PREFIX2}: ${command} answered ${describeValue(rows)}, expected a list of comments`);
|
|
6470
7156
|
}
|
|
6471
7157
|
return rows.map((row, index) => readComment(row, command, `comment ${String(index)}`));
|
|
6472
7158
|
},
|
|
6473
7159
|
comment: async (issue3, body) => {
|
|
6474
|
-
const
|
|
7160
|
+
const path2 = commentsPath(issueNumber(issue3, "comment"));
|
|
6475
7161
|
const text = commentBody2(body, "comment");
|
|
6476
|
-
const command = `gh api -X POST ${
|
|
6477
|
-
const stdout = await succeed(["api",
|
|
7162
|
+
const command = `gh api -X POST ${path2}`;
|
|
7163
|
+
const stdout = await succeed(["api", path2, "-X", "POST", "-f", `body=${text}`], command);
|
|
6478
7164
|
return readComment(parseJson(stdout, command), command, "the comment");
|
|
6479
7165
|
},
|
|
6480
7166
|
editComment: async (id, body) => {
|
|
6481
|
-
const
|
|
7167
|
+
const path2 = `repos/${REPO_PATH2}/issues/comments/${commentId(id)}`;
|
|
6482
7168
|
const text = commentBody2(body, "editComment");
|
|
6483
|
-
const command = `gh api -X PATCH ${
|
|
6484
|
-
const stdout = await succeed(["api",
|
|
7169
|
+
const command = `gh api -X PATCH ${path2}`;
|
|
7170
|
+
const stdout = await succeed(["api", path2, "-X", "PATCH", "-f", `body=${text}`], command);
|
|
6485
7171
|
return readComment(parseJson(stdout, command), command, "the comment");
|
|
6486
7172
|
},
|
|
6487
7173
|
swapLabels: async (issue3, removed, added) => {
|
|
@@ -6518,7 +7204,7 @@ class RefsBlockError extends Error {
|
|
|
6518
7204
|
}
|
|
6519
7205
|
}
|
|
6520
7206
|
var SECTION_HEADING = /^ {0,3}##(?:[ \t]|$)/u;
|
|
6521
|
-
var
|
|
7207
|
+
var FENCE_LINE3 = /^ {0,3}(`{3,}|~{3,})(.*)$/u;
|
|
6522
7208
|
var SHA256 = /^[0-9a-f]{64}$/u;
|
|
6523
7209
|
var OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
6524
7210
|
var ESCAPED = /[>\p{C}\p{Zl}\p{Zp}]/gu;
|
|
@@ -6550,7 +7236,7 @@ function sections(body) {
|
|
|
6550
7236
|
let fence = null;
|
|
6551
7237
|
for (const line of body.split(`
|
|
6552
7238
|
`)) {
|
|
6553
|
-
const found =
|
|
7239
|
+
const found = FENCE_LINE3.exec(line);
|
|
6554
7240
|
const run = found?.[1] ?? "";
|
|
6555
7241
|
if (fence === null && found !== null) {
|
|
6556
7242
|
fence = { char: run.charAt(0), length: run.length };
|
|
@@ -6755,92 +7441,14 @@ ${REFS_BLOCK_CLOSE}
|
|
|
6755
7441
|
`, after) ? copy.slice(after + 1) : copy.slice(after);
|
|
6756
7442
|
return Object.freeze({ stamps: parseStamps(copy.slice(inner, end + 1)), body });
|
|
6757
7443
|
}
|
|
6758
|
-
function carryRefsBlock(copy, text) {
|
|
6759
|
-
return writeRefsBlock(text, copy === null ? null : readRefsBlock(copy).stamps);
|
|
6760
|
-
}
|
|
6761
|
-
|
|
6762
|
-
// src/board/naming.ts
|
|
6763
|
-
import path from "path";
|
|
6764
|
-
var ID_PREFIX = "rafa";
|
|
6765
|
-
var BRANCH_PREFIX = "feat";
|
|
6766
|
-
var SPEC_EXTENSION = ".md";
|
|
6767
|
-
var NOTES_SUFFIX = "-notes";
|
|
6768
|
-
var MAX_SLUG_WORDS = 4;
|
|
6769
|
-
var MIN_SLUG_WORDS = 2;
|
|
6770
|
-
var FALLBACK_SLUG = "untitled";
|
|
6771
|
-
var STOP_WORDS = new Set([
|
|
6772
|
-
"a",
|
|
6773
|
-
"an",
|
|
6774
|
-
"and",
|
|
6775
|
-
"as",
|
|
6776
|
-
"at",
|
|
6777
|
-
"be",
|
|
6778
|
-
"by",
|
|
6779
|
-
"for",
|
|
6780
|
-
"from",
|
|
6781
|
-
"in",
|
|
6782
|
-
"into",
|
|
6783
|
-
"is",
|
|
6784
|
-
"it",
|
|
6785
|
-
"its",
|
|
6786
|
-
"of",
|
|
6787
|
-
"on",
|
|
6788
|
-
"or",
|
|
6789
|
-
"that",
|
|
6790
|
-
"the",
|
|
6791
|
-
"this",
|
|
6792
|
-
"to",
|
|
6793
|
-
"with"
|
|
6794
|
-
]);
|
|
6795
|
-
var CLAUSE_BREAK = /[,;:([\u2013\u2014]|\s-\s/u;
|
|
6796
|
-
var LEADING_ID = /^\s*#?rafa[-\s]?\d+\s*[-:.\u2013\u2014]?\s*/iu;
|
|
6797
|
-
var NON_WORD = /[^a-z0-9]+/gu;
|
|
6798
|
-
function requireIssueNumber(issue3) {
|
|
6799
|
-
if (!Number.isInteger(issue3) || issue3 <= 0) {
|
|
6800
|
-
throw new RangeError(`An issue number must be a positive integer, not ${String(issue3)}.`);
|
|
6801
|
-
}
|
|
6802
|
-
return issue3;
|
|
6803
|
-
}
|
|
6804
|
-
function wordsOf(title) {
|
|
6805
|
-
return title.toLowerCase().replace(NON_WORD, " ").split(" ").filter((word) => word !== "");
|
|
6806
|
-
}
|
|
6807
|
-
function withoutStopWords(words) {
|
|
6808
|
-
const kept = words.filter((word) => !STOP_WORDS.has(word));
|
|
6809
|
-
return kept.length > 0 ? kept : words;
|
|
6810
|
-
}
|
|
6811
|
-
function slugFromTitle(title) {
|
|
6812
|
-
const withoutId = title.replace(LEADING_ID, "");
|
|
6813
|
-
const [firstClause = ""] = withoutId.split(CLAUSE_BREAK);
|
|
6814
|
-
const whole = withoutStopWords(wordsOf(withoutId));
|
|
6815
|
-
const clause = withoutStopWords(wordsOf(firstClause));
|
|
6816
|
-
const chosen = clause.length >= MIN_SLUG_WORDS ? clause : whole;
|
|
6817
|
-
if (chosen.length === 0)
|
|
6818
|
-
return FALLBACK_SLUG;
|
|
6819
|
-
return chosen.slice(0, MAX_SLUG_WORDS).join("-");
|
|
6820
|
-
}
|
|
6821
|
-
function boardId(issue3) {
|
|
6822
|
-
return `${ID_PREFIX}-${requireIssueNumber(issue3)}`;
|
|
6823
|
-
}
|
|
6824
|
-
function planStub(issue3, title) {
|
|
6825
|
-
return `${boardId(issue3)}-${slugFromTitle(title)}`;
|
|
6826
|
-
}
|
|
6827
|
-
function specFileName(issue3, title) {
|
|
6828
|
-
return `${planStub(issue3, title)}${SPEC_EXTENSION}`;
|
|
6829
|
-
}
|
|
6830
|
-
function specPath(specsDir, issue3, title) {
|
|
6831
|
-
return path.join(specsDir, specFileName(issue3, title));
|
|
6832
|
-
}
|
|
6833
|
-
function notesFileName(issue3) {
|
|
6834
|
-
return `${boardId(issue3)}${NOTES_SUFFIX}${SPEC_EXTENSION}`;
|
|
6835
|
-
}
|
|
6836
|
-
function notesPath(specsDir, issue3) {
|
|
6837
|
-
return path.join(specsDir, notesFileName(issue3));
|
|
6838
|
-
}
|
|
7444
|
+
function carryRefsBlock(copy, text) {
|
|
7445
|
+
return writeRefsBlock(text, copy === null ? null : readRefsBlock(copy).stamps);
|
|
7446
|
+
}
|
|
6839
7447
|
|
|
6840
7448
|
// src/board/previous-copy.ts
|
|
6841
7449
|
import { linkSync as linkSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync7, statSync as statSync6, unlinkSync } from "fs";
|
|
6842
7450
|
import { basename as basename6, extname, join as join11, resolve as resolve7 } from "path";
|
|
6843
|
-
var
|
|
7451
|
+
var PREFIX3 = "board previous copy";
|
|
6844
7452
|
var PREVIOUS_DIR_NAME = "previous";
|
|
6845
7453
|
var PREVIOUS_COPY_WARN_ABOVE = 50;
|
|
6846
7454
|
var COPY_EXTENSION = ".md";
|
|
@@ -6888,7 +7496,7 @@ function movePreviousCopy(options) {
|
|
|
6888
7496
|
mkdirSync4(absoluteDirectory, { recursive: true });
|
|
6889
7497
|
name2 = linkUnderFreeName(source, absoluteDirectory, savedName, timestamp);
|
|
6890
7498
|
} catch (error) {
|
|
6891
|
-
throw new Error(`${
|
|
7499
|
+
throw new Error(`${PREFIX3}: ${path2} could not be moved to ${directory}: ${messageOf(error)}`, { cause: error });
|
|
6892
7500
|
}
|
|
6893
7501
|
const moved = {
|
|
6894
7502
|
path: join11(directory, name2),
|
|
@@ -6897,7 +7505,7 @@ function movePreviousCopy(options) {
|
|
|
6897
7505
|
try {
|
|
6898
7506
|
unlinkSync(source);
|
|
6899
7507
|
} catch (error) {
|
|
6900
|
-
throw new Error(`${
|
|
7508
|
+
throw new Error(`${PREFIX3}: ${path2} was copied to ${moved.path} but could not be removed: ${messageOf(error)}`, { cause: error });
|
|
6901
7509
|
}
|
|
6902
7510
|
return moved;
|
|
6903
7511
|
}
|
|
@@ -6911,23 +7519,23 @@ function countPreviousCopies(repoRoot, specsDir) {
|
|
|
6911
7519
|
} catch (error) {
|
|
6912
7520
|
if (isMissing(error))
|
|
6913
7521
|
return 0;
|
|
6914
|
-
throw new Error(`${
|
|
7522
|
+
throw new Error(`${PREFIX3}: ${directory} could not be read: ${messageOf(error)}`, { cause: error });
|
|
6915
7523
|
}
|
|
6916
7524
|
}
|
|
6917
7525
|
|
|
6918
7526
|
// src/board/issue.ts
|
|
6919
|
-
var
|
|
7527
|
+
var PREFIX4 = "board issue";
|
|
6920
7528
|
var ISSUE_VIEW_FIELDS = "number,title,body,state,labels,author";
|
|
6921
7529
|
var SPEC_LABEL = "type:spec";
|
|
6922
7530
|
var ISSUE_REFUSAL_EXIT = 2;
|
|
6923
7531
|
var LOCAL_NOTES_HEADING = "## Local notes";
|
|
6924
|
-
function
|
|
7532
|
+
function detailOf4(result, command) {
|
|
6925
7533
|
const written = result.stderr.trim() || result.stdout.trim();
|
|
6926
7534
|
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
6927
7535
|
}
|
|
6928
7536
|
function issueNumber2(value) {
|
|
6929
7537
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
6930
|
-
throw new TypeError(`${
|
|
7538
|
+
throw new TypeError(`${PREFIX4}: the read refused issue number ${describeValue(value)}, expected a positive whole number`);
|
|
6931
7539
|
}
|
|
6932
7540
|
return String(value);
|
|
6933
7541
|
}
|
|
@@ -6962,11 +7570,11 @@ function readViewedIssue(stdout, number, command) {
|
|
|
6962
7570
|
try {
|
|
6963
7571
|
payload = JSON.parse(stdout);
|
|
6964
7572
|
} catch (error) {
|
|
6965
|
-
throw new Error(`${
|
|
7573
|
+
throw new Error(`${PREFIX4}: ${command} wrote output that is not JSON: ${messageOf(error)}`, { cause: error });
|
|
6966
7574
|
}
|
|
6967
7575
|
const problem = viewProblem(payload, number);
|
|
6968
7576
|
if (problem !== null)
|
|
6969
|
-
throw new Error(`${
|
|
7577
|
+
throw new Error(`${PREFIX4}: ${command} answered ${problem}`);
|
|
6970
7578
|
const checked = payload;
|
|
6971
7579
|
return {
|
|
6972
7580
|
number,
|
|
@@ -6984,7 +7592,7 @@ function createGhSpecIssueReader(options) {
|
|
|
6984
7592
|
const command = `gh issue view ${number} --json ${ISSUE_VIEW_FIELDS}`;
|
|
6985
7593
|
const result = await gh(["issue", "view", number, "--json", ISSUE_VIEW_FIELDS]);
|
|
6986
7594
|
if (!result.ok)
|
|
6987
|
-
throw new Error(`${
|
|
7595
|
+
throw new Error(`${PREFIX4}: ${detailOf4(result, command)}`);
|
|
6988
7596
|
return readViewedIssue(result.stdout, issue3, command);
|
|
6989
7597
|
};
|
|
6990
7598
|
}
|
|
@@ -7027,8 +7635,8 @@ var NOTES_SEPARATOR = `
|
|
|
7027
7635
|
${LOCAL_NOTES_HEADING}
|
|
7028
7636
|
|
|
7029
7637
|
`;
|
|
7030
|
-
var
|
|
7031
|
-
var
|
|
7638
|
+
var HEADING2 = /^#{1,6}(?:[ \t]|$)/u;
|
|
7639
|
+
var FENCE3 = /^ {0,3}(?:`{3,}|~{3,})/u;
|
|
7032
7640
|
function commonLines(before, after) {
|
|
7033
7641
|
let previous = new Uint32Array(after.length + 1);
|
|
7034
7642
|
for (const line of before) {
|
|
@@ -7055,9 +7663,9 @@ function headingSections(lines) {
|
|
|
7055
7663
|
`)]);
|
|
7056
7664
|
};
|
|
7057
7665
|
for (const line of lines) {
|
|
7058
|
-
if (
|
|
7666
|
+
if (FENCE3.test(line))
|
|
7059
7667
|
fenced = !fenced;
|
|
7060
|
-
if (fenced || !
|
|
7668
|
+
if (fenced || !HEADING2.test(line)) {
|
|
7061
7669
|
current?.lines.push(line);
|
|
7062
7670
|
continue;
|
|
7063
7671
|
}
|
|
@@ -7155,7 +7763,7 @@ function writeSpecSnapshot(options) {
|
|
|
7155
7763
|
mkdirSync5(dirname8(absolute), { recursive: true });
|
|
7156
7764
|
writeFileSync5(absolute, text);
|
|
7157
7765
|
} catch (error) {
|
|
7158
|
-
throw new Error(`${
|
|
7766
|
+
throw new Error(`${PREFIX4}: the snapshot ${path2} could not be written: ${messageOf(error)}`, { cause: error });
|
|
7159
7767
|
}
|
|
7160
7768
|
return { ...written, action, previous };
|
|
7161
7769
|
}
|
|
@@ -7220,368 +7828,159 @@ var SHAPES = [
|
|
|
7220
7828
|
{ name: "unix-home", kind: "home-path", pattern: /(?<![\w.-])(\/(?:Users|home)\/)([A-Za-z0-9._-]+)/giu },
|
|
7221
7829
|
{ name: "windows-home", kind: "home-path", pattern: /(?<![\w.-])([A-Za-z]:\\Users\\)([A-Za-z0-9._-]+)/giu },
|
|
7222
7830
|
{ name: "github-token", kind: "token", pattern: /\b(gh[pousr]_)([A-Za-z0-9]{20,})\b/gu },
|
|
7223
|
-
{ name: "github-fine-grained", kind: "token", pattern: /\b(github_pat_)([A-Za-z0-9_]{20,})\b/gu },
|
|
7224
|
-
{ name: "aws-key-id", kind: "token", pattern: /\b(A[KS]IA)([A-Z0-9]{16})\b/gu },
|
|
7225
|
-
{ name: "secret-key", kind: "token", pattern: /(?<![\w-])(sk-(?:ant-)?)([A-Za-z0-9_-]{20,})(?![\w-])/gu },
|
|
7226
|
-
{ name: "slack-token", kind: "token", pattern: /(?<![\w-])(xox[abeoprs]-)([A-Za-z0-9-]{12,})(?![\w-])/gu },
|
|
7227
|
-
{ name: "google-api-key", kind: "token", pattern: /(?<![\w-])(AIza)([A-Za-z0-9_-]{35})(?![\w-])/gu }
|
|
7228
|
-
];
|
|
7229
|
-
var KIND_LABEL = Object.freeze({
|
|
7230
|
-
"home-path": "a home path",
|
|
7231
|
-
token: "a token"
|
|
7232
|
-
});
|
|
7233
|
-
var
|
|
7234
|
-
function holdsPlaceholderWord(secret) {
|
|
7235
|
-
const lowered = secret.toLowerCase();
|
|
7236
|
-
return PLACEHOLDER_WORDS.some((word) => lowered.includes(word));
|
|
7237
|
-
}
|
|
7238
|
-
function isTooPlainForAToken(secret) {
|
|
7239
|
-
return new Set(secret).size < MIN_TOKEN_CHARSET;
|
|
7240
|
-
}
|
|
7241
|
-
function isPlaceholder(kind, secret) {
|
|
7242
|
-
if (holdsPlaceholderWord(secret))
|
|
7243
|
-
return true;
|
|
7244
|
-
if (kind === "home-path")
|
|
7245
|
-
return PLACEHOLDER_USERS.has(secret.toLowerCase());
|
|
7246
|
-
return isTooPlainForAToken(secret);
|
|
7247
|
-
}
|
|
7248
|
-
function maskedEvidence(keep) {
|
|
7249
|
-
return `${keep}${MASK}`;
|
|
7250
|
-
}
|
|
7251
|
-
function findLeaksInLine(line, number) {
|
|
7252
|
-
const found = [];
|
|
7253
|
-
for (const shape of SHAPES) {
|
|
7254
|
-
const pattern = new RegExp(shape.pattern.source, shape.pattern.flags);
|
|
7255
|
-
let match = pattern.exec(line);
|
|
7256
|
-
while (match !== null) {
|
|
7257
|
-
const [, keep = "", secret = ""] = match;
|
|
7258
|
-
if (!isPlaceholder(shape.kind, secret)) {
|
|
7259
|
-
found.push({
|
|
7260
|
-
kind: shape.kind,
|
|
7261
|
-
shape: shape.name,
|
|
7262
|
-
line: number,
|
|
7263
|
-
evidence: maskedEvidence(keep)
|
|
7264
|
-
});
|
|
7265
|
-
}
|
|
7266
|
-
match = pattern.exec(line);
|
|
7267
|
-
}
|
|
7268
|
-
}
|
|
7269
|
-
return found;
|
|
7270
|
-
}
|
|
7271
|
-
function findLeaks(body) {
|
|
7272
|
-
return body.split(/\r?\n/u).flatMap((line, index) => findLeaksInLine(line, index + 1));
|
|
7273
|
-
}
|
|
7274
|
-
function describeFinding(finding) {
|
|
7275
|
-
return `line ${String(finding.line)} holds ${KIND_LABEL[finding.kind]} (${finding.evidence})`;
|
|
7276
|
-
}
|
|
7277
|
-
function leakRefusalMessage(source, findings) {
|
|
7278
|
-
if (findings.length === 0) {
|
|
7279
|
-
throw new TypeError(`board leak: ${source} carries no leak, and has no refusal to name`);
|
|
7280
|
-
}
|
|
7281
|
-
const named = findings.map(describeFinding).join(", ");
|
|
7282
|
-
return `${source} names a machine path or a credential: ${named}; ${REMEDY3}`;
|
|
7283
|
-
}
|
|
7284
|
-
function requireNoLeak(source, body) {
|
|
7285
|
-
const findings = findLeaks(body);
|
|
7286
|
-
if (findings.length === 0)
|
|
7287
|
-
return;
|
|
7288
|
-
throw new CommandExit(LEAK_REFUSAL_EXIT, leakRefusalMessage(source, findings));
|
|
7289
|
-
}
|
|
7290
|
-
|
|
7291
|
-
// src/board/roadmap.ts
|
|
7292
|
-
var PREFIX4 = "board roadmap";
|
|
7293
|
-
var ROADMAP_TITLE = "Roadmap";
|
|
7294
|
-
var ROADMAP_SETTING = "roadmap.issue";
|
|
7295
|
-
var ROADMAP_REFUSAL_EXIT = 2;
|
|
7296
|
-
var DEFAULT_REMOTE = "origin";
|
|
7297
|
-
var OPEN_PULL_REQUEST_LIMIT = 100;
|
|
7298
|
-
var PR_LIST_FIELDS = "number,headRefName,body";
|
|
7299
|
-
var ISSUE_LIST_FIELDS = "number,title";
|
|
7300
|
-
var TASK_LINE = /^\s*[-*+]\s+\[([ xX])\]\s+#(\d+)\s*(.*)$/u;
|
|
7301
|
-
var FENCE2 = /^\s*(?:`{3,}|~{3,})/u;
|
|
7302
|
-
var WHY_SEPARATOR = /^[-:,.\u2013\u2014]\s*/u;
|
|
7303
|
-
var CLOSING = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\b\s*:?\s*#(\d+)\b/giu;
|
|
7304
|
-
function whyOf(rest) {
|
|
7305
|
-
return rest.trim().replace(WHY_SEPARATOR, "").trim();
|
|
7306
|
-
}
|
|
7307
|
-
function parseRoadmapBody(body) {
|
|
7308
|
-
let lines = [];
|
|
7309
|
-
let fenced = false;
|
|
7310
|
-
body.split(/\r\n?|\n/u).forEach((text, index) => {
|
|
7311
|
-
if (FENCE2.test(text)) {
|
|
7312
|
-
fenced = !fenced;
|
|
7313
|
-
return;
|
|
7314
|
-
}
|
|
7315
|
-
if (fenced)
|
|
7316
|
-
return;
|
|
7317
|
-
const found = TASK_LINE.exec(text);
|
|
7318
|
-
if (found === null)
|
|
7319
|
-
return;
|
|
7320
|
-
const [, box = "", digits = "", rest = ""] = found;
|
|
7321
|
-
const issue3 = Number.parseInt(digits, 10);
|
|
7322
|
-
if (!Number.isSafeInteger(issue3) || issue3 < 1)
|
|
7323
|
-
return;
|
|
7324
|
-
lines = [...lines, {
|
|
7325
|
-
issue: issue3,
|
|
7326
|
-
ticked: box.trim() !== "",
|
|
7327
|
-
why: whyOf(rest),
|
|
7328
|
-
lineNumber: index + 1
|
|
7329
|
-
}];
|
|
7330
|
-
});
|
|
7331
|
-
return Object.freeze(lines);
|
|
7332
|
-
}
|
|
7333
|
-
function detailOf4(result, command) {
|
|
7334
|
-
const written = result.stderr.trim() || result.stdout.trim();
|
|
7335
|
-
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
7336
|
-
}
|
|
7337
|
-
function parseRows(stdout, command) {
|
|
7338
|
-
let payload;
|
|
7339
|
-
try {
|
|
7340
|
-
payload = JSON.parse(stdout);
|
|
7341
|
-
} catch (error) {
|
|
7342
|
-
throw new Error(`${PREFIX4}: ${command} wrote output that is not JSON: ${messageOf(error)}`, { cause: error });
|
|
7343
|
-
}
|
|
7344
|
-
if (!Array.isArray(payload)) {
|
|
7345
|
-
throw new Error(`${PREFIX4}: ${command} answered ${describeValue(payload)}, expected a list`);
|
|
7346
|
-
}
|
|
7347
|
-
return payload;
|
|
7348
|
-
}
|
|
7349
|
-
function rowNumber(row, key, command, where) {
|
|
7350
|
-
const value = isMapping(row) ? row[key] : null;
|
|
7351
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
7352
|
-
throw new Error(`${PREFIX4}: ${command} answered ${where}.${key} as ${describeValue(value)}, expected a positive whole number`);
|
|
7353
|
-
}
|
|
7354
|
-
return value;
|
|
7355
|
-
}
|
|
7356
|
-
function rowText(row, key, command, where) {
|
|
7357
|
-
const value = isMapping(row) ? row[key] : null;
|
|
7358
|
-
if (typeof value !== "string") {
|
|
7359
|
-
throw new Error(`${PREFIX4}: ${command} answered ${where}.${key} as ${describeValue(value)}, expected a string`);
|
|
7360
|
-
}
|
|
7361
|
-
return value;
|
|
7362
|
-
}
|
|
7363
|
-
function createGhRoadmapSearch(options) {
|
|
7364
|
-
const { gh } = options;
|
|
7365
|
-
const search = `${ROADMAP_TITLE} in:title`;
|
|
7366
|
-
const args = ["issue", "list", "--state", "open", "--search", search, "--json", ISSUE_LIST_FIELDS];
|
|
7367
|
-
const command = `gh issue list --state open --search "${search}" --json ${ISSUE_LIST_FIELDS}`;
|
|
7368
|
-
return async () => {
|
|
7369
|
-
const result = await gh(args);
|
|
7370
|
-
if (!result.ok)
|
|
7371
|
-
throw new Error(`${PREFIX4}: ${detailOf4(result, command)}`);
|
|
7372
|
-
return Object.freeze(parseRows(result.stdout, command).map((row, index) => ({
|
|
7373
|
-
number: rowNumber(row, "number", command, `issue ${String(index)}`),
|
|
7374
|
-
title: rowText(row, "title", command, `issue ${String(index)}`)
|
|
7375
|
-
})));
|
|
7376
|
-
};
|
|
7377
|
-
}
|
|
7378
|
-
function noRoadmapMessage() {
|
|
7379
|
-
return `no open issue is titled ${ROADMAP_TITLE}, so there is no roadmap to read;` + ` run rafa init --board to open one, or set ${ROADMAP_SETTING} to the issue that holds the order`;
|
|
7380
|
-
}
|
|
7381
|
-
function severalRoadmapsMessage(numbers) {
|
|
7382
|
-
const named = numbers.map((number) => `#${String(number)}`).join(", ");
|
|
7383
|
-
return `${String(numbers.length)} open issues are titled ${ROADMAP_TITLE} (${named}),` + ` so which one holds the order is not this command to guess; set ${ROADMAP_SETTING} to it`;
|
|
7384
|
-
}
|
|
7385
|
-
async function resolveRoadmapIssue(options) {
|
|
7386
|
-
const { configured, search } = options;
|
|
7387
|
-
if (configured !== null)
|
|
7388
|
-
return configured;
|
|
7389
|
-
const wanted = ROADMAP_TITLE.toLowerCase();
|
|
7390
|
-
const titled = (await search()).filter((issue3) => issue3.title.trim().toLowerCase() === wanted);
|
|
7391
|
-
if (titled.length === 0)
|
|
7392
|
-
throw new CommandExit(ROADMAP_REFUSAL_EXIT, noRoadmapMessage());
|
|
7393
|
-
if (titled.length > 1) {
|
|
7394
|
-
throw new CommandExit(ROADMAP_REFUSAL_EXIT, severalRoadmapsMessage(titled.map((issue3) => issue3.number)));
|
|
7395
|
-
}
|
|
7396
|
-
return titled[0]?.number ?? 0;
|
|
7397
|
-
}
|
|
7398
|
-
function createGhOpenPullRequests(options) {
|
|
7399
|
-
const { gh } = options;
|
|
7400
|
-
const limit = String(OPEN_PULL_REQUEST_LIMIT);
|
|
7401
|
-
const args = ["pr", "list", "--state", "open", "--json", PR_LIST_FIELDS, "--limit", limit];
|
|
7402
|
-
const command = `gh pr list --state open --json ${PR_LIST_FIELDS} --limit ${limit}`;
|
|
7403
|
-
return async () => {
|
|
7404
|
-
const result = await gh(args);
|
|
7405
|
-
if (!result.ok)
|
|
7406
|
-
throw new Error(`${PREFIX4}: ${detailOf4(result, command)}`);
|
|
7407
|
-
return Object.freeze(parseRows(result.stdout, command).map((row, index) => ({
|
|
7408
|
-
number: rowNumber(row, "number", command, `pull request ${String(index)}`),
|
|
7409
|
-
headRefName: rowText(row, "headRefName", command, `pull request ${String(index)}`),
|
|
7410
|
-
body: rowText(row, "body", command, `pull request ${String(index)}`)
|
|
7411
|
-
})));
|
|
7412
|
-
};
|
|
7413
|
-
}
|
|
7414
|
-
function closedIssuesIn(body) {
|
|
7415
|
-
return Object.freeze([...body.matchAll(CLOSING)].map((found) => Number.parseInt(found[1] ?? "", 10)).filter((issue3) => Number.isSafeInteger(issue3) && issue3 > 0));
|
|
7416
|
-
}
|
|
7417
|
-
function branchClaims(ref, issue3) {
|
|
7418
|
-
const claim = `${BRANCH_PREFIX}/${boardId(issue3)}`;
|
|
7419
|
-
return new RegExp(`(?:^|/)${claim}(?:-|$)`, "u").test(ref);
|
|
7420
|
-
}
|
|
7421
|
-
function outputLines(stdout) {
|
|
7422
|
-
return stdout.split(`
|
|
7423
|
-
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
7424
|
-
}
|
|
7425
|
-
function scanClaimBranches(git, remote = DEFAULT_REMOTE) {
|
|
7426
|
-
const local = git(["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes"]);
|
|
7427
|
-
const pushed = git(["ls-remote", "--heads", remote]);
|
|
7428
|
-
const localRefs = local.ok ? outputLines(local.stdout) : [];
|
|
7429
|
-
const pushedRefs = pushed.ok ? outputLines(pushed.stdout).map((line) => line.split("\t").at(-1) ?? "") : [];
|
|
7430
|
-
const problems = [
|
|
7431
|
-
local.ok ? "" : `the branches of this checkout could not be read, so a branch held only here was not seen: ${detailOf4(local, "git for-each-ref")}`,
|
|
7432
|
-
pushed.ok ? "" : `the branches on ${remote} could not be read, so a branch pushed but not fetched was not seen: ${detailOf4(pushed, `git ls-remote --heads ${remote}`)}`
|
|
7433
|
-
].filter((problem) => problem !== "");
|
|
7434
|
-
return Object.freeze({
|
|
7435
|
-
refs: Object.freeze([...localRefs, ...pushedRefs].filter((ref) => ref !== "")),
|
|
7436
|
-
problems: Object.freeze(problems)
|
|
7437
|
-
});
|
|
7438
|
-
}
|
|
7439
|
-
function createRoadmapReadings(options) {
|
|
7440
|
-
const { issues, branches, pullRequests } = options;
|
|
7441
|
-
let open = null;
|
|
7442
|
-
return Object.freeze({
|
|
7443
|
-
isClosed: async (issue3) => (await issues(issue3)).state === "CLOSED",
|
|
7444
|
-
branchFor: (issue3) => branches.refs.find((ref) => branchClaims(ref, issue3)) ?? null,
|
|
7445
|
-
pullRequestFor: async (issue3) => {
|
|
7446
|
-
open = open ?? pullRequests();
|
|
7447
|
-
const closing = (await open).find((pull) => closedIssuesIn(pull.body).includes(issue3));
|
|
7448
|
-
return closing?.number ?? null;
|
|
7449
|
-
}
|
|
7450
|
-
});
|
|
7451
|
-
}
|
|
7452
|
-
function skipSentence(skip) {
|
|
7453
|
-
const id = `#${String(skip.line.issue)}`;
|
|
7454
|
-
if (skip.reason === "ticked")
|
|
7455
|
-
return `${id} done: ticked on the roadmap`;
|
|
7456
|
-
if (skip.reason === "closed")
|
|
7457
|
-
return `${id} done: the issue is closed`;
|
|
7458
|
-
return skip.reason === "branch" ? `${id} taken: branch ${skip.detail} exists` : `${id} taken: PR ${skip.detail} open`;
|
|
7459
|
-
}
|
|
7460
|
-
async function readRoadmapSkip(line, readings) {
|
|
7461
|
-
if (line.ticked)
|
|
7462
|
-
return { line, reason: "ticked", detail: "" };
|
|
7463
|
-
if (await readings.isClosed(line.issue))
|
|
7464
|
-
return { line, reason: "closed", detail: "" };
|
|
7465
|
-
const branch = readings.branchFor(line.issue);
|
|
7466
|
-
if (branch !== null)
|
|
7467
|
-
return { line, reason: "branch", detail: branch };
|
|
7468
|
-
const pull = await readings.pullRequestFor(line.issue);
|
|
7469
|
-
return pull === null ? null : { line, reason: "pull-request", detail: `#${String(pull)}` };
|
|
7470
|
-
}
|
|
7471
|
-
async function pickNextRoadmapLine(lines, readings) {
|
|
7472
|
-
let skipped2 = [];
|
|
7473
|
-
for (const line of lines) {
|
|
7474
|
-
const skip = await readRoadmapSkip(line, readings);
|
|
7475
|
-
if (skip === null)
|
|
7476
|
-
return Object.freeze({ line, skipped: Object.freeze(skipped2) });
|
|
7477
|
-
skipped2 = [...skipped2, skip];
|
|
7478
|
-
}
|
|
7479
|
-
return Object.freeze({ line: null, skipped: Object.freeze(skipped2) });
|
|
7831
|
+
{ name: "github-fine-grained", kind: "token", pattern: /\b(github_pat_)([A-Za-z0-9_]{20,})\b/gu },
|
|
7832
|
+
{ name: "aws-key-id", kind: "token", pattern: /\b(A[KS]IA)([A-Z0-9]{16})\b/gu },
|
|
7833
|
+
{ name: "secret-key", kind: "token", pattern: /(?<![\w-])(sk-(?:ant-)?)([A-Za-z0-9_-]{20,})(?![\w-])/gu },
|
|
7834
|
+
{ name: "slack-token", kind: "token", pattern: /(?<![\w-])(xox[abeoprs]-)([A-Za-z0-9-]{12,})(?![\w-])/gu },
|
|
7835
|
+
{ name: "google-api-key", kind: "token", pattern: /(?<![\w-])(AIza)([A-Za-z0-9_-]{35})(?![\w-])/gu }
|
|
7836
|
+
];
|
|
7837
|
+
var KIND_LABEL = Object.freeze({
|
|
7838
|
+
"home-path": "a home path",
|
|
7839
|
+
token: "a token"
|
|
7840
|
+
});
|
|
7841
|
+
var REMEDY4 = "the board is public, so move it to the local notes file beside the spec and edit the body";
|
|
7842
|
+
function holdsPlaceholderWord(secret) {
|
|
7843
|
+
const lowered = secret.toLowerCase();
|
|
7844
|
+
return PLACEHOLDER_WORDS.some((word) => lowered.includes(word));
|
|
7480
7845
|
}
|
|
7481
|
-
function
|
|
7482
|
-
|
|
7483
|
-
return skipped2.length === 0 ? `the roadmap, ${id}, carries no "- [ ] #<n>" line; add the next spec to its task list` : `every line of the roadmap, ${id}, is done or taken (${String(skipped2.length)} of them);` + " open the next spec issue and add it to the task list";
|
|
7846
|
+
function isTooPlainForAToken(secret) {
|
|
7847
|
+
return new Set(secret).size < MIN_TOKEN_CHARSET;
|
|
7484
7848
|
}
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
var ISSUE_ID = /#(\d+)/gu;
|
|
7492
|
-
var FOREIGN_ID = /[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+#\d+/gu;
|
|
7493
|
-
var REMEDY4 = `name them as "${BLOCKED_BY_FIELD}: #24 #26", or take the ${SPEC_BLOCKED_LABEL} label off`;
|
|
7494
|
-
function hasSpecBlockedLabel(labels) {
|
|
7495
|
-
return labels.some((label) => label.trim().toLowerCase() === SPEC_BLOCKED_LABEL);
|
|
7849
|
+
function isPlaceholder(kind, secret) {
|
|
7850
|
+
if (holdsPlaceholderWord(secret))
|
|
7851
|
+
return true;
|
|
7852
|
+
if (kind === "home-path")
|
|
7853
|
+
return PLACEHOLDER_USERS.has(secret.toLowerCase());
|
|
7854
|
+
return isTooPlainForAToken(secret);
|
|
7496
7855
|
}
|
|
7497
|
-
function
|
|
7498
|
-
|
|
7499
|
-
`).map((line) => line.replace(/\r$/u, ""));
|
|
7500
|
-
let fenced = false;
|
|
7501
|
-
for (const [index, line] of lines.entries()) {
|
|
7502
|
-
if (FENCE_LINE3.test(line)) {
|
|
7503
|
-
fenced = !fenced;
|
|
7504
|
-
continue;
|
|
7505
|
-
}
|
|
7506
|
-
if (fenced)
|
|
7507
|
-
continue;
|
|
7508
|
-
const found = FIELD_LINE.exec(line);
|
|
7509
|
-
if (found === null)
|
|
7510
|
-
continue;
|
|
7511
|
-
return { number: index + 1, text: (found[1] ?? "").trim() };
|
|
7512
|
-
}
|
|
7513
|
-
return null;
|
|
7856
|
+
function maskedEvidence(keep) {
|
|
7857
|
+
return `${keep}${MASK}`;
|
|
7514
7858
|
}
|
|
7515
|
-
function
|
|
7859
|
+
function findLeaksInLine(line, number) {
|
|
7516
7860
|
const found = [];
|
|
7517
|
-
for (const
|
|
7518
|
-
const
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7861
|
+
for (const shape of SHAPES) {
|
|
7862
|
+
const pattern = new RegExp(shape.pattern.source, shape.pattern.flags);
|
|
7863
|
+
let match = pattern.exec(line);
|
|
7864
|
+
while (match !== null) {
|
|
7865
|
+
const [, keep = "", secret = ""] = match;
|
|
7866
|
+
if (!isPlaceholder(shape.kind, secret)) {
|
|
7867
|
+
found.push({
|
|
7868
|
+
kind: shape.kind,
|
|
7869
|
+
shape: shape.name,
|
|
7870
|
+
line: number,
|
|
7871
|
+
evidence: maskedEvidence(keep)
|
|
7872
|
+
});
|
|
7873
|
+
}
|
|
7874
|
+
match = pattern.exec(line);
|
|
7875
|
+
}
|
|
7522
7876
|
}
|
|
7523
7877
|
return found;
|
|
7524
7878
|
}
|
|
7525
|
-
function
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7879
|
+
function findLeaks(body) {
|
|
7880
|
+
return body.split(/\r?\n/u).flatMap((line, index) => findLeaksInLine(line, index + 1));
|
|
7881
|
+
}
|
|
7882
|
+
function describeFinding(finding) {
|
|
7883
|
+
return `line ${String(finding.line)} holds ${KIND_LABEL[finding.kind]} (${finding.evidence})`;
|
|
7884
|
+
}
|
|
7885
|
+
function leakRefusalMessage(source, findings) {
|
|
7886
|
+
if (findings.length === 0) {
|
|
7887
|
+
throw new TypeError(`board leak: ${source} carries no leak, and has no refusal to name`);
|
|
7530
7888
|
}
|
|
7531
|
-
|
|
7889
|
+
const named = findings.map(describeFinding).join(", ");
|
|
7890
|
+
return `${source} names a machine path or a credential: ${named}; ${REMEDY4}`;
|
|
7532
7891
|
}
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
line: found?.number ?? null,
|
|
7539
|
-
text: found?.text ?? null,
|
|
7540
|
-
blockers: named.ids,
|
|
7541
|
-
foreign: named.foreign,
|
|
7542
|
-
unknown
|
|
7543
|
-
};
|
|
7892
|
+
function requireNoLeak(source, body) {
|
|
7893
|
+
const findings = findLeaks(body);
|
|
7894
|
+
if (findings.length === 0)
|
|
7895
|
+
return;
|
|
7896
|
+
throw new CommandExit(LEAK_REFUSAL_EXIT, leakRefusalMessage(source, findings));
|
|
7544
7897
|
}
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
if (ids.length === 0)
|
|
7553
|
-
return reading("no-ids", issue3, found, named, []);
|
|
7554
|
-
const unknown = known === undefined ? [] : ids.filter((id) => id !== issue3 && !known.has(id));
|
|
7555
|
-
if (ids.includes(issue3))
|
|
7556
|
-
return reading("self-reference", issue3, found, named, unknown);
|
|
7557
|
-
if (unknown.length > 0)
|
|
7558
|
-
return reading("unknown-issue", issue3, found, named, unknown);
|
|
7559
|
-
return reading("blocked", issue3, found, named, unknown);
|
|
7898
|
+
|
|
7899
|
+
// src/board/roadmap-board.ts
|
|
7900
|
+
var PREFIX5 = "board listing";
|
|
7901
|
+
var BOARD_LIST_FIELDS = "number,title,body,state,stateReason,labels";
|
|
7902
|
+
var BOARD_LISTING_LIMIT = 1000;
|
|
7903
|
+
function listingArgs(limit) {
|
|
7904
|
+
return ["issue", "list", "--state", "all", "--limit", String(limit), "--json", BOARD_LIST_FIELDS];
|
|
7560
7905
|
}
|
|
7561
|
-
function
|
|
7562
|
-
return
|
|
7906
|
+
function boardListingCommand(limit) {
|
|
7907
|
+
return `gh ${listingArgs(limit).join(" ")}`;
|
|
7563
7908
|
}
|
|
7564
|
-
function
|
|
7565
|
-
const
|
|
7566
|
-
|
|
7567
|
-
|
|
7909
|
+
function detailOf5(result, command) {
|
|
7910
|
+
const written = result.stderr.trim() || result.stdout.trim();
|
|
7911
|
+
return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
|
|
7912
|
+
}
|
|
7913
|
+
function labelsProblem(value) {
|
|
7914
|
+
if (!Array.isArray(value))
|
|
7915
|
+
return `labels ${describeValue(value)}, expected a list of named labels`;
|
|
7916
|
+
const bad = value.findIndex((label) => !isMapping(label) || typeof label["name"] !== "string");
|
|
7917
|
+
return bad === -1 ? null : `labels[${bad}] ${describeValue(value[bad])}, expected a mapping with a string name`;
|
|
7918
|
+
}
|
|
7919
|
+
function rowProblem(row) {
|
|
7920
|
+
if (!isMapping(row))
|
|
7921
|
+
return `${describeValue(row)}, expected a mapping`;
|
|
7922
|
+
const { number, title, body, state, stateReason, labels } = row;
|
|
7923
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1) {
|
|
7924
|
+
return `number ${describeValue(number)}, expected a positive whole number`;
|
|
7568
7925
|
}
|
|
7569
|
-
if (
|
|
7570
|
-
return `
|
|
7926
|
+
if (typeof title !== "string")
|
|
7927
|
+
return `title ${describeValue(title)}, expected a string`;
|
|
7928
|
+
if (typeof body !== "string")
|
|
7929
|
+
return `body ${describeValue(body)}, expected a string`;
|
|
7930
|
+
if (state !== "OPEN" && state !== "CLOSED")
|
|
7931
|
+
return `state ${describeValue(state)}, expected "OPEN" or "CLOSED"`;
|
|
7932
|
+
if (stateReason !== null && typeof stateReason !== "string") {
|
|
7933
|
+
return `stateReason ${describeValue(stateReason)}, expected a string or null`;
|
|
7571
7934
|
}
|
|
7572
|
-
|
|
7573
|
-
|
|
7935
|
+
return labelsProblem(labels);
|
|
7936
|
+
}
|
|
7937
|
+
function reasonOf(value) {
|
|
7938
|
+
return value === "" ? null : value;
|
|
7939
|
+
}
|
|
7940
|
+
function issueOf2(row) {
|
|
7941
|
+
const labels = row["labels"].map((label) => label.name);
|
|
7942
|
+
return Object.freeze({
|
|
7943
|
+
number: row["number"],
|
|
7944
|
+
title: row["title"],
|
|
7945
|
+
body: row["body"],
|
|
7946
|
+
state: row["state"],
|
|
7947
|
+
stateReason: reasonOf(row["stateReason"]),
|
|
7948
|
+
labels: Object.freeze(labels),
|
|
7949
|
+
type: typeOfLabels(labels),
|
|
7950
|
+
module: moduleOfLabels(labels)
|
|
7951
|
+
});
|
|
7952
|
+
}
|
|
7953
|
+
function parseBoardListing(stdout, command) {
|
|
7954
|
+
let payload;
|
|
7955
|
+
try {
|
|
7956
|
+
payload = JSON.parse(stdout);
|
|
7957
|
+
} catch (error) {
|
|
7958
|
+
throw new Error(`${PREFIX5}: ${command} wrote output that is not JSON: ${messageOf(error)}`, { cause: error });
|
|
7574
7959
|
}
|
|
7575
|
-
if (
|
|
7576
|
-
|
|
7960
|
+
if (!Array.isArray(payload)) {
|
|
7961
|
+
throw new Error(`${PREFIX5}: ${command} answered ${describeValue(payload)}, expected a list`);
|
|
7577
7962
|
}
|
|
7578
|
-
|
|
7963
|
+
const issues = payload.map((row, index) => {
|
|
7964
|
+
const problem = rowProblem(row);
|
|
7965
|
+
if (problem !== null)
|
|
7966
|
+
throw new Error(`${PREFIX5}: ${command} answered row ${index} with ${problem}`);
|
|
7967
|
+
return issueOf2(row);
|
|
7968
|
+
});
|
|
7969
|
+
return Object.freeze(issues);
|
|
7579
7970
|
}
|
|
7580
|
-
function
|
|
7581
|
-
|
|
7582
|
-
|
|
7971
|
+
function createGhBoardListing(options) {
|
|
7972
|
+
const { gh, limit = BOARD_LISTING_LIMIT } = options;
|
|
7973
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
7974
|
+
throw new TypeError(`${PREFIX5}: limit ${describeValue(limit)}, expected a positive whole number`);
|
|
7583
7975
|
}
|
|
7584
|
-
|
|
7976
|
+
const args = listingArgs(limit);
|
|
7977
|
+
const command = boardListingCommand(limit);
|
|
7978
|
+
return async () => {
|
|
7979
|
+
const result = await gh(args);
|
|
7980
|
+
if (!result.ok)
|
|
7981
|
+
throw new Error(`${PREFIX5}: ${detailOf5(result, command)}`);
|
|
7982
|
+
return parseBoardListing(result.stdout, command);
|
|
7983
|
+
};
|
|
7585
7984
|
}
|
|
7586
7985
|
|
|
7587
7986
|
// src/board/blocked-line.ts
|
|
@@ -7678,10 +8077,139 @@ function declinedMessage(alternative) {
|
|
|
7678
8077
|
return `#${String(alternative)} was not planned, and nothing was written`;
|
|
7679
8078
|
}
|
|
7680
8079
|
|
|
8080
|
+
// src/board/epic-walk.ts
|
|
8081
|
+
var PREFIX6 = "board epic walk";
|
|
8082
|
+
var NOW_HORIZON_LABEL = `${HORIZON_LABEL_PREFIX}now`;
|
|
8083
|
+
var NO_CLAIMS = new Set;
|
|
8084
|
+
var UNREAD_DAY = new Date(0);
|
|
8085
|
+
var LINE_BREAK = /\r\n?|\n/u;
|
|
8086
|
+
function listOnce(listing2) {
|
|
8087
|
+
let read = null;
|
|
8088
|
+
return () => {
|
|
8089
|
+
read = read ?? listing2();
|
|
8090
|
+
return read;
|
|
8091
|
+
};
|
|
8092
|
+
}
|
|
8093
|
+
function isEpicIssue(issue3) {
|
|
8094
|
+
return typeOfLabels(issue3.labels) === "epic";
|
|
8095
|
+
}
|
|
8096
|
+
function isNowEpic(labels) {
|
|
8097
|
+
const horizons2 = labels.filter((label) => label.startsWith(HORIZON_LABEL_PREFIX));
|
|
8098
|
+
return horizons2.length === 1 && horizons2[0] === NOW_HORIZON_LABEL;
|
|
8099
|
+
}
|
|
8100
|
+
function epicOnListing(number, listing2) {
|
|
8101
|
+
const epic = readEpics({ issues: listing2, claims: NO_CLAIMS, today: UNREAD_DAY }).epics.find((read) => read.number === number);
|
|
8102
|
+
const row = listing2.find((issue3) => issue3.number === number);
|
|
8103
|
+
if (epic === undefined || row === undefined) {
|
|
8104
|
+
throw new Error(`${PREFIX6}: epic #${String(number)} is not on the board listing, so its members cannot be read;` + " the listing reads the newest issues only, and an epic it leaves out is not walked as if it had none");
|
|
8105
|
+
}
|
|
8106
|
+
return { epic, row };
|
|
8107
|
+
}
|
|
8108
|
+
function epicLines(epic, row) {
|
|
8109
|
+
const checklist = epic.body?.lines ?? [];
|
|
8110
|
+
const listed = new Set(checklist.map((item) => item.issue));
|
|
8111
|
+
const labelOnly = epic.members.filter((member) => member.state === "OPEN" && !listed.has(member.number));
|
|
8112
|
+
const past = row.body.split(LINE_BREAK).length;
|
|
8113
|
+
const extra = labelOnly.map((member, index) => Object.freeze({
|
|
8114
|
+
issue: member.number,
|
|
8115
|
+
ticked: false,
|
|
8116
|
+
why: member.title,
|
|
8117
|
+
lineNumber: past + index + 1
|
|
8118
|
+
}));
|
|
8119
|
+
return Object.freeze({
|
|
8120
|
+
checklist,
|
|
8121
|
+
labelOnly: Object.freeze([...labelOnly]),
|
|
8122
|
+
lines: Object.freeze([...checklist, ...extra])
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
function descended(line, epic, row) {
|
|
8126
|
+
const { checklist, labelOnly, lines } = epicLines(epic, row);
|
|
8127
|
+
return {
|
|
8128
|
+
epic: Object.freeze({
|
|
8129
|
+
line,
|
|
8130
|
+
number: epic.number,
|
|
8131
|
+
title: epic.title,
|
|
8132
|
+
slug: epic.slug,
|
|
8133
|
+
progress: epic.progress,
|
|
8134
|
+
checklist,
|
|
8135
|
+
labelOnly
|
|
8136
|
+
}),
|
|
8137
|
+
lines
|
|
8138
|
+
};
|
|
8139
|
+
}
|
|
8140
|
+
async function readEpicLine(line, issue3, listing2) {
|
|
8141
|
+
if (issue3.state === "CLOSED")
|
|
8142
|
+
return { skip: { line, reason: "closed", detail: "" } };
|
|
8143
|
+
if (!isNowEpic(issue3.labels)) {
|
|
8144
|
+
const horizons2 = issue3.labels.filter((label) => label.startsWith(HORIZON_LABEL_PREFIX));
|
|
8145
|
+
return { skip: { line, reason: "horizon", detail: horizons2.join(", ") } };
|
|
8146
|
+
}
|
|
8147
|
+
const { epic, row } = epicOnListing(line.issue, await listing2());
|
|
8148
|
+
if (epic.state === "done") {
|
|
8149
|
+
const { done, total } = epic.progress;
|
|
8150
|
+
return { skip: { line, reason: "done", detail: `${String(done)}/${String(total)}` } };
|
|
8151
|
+
}
|
|
8152
|
+
return descended(line, epic, row);
|
|
8153
|
+
}
|
|
8154
|
+
function stoppedAt(lines, index, passed2, metEpic) {
|
|
8155
|
+
return metEpic ? Object.freeze({ lines: Object.freeze(lines.slice(index)), passed: Object.freeze(passed2), epic: null }) : Object.freeze({ lines, passed: Object.freeze([]), epic: null });
|
|
8156
|
+
}
|
|
8157
|
+
async function descendRoadmap(lines, seams) {
|
|
8158
|
+
const listing2 = listOnce(seams.listing);
|
|
8159
|
+
let passed2 = [];
|
|
8160
|
+
let metEpic = false;
|
|
8161
|
+
for (const [index, line] of lines.entries()) {
|
|
8162
|
+
const issue3 = line.ticked ? null : await seams.issues(line.issue);
|
|
8163
|
+
if (issue3 === null || !isEpicIssue(issue3)) {
|
|
8164
|
+
const skip = await readRoadmapSkip(line, seams.readings);
|
|
8165
|
+
if (skip === null)
|
|
8166
|
+
return stoppedAt(lines, index, passed2, metEpic);
|
|
8167
|
+
passed2 = [...passed2, { kind: "line", skip }];
|
|
8168
|
+
continue;
|
|
8169
|
+
}
|
|
8170
|
+
metEpic = true;
|
|
8171
|
+
const outcome = await readEpicLine(line, issue3, listing2);
|
|
8172
|
+
if ("skip" in outcome) {
|
|
8173
|
+
passed2 = [...passed2, { kind: "epic", skip: outcome.skip }];
|
|
8174
|
+
continue;
|
|
8175
|
+
}
|
|
8176
|
+
return Object.freeze({ lines: outcome.lines, passed: Object.freeze(passed2), epic: outcome.epic });
|
|
8177
|
+
}
|
|
8178
|
+
return stoppedAt(lines, lines.length, passed2, metEpic);
|
|
8179
|
+
}
|
|
8180
|
+
async function pickDescendedLine(lines, seams) {
|
|
8181
|
+
const descent = await descendRoadmap(lines, seams);
|
|
8182
|
+
const pick = await pickNextRoadmapLine(descent.lines, seams.readings);
|
|
8183
|
+
return Object.freeze({ descent, pick, dry: descent.epic !== null && pick.line === null });
|
|
8184
|
+
}
|
|
8185
|
+
function epicSkipSentence(skip) {
|
|
8186
|
+
const id = `epic #${String(skip.line.issue)}`;
|
|
8187
|
+
if (skip.reason === "closed")
|
|
8188
|
+
return `${id} done: the epic is closed`;
|
|
8189
|
+
if (skip.reason === "done")
|
|
8190
|
+
return `${id} done: every member is closed (${skip.detail})`;
|
|
8191
|
+
return skip.detail === "" ? `${id} passed: it carries no ${HORIZON_LABEL_PREFIX} label, and only a ${NOW_HORIZON_LABEL} epic is walked into` : `${id} passed: it carries ${skip.detail}, and only a ${NOW_HORIZON_LABEL} epic is walked into`;
|
|
8192
|
+
}
|
|
8193
|
+
function descentPassSentence(pass) {
|
|
8194
|
+
return pass.kind === "line" ? skipSentence(pass.skip) : epicSkipSentence(pass.skip);
|
|
8195
|
+
}
|
|
8196
|
+
function epicHeaderSentence(epic) {
|
|
8197
|
+
const { done, total } = epic.progress;
|
|
8198
|
+
return `walking into epic #${String(epic.number)} ${epic.title} (${String(done)}/${String(total)} done):` + " its checklist, then its labelled members missing from it";
|
|
8199
|
+
}
|
|
8200
|
+
function labelOnlySentence(epic, member) {
|
|
8201
|
+
const label = epic.slug === null ? "its label" : `${EPIC_LABEL_PREFIX}${epic.slug}`;
|
|
8202
|
+
return `#${String(member.number)} ${member.title} carries ${label} but is not on epic #${String(epic.number)}'s checklist;` + " it is walked after the checklist, by issue number";
|
|
8203
|
+
}
|
|
8204
|
+
function dryEpicSentence(epic) {
|
|
8205
|
+
const { done, total } = epic.progress;
|
|
8206
|
+
return `epic #${String(epic.number)} ${epic.title} has run dry: every line of its checklist and every open member it labels` + ` is done or taken, though the epic is not done (${String(done)}/${String(total)});` + " the walk stops here and does not move on to another epic";
|
|
8207
|
+
}
|
|
8208
|
+
|
|
7681
8209
|
// src/board/snapshot-settle.ts
|
|
7682
8210
|
import { existsSync as existsSync15, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
7683
8211
|
import { resolve as resolve9 } from "path";
|
|
7684
|
-
var
|
|
8212
|
+
var PREFIX7 = "board snapshot settle";
|
|
7685
8213
|
var DATE_LENGTH = 10;
|
|
7686
8214
|
function savedCopyDate(savedAt) {
|
|
7687
8215
|
return savedAt.toISOString().slice(0, DATE_LENGTH);
|
|
@@ -7701,7 +8229,7 @@ function readSavedCopy(absolute, path2) {
|
|
|
7701
8229
|
return null;
|
|
7702
8230
|
return { text: readFileSync13(absolute, "utf8"), savedAt: stat.mtime };
|
|
7703
8231
|
} catch (error) {
|
|
7704
|
-
throw new Error(`${
|
|
8232
|
+
throw new Error(`${PREFIX7}: the saved copy ${path2} could not be read: ${messageOf(error)}`, { cause: error });
|
|
7705
8233
|
}
|
|
7706
8234
|
}
|
|
7707
8235
|
function printChange(change, output) {
|
|
@@ -7745,7 +8273,7 @@ async function settleSpecSnapshot(options) {
|
|
|
7745
8273
|
}
|
|
7746
8274
|
|
|
7747
8275
|
// src/board/spec-source.ts
|
|
7748
|
-
var
|
|
8276
|
+
var PREFIX8 = "board spec source";
|
|
7749
8277
|
var SOURCE_FLAGS = [SPEC_FLAG, ISSUE_FLAG, NEXT_FLAG];
|
|
7750
8278
|
var SOURCE_REFUSAL_EXIT = 1;
|
|
7751
8279
|
function severalSourcesMessage(given) {
|
|
@@ -7809,6 +8337,15 @@ function roadmapHeaderLine(roadmap) {
|
|
|
7809
8337
|
function skipLine(skip) {
|
|
7810
8338
|
return ` \u23ED ${skipSentence(skip)}`;
|
|
7811
8339
|
}
|
|
8340
|
+
function descentPassLine(pass) {
|
|
8341
|
+
return ` \u23ED ${descentPassSentence(pass)}`;
|
|
8342
|
+
}
|
|
8343
|
+
function epicHeaderLine(epic) {
|
|
8344
|
+
return ` \uD83E\uDDED ${epicHeaderSentence(epic)}`;
|
|
8345
|
+
}
|
|
8346
|
+
function labelOnlyLine(epic, member) {
|
|
8347
|
+
return ` \uD83C\uDFF7 ${labelOnlySentence(epic, member)}`;
|
|
8348
|
+
}
|
|
7812
8349
|
function pickLine(line) {
|
|
7813
8350
|
const id = `\u25B6 Next on the roadmap: issue #${String(line.issue)}`;
|
|
7814
8351
|
return line.why === "" ? id : `${id} \u2014 ${line.why}`;
|
|
@@ -7864,7 +8401,7 @@ async function settleBlockedPick(options) {
|
|
|
7864
8401
|
async function pickRoadmapIssue(request, options, issues, output) {
|
|
7865
8402
|
const seams = options.roadmap;
|
|
7866
8403
|
if (seams === undefined) {
|
|
7867
|
-
throw new TypeError(`${
|
|
8404
|
+
throw new TypeError(`${PREFIX8}: ${NEXT_FLAG} was resolved with no roadmap seams`);
|
|
7868
8405
|
}
|
|
7869
8406
|
const roadmap = await resolveRoadmapIssue({
|
|
7870
8407
|
configured: request.roadmap ?? seams.configured,
|
|
@@ -7880,15 +8417,29 @@ async function pickRoadmapIssue(request, options, issues, output) {
|
|
|
7880
8417
|
branches,
|
|
7881
8418
|
pullRequests: seams.pullRequests
|
|
7882
8419
|
});
|
|
7883
|
-
const
|
|
7884
|
-
|
|
7885
|
-
pick
|
|
8420
|
+
const walked = await pickDescendedLine(parseRoadmapBody(read.body), { issues, readings, listing: seams.listing });
|
|
8421
|
+
printDescent(walked, output);
|
|
8422
|
+
const { descent, pick } = walked;
|
|
8423
|
+
if (descent.epic !== null && walked.dry) {
|
|
8424
|
+
output.info(dryEpicSentence(descent.epic));
|
|
8425
|
+
return { stop: "exhausted" };
|
|
8426
|
+
}
|
|
7886
8427
|
if (pick.line === null) {
|
|
7887
|
-
output.info(exhaustedMessage(roadmap, pick.skipped));
|
|
8428
|
+
output.info(exhaustedMessage(roadmap, [...descent.passed, ...pick.skipped]));
|
|
7888
8429
|
return { stop: "exhausted" };
|
|
7889
8430
|
}
|
|
7890
8431
|
output.info(pickLine(pick.line));
|
|
7891
|
-
return settlePick(pick.line, { lines, issues, readings, seams, output });
|
|
8432
|
+
return settlePick(pick.line, { lines: descent.lines, issues, readings, seams, output });
|
|
8433
|
+
}
|
|
8434
|
+
function printDescent(walked, output) {
|
|
8435
|
+
const { descent, pick } = walked;
|
|
8436
|
+
descent.passed.forEach((pass) => output.info(descentPassLine(pass)));
|
|
8437
|
+
const { epic } = descent;
|
|
8438
|
+
if (epic !== null) {
|
|
8439
|
+
output.info(epicHeaderLine(epic));
|
|
8440
|
+
epic.labelOnly.forEach((member) => output.info(labelOnlyLine(epic, member)));
|
|
8441
|
+
}
|
|
8442
|
+
pick.skipped.forEach((skip) => output.info(skipLine(skip)));
|
|
7892
8443
|
}
|
|
7893
8444
|
async function settlePick(line, settlement) {
|
|
7894
8445
|
const { issues } = settlement;
|
|
@@ -8034,6 +8585,7 @@ async function resolvePlanSpec(options) {
|
|
|
8034
8585
|
search: createGhRoadmapSearch({ gh }),
|
|
8035
8586
|
git,
|
|
8036
8587
|
pullRequests: createGhOpenPullRequests({ gh }),
|
|
8588
|
+
listing: createGhBoardListing({ gh }),
|
|
8037
8589
|
inspectRoadmap: (issue3) => inspectRoadmapIssue(issue3, trust()),
|
|
8038
8590
|
offerAlternative: alternative
|
|
8039
8591
|
},
|
|
@@ -8399,7 +8951,7 @@ function compareUrl(remote, branch) {
|
|
|
8399
8951
|
return `https://${segments[0] ?? host}/${path2}/compare/${encodeBranch(branch)}${COMPARE_QUERY}`;
|
|
8400
8952
|
}
|
|
8401
8953
|
// src/pr/gh.ts
|
|
8402
|
-
var
|
|
8954
|
+
var PREFIX9 = "gh pull requests";
|
|
8403
8955
|
var REPO_PATH3 = "{owner}/{repo}";
|
|
8404
8956
|
var SUMMARY_FIELDS = "author,baseRefName,headRefName,isCrossRepository,number,state,title,updatedAt,url";
|
|
8405
8957
|
var DETAIL_FIELDS = `${SUMMARY_FIELDS},body,headRefOid,labels,mergeStateStatus,mergeable`;
|
|
@@ -8423,23 +8975,23 @@ var MERGEABILITY = new Map([
|
|
|
8423
8975
|
["CONFLICTING", "conflicting"],
|
|
8424
8976
|
["UNKNOWN", "unknown"]
|
|
8425
8977
|
]);
|
|
8426
|
-
function
|
|
8978
|
+
function detailOf6(result, command) {
|
|
8427
8979
|
const written = result.stderr.trim() || result.stdout.trim();
|
|
8428
8980
|
if (written !== "")
|
|
8429
8981
|
return written;
|
|
8430
8982
|
return result.ok ? `${command} exited 0 and wrote nothing` : `${command} exited non-zero and wrote nothing`;
|
|
8431
8983
|
}
|
|
8432
8984
|
function failure2(command, result) {
|
|
8433
|
-
return new Error(`${
|
|
8985
|
+
return new Error(`${PREFIX9}: ${command} failed: ${detailOf6(result, command)}`);
|
|
8434
8986
|
}
|
|
8435
8987
|
function refuse3(command, problem) {
|
|
8436
|
-
throw new Error(`${
|
|
8988
|
+
throw new Error(`${PREFIX9}: ${command} answered ${problem}`);
|
|
8437
8989
|
}
|
|
8438
8990
|
function parseJson2(stdout, command) {
|
|
8439
8991
|
try {
|
|
8440
8992
|
return JSON.parse(stdout);
|
|
8441
8993
|
} catch (error) {
|
|
8442
|
-
throw new Error(`${
|
|
8994
|
+
throw new Error(`${PREFIX9}: ${command} wrote output that is not JSON: ${messageOf(error)}`, { cause: error });
|
|
8443
8995
|
}
|
|
8444
8996
|
}
|
|
8445
8997
|
function readString2(value, command, where) {
|
|
@@ -8551,7 +9103,7 @@ function readWorkflowCount(stdout) {
|
|
|
8551
9103
|
}
|
|
8552
9104
|
function pullNumber(value, member) {
|
|
8553
9105
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
8554
|
-
throw new TypeError(`${
|
|
9106
|
+
throw new TypeError(`${PREFIX9}: ${member} refused pull request number ${describeValue(value)},` + " expected a positive whole number");
|
|
8555
9107
|
}
|
|
8556
9108
|
return String(value);
|
|
8557
9109
|
}
|
|
@@ -8559,13 +9111,13 @@ function textArgument(value, member, what, whole) {
|
|
|
8559
9111
|
const bad = typeof value !== "string" || value === "" || whole && !(WHOLE_NUMBER.test(value) && Number.isSafeInteger(Number(value)));
|
|
8560
9112
|
if (bad) {
|
|
8561
9113
|
const expected2 = whole ? "a positive whole number with no leading zero" : "a non-empty string";
|
|
8562
|
-
throw new TypeError(`${
|
|
9114
|
+
throw new TypeError(`${PREFIX9}: ${member} refused ${what} ${describeValue(value)}, expected ${expected2}`);
|
|
8563
9115
|
}
|
|
8564
9116
|
return value;
|
|
8565
9117
|
}
|
|
8566
9118
|
function bodyText(value, member) {
|
|
8567
9119
|
if (typeof value !== "string") {
|
|
8568
|
-
throw new TypeError(`${
|
|
9120
|
+
throw new TypeError(`${PREFIX9}: ${member} refused a body ${describeValue(value)}, expected a string`);
|
|
8569
9121
|
}
|
|
8570
9122
|
return value;
|
|
8571
9123
|
}
|
|
@@ -8641,11 +9193,11 @@ function createGhPullRequests(options) {
|
|
|
8641
9193
|
merge: async (number, method) => {
|
|
8642
9194
|
const target = pullNumber(number, "merge");
|
|
8643
9195
|
if (!isMergeMethod(method)) {
|
|
8644
|
-
throw new TypeError(`${
|
|
9196
|
+
throw new TypeError(`${PREFIX9}: merge refused method ${describeValue(method)},` + ` expected one of: ${MERGE_METHODS.join(", ")}`);
|
|
8645
9197
|
}
|
|
8646
9198
|
const command = `gh pr merge ${target} --${method}`;
|
|
8647
9199
|
const result = await gh(["pr", "merge", target, `--${method}`]);
|
|
8648
|
-
return { merged: result.ok, detail:
|
|
9200
|
+
return { merged: result.ok, detail: detailOf6(result, command) };
|
|
8649
9201
|
},
|
|
8650
9202
|
editBody: async (number, body) => {
|
|
8651
9203
|
const target = pullNumber(number, "editBody");
|
|
@@ -9144,7 +9696,7 @@ function etaLine(eta) {
|
|
|
9144
9696
|
}
|
|
9145
9697
|
|
|
9146
9698
|
// src/next/sources.ts
|
|
9147
|
-
var
|
|
9699
|
+
var PREFIX10 = "rafa next";
|
|
9148
9700
|
var DEFAULT_BASE_BRANCH = "main";
|
|
9149
9701
|
var DEFAULT_NEXT_SOURCE_SEAMS = Object.freeze({});
|
|
9150
9702
|
function memoiseIssues2(issues) {
|
|
@@ -9168,11 +9720,15 @@ function ghNextBoard(options) {
|
|
|
9168
9720
|
branches,
|
|
9169
9721
|
pullRequests: createGhOpenPullRequests({ gh })
|
|
9170
9722
|
});
|
|
9171
|
-
const pick = await
|
|
9723
|
+
const { descent, pick } = await pickDescendedLine(parseRoadmapBody(read.body), {
|
|
9724
|
+
issues,
|
|
9725
|
+
readings,
|
|
9726
|
+
listing: createGhBoardListing({ gh })
|
|
9727
|
+
});
|
|
9172
9728
|
return {
|
|
9173
9729
|
roadmap,
|
|
9174
9730
|
line: pick.line,
|
|
9175
|
-
passed: pick.skipped.length,
|
|
9731
|
+
passed: descent.passed.length + pick.skipped.length,
|
|
9176
9732
|
problems: branches.problems
|
|
9177
9733
|
};
|
|
9178
9734
|
},
|
|
@@ -9182,7 +9738,7 @@ function ghNextBoard(options) {
|
|
|
9182
9738
|
}
|
|
9183
9739
|
function nextProject(context) {
|
|
9184
9740
|
if (context.project === null)
|
|
9185
|
-
throw new Error(`${
|
|
9741
|
+
throw new Error(`${PREFIX10} runs inside a project, and was handed none`);
|
|
9186
9742
|
return context.project;
|
|
9187
9743
|
}
|
|
9188
9744
|
function nextConfig(project, warn) {
|
|
@@ -9192,7 +9748,7 @@ function nextConfig(project, warn) {
|
|
|
9192
9748
|
if (!(error instanceof ConfigError))
|
|
9193
9749
|
throw error;
|
|
9194
9750
|
throw new CommandExit(1, [
|
|
9195
|
-
`\u274C ${
|
|
9751
|
+
`\u274C ${PREFIX10}: the config cannot be used:`,
|
|
9196
9752
|
...error.problems.map((problem) => ` ${problem}`)
|
|
9197
9753
|
].join(`
|
|
9198
9754
|
`));
|
|
@@ -9368,7 +9924,7 @@ function branchLabel(world) {
|
|
|
9368
9924
|
}
|
|
9369
9925
|
|
|
9370
9926
|
// src/next/state.ts
|
|
9371
|
-
var
|
|
9927
|
+
var PREFIX11 = "rafa next";
|
|
9372
9928
|
var MAX_NAMED_FILES = 5;
|
|
9373
9929
|
var NEXT_PRECONDITIONS = Object.freeze([
|
|
9374
9930
|
"tree-modified",
|
|
@@ -9646,7 +10202,7 @@ async function readNextState(sources) {
|
|
|
9646
10202
|
if (found !== null)
|
|
9647
10203
|
return answer(found, world.problems());
|
|
9648
10204
|
}
|
|
9649
|
-
throw new Error(`${
|
|
10205
|
+
throw new Error(`${PREFIX11}: no row of the table answered, and the last row answers for every reading`);
|
|
9650
10206
|
}
|
|
9651
10207
|
|
|
9652
10208
|
// src/next/hint.ts
|
|
@@ -9727,17 +10283,17 @@ async function nextStepHint(context, seams = DEFAULT_HINT_SEAMS) {
|
|
|
9727
10283
|
}
|
|
9728
10284
|
|
|
9729
10285
|
// src/next/actions.ts
|
|
9730
|
-
var
|
|
10286
|
+
var PREFIX12 = "rafa next";
|
|
9731
10287
|
function needed(state, value, what) {
|
|
9732
10288
|
if (value === null) {
|
|
9733
|
-
throw new Error(`${
|
|
10289
|
+
throw new Error(`${PREFIX12}: state "${state.id}" proposes "${state.action}" and names no ${what}`);
|
|
9734
10290
|
}
|
|
9735
10291
|
return String(value);
|
|
9736
10292
|
}
|
|
9737
10293
|
function pullRequestOf(state) {
|
|
9738
10294
|
return needed(state, state.pullRequest, "pull request");
|
|
9739
10295
|
}
|
|
9740
|
-
function
|
|
10296
|
+
function issueOf3(state) {
|
|
9741
10297
|
return needed(state, state.issue, "issue");
|
|
9742
10298
|
}
|
|
9743
10299
|
function planFlagOf(state) {
|
|
@@ -9751,8 +10307,8 @@ var ACTION_COMMANDS = Object.freeze({
|
|
|
9751
10307
|
"merge-unchecked": { subject: "pr", action: "merge", argv: (state) => [pullRequestOf(state), "--skip-checks"] },
|
|
9752
10308
|
start: { subject: "loop", action: "start", argv: (state) => [planFlagOf(state), "--create-branch"] },
|
|
9753
10309
|
plan: { subject: "plan", action: "create", argv: () => ["--next"] },
|
|
9754
|
-
unblock: { subject: "issue", action: "unblock", argv: (state) => [
|
|
9755
|
-
ready: { subject: "issue", action: "ready", argv: (state) => [
|
|
10310
|
+
unblock: { subject: "issue", action: "unblock", argv: (state) => [issueOf3(state)] },
|
|
10311
|
+
ready: { subject: "issue", action: "ready", argv: (state) => [issueOf3(state)] }
|
|
9756
10312
|
});
|
|
9757
10313
|
var NEXT_COMMAND_ACTIONS = Object.freeze(Object.keys(ACTION_COMMANDS));
|
|
9758
10314
|
function runsCommand(action) {
|
|
@@ -9780,12 +10336,12 @@ function actionContext(caller, command, argv) {
|
|
|
9780
10336
|
async function runAction(caller, state) {
|
|
9781
10337
|
const invocation = actionInvocation(state);
|
|
9782
10338
|
if (invocation === null) {
|
|
9783
|
-
throw new Error(`${
|
|
10339
|
+
throw new Error(`${PREFIX12}: the action "${state.action}" of state "${state.id}" runs no registered command`);
|
|
9784
10340
|
}
|
|
9785
10341
|
const spec = ACTION_COMMANDS[invocation.action];
|
|
9786
10342
|
const command = caller.registry.find(spec.subject, spec.action);
|
|
9787
10343
|
if (command === undefined) {
|
|
9788
|
-
throw new CommandExit(1, `\u274C ${
|
|
10344
|
+
throw new CommandExit(1, `\u274C ${PREFIX12}: the "${invocation.action}" step runs "rafa ${invocation.command}", which is registered by nothing`);
|
|
9789
10345
|
}
|
|
9790
10346
|
await command.run(actionContext(caller, command, invocation.argv));
|
|
9791
10347
|
}
|
|
@@ -9833,7 +10389,7 @@ function endingWith(command, seams = DEFAULT_ENDING_SEAMS) {
|
|
|
9833
10389
|
}
|
|
9834
10390
|
|
|
9835
10391
|
// src/adapters/tracker/resolve.ts
|
|
9836
|
-
var
|
|
10392
|
+
var PREFIX13 = "tracker chain";
|
|
9837
10393
|
var FAILURE_SEPARATOR = "; ";
|
|
9838
10394
|
function warnThroughActiveOutput(message) {
|
|
9839
10395
|
activeOutput().warn(message);
|
|
@@ -9874,9 +10430,9 @@ async function resolveTracker(options) {
|
|
|
9874
10430
|
}
|
|
9875
10431
|
attempts = [...attempts, Object.freeze({ kind, ok: false, reason: outcome.reason })];
|
|
9876
10432
|
failures = [...failures, `${kind}: ${outcome.reason}`];
|
|
9877
|
-
log(`${
|
|
10433
|
+
log(`${PREFIX13}: ${kind} unavailable: ${outcome.reason}`);
|
|
9878
10434
|
}
|
|
9879
|
-
throw new Error(`${
|
|
10435
|
+
throw new Error(`${PREFIX13}: no tracker available; tried ${failures.join(FAILURE_SEPARATOR)}`);
|
|
9880
10436
|
}
|
|
9881
10437
|
|
|
9882
10438
|
// src/commands/issue/issue-tracker.ts
|
|
@@ -9993,6 +10549,22 @@ function unaskedMessage2(issue3) {
|
|
|
9993
10549
|
const number = String(issue3);
|
|
9994
10550
|
return `#${number} is ready to mark; there is no terminal to ask on, so ${SPEC_READY_LABEL} was not added.` + ` Run rafa issue ready ${number} where an answer can be typed`;
|
|
9995
10551
|
}
|
|
10552
|
+
function requireOneEpic(found) {
|
|
10553
|
+
const listed = {
|
|
10554
|
+
number: found.number,
|
|
10555
|
+
title: found.title,
|
|
10556
|
+
body: found.body,
|
|
10557
|
+
state: found.state,
|
|
10558
|
+
stateReason: null,
|
|
10559
|
+
labels: found.labels,
|
|
10560
|
+
type: typeOfLabels(found.labels),
|
|
10561
|
+
module: moduleOfLabels(found.labels)
|
|
10562
|
+
};
|
|
10563
|
+
const several = readEpicProblems([listed]).find((problem) => problem.kind === "several-epic-labels");
|
|
10564
|
+
if (several === undefined)
|
|
10565
|
+
return;
|
|
10566
|
+
throw new CommandExit(READINESS_REFUSAL_EXIT, `issue #${String(found.number)} cannot be marked ${SPEC_READY_LABEL}: ${epicProblemMessage(several)}`);
|
|
10567
|
+
}
|
|
9996
10568
|
function readyReport(issue3, status, reading2, lines) {
|
|
9997
10569
|
return Object.freeze({
|
|
9998
10570
|
issue: issue3,
|
|
@@ -10011,6 +10583,7 @@ async function runIssueReady(options) {
|
|
|
10011
10583
|
const found = await readIssue(issue3);
|
|
10012
10584
|
const reading2 = await requireTrustedBoardAuthor({ kind: "issue", number: issue3 }, trust, found.author);
|
|
10013
10585
|
requireCompleteSpec(issueSource(issue3), found.body);
|
|
10586
|
+
requireOneEpic(found);
|
|
10014
10587
|
const lines = { trust: trustPassLine(issue3, trust.repo, reading2), checked: completeLine(issue3) };
|
|
10015
10588
|
if (hasSpecReadyLabel(found.labels)) {
|
|
10016
10589
|
return readyReport(issue3, "already", reading2, {
|
|
@@ -10104,7 +10677,7 @@ function createIssueReadyCommand(seams = DEFAULT_READY_SEAMS) {
|
|
|
10104
10677
|
subject: "issue",
|
|
10105
10678
|
action: "ready",
|
|
10106
10679
|
summary: `mark an issue ${SPEC_READY_LABEL} once its author and its body check out`,
|
|
10107
|
-
description: "Reads the issue on the GitHub board, refuses one opened by an account without write access to" + " the repository, refuses a body that does not fill the spec template, and then asks whether to mark it" + ` ${SPEC_READY_LABEL}. On a yes it swaps the labels in one write, adding ${SPEC_READY_LABEL} and` + ` removing ${SPEC_NEEDS_WORK_LABEL}. It always asks and declares no flag that skips the question;` + " without a terminal it prints both readings and adds no label. A run that marks the issue, or finds" + " it marked already, ends by naming the one step that follows, which `--no-hint` turns off. With" + " `--output=json` the report is the data of the terminal result event.",
|
|
10680
|
+
description: "Reads the issue on the GitHub board, refuses one opened by an account without write access to" + " the repository, refuses a body that does not fill the spec template, refuses an issue carrying two" + " `epic:` labels, and then asks whether to mark it" + ` ${SPEC_READY_LABEL}. On a yes it swaps the labels in one write, adding ${SPEC_READY_LABEL} and` + ` removing ${SPEC_NEEDS_WORK_LABEL}. It always asks and declares no flag that skips the question;` + " without a terminal it prints both readings and adds no label. A run that marks the issue, or finds" + " it marked already, ends by naming the one step that follows, which `--no-hint` turns off. With" + " `--output=json` the report is the data of the terminal result event.",
|
|
10108
10681
|
args: [
|
|
10109
10682
|
{
|
|
10110
10683
|
name: "n",
|
|
@@ -10788,12 +11361,12 @@ var SENTENCE_ABBREVIATIONS = ["e.g", "i.e", "vs", "cf", "etc"];
|
|
|
10788
11361
|
function isFilled2(value) {
|
|
10789
11362
|
return typeof value === "string" && value.trim() !== "";
|
|
10790
11363
|
}
|
|
10791
|
-
function
|
|
11364
|
+
function bodyLines2(body) {
|
|
10792
11365
|
return body.split(`
|
|
10793
11366
|
`).map((line) => line.replace(/\r$/, ""));
|
|
10794
11367
|
}
|
|
10795
11368
|
function bodySections(body) {
|
|
10796
|
-
const lines =
|
|
11369
|
+
const lines = bodyLines2(body);
|
|
10797
11370
|
const headings = [];
|
|
10798
11371
|
let fenced = false;
|
|
10799
11372
|
lines.forEach((line, index) => {
|
|
@@ -10835,7 +11408,7 @@ function numberedRuns(body) {
|
|
|
10835
11408
|
runs.push({ steps, line });
|
|
10836
11409
|
steps = 0;
|
|
10837
11410
|
};
|
|
10838
|
-
|
|
11411
|
+
bodyLines2(body).forEach((text, index) => {
|
|
10839
11412
|
if (FENCE_LINE4.test(text)) {
|
|
10840
11413
|
fenced = !fenced;
|
|
10841
11414
|
return;
|
|
@@ -10923,7 +11496,7 @@ function firstSection(sections2, heading) {
|
|
|
10923
11496
|
function withoutFences(text) {
|
|
10924
11497
|
const kept = [];
|
|
10925
11498
|
let fenced = false;
|
|
10926
|
-
for (const line of
|
|
11499
|
+
for (const line of bodyLines2(text)) {
|
|
10927
11500
|
if (FENCE_LINE4.test(line)) {
|
|
10928
11501
|
fenced = !fenced;
|
|
10929
11502
|
continue;
|
|
@@ -10982,7 +11555,7 @@ function extractedDate(body) {
|
|
|
10982
11555
|
}
|
|
10983
11556
|
function errorString(body) {
|
|
10984
11557
|
let fenced = false;
|
|
10985
|
-
for (const line of
|
|
11558
|
+
for (const line of bodyLines2(body)) {
|
|
10986
11559
|
if (FENCE_LINE4.test(line)) {
|
|
10987
11560
|
fenced = !fenced;
|
|
10988
11561
|
continue;
|
|
@@ -12417,7 +12990,7 @@ async function storeTaskReport(options) {
|
|
|
12417
12990
|
// src/start/pause.ts
|
|
12418
12991
|
var PAUSE_POLL_MS = 1000;
|
|
12419
12992
|
var OWN_PID = () => true;
|
|
12420
|
-
function
|
|
12993
|
+
function storedState2(repoRoot, sessionId) {
|
|
12421
12994
|
try {
|
|
12422
12995
|
return readSession(repoRoot, sessionId, { isAlive: OWN_PID }).state;
|
|
12423
12996
|
} catch (error) {
|
|
@@ -12438,7 +13011,7 @@ async function holdWhilePaused(options) {
|
|
|
12438
13011
|
const { repoRoot, sessionId, isInterrupted } = options;
|
|
12439
13012
|
const sleep = options.seams?.sleep ?? ((ms) => Bun.sleep(ms));
|
|
12440
13013
|
const pollMs = options.seams?.pollMs ?? PAUSE_POLL_MS;
|
|
12441
|
-
const first =
|
|
13014
|
+
const first = storedState2(repoRoot, sessionId);
|
|
12442
13015
|
if (first === null)
|
|
12443
13016
|
return "unreadable";
|
|
12444
13017
|
if (first !== "paused")
|
|
@@ -12452,7 +13025,7 @@ async function holdWhilePaused(options) {
|
|
|
12452
13025
|
await sleep(pollMs);
|
|
12453
13026
|
if (isInterrupted())
|
|
12454
13027
|
return "interrupted";
|
|
12455
|
-
const state =
|
|
13028
|
+
const state = storedState2(repoRoot, sessionId);
|
|
12456
13029
|
if (state === null)
|
|
12457
13030
|
return "unreadable";
|
|
12458
13031
|
if (state !== "paused") {
|
|
@@ -14981,7 +15554,7 @@ function parsedJson(text) {
|
|
|
14981
15554
|
return;
|
|
14982
15555
|
}
|
|
14983
15556
|
}
|
|
14984
|
-
function
|
|
15557
|
+
function issueOf4(payload) {
|
|
14985
15558
|
const title = field4(payload, "title");
|
|
14986
15559
|
const body = field4(payload, "body");
|
|
14987
15560
|
const state = field4(payload, "state");
|
|
@@ -15000,7 +15573,7 @@ function ghIssueReader(gh) {
|
|
|
15000
15573
|
return { kind: "missing" };
|
|
15001
15574
|
return { kind: "failed", detail: result.stderr.trim() || result.stdout.trim() || "gh exited non-zero and wrote nothing" };
|
|
15002
15575
|
}
|
|
15003
|
-
return
|
|
15576
|
+
return issueOf4(parsedJson(result.stdout)) ?? { kind: "failed", detail: `gh issue view ${String(number)} answered no title, body and state` };
|
|
15004
15577
|
};
|
|
15005
15578
|
}
|
|
15006
15579
|
function exportedNames(payload) {
|
|
@@ -15320,8 +15893,8 @@ function issueTitle(redactedWhat) {
|
|
|
15320
15893
|
const kept = points.slice(0, TITLE_MAX_LENGTH - TITLE_CUT.length).join("");
|
|
15321
15894
|
return `${kept.trimEnd()}${TITLE_CUT}`;
|
|
15322
15895
|
}
|
|
15323
|
-
function section(heading, value,
|
|
15324
|
-
const shown = value === null ?
|
|
15896
|
+
function section(heading, value, absent2, redact) {
|
|
15897
|
+
const shown = value === null ? absent2 : fenced(redact(value));
|
|
15325
15898
|
return `## ${heading}
|
|
15326
15899
|
|
|
15327
15900
|
${shown}`;
|
|
@@ -16297,6 +16870,73 @@ import { homedir as homedir7 } from "os";
|
|
|
16297
16870
|
import path6 from "path";
|
|
16298
16871
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
16299
16872
|
|
|
16873
|
+
// src/board/epic-context.ts
|
|
16874
|
+
var EPIC_TYPE_LABEL = "type:epic";
|
|
16875
|
+
var EPIC_CONTEXT_LIMIT = 20;
|
|
16876
|
+
var PREFIX14 = "epic context";
|
|
16877
|
+
function epicContextArgs(slug) {
|
|
16878
|
+
return Object.freeze([
|
|
16879
|
+
"issue",
|
|
16880
|
+
"list",
|
|
16881
|
+
"--state",
|
|
16882
|
+
"all",
|
|
16883
|
+
"--label",
|
|
16884
|
+
EPIC_TYPE_LABEL,
|
|
16885
|
+
"--label",
|
|
16886
|
+
`${EPIC_LABEL_PREFIX}${slug}`,
|
|
16887
|
+
"--limit",
|
|
16888
|
+
String(EPIC_CONTEXT_LIMIT),
|
|
16889
|
+
"--json",
|
|
16890
|
+
BOARD_LIST_FIELDS
|
|
16891
|
+
]);
|
|
16892
|
+
}
|
|
16893
|
+
function numbersOf(numbers) {
|
|
16894
|
+
return numbers.map((number) => `#${String(number)}`).join(", ");
|
|
16895
|
+
}
|
|
16896
|
+
async function findEpic(slug, options, output) {
|
|
16897
|
+
const id = `issue #${String(options.issue)}`;
|
|
16898
|
+
const label = `${EPIC_LABEL_PREFIX}${slug}`;
|
|
16899
|
+
const args = epicContextArgs(slug);
|
|
16900
|
+
const command = `gh ${args.join(" ")}`;
|
|
16901
|
+
const result = await options.gh(args);
|
|
16902
|
+
if (!result.ok) {
|
|
16903
|
+
const written = result.stderr.trim() || result.stdout.trim() || "nothing";
|
|
16904
|
+
output.warn(`${PREFIX14}: ${command} failed (${written}); ${id} is planned without its epic`);
|
|
16905
|
+
return null;
|
|
16906
|
+
}
|
|
16907
|
+
let epics;
|
|
16908
|
+
try {
|
|
16909
|
+
epics = parseBoardListing(result.stdout, command);
|
|
16910
|
+
} catch (error) {
|
|
16911
|
+
output.warn(`${PREFIX14}: ${messageOf(error)}; ${id} is planned without its epic`);
|
|
16912
|
+
return null;
|
|
16913
|
+
}
|
|
16914
|
+
const [epic, ...others] = epics;
|
|
16915
|
+
if (epic === undefined) {
|
|
16916
|
+
output.warn(`${PREFIX14}: ${id} carries "${label}" but no "${EPIC_TYPE_LABEL}" issue does; it is planned without an epic`);
|
|
16917
|
+
return null;
|
|
16918
|
+
}
|
|
16919
|
+
if (others.length > 0) {
|
|
16920
|
+
const found = numbersOf(epics.map((each) => each.number));
|
|
16921
|
+
output.warn(`${PREFIX14}: ${id} carries "${label}" and ${String(epics.length)} epics carry it (${found}); it is planned without an epic`);
|
|
16922
|
+
return null;
|
|
16923
|
+
}
|
|
16924
|
+
return Object.freeze({ number: epic.number, title: epic.title, slug, criteria: readEpicBody(epic.body).criteria });
|
|
16925
|
+
}
|
|
16926
|
+
async function readEpicContext(options) {
|
|
16927
|
+
const slugs = epicSlugsOf(options.labels);
|
|
16928
|
+
const [slug, ...others] = slugs;
|
|
16929
|
+
if (slug === undefined)
|
|
16930
|
+
return null;
|
|
16931
|
+
const output = options.output ?? activeOutput();
|
|
16932
|
+
if (others.length > 0) {
|
|
16933
|
+
const labels = slugs.map((each) => `"${EPIC_LABEL_PREFIX}${each}"`).join(", ");
|
|
16934
|
+
output.warn(`${PREFIX14}: issue #${String(options.issue)} carries ${String(slugs.length)} epic labels (${labels}); it is planned without an epic`);
|
|
16935
|
+
return null;
|
|
16936
|
+
}
|
|
16937
|
+
return findEpic(slug, options, output);
|
|
16938
|
+
}
|
|
16939
|
+
|
|
16300
16940
|
// src/commands/plan/plan-record.ts
|
|
16301
16941
|
import fs2 from "fs";
|
|
16302
16942
|
import path3 from "path";
|
|
@@ -16525,7 +17165,7 @@ async function enforceRefsGate(options) {
|
|
|
16525
17165
|
// src/commands/plan/refs-check.ts
|
|
16526
17166
|
async function coreRoster() {
|
|
16527
17167
|
const [{ CORE_REGISTRY }, { describeRegistry }] = await Promise.all([
|
|
16528
|
-
import("./index-
|
|
17168
|
+
import("./index-gzr2gtmh.js"),
|
|
16529
17169
|
import("./describe-0tefrkk7.js")
|
|
16530
17170
|
]);
|
|
16531
17171
|
return describeRegistry(CORE_REGISTRY, version);
|
|
@@ -16821,6 +17461,43 @@ function formatSkillIndexSection(index) {
|
|
|
16821
17461
|
function readPlanSkillIndex(repoRoot, home, settings, entry) {
|
|
16822
17462
|
return renderSkillIndex(resolveSessionTiers({ root: repoRoot, home, settings, entry }));
|
|
16823
17463
|
}
|
|
17464
|
+
var EPIC_CONTEXT_HEADING = "## Epic (the epic this spec belongs to)";
|
|
17465
|
+
function formatEpicContextSection(epic) {
|
|
17466
|
+
if (epic === null)
|
|
17467
|
+
return "";
|
|
17468
|
+
const named = `This spec belongs to epic #${String(epic.number)}, "${epic.title}" (\`epic:${epic.slug}\`).`;
|
|
17469
|
+
if (epic.criteria === null) {
|
|
17470
|
+
return [
|
|
17471
|
+
"",
|
|
17472
|
+
"",
|
|
17473
|
+
EPIC_CONTEXT_HEADING,
|
|
17474
|
+
"",
|
|
17475
|
+
named,
|
|
17476
|
+
"Its body holds no acceptance criteria section, so there is none to carry into the plan."
|
|
17477
|
+
].join(`
|
|
17478
|
+
`);
|
|
17479
|
+
}
|
|
17480
|
+
return [
|
|
17481
|
+
"",
|
|
17482
|
+
"",
|
|
17483
|
+
EPIC_CONTEXT_HEADING,
|
|
17484
|
+
"",
|
|
17485
|
+
named,
|
|
17486
|
+
"Carry the epic's acceptance criteria below, verbatim, into the plan's `rafa:context` block,",
|
|
17487
|
+
"under a line naming the epic, so every task is shown them; plan the spec so that it serves",
|
|
17488
|
+
"them, and name in the plan any criterion the spec leaves to another issue of the epic.",
|
|
17489
|
+
"",
|
|
17490
|
+
"### Acceptance criteria",
|
|
17491
|
+
"",
|
|
17492
|
+
epic.criteria
|
|
17493
|
+
].join(`
|
|
17494
|
+
`);
|
|
17495
|
+
}
|
|
17496
|
+
async function readPlanEpicContext(spec, gh) {
|
|
17497
|
+
if (spec.read === null)
|
|
17498
|
+
return null;
|
|
17499
|
+
return readEpicContext({ issue: spec.read.number, labels: spec.read.labels, gh });
|
|
17500
|
+
}
|
|
16824
17501
|
var PLAN_PROMPT_SLOTS = [
|
|
16825
17502
|
"PLAN_FILE",
|
|
16826
17503
|
"PREREQUISITES_FILE",
|
|
@@ -16828,10 +17505,11 @@ var PLAN_PROMPT_SLOTS = [
|
|
|
16828
17505
|
"PLAN_FORMAT",
|
|
16829
17506
|
"ROUTING",
|
|
16830
17507
|
"SKILL_INDEX",
|
|
17508
|
+
"EPIC_CONTEXT",
|
|
16831
17509
|
"SPEC_CONTENT"
|
|
16832
17510
|
];
|
|
16833
17511
|
var SLOT_PATTERN = new RegExp(`\\{(${PLAN_PROMPT_SLOTS.join("|")})\\}`, "g");
|
|
16834
|
-
function buildPlanPrompt(template, planFormat, specContent, stub, planDir, progressContent, routing = DEFAULT_ROUTING, skillIndex = "") {
|
|
17512
|
+
function buildPlanPrompt(template, planFormat, specContent, stub, planDir, progressContent, routing = DEFAULT_ROUTING, skillIndex = "", epic = null) {
|
|
16835
17513
|
const values = {
|
|
16836
17514
|
PLAN_FILE: planFilePath(planDir, `PLAN-${stub}.md`),
|
|
16837
17515
|
PREREQUISITES_FILE: planFilePath(planDir, `PREREQUISITES-${stub}.md`),
|
|
@@ -16839,6 +17517,7 @@ function buildPlanPrompt(template, planFormat, specContent, stub, planDir, progr
|
|
|
16839
17517
|
PLAN_FORMAT: planFormatBody(planFormat),
|
|
16840
17518
|
ROUTING: formatRoutingSection(routing),
|
|
16841
17519
|
SKILL_INDEX: formatSkillIndexSection(skillIndex),
|
|
17520
|
+
EPIC_CONTEXT: formatEpicContextSection(epic),
|
|
16842
17521
|
SPEC_CONTENT: specContent
|
|
16843
17522
|
};
|
|
16844
17523
|
return template.replace(SLOT_PATTERN, (_slot, name2) => values[name2]);
|
|
@@ -16898,6 +17577,7 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
|
|
|
16898
17577
|
if (progressContent?.trim()) {
|
|
16899
17578
|
activeOutput().info("\uD83D\uDCCE Including findings from progress.txt (disable with --no-progress).");
|
|
16900
17579
|
}
|
|
17580
|
+
const epic = await readPlanEpicContext(resolved.spec, createGhRunner({ cwd: repoRoot }));
|
|
16901
17581
|
const template = fs4.readFileSync(path6.join(__dirname3, "plan-prompt.md"), "utf8");
|
|
16902
17582
|
const planFormat = readPlanFormat(__dirname3);
|
|
16903
17583
|
const skillIndex = readPlanSkillIndex(repoRoot, home, config, fileURLToPath2(import.meta.url));
|
|
@@ -16905,7 +17585,7 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
|
|
|
16905
17585
|
repoRoot,
|
|
16906
17586
|
planDir,
|
|
16907
17587
|
settingSources,
|
|
16908
|
-
planPrompt: (specContent, planStub2) => buildPlanPrompt(template, planFormat, specContent, planStub2, planDir, progressContent, routing, skillIndex)
|
|
17588
|
+
planPrompt: (specContent, planStub2) => buildPlanPrompt(template, planFormat, specContent, planStub2, planDir, progressContent, routing, skillIndex, epic)
|
|
16909
17589
|
});
|
|
16910
17590
|
const gate = {
|
|
16911
17591
|
source: resolved.spec.source,
|
|
@@ -16934,4 +17614,4 @@ async function plan(args, repoRoot, registry = CORE_ADAPTER_REGISTRY) {
|
|
|
16934
17614
|
}
|
|
16935
17615
|
}
|
|
16936
17616
|
|
|
16937
|
-
export { readConfigFile, loadConfig, planStubsFromFileNames, resolvePlanStub, SERVE_CLI_VERSION, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, SessionRecordError, runsDir, errorCode, readSessions, readSession, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, sessionLogDir, collectSessionRow, collect, AGENT_DEFINITION_DIR, VENDOR_COMMAND, readAgentDirectory, resolveAgentRoster, missingPlanAgents, isStackList, stackPaths, stacksFromEccSkillName, DESCRIPTION_LIMIT, LISTING_LIMIT, SKILL_SIGNALS, countCharacters, toolOf, pathDirectories, checkReferences, checkFile, checkDirectory, bundledAgentsDirectory, sourceItem, byName, skillItem, readTrees, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, report, name, version, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, validate_default, SPEC_READY_LABEL, TEMPLATE_HEADINGS, findReadinessGaps, hasSpecReadyLabel, createGhPermissions, readAuthorTrust, trustRefusalClause, requireTrustedAuthor, SPEC_NEEDS_WORK_LABEL,
|
|
17617
|
+
export { readConfigFile, loadConfig, planStubsFromFileNames, resolvePlanStub, SERVE_CLI_VERSION, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, SessionRecordError, runsDir, errorCode, readSessions, readSession, PROVENANCE_FIELD, SKILL_FILE2 as SKILL_FILE, provenanceBlock, serveVerdict, SKILL_USE_CLI_VERSION, sessionLogDir, collectSessionRow, collect, AGENT_DEFINITION_DIR, VENDOR_COMMAND, readAgentDirectory, resolveAgentRoster, missingPlanAgents, isStackList, stackPaths, stacksFromEccSkillName, DESCRIPTION_LIMIT, LISTING_LIMIT, SKILL_SIGNALS, countCharacters, toolOf, pathDirectories, checkReferences, checkFile, checkDirectory, bundledAgentsDirectory, sourceItem, byName, skillItem, readTrees, verdictOf, failingRows, formatRows, waitForChecks, recordPlanCi, report, name, version, MANIFEST_VERSION, FEATURE_TYPES, OUTPUT_CHANNELS, RUNNING_MANIFEST_SEAMS, validateManifest, ID_PREFIX, BRANCH_PREFIX, SPEC_EXTENSION, boardId, notesFileName, ROADMAP_TITLE, ROADMAP_SETTING, ROADMAP_REFUSAL_EXIT, parseRoadmapBody, createGhRoadmapSearch, severalRoadmapsMessage, resolveRoadmapIssue, createGhOpenPullRequests, closedIssuesIn, branchClaims, scanClaimBranches, SPEC_BLOCKED_LABEL, hasSpecBlockedLabel, readBlockedBy, blockedFaultMessage, EPIC_LABEL_PREFIX, epicSlugsOf, isNotPlanned, readEpics, BOARD_LISTING_LIMIT, createGhBoardListing, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, validate_default, SPEC_READY_LABEL, TEMPLATE_HEADINGS, findReadinessGaps, hasSpecReadyLabel, createGhPermissions, readAuthorTrust, trustRefusalClause, requireTrustedAuthor, SPEC_NEEDS_WORK_LABEL, UNREADABLE, RefsBlockError, fingerprintText, readRefsText, readCopyRefs, restampCopyRefs, createGitRunner, gitSaid, RefVerifyError, ghIssueReader, tsSymbolsOutliner, createRefVerifier, createGhIssueBoard, PREVIOUS_COPY_WARN_ABOVE, previousDir, countPreviousCopies, SPEC_LABEL, createGhSpecIssueReader, blockedLineSentence, HORIZON_LABEL_PREFIX, readEpicProblems, epicProblemMessage, isNowEpic, epicLines, memoiseVerifier, LEARNED_DIRECTORY, ORIGIN_FIELD, DEMOTION_VERDICTS, WHEN_TO_USE_HEADING, bodySections, classifySkill, firstSentence, convertToInstinct, readPlanChanges, rankCandidates, readRankCandidate, shellQuote, ghMissingMessage, ghUnauthenticatedMessage, ghHostOf, ghPreflightItems, resolvePrProvider, requireGhProvider, isFirstDispatch, SESSION_ID_FLAG2 as SESSION_ID_FLAG, resolveLoopSeams, sessionIdFlag, refusal2 as refusal, isLive, planLabel, sessionLine, projectRoot, readRecords, pickSession, writeSession, readSessionChecklist, checkboxAt, estimateEta, readSessionFinishes, etaLine, parseWorkingTree, parseWorktrees, readMergeRefusal, cleanUpSteps, commandLine, remainingFrom, createGhPullRequests, ghPullRequestsIn, DEFAULT_BASE_BRANCH, ghNextBoard, openNextSources, createLinePrompter, REMOTE, parseBaseStanding, hasDiverged, list_default, readBranch, readPlans, readOpenPull, readBranchPlan, readNextState, commandWords, nextQuestion, NEXT_COMMAND_ACTIONS, actionInvocation, runAction, HINT_FLAG_SPEC, actionOutput, endWithNextStep, endingWith, DEFAULT_ISSUE_SEAMS, lineRefusal2 as lineRefusal, expectTwoArguments, readTextFlag, readNonBlankFlag, readRequiredFlag, readChoice, readChoiceFlag, issueProject, issueSubjectConfig, resolveIssueTracker, issueRef, issueName, urlLines, onTracker, lazyPrompter, ready_default, versionLine, DEFAULT_PLAN_FILE, resolvePlanPath, assessPlanRisk, renderRiskText, announceRiskTotal, resolveCreateSpec, resolveReleaseEnabled, TRIAGE_MODULE, DEFAULT_CI_TIMEOUT_MIN, CI_POLL_INTERVAL_MS, DEFAULT_CI_ATTEMPTS, RUNTIME_SUBDIR, RUNTIME_ENTRY, VERSION_SHAPE2 as VERSION_SHAPE, refuseMisplacedRuntime, groupChangeNotes, renderNoteLines, highestChangeLevel, parseSemanticVersion, readManifestVersion, MECHANICAL_CONFLICT_SENTENCE, start, usage, coreRoster, createPlanRefsVerifier, plan };
|