homeostat 0.1.1 → 0.1.2

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 CHANGED
@@ -6,7 +6,7 @@ Homeostat turns review feedback that led to a code change into short, scored rul
6
6
 
7
7
  ```sh
8
8
  # In a repo for your team's knowledge base:
9
- npx --yes homeostat@0.1.1 init
9
+ npx --yes homeostat@0.1.2 init
10
10
 
11
11
  # In Claude Code:
12
12
  claude plugin marketplace add ysugarmen/homeostat
@@ -108,7 +108,9 @@ function pinnedVersions(root) {
108
108
  const checks = [];
109
109
  for (const path of ci) {
110
110
  const text = readFileSync(join(root, path), "utf8");
111
- for (const m of text.matchAll(/\bhomeostat(@[^\s"']*)?\s+(validate|daily|curate|deep-dive|export)\b/g)) {
111
+ // Only command lines (npx, dlx, a run: step or a script list item): a job's `name:` or a
112
+ // `git config user.name "homeostat deep-dive"` line reads the same but runs nothing.
113
+ for (const m of text.matchAll(/(?:\bnpx(?:\s+(?:--yes|-y))?|\bdlx|\brun:|^\s*-)\s+homeostat(@[^\s"']*)?\s+(validate|daily|curate|deep-dive|digest|export)\b/gm)) {
112
114
  const version = m[1]?.slice(1);
113
115
  if (!version || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
114
116
  checks.push(fail(`${path} runs homeostat${m[1] ?? ""}: pin an exact version (homeostat@${VERSION}) so a new release cannot run unreviewed`));
@@ -147,8 +149,8 @@ export async function securityReport(root, config, io) {
147
149
  const changes = [
148
150
  ...access,
149
151
  am.enabled
150
- ? info(`auto-merge: on for review feedback scoring ${config.scoring.autoMergeThreshold}+ with a code change and no findings; ${am.areas.length ? `areas ${am.areas.join(", ")}` : "all areas"}${am.excludeAreas.length ? ` except ${am.excludeAreas.join(", ")}` : ""}; at most ${am.maxPerRun} per run`)
151
- : ok("auto-merge is off: every entry needs a reviewer"),
152
+ ? info(`the daily run merges lessons at confidence ${config.scoring.confidence.accept}+ (${config.scoring.confidence.coreAccept}+ for core), declines below ${config.scoring.confidence.decline}, and asks a reviewer in between; risky content, conflicts and document edits always go to a reviewer; ${am.areas.length ? `areas ${am.areas.join(", ")}` : "all areas"}${am.excludeAreas.length ? ` except ${am.excludeAreas.join(", ")}` : ""}`)
153
+ : ok("auto-merge is off: the daily run sends every lesson to a reviewer"),
152
154
  existsSync(join(root, ".github/CODEOWNERS")) || existsSync(join(root, "CODEOWNERS"))
153
155
  ? ok("CODEOWNERS routes each area to its reviewers")
154
156
  : warn("no CODEOWNERS file: run homeostat codeowners"),
package/dist/src/learn.js CHANGED
@@ -134,7 +134,9 @@ async function learn(url, opts, io) {
134
134
  };
135
135
  frontmatter.score = score(frontmatter, { config }).score;
136
136
  const text = formatEntry(frontmatter, body);
137
- const issues = validate({ ...files, [path]: text }).issues.filter((i) => i.file === path && i.severity === "error");
137
+ const found = validate({ ...files, [path]: text }).issues.filter((i) => i.file === path);
138
+ const issues = found.filter((i) => i.severity === "error");
139
+ const warnings = found.filter((i) => i.severity !== "error");
138
140
  if (issues.length > 0) {
139
141
  io.stdout(`${formatIssues(issues, io).text}nothing written\n`);
140
142
  return 1;
@@ -166,6 +168,8 @@ async function learn(url, opts, io) {
166
168
  writePendingFile(root, path, text);
167
169
  sendPending(root, io);
168
170
  io.stdout(`saved ${id} ${summary}: it applies to your sessions now, and tomorrow's daily run decides it for the team\n`);
171
+ if (warnings.length)
172
+ io.stdout(formatIssues(warnings, io).text);
169
173
  return 0;
170
174
  }
171
175
  /**
@@ -203,7 +207,9 @@ function learnDoc(root, files, config, opts, io) {
203
207
  throw new LearnError(e.message);
204
208
  throw e;
205
209
  }
206
- const issues = validate({ ...files, [path]: text }).issues.filter((i) => i.file === path && i.severity === "error");
210
+ const found = validate({ ...files, [path]: text }).issues.filter((i) => i.file === path);
211
+ const issues = found.filter((i) => i.severity === "error");
212
+ const warnings = found.filter((i) => i.severity !== "error");
207
213
  if (issues.length > 0) {
208
214
  io.stdout(`${formatIssues(issues, io).text}nothing written\n`);
209
215
  return 1;
@@ -226,6 +232,8 @@ function learnDoc(root, files, config, opts, io) {
226
232
  writePendingFile(root, path, text);
227
233
  sendPending(root, io);
228
234
  io.stdout(`${what}: it applies to your sessions now, and tomorrow's daily run decides it for the team\n`);
235
+ if (warnings.length)
236
+ io.stdout(formatIssues(warnings, io).text);
229
237
  return 0;
230
238
  }
231
239
  /** Sends pending lessons to the daily run in the background, when the KB has an origin. */
package/dist/src/query.js CHANGED
@@ -18,6 +18,7 @@ function load(opts, io, view = {}) {
18
18
  const pendingIds = new Set(kb.entries.filter((e) => pending.has(e.path)).map((e) => e.entry.frontmatter.id));
19
19
  return {
20
20
  root,
21
+ issues: kb.issues,
21
22
  config: kb.config,
22
23
  entries: kb.entries.map((e) => e.entry),
23
24
  docs: kb.docs,
@@ -75,9 +76,11 @@ export function runExplain(id, opts, io) {
75
76
  }
76
77
  io.stdout(opts.json
77
78
  ? `${JSON.stringify({ ...entry.frontmatter, sections: entry.sections }, null, 2)}\n`
78
- : `${explainEntry(entry, config)}\n${pendingIds.has(id)
79
- ? "Pending in this clone: it applies to your sessions now; the next daily run decides it for the team.\n"
80
- : ""}`);
79
+ : `${explainEntry(entry, config)}\n${!pendingIds.has(id)
80
+ ? ""
81
+ : entry.frontmatter.flags?.length
82
+ ? "Pending in this clone, flagged: it is out of your sessions until the next daily run takes the flag to the team.\n"
83
+ : "Pending in this clone: it applies to your sessions now; the next daily run decides it for the team.\n"}`);
81
84
  return 0;
82
85
  });
83
86
  }
@@ -105,7 +108,7 @@ export function runList(opts, io) {
105
108
  }
106
109
  export function runStatus(opts, io) {
107
110
  return guard(io, () => {
108
- const { root, config, entries, docs } = load(opts, io);
111
+ const { root, config, entries, docs, issues } = load(opts, io);
109
112
  const days = opts.days ? Number(opts.days) : 7;
110
113
  const now = io.now?.() ?? new Date();
111
114
  // The report counts its own window; document decay looks back 90 days.
@@ -113,6 +116,14 @@ export function runStatus(opts, io) {
113
116
  since: new Date(now.getTime() - Math.max(days, 90) * 86_400_000).toISOString(),
114
117
  });
115
118
  io.stdout(`${kbReport(entries, config, { days, now, events, docs })}\n`);
119
+ // Includes pending lessons, which CI's validate never sees: say so here, before the daily run.
120
+ const errors = issues.filter((i) => i.severity === "error");
121
+ if (errors.length) {
122
+ io.stdout(`- Problems: ${errors.length}, including pending lessons\n${errors
123
+ .slice(0, 5)
124
+ .map((i) => ` ${i.file ?? ""}: ${i.message}\n`)
125
+ .join("")}`);
126
+ }
116
127
  return 0;
117
128
  });
118
129
  }
package/dist/src/sync.js CHANGED
@@ -28,7 +28,9 @@ export function pendingFingerprint(root) {
28
28
  const idOf = (path) => path.split("/").at(-1)?.replace(/\.md$/, "") ?? path;
29
29
  export async function runSync(opts, io) {
30
30
  const root = resolve(opts.dir ?? io.env?.HOMEOSTAT_DIR ?? ".");
31
+ let said = false;
31
32
  const say = (s) => {
33
+ said = true;
32
34
  if (!opts.quiet)
33
35
  io.stdout(s);
34
36
  };
@@ -124,6 +126,15 @@ export async function runSync(opts, io) {
124
126
  }
125
127
  for (const n of notices)
126
128
  say(`${n.text}\n`);
129
+ // A sync by hand that changed nothing still says so, rather than printing nothing.
130
+ if (!said) {
131
+ const count = Object.keys(readPendingFiles(root)).length;
132
+ say(!isMember
133
+ ? `up to date with ${base}; "${member}" is not a member in homeostat.yaml, so lessons stay local\n`
134
+ : count
135
+ ? `up to date: ${count} pending lesson(s) already on ${candidatesBranch(member)}\n`
136
+ : `up to date with ${base}; no pending lessons\n`);
137
+ }
127
138
  writeSyncState(root, {
128
139
  lastSync: now,
129
140
  ...(lastHeartbeat ? { lastHeartbeat } : {}),
@@ -1,10 +1,12 @@
1
1
  import { resolve } from "node:path";
2
- import { applyFixes, readKbFiles, validate, writeKbFile, } from "@homeostat/core";
2
+ import { applyFixes, readKbView, validate, writeKbFile, writePendingFile, } from "@homeostat/core";
3
3
  import { formatIssues, painter, plural } from "./format.js";
4
4
  /** `homeostat validate [dir]`. Returns the process exit code: 1 when there are errors. */
5
5
  export function runValidate(dir, opts, io) {
6
6
  const root = resolve(dir);
7
- let files = readKbFiles(root);
7
+ // Pending lessons are checked too: CI never sees them, so this is where a developer would.
8
+ let view = readKbView(root);
9
+ let files = view.files;
8
10
  let result = validate(files);
9
11
  if (opts.fix) {
10
12
  const byFile = new Map();
@@ -15,16 +17,24 @@ export function runValidate(dir, opts, io) {
15
17
  }
16
18
  let fields = 0;
17
19
  for (const [path, fixes] of byFile) {
18
- writeKbFile(root, path, applyFixes(files[path] ?? "", fixes));
20
+ const fixed = applyFixes(files[path] ?? "", fixes);
21
+ if (view.pending.has(path))
22
+ writePendingFile(root, path, fixed);
23
+ else
24
+ writeKbFile(root, path, fixed);
19
25
  fields += fixes.length;
20
26
  }
21
27
  if (byFile.size > 0 && !opts.json) {
22
28
  io.stdout(`fixed ${plural(fields, "field")} in ${plural(byFile.size, "file")}\n`);
23
- files = readKbFiles(root);
29
+ view = readKbView(root);
30
+ files = view.files;
24
31
  result = validate(files);
25
32
  }
26
33
  }
27
34
  io.stdout(opts.json ? `${JSON.stringify(toJson(result), null, 2)}\n` : format(result, io));
35
+ if (!opts.json && view.pending.size > 0) {
36
+ io.stdout(`(including ${plural(view.pending.size, "pending lesson")} from .homeostat/pending, which the daily run decides)\n`);
37
+ }
28
38
  return result.ok ? 0 : 1;
29
39
  }
30
40
  function toJson(result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeostat",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Turn your team's code-review lessons into rules every coding agent follows: Claude Code, MCP, Cursor, Copilot, AGENTS.md.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -39,9 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "commander": "^15.0.0",
42
- "@homeostat/core": "0.1.1",
43
- "@homeostat/dashboard": "0.1.1",
44
- "@homeostat/mcp": "0.1.1"
42
+ "@homeostat/mcp": "0.1.2",
43
+ "@homeostat/core": "0.1.2",
44
+ "@homeostat/dashboard": "0.1.2"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public",