@spardutti/claude-skills 2.62.0 → 2.63.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.
@@ -62,6 +62,15 @@ export function symbolsOf(extendBy = "") {
62
62
  return [...new Set([...ticked, ...shaped])];
63
63
  }
64
64
 
65
+ export function planMarks(planPath) {
66
+ const text = existsSync(planPath) ? readFileSync(planPath, "utf8") : null;
67
+ if (text === null) return null;
68
+ const section = text.split(/^## /m).find((s) => /^Registry\s*$/m.test(s.split("\n")[0])) ?? "";
69
+ const marks = new Map();
70
+ for (const m of section.matchAll(/^[-*]\s*`?([\w.-]+)`?\s*:\s*(reuse|skip)\b/gim)) marks.set(m[1], m[2].toLowerCase());
71
+ return marks;
72
+ }
73
+
65
74
  export const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
75
 
67
76
  export function triggerHits(entry, text) {
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  import {
5
- REGISTRY_SUFFIX, addedText, escape, insideLocation, loadRegistry, projectFiles, projectRoot, symbolsOf, triggerHits,
5
+ REGISTRY_SUFFIX, addedText, escape, insideLocation, loadRegistry, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
6
6
  } from "./registry-lib.mjs";
7
7
  import { loadEditRules } from "./text-rules.mjs";
8
8
 
@@ -26,15 +26,6 @@ function list() {
26
26
  stale();
27
27
  }
28
28
 
29
- function planMarks(planPath) {
30
- const text = existsSync(planPath) ? readFileSync(planPath, "utf8") : null;
31
- if (text === null) return null;
32
- const section = text.split(/^## /m).find((s) => /^Registry\s*$/m.test(s.split("\n")[0])) ?? "";
33
- const marks = new Map();
34
- for (const m of section.matchAll(/^[-*]\s*`?([\w.-]+)`?\s*:\s*(reuse|skip)\b/gim)) marks.set(m[1], m[2].toLowerCase());
35
- return marks;
36
- }
37
-
38
29
  function checkPlan(planPath, stubs) {
39
30
  if (!entries.length) return console.log("registry ok — no entries");
40
31
  const marks = planMarks(planPath);
@@ -1,5 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { parseLine, readLines } from "./diary-lib.mjs";
5
+ import { planMarks } from "./registry-lib.mjs";
3
6
 
4
7
  const PROJECT = /^ {2}(\S.*?) (stryker|mutmut|config)(?: \((\d+)s\))? — (.*)$/;
5
8
  const SURVIVOR = /^ {6}\[(Survived|NoCoverage)\] (\S+)\s+(\S+)$/;
@@ -100,3 +103,15 @@ export function prSize(base, testRe, sourceExt) {
100
103
  }
101
104
  return size;
102
105
  }
106
+
107
+ export function planRegistry(root) {
108
+ const marks = planMarks(join(root, "PLAN.md"));
109
+ return marks && Object.fromEntries(marks);
110
+ }
111
+
112
+ // Worktrees share one diary, so only this branch's blocks during the ship count.
113
+ export function ruleBlocks(diary, branch, since, until) {
114
+ return readLines(join(diary, "edit-blocks.jsonl")).map(parseLine)
115
+ .filter((r) => r?.branch === branch && r.ts >= since && r.ts <= until)
116
+ .map(({ ts, file, rules }) => ({ ts, file, rules: [].concat(rules) }));
117
+ }
@@ -2,7 +2,8 @@
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
- import { git, parseReport, prSize } from "./ship-log-parse.mjs";
5
+ import { parseLine, readLines } from "./diary-lib.mjs";
6
+ import { git, parseReport, planRegistry, prSize, ruleBlocks } from "./ship-log-parse.mjs";
6
7
  import { printReport, tally } from "./ship-log-report.mjs";
7
8
 
8
9
  const HERE = dirname(resolve(process.argv[1]));
@@ -58,13 +59,14 @@ function settleSurvivors(record) {
58
59
  }
59
60
  }
60
61
 
61
- function finalize(dir, path, record, tag, status, reason, at) {
62
+ function finalize(c, path, record, tag, status, reason, at) {
62
63
  settleSurvivors(record);
63
64
  Object.assign(record, { status, reason: reason ?? null, finished: at ?? now() });
65
+ record.rules = ruleBlocks(c.diary, record.branch, record.started, record.finished);
64
66
  record.summary = summarize(record);
65
67
  const stem = `${record.app}_${record.finished.slice(0, 10)}_${tag}`;
66
- let out = join(dir, `${stem}.json`);
67
- for (let i = 2; existsSync(out); i++) out = join(dir, `${stem}-${i}.json`);
68
+ let out = join(c.dir, `${stem}.json`);
69
+ for (let i = 2; existsSync(out); i++) out = join(c.dir, `${stem}-${i}.json`);
68
70
  save(path, record);
69
71
  renameSync(path, out);
70
72
  console.log(`ship-log: saved ${out}`);
@@ -78,8 +80,8 @@ function prOf(branch) {
78
80
  // Agents skip the `pr` call, so a log whose branch has a PR by now is closed at that PR.
79
81
  // A PR older than the log was only updated by it, so that log ends at its own last event.
80
82
  // A long-lived branch like dev keeps its merged PRs, so an old one that is not open is not this log's.
81
- function closeOpened(dir, app) {
82
- const open = join(dir, "open");
83
+ function closeOpened(c, app) {
84
+ const open = join(c.dir, "open");
83
85
  for (const f of existsSync(open) ? readdirSync(open).filter((n) => n.startsWith(`${app}__`)) : []) {
84
86
  const path = join(open, f), record = JSON.parse(readFileSync(path, "utf8"));
85
87
  const pr = prOf(record.branch), empty = !record.gate.length && !record.findings.length;
@@ -88,16 +90,16 @@ function closeOpened(dir, app) {
88
90
  if (empty) continue;
89
91
  const opened = pr && pr.createdAt >= record.started;
90
92
  if (!pr || (!opened && pr.state !== "OPEN")) continue;
91
- finalize(dir, path, Object.assign(record, { pr: pr.number }), `pr${pr.number}`,
93
+ finalize(c, path, Object.assign(record, { pr: pr.number }), `pr${pr.number}`,
92
94
  opened ? "pr-opened" : "pr-updated", null, opened ? pr.createdAt : lastEvent(record));
93
95
  }
94
96
  }
95
97
 
96
98
  // A ship that never reached pr or end was abandoned; keep it, but never fold it into the next one.
97
- function restart(dir, path, record, id) {
98
- closeOpened(dir, id.app);
99
- if (existsSync(path)) finalize(dir, path, record, id.branch.replace(/[^\w.-]+/g, "-"), "abandoned");
100
- save(path, load(path, id));
99
+ function restart(c, path, record, id) {
100
+ closeOpened(c, id.app);
101
+ if (existsSync(path)) finalize(c, path, record, id.branch.replace(/[^\w.-]+/g, "-"), "abandoned");
102
+ save(path, { ...load(path, id), registry: planRegistry(c.root) });
101
103
  }
102
104
 
103
105
  function addGateRun(record, [report, exit, seconds, base]) {
@@ -129,9 +131,7 @@ function addStep(record, [name, edge]) {
129
131
 
130
132
  // Worktrees share one diary, so only this branch's latest review since the ship started counts.
131
133
  function lastReview(diary, branch, since) {
132
- const file = join(diary, "review-findings.jsonl");
133
- const rows = existsSync(file) ? readFileSync(file, "utf8").split("\n").filter(Boolean) : [];
134
- const last = rows.map((l) => { try { return JSON.parse(l); } catch { return null; } })
134
+ const last = readLines(join(diary, "review-findings.jsonl")).map(parseLine)
135
135
  .filter((r) => r?.branch === branch && r.ts >= since && typeof r.reply === "string").at(-1);
136
136
  const json = last?.reply.match(/```json\s*([\s\S]*?)```/)?.[1];
137
137
  try {
@@ -161,11 +161,11 @@ function run(mode, args) {
161
161
  if (!c.on) return 0;
162
162
  const id = identity(c.root), path = openPath(c.dir, id), record = load(path, id);
163
163
  if (mode === "start" && RELEASE.test(id.branch)) {
164
- closeOpened(c.dir, id.app);
164
+ closeOpened(c, id.app);
165
165
  if (existsSync(path)) rmSync(path);
166
166
  return 0;
167
167
  }
168
- if (mode === "start") return restart(c.dir, path, record, id), 0;
168
+ if (mode === "start") return restart(c, path, record, id), 0;
169
169
  if (mode === "ready") {
170
170
  const todo = existsSync(path) ? missing(record, c) : [];
171
171
  if (todo.length) console.log(todo.map((t) => ` - ${t}`).join("\n"));
@@ -178,9 +178,9 @@ function run(mode, args) {
178
178
  else if (mode === "step") addStep(record, args);
179
179
  else if (mode === "pr" && /^\d+$/.test(args[0] ?? "")) {
180
180
  record.pr = Number(args[0]);
181
- return finalize(c.dir, path, record, `pr${args[0]}`, "pr-opened"), 0;
181
+ return finalize(c, path, record, `pr${args[0]}`, "pr-opened"), 0;
182
182
  }
183
- else if (mode === "end") return finalize(c.dir, path, record, id.branch.replace(/[^\w.-]+/g, "-"), "stopped", args.join(" ") || null), 0;
183
+ else if (mode === "end") return finalize(c, path, record, id.branch.replace(/[^\w.-]+/g, "-"), "stopped", args.join(" ") || null), 0;
184
184
  else throw new Error("usage: ship-log.mjs on | start | ready | gate <report> <exit> <seconds> <base> | step <name> start|end | finding '<json>' | pr <number> | end [reason] | report [dir]");
185
185
  save(path, record);
186
186
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.62.0",
3
+ "version": "2.63.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {