@jwilger/pi-development-system 0.57.0 → 0.58.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -32,7 +32,8 @@ const STEP = /^\s*(?:\d+[.)]|[-*])\s+(.*)$/;
32
32
  const STEPS_MIN = 3;
33
33
  const STEPS_MAX = 7;
34
34
  /** Words that stand in for a command or an observable result. */
35
- const PLACEHOLDER = /^(?:n\/?a|none|todo|tbc|-|…|\.\.\.|works?|it works|passes|ok)\.?$/i;
35
+ const PLACEHOLDER =
36
+ /^(?:(?:todo|tbd|tbc)\b.*|n\/?a|none|-|…|\.\.\.|works?|it works|passes|ok)\.?$/i;
36
37
 
37
38
  const isSection = (name: string): name is Section => SECTIONS.some((s) => s === name);
38
39
 
@@ -85,7 +86,7 @@ const stepsOf = (text: string): string[] =>
85
86
 
86
87
  function concrete(name: "Run" | "Expected", text: string): string | undefined {
87
88
  const bare = stripTicks(text);
88
- if (PLACEHOLDER.test(bare))
89
+ if (bare === "" || PLACEHOLDER.test(bare))
89
90
  return `${name} must be a concrete ${name === "Run" ? "command" : "observable result"}, not "${bare}"`;
90
91
  if (name === "Expected" && bare.split(/\s+/).length < 2) {
91
92
  return "Expected must say what is observed (at least two words, e.g. a count, output or exit status)";
@@ -142,7 +143,7 @@ function locate(markdown: string, id: string | undefined): Located | ParseError
142
143
 
143
144
  /** Parses one task record (the only one in the file, or the one named by `id`), reporting every problem together. */
144
145
  export function parseTaskRecord(markdown: string, id?: string): TaskRecord | ParseError {
145
- const found = locate(markdown, id);
146
+ const found = locate(markdown.replace(/\r\n?/g, "\n"), id);
146
147
  if (isParseError(found)) return found;
147
148
  const split = splitSections(found.lines);
148
149
  const problems = problemsOf(found.lines.join("\n"), split);
@@ -135,13 +135,14 @@ const createIssue = async (gh: Gh, input: NewWorkItem): Promise<TrackerResult<Wo
135
135
  const labelFlags = (current: WorkItem, patch: WorkItemPatch): string[] => {
136
136
  const wanted = patch.labels ?? current.labels;
137
137
  const marked = (patch.status ?? current.status) === "in-progress";
138
+ const hadMarker = current.status === "in-progress";
138
139
  const add = [
139
140
  ...wanted.filter((l) => !current.labels.includes(l)),
140
- ...(marked ? [IN_PROGRESS] : []),
141
+ ...(marked && !hadMarker ? [IN_PROGRESS] : []),
141
142
  ];
142
143
  const remove = [
143
144
  ...current.labels.filter((l) => !wanted.includes(l)),
144
- ...(marked ? [] : [IN_PROGRESS]),
145
+ ...(!marked && hadMarker ? [IN_PROGRESS] : []),
145
146
  ];
146
147
  return [
147
148
  ...add.flatMap((l) => ["--add-label", l]),
@@ -175,8 +176,9 @@ const updateIssue = async (
175
176
  const current = await view(gh, id);
176
177
  if (!current.ok) return current;
177
178
  if (patch.status === "in-progress") {
178
- const made = await gh(["label", "create", IN_PROGRESS, "--force"]);
179
- if (!made.ok) return made;
179
+ // No --force: that would recolour the team's existing label. "Already exists" is success.
180
+ const made = await gh(["label", "create", IN_PROGRESS]);
181
+ if (!(made.ok || /already exists/i.test(made.error.message))) return made;
180
182
  }
181
183
  const flags = editFlags(current.value, patch);
182
184
  if (flags.length > 0) {
@@ -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) => (f.endsWith(".md") ? [f.slice(0, -3)] : []));
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 = await readAll(root);
72
- if (!items.ok) return items;
73
- await writeFile(join(root, "work", "backlog.md"), renderBacklog(items.value));
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