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 +1 -1
- package/dist/src/doctor.js +5 -3
- package/dist/src/learn.js +10 -2
- package/dist/src/query.js +15 -4
- package/dist/src/sync.js +11 -0
- package/dist/src/validate.js +14 -4
- package/package.json +4 -4
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.
|
|
9
|
+
npx --yes homeostat@0.1.2 init
|
|
10
10
|
|
|
11
11
|
# In Claude Code:
|
|
12
12
|
claude plugin marketplace add ysugarmen/homeostat
|
package/dist/src/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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(`
|
|
151
|
-
: ok("auto-merge is off: every
|
|
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
|
|
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
|
|
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
|
-
? "
|
|
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 } : {}),
|
package/dist/src/validate.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { applyFixes,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
43
|
-
"@homeostat/
|
|
44
|
-
"@homeostat/
|
|
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",
|