nightralph 0.0.33 → 0.0.35

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.
@@ -6,18 +6,26 @@ Issues and specs for this repo live as markdown files in `.scratch/`.
6
6
 
7
7
  - One feature per directory: `.scratch/<feature-slug>/`
8
8
  - The spec is `.scratch/<feature-slug>/spec.md`
9
- - Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`, never a single combined tickets file
10
- - Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
11
- - Dependencies are recorded as a `**Blocked by:**` line near the top of each issue file, listing the numbers of the tickets that must complete first, or `None`
12
- - Comments and conversation history append to the bottom of the file under a `## Comments` heading
9
+ - Implementation issues are one file per ticket at
10
+ `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`,
11
+ never a single combined tickets file
12
+ - Triage state is recorded as a `**Status:**` line near the top of each
13
+ issue file (see `triage-labels.md` for the role strings)
14
+ - Dependencies are recorded as a `**Blocked by:**` line near the top of
15
+ each issue file, listing the numbers of the tickets that must complete
16
+ first, or `None`
17
+ - Comments and conversation history append to the bottom of the file under
18
+ a `## Comments` heading
13
19
 
14
20
  ## When a skill says "publish to the issue tracker"
15
21
 
16
- Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
22
+ Create a new file under `.scratch/<feature-slug>/` (creating the directory
23
+ if needed).
17
24
 
18
25
  ## When a skill says "fetch the relevant ticket"
19
26
 
20
- Read the file at the referenced path. The user will normally pass the path or the issue number directly.
27
+ Read the file at the referenced path. The user will normally pass the path
28
+ or the issue number directly.
21
29
 
22
30
  ## Tickets are executed within hours, not weeks
23
31
 
package/dist/index.js CHANGED
@@ -1152,30 +1152,110 @@ function findReadyTickets(tickets) {
1152
1152
  });
1153
1153
  }
1154
1154
  __name(findReadyTickets, "findReadyTickets");
1155
- var COMPLETED_RE = /^\[x\]\s+(.+)$/i;
1155
+ var COMPLETED_RE = /^\s*[-*+]?\s*\[x\]\s+(.+)$/i;
1156
1156
  var TOOL_USE_RE = /^\[[\w.]+\]/;
1157
+ var CHECKBOX_RE = /^(\s*[-*+] )\[( |x|X)\]( .*)$/;
1158
+ var UNCHECKED_BOX_RE = /^(\s*[-*+] )\[( )\]/;
1159
+ function normalizeItemText(s) {
1160
+ return s.replace(/\s+/g, " ").trim().toLowerCase();
1161
+ }
1162
+ __name(normalizeItemText, "normalizeItemText");
1163
+ function stripCheckboxPrefix(s) {
1164
+ return s.replace(/^\s*[-*+]?\s*\[[ xX]\]\s+/, "");
1165
+ }
1166
+ __name(stripCheckboxPrefix, "stripCheckboxPrefix");
1167
+ function parseChecklistItems(lines) {
1168
+ const items = [];
1169
+ let inFence = false;
1170
+ let i = 0;
1171
+ while (i < lines.length) {
1172
+ if (/^\s*```/.test(lines[i])) {
1173
+ inFence = !inFence;
1174
+ i++;
1175
+ continue;
1176
+ }
1177
+ if (inFence) {
1178
+ i++;
1179
+ continue;
1180
+ }
1181
+ const m = lines[i].match(CHECKBOX_RE);
1182
+ if (!m) {
1183
+ i++;
1184
+ continue;
1185
+ }
1186
+ const start = i;
1187
+ let text = m[3];
1188
+ i++;
1189
+ while (i < lines.length && /^\s+\S/.test(lines[i]) && !CHECKBOX_RE.test(lines[i])) {
1190
+ text += " " + lines[i].trim();
1191
+ i++;
1192
+ }
1193
+ items.push({
1194
+ start,
1195
+ text: normalizeItemText(text)
1196
+ });
1197
+ }
1198
+ return items;
1199
+ }
1200
+ __name(parseChecklistItems, "parseChecklistItems");
1157
1201
  function checkItems(ticket, completedItems) {
1158
1202
  if (completedItems.length === 0) return;
1159
- let body = ticket.body;
1160
- for (const item of completedItems) {
1161
- const escaped = item.replace(
1162
- /[.*+?^${}()|[\]\\]/g,
1163
- "\\$&"
1164
- );
1165
- body = body.replace(
1166
- new RegExp(
1167
- `- \\[ \\] ${escaped}`,
1168
- "i"
1169
- ),
1170
- `- [x] ${item}`
1203
+ let body;
1204
+ try {
1205
+ body = readFileSync4(ticket.filepath, "utf8");
1206
+ } catch {
1207
+ body = ticket.body;
1208
+ }
1209
+ const wanted = new Set(
1210
+ completedItems.map(
1211
+ (item) => normalizeItemText(stripCheckboxPrefix(item))
1212
+ )
1213
+ );
1214
+ const lines = body.split("\n");
1215
+ let changed = false;
1216
+ for (const item of parseChecklistItems(lines)) {
1217
+ if (!wanted.has(item.text)) continue;
1218
+ const flipped = lines[item.start].replace(
1219
+ UNCHECKED_BOX_RE,
1220
+ "$1[x]"
1171
1221
  );
1222
+ if (flipped !== lines[item.start]) {
1223
+ lines[item.start] = flipped;
1224
+ changed = true;
1225
+ }
1172
1226
  }
1227
+ if (!changed) return;
1228
+ body = lines.join("\n");
1173
1229
  writeFileSync3(ticket.filepath, body);
1174
1230
  ticket.body = body;
1175
1231
  }
1176
1232
  __name(checkItems, "checkItems");
1233
+ function checkAllItems(body) {
1234
+ const lines = body.split("\n");
1235
+ let inFence = false;
1236
+ for (let i = 0; i < lines.length; i++) {
1237
+ if (/^\s*```/.test(lines[i])) {
1238
+ inFence = !inFence;
1239
+ continue;
1240
+ }
1241
+ if (inFence) continue;
1242
+ lines[i] = lines[i].replace(
1243
+ UNCHECKED_BOX_RE,
1244
+ "$1[x]"
1245
+ );
1246
+ }
1247
+ return lines.join("\n");
1248
+ }
1249
+ __name(checkAllItems, "checkAllItems");
1177
1250
  function markDone(ticket) {
1178
- const updated = ticket.body.replace(
1251
+ let body;
1252
+ try {
1253
+ body = readFileSync4(ticket.filepath, "utf8");
1254
+ } catch {
1255
+ body = ticket.body;
1256
+ }
1257
+ body = checkAllItems(body);
1258
+ const updated = body.replace(
1179
1259
  /\*\*Status:\*\*\s*.+/i,
1180
1260
  "**Status:** done"
1181
1261
  );
@@ -1590,9 +1670,16 @@ function spawnAgent(opts) {
1590
1670
  proc.stdout.pipe(logStream);
1591
1671
  const piFormatter = isPi ? createPiStreamFormatter() : null;
1592
1672
  const completedItems = [];
1673
+ const seenItems = /* @__PURE__ */ new Set();
1593
1674
  function collectCompleted(text) {
1594
- const match = text.match(COMPLETED_RE);
1595
- if (match) completedItems.push(match[1]);
1675
+ for (const line of text.split("\n")) {
1676
+ const match = line.match(COMPLETED_RE);
1677
+ if (!match) continue;
1678
+ const item = match[1].trim();
1679
+ if (!item || seenItems.has(item)) continue;
1680
+ seenItems.add(item);
1681
+ completedItems.push(item);
1682
+ }
1596
1683
  }
1597
1684
  __name(collectCompleted, "collectCompleted");
1598
1685
  const rl = createInterface({ input: proc.stdout });