@jwilger/pi-development-system 0.57.0 → 0.59.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/package.json
CHANGED
|
@@ -27,6 +27,9 @@ export const READINESS_QUESTIONS = {
|
|
|
27
27
|
steps: vague(
|
|
28
28
|
"`task.steps` should be small steps a reviewer could reject independently. Are any steps large, combined ('implement and wire everything'), or out of order with the test-first step?",
|
|
29
29
|
),
|
|
30
|
+
check: vague(
|
|
31
|
+
"`task.run` should be an actual command a person can type, and `task.expected` an observable result such as a count, output or exit status. Is `run` prose or a placeholder rather than a command (for example 'run the tests'), or is `expected` a restatement of success (for example 'tests pass', 'should work') rather than something observed?",
|
|
32
|
+
),
|
|
30
33
|
tooBig: {
|
|
31
34
|
type: "bool",
|
|
32
35
|
instructions:
|
|
@@ -60,6 +63,8 @@ export async function judgeTaskReadiness(
|
|
|
60
63
|
interfaces: clip(record.interfaces, 1500),
|
|
61
64
|
firstFailingTest: clip(record.firstFailingTest, 600),
|
|
62
65
|
steps: record.steps.slice(0, 10).map((s) => clip(s, 300)),
|
|
66
|
+
run: clip(record.run, 300),
|
|
67
|
+
expected: clip(record.expected, 300),
|
|
63
68
|
},
|
|
64
69
|
},
|
|
65
70
|
READINESS_QUESTIONS,
|
|
@@ -28,11 +28,13 @@ type Section = (typeof SECTIONS)[number];
|
|
|
28
28
|
|
|
29
29
|
const HEADER = /^## (\S+) [—-] (.+)$/;
|
|
30
30
|
const SECTION_LINE = /^\*\*([^:*]+)(?::\*\*|\*\*:)\s*(.*)$/;
|
|
31
|
-
|
|
31
|
+
/** Top-level list items only; indented sub-items belong to the step above them. */
|
|
32
|
+
const STEP = /^(?:\d+[.)]|[-*])\s+(.*)$/;
|
|
32
33
|
const STEPS_MIN = 3;
|
|
33
34
|
const STEPS_MAX = 7;
|
|
34
35
|
/** Words that stand in for a command or an observable result. */
|
|
35
|
-
const PLACEHOLDER =
|
|
36
|
+
const PLACEHOLDER =
|
|
37
|
+
/^(?:(?:tbd|tbc)\b.*|todo\s*(?::.*)?|n\/?a|none|-|…|\.\.\.|works?|it works|passes|ok)\.?$/i;
|
|
36
38
|
|
|
37
39
|
const isSection = (name: string): name is Section => SECTIONS.some((s) => s === name);
|
|
38
40
|
|
|
@@ -85,7 +87,7 @@ const stepsOf = (text: string): string[] =>
|
|
|
85
87
|
|
|
86
88
|
function concrete(name: "Run" | "Expected", text: string): string | undefined {
|
|
87
89
|
const bare = stripTicks(text);
|
|
88
|
-
if (PLACEHOLDER.test(bare))
|
|
90
|
+
if (bare === "" || PLACEHOLDER.test(bare))
|
|
89
91
|
return `${name} must be a concrete ${name === "Run" ? "command" : "observable result"}, not "${bare}"`;
|
|
90
92
|
if (name === "Expected" && bare.split(/\s+/).length < 2) {
|
|
91
93
|
return "Expected must say what is observed (at least two words, e.g. a count, output or exit status)";
|
|
@@ -142,7 +144,7 @@ function locate(markdown: string, id: string | undefined): Located | ParseError
|
|
|
142
144
|
|
|
143
145
|
/** Parses one task record (the only one in the file, or the one named by `id`), reporting every problem together. */
|
|
144
146
|
export function parseTaskRecord(markdown: string, id?: string): TaskRecord | ParseError {
|
|
145
|
-
const found = locate(markdown, id);
|
|
147
|
+
const found = locate(markdown.replace(/\r\n?/g, "\n"), id);
|
|
146
148
|
if (isParseError(found)) return found;
|
|
147
149
|
const split = splitSections(found.lines);
|
|
148
150
|
const problems = problemsOf(found.lines.join("\n"), split);
|
package/src/tracker/github.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Exec } from "../core/exec.ts";
|
|
2
2
|
import { ok } from "../core/result.ts";
|
|
3
|
+
import { unwritable } from "./item-format.ts";
|
|
3
4
|
import {
|
|
4
5
|
type ItemFilter,
|
|
5
6
|
type NewWorkItem,
|
|
@@ -94,27 +95,55 @@ const bindGh =
|
|
|
94
95
|
return trackerError(`gh ${args.slice(0, 2).join(" ")} failed: ${why}`);
|
|
95
96
|
};
|
|
96
97
|
|
|
97
|
-
const
|
|
98
|
+
const viewRaw = async (gh: Gh, id: string): Promise<TrackerResult<GhIssue>> => {
|
|
98
99
|
if (!ISSUE_NUMBER.test(id)) return notNumber(id);
|
|
99
100
|
const out = await gh(["issue", "view", id, "--json", FIELDS]);
|
|
100
101
|
if (!out.ok) return out;
|
|
101
102
|
const parsed = parseIssue(out.value);
|
|
102
|
-
return parsed === undefined
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
return parsed === undefined ? trackerError(`gh issue view ${id}: unexpected output`) : ok(parsed);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const view = async (gh: Gh, id: string): Promise<TrackerResult<WorkItem>> => {
|
|
107
|
+
const raw = await viewRaw(gh, id);
|
|
108
|
+
return raw.ok ? ok(toItem(raw.value)) : raw;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const LIST_LIMIT = 500;
|
|
112
|
+
|
|
113
|
+
/** The `gh issue list` arguments that select exactly the wanted status where gh can, so the limit bites last. */
|
|
114
|
+
const listArgs = (status: WorkStatus | undefined): string[] => {
|
|
115
|
+
const state = status === undefined ? "all" : STATE_ARG[status];
|
|
116
|
+
const marker = status === "in-progress" ? ["--label", IN_PROGRESS] : [];
|
|
117
|
+
return [
|
|
118
|
+
"issue",
|
|
119
|
+
"list",
|
|
120
|
+
"--state",
|
|
121
|
+
state,
|
|
122
|
+
...marker,
|
|
123
|
+
"--limit",
|
|
124
|
+
String(LIST_LIMIT),
|
|
125
|
+
"--json",
|
|
126
|
+
FIELDS,
|
|
127
|
+
];
|
|
105
128
|
};
|
|
106
129
|
|
|
107
130
|
const listIssues = async (gh: Gh, filter: ItemFilter): Promise<TrackerResult<WorkItem[]>> => {
|
|
108
|
-
const
|
|
109
|
-
const out = await gh(["issue", "list", "--state", state, "--limit", "200", "--json", FIELDS]);
|
|
131
|
+
const out = await gh(listArgs(filter.status));
|
|
110
132
|
if (!out.ok) return out;
|
|
111
133
|
const parsed = parseIssues(out.value);
|
|
112
134
|
if (parsed === undefined) return trackerError("gh issue list: unexpected output");
|
|
135
|
+
if (parsed.length >= LIST_LIMIT) {
|
|
136
|
+
return trackerError(
|
|
137
|
+
`gh issue list returned ${LIST_LIMIT} issues, so the list may be cut short; ask for one status (open, in-progress or done) to narrow it`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
113
140
|
const items = parsed.map(toItem);
|
|
114
141
|
return ok(filter.status === undefined ? items : items.filter((i) => i.status === filter.status));
|
|
115
142
|
};
|
|
116
143
|
|
|
117
144
|
const createIssue = async (gh: Gh, input: NewWorkItem): Promise<TrackerResult<WorkItem>> => {
|
|
145
|
+
const problem = unwritable({ title: input.title, labels: input.labels ?? [] });
|
|
146
|
+
if (problem !== undefined) return trackerError(problem);
|
|
118
147
|
const labels = (input.labels ?? []).flatMap((l) => ["--label", l]);
|
|
119
148
|
const out = await gh([
|
|
120
149
|
"issue",
|
|
@@ -132,16 +161,16 @@ const createIssue = async (gh: Gh, input: NewWorkItem): Promise<TrackerResult<Wo
|
|
|
132
161
|
};
|
|
133
162
|
|
|
134
163
|
/** Label changes that make the issue's labels exactly `wanted` (plus the in-progress marker when asked for). */
|
|
135
|
-
const labelFlags = (current: WorkItem, patch: WorkItemPatch): string[] => {
|
|
164
|
+
const labelFlags = (current: WorkItem, hadMarker: boolean, patch: WorkItemPatch): string[] => {
|
|
136
165
|
const wanted = patch.labels ?? current.labels;
|
|
137
166
|
const marked = (patch.status ?? current.status) === "in-progress";
|
|
138
167
|
const add = [
|
|
139
168
|
...wanted.filter((l) => !current.labels.includes(l)),
|
|
140
|
-
...(marked ? [IN_PROGRESS] : []),
|
|
169
|
+
...(marked && !hadMarker ? [IN_PROGRESS] : []),
|
|
141
170
|
];
|
|
142
171
|
const remove = [
|
|
143
172
|
...current.labels.filter((l) => !wanted.includes(l)),
|
|
144
|
-
...(marked ? [] : [
|
|
173
|
+
...(!marked && hadMarker ? [IN_PROGRESS] : []),
|
|
145
174
|
];
|
|
146
175
|
return [
|
|
147
176
|
...add.flatMap((l) => ["--add-label", l]),
|
|
@@ -149,10 +178,12 @@ const labelFlags = (current: WorkItem, patch: WorkItemPatch): string[] => {
|
|
|
149
178
|
];
|
|
150
179
|
};
|
|
151
180
|
|
|
152
|
-
const editFlags = (current: WorkItem, patch: WorkItemPatch): string[] => [
|
|
181
|
+
const editFlags = (current: WorkItem, hadMarker: boolean, patch: WorkItemPatch): string[] => [
|
|
153
182
|
...(patch.title === undefined ? [] : ["--title", patch.title]),
|
|
154
183
|
...(patch.body === undefined ? [] : ["--body", patch.body]),
|
|
155
|
-
...(patch.labels === undefined && patch.status === undefined
|
|
184
|
+
...(patch.labels === undefined && patch.status === undefined
|
|
185
|
+
? []
|
|
186
|
+
: labelFlags(current, hadMarker, patch)),
|
|
156
187
|
];
|
|
157
188
|
|
|
158
189
|
/** Closing or reopening only when the issue is not already in the wanted state. */
|
|
@@ -172,18 +203,23 @@ const updateIssue = async (
|
|
|
172
203
|
id: string,
|
|
173
204
|
patch: WorkItemPatch,
|
|
174
205
|
): Promise<TrackerResult<WorkItem>> => {
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
206
|
+
const problem = unwritable({ title: patch.title ?? "x", labels: patch.labels ?? [] });
|
|
207
|
+
if (problem !== undefined) return trackerError(problem);
|
|
208
|
+
const raw = await viewRaw(gh, id);
|
|
209
|
+
if (!raw.ok) return raw;
|
|
210
|
+
const current = toItem(raw.value);
|
|
211
|
+
const hadMarker = (raw.value.labels ?? []).some((l) => l.name === IN_PROGRESS);
|
|
177
212
|
if (patch.status === "in-progress") {
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
// No --force: that would recolour the team's existing label. "Already exists" is success.
|
|
214
|
+
const made = await gh(["label", "create", IN_PROGRESS]);
|
|
215
|
+
if (!(made.ok || /already exists/i.test(made.error.message))) return made;
|
|
180
216
|
}
|
|
181
|
-
const flags = editFlags(current
|
|
217
|
+
const flags = editFlags(current, hadMarker, patch);
|
|
182
218
|
if (flags.length > 0) {
|
|
183
219
|
const edited = await gh(["issue", "edit", id, ...flags]);
|
|
184
220
|
if (!edited.ok) return edited;
|
|
185
221
|
}
|
|
186
|
-
const command = stateCommand(current
|
|
222
|
+
const command = stateCommand(current, patch.status);
|
|
187
223
|
if (command !== undefined) {
|
|
188
224
|
const changed = await gh(["issue", command, id]);
|
|
189
225
|
if (!changed.ok) return changed;
|
|
@@ -54,7 +54,8 @@ const commentsOf = (section: string): string[] =>
|
|
|
54
54
|
.slice(1)
|
|
55
55
|
.map((c) => unquote(c.replace(/\n+$/, "")));
|
|
56
56
|
|
|
57
|
-
export const parseItem = (id: string,
|
|
57
|
+
export const parseItem = (id: string, raw: string): TrackerResult<WorkItem> => {
|
|
58
|
+
const text = raw.replace(/\r\n?/g, "\n");
|
|
58
59
|
const marker = text.lastIndexOf(COMMENTS);
|
|
59
60
|
const main = marker < 0 ? text : text.slice(0, marker);
|
|
60
61
|
const comments = marker < 0 ? [] : commentsOf(text.slice(marker + COMMENTS.length));
|
|
@@ -46,7 +46,9 @@ const readItem = async (root: string, id: string): Promise<TrackerResult<WorkIte
|
|
|
46
46
|
const itemIds = async (root: string): Promise<string[]> => {
|
|
47
47
|
try {
|
|
48
48
|
const files = await readdir(itemsDir(root));
|
|
49
|
-
return files.flatMap((f) =>
|
|
49
|
+
return files.flatMap((f) =>
|
|
50
|
+
f.endsWith(".md") && ID.test(f.slice(0, -3)) ? [f.slice(0, -3)] : [],
|
|
51
|
+
);
|
|
50
52
|
} catch (cause) {
|
|
51
53
|
if (isMissing(cause)) return [];
|
|
52
54
|
throw cause;
|
|
@@ -66,11 +68,14 @@ const readAll = async (root: string): Promise<TrackerResult<WorkItem[]>> => {
|
|
|
66
68
|
const save = async (root: string, item: WorkItem): Promise<TrackerResult<WorkItem>> => {
|
|
67
69
|
const problem = unwritable(item);
|
|
68
70
|
if (problem !== undefined) return trackerError(problem);
|
|
71
|
+
const others = await readAll(root);
|
|
72
|
+
if (!others.ok) return others;
|
|
69
73
|
await mkdir(itemsDir(root), { recursive: true });
|
|
70
74
|
await writeFile(fileOf(root, item.id), renderItem(item));
|
|
71
|
-
const items =
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const items = [...others.value.filter((i) => i.id !== item.id), item].toSorted((a, b) =>
|
|
76
|
+
byCodeUnit(a.id, b.id),
|
|
77
|
+
);
|
|
78
|
+
await writeFile(join(root, "work", "backlog.md"), renderBacklog(items));
|
|
74
79
|
return ok(item);
|
|
75
80
|
};
|
|
76
81
|
|