@hiveai/cli 0.9.10 → 0.9.11
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 +18 -0
- package/dist/index.js +437 -135
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command50 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -198,7 +198,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
198
198
|
if (!f) continue;
|
|
199
199
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
200
200
|
}
|
|
201
|
-
let entries = [...counts.entries()].map(([
|
|
201
|
+
let entries = [...counts.entries()].map(([path47, changes]) => ({ path: path47, changes }));
|
|
202
202
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
203
203
|
if (lowerPaths.length > 0) {
|
|
204
204
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -308,12 +308,14 @@ function registerBriefing(program2) {
|
|
|
308
308
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
309
309
|
const root = findProjectRoot(opts.dir);
|
|
310
310
|
const paths = resolveHaivePaths(root);
|
|
311
|
+
const markerFiles = parseCsv(opts.files);
|
|
311
312
|
if (existsSync(paths.haiveDir)) {
|
|
312
313
|
await mkdir(paths.runtimeDir, { recursive: true });
|
|
313
314
|
await writeBriefingMarker(paths, {
|
|
314
315
|
task: opts.task ?? "CLI briefing",
|
|
315
316
|
source: "haive-briefing-cli",
|
|
316
|
-
sessionId: process.env.HAIVE_SESSION_ID
|
|
317
|
+
sessionId: process.env.HAIVE_SESSION_ID,
|
|
318
|
+
files: markerFiles
|
|
317
319
|
}).catch(() => {
|
|
318
320
|
});
|
|
319
321
|
}
|
|
@@ -386,7 +388,7 @@ function registerBriefing(program2) {
|
|
|
386
388
|
}
|
|
387
389
|
}
|
|
388
390
|
const all = ownMemories;
|
|
389
|
-
const filePaths =
|
|
391
|
+
const filePaths = markerFiles;
|
|
390
392
|
const tokens = opts.task ? tokenizeQuery(opts.task) : null;
|
|
391
393
|
const scopeFilter = opts.scope ?? "all";
|
|
392
394
|
const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
|
|
@@ -498,6 +500,14 @@ function registerBriefing(program2) {
|
|
|
498
500
|
if (ids.length > 0) {
|
|
499
501
|
await trackReads(paths, ids).catch(() => {
|
|
500
502
|
});
|
|
503
|
+
await writeBriefingMarker(paths, {
|
|
504
|
+
task: opts.task ?? "CLI briefing",
|
|
505
|
+
source: "haive-briefing-cli",
|
|
506
|
+
sessionId: process.env.HAIVE_SESSION_ID,
|
|
507
|
+
memoryIds: ids,
|
|
508
|
+
files: filePaths
|
|
509
|
+
}).catch(() => {
|
|
510
|
+
});
|
|
501
511
|
}
|
|
502
512
|
const radarForced = opts.radar === true;
|
|
503
513
|
const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
|
|
@@ -6072,7 +6082,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6072
6082
|
};
|
|
6073
6083
|
}
|
|
6074
6084
|
var SERVER_NAME = "haive";
|
|
6075
|
-
var SERVER_VERSION = "0.9.
|
|
6085
|
+
var SERVER_VERSION = "0.9.11";
|
|
6076
6086
|
function jsonResult(data) {
|
|
6077
6087
|
return {
|
|
6078
6088
|
content: [
|
|
@@ -6086,14 +6096,11 @@ function jsonResult(data) {
|
|
|
6086
6096
|
var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
|
|
6087
6097
|
"get_briefing",
|
|
6088
6098
|
"mem_save",
|
|
6089
|
-
"mem_tried",
|
|
6090
6099
|
"mem_search",
|
|
6091
|
-
"mem_get",
|
|
6092
|
-
"mem_update",
|
|
6093
6100
|
"mem_verify",
|
|
6094
6101
|
"mem_relevant_to",
|
|
6095
|
-
"
|
|
6096
|
-
"
|
|
6102
|
+
"pre_commit_check",
|
|
6103
|
+
"mem_session_end"
|
|
6097
6104
|
]);
|
|
6098
6105
|
var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
|
|
6099
6106
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8821,8 +8828,8 @@ function parseChangelog(content) {
|
|
|
8821
8828
|
const entries = [];
|
|
8822
8829
|
const versionRe = /^#{1,3}\s+(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/m;
|
|
8823
8830
|
const sections = content.split(/^#{1,3}\s+/m).slice(1);
|
|
8824
|
-
for (const
|
|
8825
|
-
const versionMatch =
|
|
8831
|
+
for (const section2 of sections) {
|
|
8832
|
+
const versionMatch = section2.match(/^(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/);
|
|
8826
8833
|
const version = versionMatch?.[1];
|
|
8827
8834
|
if (!version) continue;
|
|
8828
8835
|
const entry = {
|
|
@@ -8833,7 +8840,7 @@ function parseChangelog(content) {
|
|
|
8833
8840
|
fixed: [],
|
|
8834
8841
|
added: []
|
|
8835
8842
|
};
|
|
8836
|
-
const subSections =
|
|
8843
|
+
const subSections = section2.split(/^#{2,4}\s+/m);
|
|
8837
8844
|
for (const sub of subSections) {
|
|
8838
8845
|
const firstLine = (sub.split("\n")[0] ?? "").toLowerCase().trim();
|
|
8839
8846
|
const items = sub.split("\n").slice(1).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).map((l) => l.replace(/^[\s\-*]+/, "").trim()).filter(Boolean);
|
|
@@ -8859,7 +8866,7 @@ function parseChangelog(content) {
|
|
|
8859
8866
|
}
|
|
8860
8867
|
}
|
|
8861
8868
|
if (entry.breaking.length === 0) {
|
|
8862
|
-
for (const line of
|
|
8869
|
+
for (const line of section2.split("\n")) {
|
|
8863
8870
|
if (/breaking|⚠|deprecated|removed/.test(line.toLowerCase())) {
|
|
8864
8871
|
const item = line.replace(/^[\s\-*#]+/, "").trim();
|
|
8865
8872
|
if (item) entry.breaking.push(item);
|
|
@@ -9611,8 +9618,8 @@ Next steps:
|
|
|
9611
9618
|
return;
|
|
9612
9619
|
}
|
|
9613
9620
|
const projectName = path35.basename(root);
|
|
9614
|
-
const { readdir:
|
|
9615
|
-
const projectDirs = (await
|
|
9621
|
+
const { readdir: readdir6 } = await import("fs/promises");
|
|
9622
|
+
const projectDirs = (await readdir6(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
9616
9623
|
if (projectDirs.length === 0) {
|
|
9617
9624
|
console.log(ui.dim("No other projects have pushed to the hub yet."));
|
|
9618
9625
|
return;
|
|
@@ -9623,7 +9630,7 @@ Next steps:
|
|
|
9623
9630
|
const sourceDir = path35.join(hubSharedDir, sourceName);
|
|
9624
9631
|
const destDir = path35.join(paths.memoriesDir, "shared", sourceName);
|
|
9625
9632
|
await mkdir15(destDir, { recursive: true });
|
|
9626
|
-
const sourceFiles = (await
|
|
9633
|
+
const sourceFiles = (await readdir6(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
9627
9634
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
9628
9635
|
const existingInDest = await loadDir(destDir);
|
|
9629
9636
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
@@ -9662,12 +9669,12 @@ Next steps:
|
|
|
9662
9669
|
);
|
|
9663
9670
|
const sharedDir = path35.join(paths.memoriesDir, "shared");
|
|
9664
9671
|
if (existsSync55(sharedDir)) {
|
|
9665
|
-
const { readdir:
|
|
9666
|
-
const sources = (await
|
|
9672
|
+
const { readdir: readdir6 } = await import("fs/promises");
|
|
9673
|
+
const sources = (await readdir6(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
9667
9674
|
console.log(`
|
|
9668
9675
|
Imported from ${sources.length} source(s):`);
|
|
9669
9676
|
for (const src of sources) {
|
|
9670
|
-
const files = (await
|
|
9677
|
+
const files = (await readdir6(path35.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
9671
9678
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
9672
9679
|
}
|
|
9673
9680
|
} else {
|
|
@@ -9987,15 +9994,160 @@ function summarize(name, t0, payload, notes) {
|
|
|
9987
9994
|
};
|
|
9988
9995
|
}
|
|
9989
9996
|
|
|
9990
|
-
// src/commands/
|
|
9991
|
-
import { mkdir as mkdir17, writeFile as writeFile28 } from "fs/promises";
|
|
9997
|
+
// src/commands/benchmark.ts
|
|
9992
9998
|
import { existsSync as existsSync57 } from "fs";
|
|
9999
|
+
import { readdir as readdir5, readFile as readFile16, writeFile as writeFile28 } from "fs/promises";
|
|
9993
10000
|
import path37 from "path";
|
|
9994
10001
|
import "commander";
|
|
10002
|
+
import { estimateTokens as estimateTokens4, findProjectRoot as findProjectRoot36 } from "@hiveai/core";
|
|
10003
|
+
function registerBenchmark(program2) {
|
|
10004
|
+
const benchmark = program2.command("benchmark").description("Official hAIve benchmark/demo utilities for measuring agent enforcement value.");
|
|
10005
|
+
benchmark.command("report").description("Summarize BENCHMARK_AGENT_REPORT.md files from a paired hAIve/plain agent benchmark.").option("-d, --dir <dir>", "benchmark root", "benchmarks/agent-benchmark").option("--out <file>", "write a Markdown report").option("--json", "emit JSON", false).action(async (opts) => {
|
|
10006
|
+
const root = resolveBenchmarkRoot(opts.dir);
|
|
10007
|
+
const rows = await collectRows(root);
|
|
10008
|
+
const summary = summarizeRows(rows);
|
|
10009
|
+
if (opts.json) {
|
|
10010
|
+
console.log(JSON.stringify({ root, summary, rows }, null, 2));
|
|
10011
|
+
return;
|
|
10012
|
+
}
|
|
10013
|
+
const markdown = renderMarkdown(root, summary, rows);
|
|
10014
|
+
if (opts.out) {
|
|
10015
|
+
const outFile = path37.isAbsolute(opts.out) ? opts.out : path37.join(root, opts.out);
|
|
10016
|
+
await writeFile28(outFile, markdown, "utf8");
|
|
10017
|
+
ui.success(`wrote ${path37.relative(process.cwd(), outFile)}`);
|
|
10018
|
+
return;
|
|
10019
|
+
}
|
|
10020
|
+
console.log(markdown);
|
|
10021
|
+
});
|
|
10022
|
+
benchmark.command("demo").description("Print the recommended protocol for running a hAIve vs plain agent benchmark.").action(() => {
|
|
10023
|
+
console.log([
|
|
10024
|
+
"# hAIve Agent Benchmark Demo",
|
|
10025
|
+
"",
|
|
10026
|
+
"1. Create paired fixtures: one `*-haive`, one `*-plain`.",
|
|
10027
|
+
"2. Put the same failing tests in both fixtures.",
|
|
10028
|
+
"3. Add precise `.ai/memories/team/*.md` policy memories only to the hAIve fixture.",
|
|
10029
|
+
"4. Run equal agents in parallel:",
|
|
10030
|
+
" - hAIve agents must run `haive briefing --files ... --task ...` first.",
|
|
10031
|
+
" - Plain agents must not read `.ai` or call hAIve.",
|
|
10032
|
+
"5. Require every agent to write `BENCHMARK_AGENT_REPORT.md`.",
|
|
10033
|
+
"6. Run `haive benchmark report --dir <benchmark-root> --out RESULTS.md`.",
|
|
10034
|
+
"",
|
|
10035
|
+
"Recommended metrics: pass rate, test iterations, files read, files changed, visible artifacts, decision quality, and token proxy."
|
|
10036
|
+
].join("\n"));
|
|
10037
|
+
});
|
|
10038
|
+
}
|
|
10039
|
+
function resolveBenchmarkRoot(dir) {
|
|
10040
|
+
const candidate = dir ?? "benchmarks/agent-benchmark";
|
|
10041
|
+
if (path37.isAbsolute(candidate)) return candidate;
|
|
10042
|
+
const projectRoot = findProjectRoot36(process.cwd());
|
|
10043
|
+
return path37.join(projectRoot, candidate);
|
|
10044
|
+
}
|
|
10045
|
+
async function collectRows(root) {
|
|
10046
|
+
if (!existsSync57(root)) throw new Error(`Benchmark directory not found: ${root}`);
|
|
10047
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
10048
|
+
const rows = [];
|
|
10049
|
+
for (const entry of entries) {
|
|
10050
|
+
if (!entry.isDirectory()) continue;
|
|
10051
|
+
const fixtureDir = path37.join(root, entry.name);
|
|
10052
|
+
const reportFile = path37.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
|
|
10053
|
+
if (!existsSync57(reportFile)) continue;
|
|
10054
|
+
const report = await readFile16(reportFile, "utf8");
|
|
10055
|
+
rows.push(parseAgentReport(entry.name, report));
|
|
10056
|
+
}
|
|
10057
|
+
return rows.sort((a, b) => a.fixture.localeCompare(b.fixture));
|
|
10058
|
+
}
|
|
10059
|
+
function parseAgentReport(fixture, report) {
|
|
10060
|
+
const group = fixture.endsWith("-haive") ? "haive" : fixture.endsWith("-plain") ? "plain" : "unknown";
|
|
10061
|
+
return {
|
|
10062
|
+
fixture,
|
|
10063
|
+
group,
|
|
10064
|
+
commands: sectionBulletCount(report, "Commands"),
|
|
10065
|
+
files_read: sectionBulletCount(report, "Files Read"),
|
|
10066
|
+
files_modified: sectionBulletCount(report, "Files Modified"),
|
|
10067
|
+
test_iterations: countMatches(section(report, "Test Iterations"), /Iteration\s+\d+|^- /gim),
|
|
10068
|
+
terminal_failures: countMatches(section(report, "Terminal Errors"), /fail|error|not raised|exited with code 1/gi),
|
|
10069
|
+
decision_mentions: sectionBulletCount(report, "Key Decisions"),
|
|
10070
|
+
token_proxy: estimateTokens4(report),
|
|
10071
|
+
haive_impact: /hAIve Memory Impact[\s\S]*?\b(yes|directly|changed|shaped|confirmed)\b/i.test(report)
|
|
10072
|
+
};
|
|
10073
|
+
}
|
|
10074
|
+
function summarizeRows(rows) {
|
|
10075
|
+
const byGroup = (group) => rows.filter((r) => r.group === group);
|
|
10076
|
+
return {
|
|
10077
|
+
fixtures: rows.length,
|
|
10078
|
+
haive: summarizeGroup(byGroup("haive")),
|
|
10079
|
+
plain: summarizeGroup(byGroup("plain"))
|
|
10080
|
+
};
|
|
10081
|
+
}
|
|
10082
|
+
function summarizeGroup(rows) {
|
|
10083
|
+
const sum = (key) => rows.reduce((total, row) => total + Number(row[key] ?? 0), 0);
|
|
10084
|
+
return {
|
|
10085
|
+
fixtures: rows.length,
|
|
10086
|
+
commands: sum("commands"),
|
|
10087
|
+
files_read: sum("files_read"),
|
|
10088
|
+
files_modified: sum("files_modified"),
|
|
10089
|
+
test_iterations: sum("test_iterations"),
|
|
10090
|
+
terminal_failures: sum("terminal_failures"),
|
|
10091
|
+
decision_mentions: sum("decision_mentions"),
|
|
10092
|
+
token_proxy: sum("token_proxy"),
|
|
10093
|
+
haive_impact_count: rows.filter((r) => r.haive_impact).length
|
|
10094
|
+
};
|
|
10095
|
+
}
|
|
10096
|
+
function renderMarkdown(root, summary, rows) {
|
|
10097
|
+
const lines = [
|
|
10098
|
+
"# hAIve Agent Benchmark Report",
|
|
10099
|
+
"",
|
|
10100
|
+
`Benchmark root: \`${root}\``,
|
|
10101
|
+
"",
|
|
10102
|
+
"## Summary",
|
|
10103
|
+
"",
|
|
10104
|
+
"| Group | Fixtures | Commands | Files read | Files modified | Test iterations | Terminal failures | Decision mentions | Token proxy | hAIve impact |",
|
|
10105
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
10106
|
+
groupLine("hAIve", summary.haive),
|
|
10107
|
+
groupLine("Plain", summary.plain),
|
|
10108
|
+
"",
|
|
10109
|
+
"## Fixtures",
|
|
10110
|
+
"",
|
|
10111
|
+
"| Fixture | Group | Commands | Files read | Files modified | Test iterations | Terminal failures | Decisions | Token proxy | hAIve impact |",
|
|
10112
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
|
|
10113
|
+
...rows.map(
|
|
10114
|
+
(row) => `| \`${row.fixture}\` | ${row.group} | ${row.commands} | ${row.files_read} | ${row.files_modified} | ${row.test_iterations} | ${row.terminal_failures} | ${row.decision_mentions} | ${row.token_proxy} | ${row.haive_impact ? "yes" : "no"} |`
|
|
10115
|
+
),
|
|
10116
|
+
"",
|
|
10117
|
+
"## Reading",
|
|
10118
|
+
"",
|
|
10119
|
+
"The token proxy is estimated from the agent report size, not from private model billing data.",
|
|
10120
|
+
"Use this report to compare relative effort and decision quality, then pair it with final test results and a human review of the diffs.",
|
|
10121
|
+
""
|
|
10122
|
+
];
|
|
10123
|
+
return lines.join("\n");
|
|
10124
|
+
}
|
|
10125
|
+
function groupLine(label, group) {
|
|
10126
|
+
return `| ${label} | ${group.fixtures} | ${group.commands} | ${group.files_read} | ${group.files_modified} | ${group.test_iterations} | ${group.terminal_failures} | ${group.decision_mentions} | ${group.token_proxy} | ${group.haive_impact_count} |`;
|
|
10127
|
+
}
|
|
10128
|
+
function sectionBulletCount(markdown, title) {
|
|
10129
|
+
return countMatches(section(markdown, title), /^- |^\d+\.\s/gm);
|
|
10130
|
+
}
|
|
10131
|
+
function section(markdown, title) {
|
|
10132
|
+
const re = new RegExp(`##\\s+[^\\n]*${escapeRegExp(title)}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s+|$)`, "i");
|
|
10133
|
+
return re.exec(markdown)?.[1] ?? "";
|
|
10134
|
+
}
|
|
10135
|
+
function countMatches(text, re) {
|
|
10136
|
+
return [...text.matchAll(re)].length;
|
|
10137
|
+
}
|
|
10138
|
+
function escapeRegExp(value) {
|
|
10139
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10140
|
+
}
|
|
10141
|
+
|
|
10142
|
+
// src/commands/memory-suggest.ts
|
|
10143
|
+
import { mkdir as mkdir17, writeFile as writeFile29 } from "fs/promises";
|
|
10144
|
+
import { existsSync as existsSync58 } from "fs";
|
|
10145
|
+
import path38 from "path";
|
|
10146
|
+
import "commander";
|
|
9995
10147
|
import {
|
|
9996
10148
|
aggregateUsage as aggregateUsage2,
|
|
9997
10149
|
buildFrontmatter as buildFrontmatter11,
|
|
9998
|
-
findProjectRoot as
|
|
10150
|
+
findProjectRoot as findProjectRoot37,
|
|
9999
10151
|
loadMemoriesFromDir as loadMemoriesFromDir30,
|
|
10000
10152
|
memoryFilePath as memoryFilePath10,
|
|
10001
10153
|
parseSince as parseSince2,
|
|
@@ -10013,7 +10165,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10013
10165
|
memory2.command("suggest").description(
|
|
10014
10166
|
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
|
|
10015
10167
|
).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10016
|
-
const root =
|
|
10168
|
+
const root = findProjectRoot37(opts.dir);
|
|
10017
10169
|
const paths = resolveHaivePaths33(root);
|
|
10018
10170
|
const events = await readUsageEvents3(paths);
|
|
10019
10171
|
if (events.length === 0) {
|
|
@@ -10060,7 +10212,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10060
10212
|
}
|
|
10061
10213
|
const created = [];
|
|
10062
10214
|
const skipped = [];
|
|
10063
|
-
const existing =
|
|
10215
|
+
const existing = existsSync58(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
|
|
10064
10216
|
for (const s of top) {
|
|
10065
10217
|
const slug = slugify(s.query);
|
|
10066
10218
|
if (!slug) {
|
|
@@ -10083,13 +10235,13 @@ function registerMemorySuggest(memory2) {
|
|
|
10083
10235
|
fm.status = "draft";
|
|
10084
10236
|
const body = renderTemplate(s);
|
|
10085
10237
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
10086
|
-
await mkdir17(
|
|
10087
|
-
if (
|
|
10088
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
10238
|
+
await mkdir17(path38.dirname(file), { recursive: true });
|
|
10239
|
+
if (existsSync58(file)) {
|
|
10240
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path38.relative(root, file)}` });
|
|
10089
10241
|
continue;
|
|
10090
10242
|
}
|
|
10091
|
-
await
|
|
10092
|
-
created.push({ id: fm.id, file:
|
|
10243
|
+
await writeFile29(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
|
|
10244
|
+
created.push({ id: fm.id, file: path38.relative(root, file), query: s.query });
|
|
10093
10245
|
}
|
|
10094
10246
|
if (opts.json) {
|
|
10095
10247
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -10182,12 +10334,12 @@ function truncate2(text, max) {
|
|
|
10182
10334
|
}
|
|
10183
10335
|
|
|
10184
10336
|
// src/commands/memory-archive.ts
|
|
10185
|
-
import { existsSync as
|
|
10186
|
-
import { writeFile as
|
|
10187
|
-
import
|
|
10337
|
+
import { existsSync as existsSync59 } from "fs";
|
|
10338
|
+
import { writeFile as writeFile30 } from "fs/promises";
|
|
10339
|
+
import path39 from "path";
|
|
10188
10340
|
import "commander";
|
|
10189
10341
|
import {
|
|
10190
|
-
findProjectRoot as
|
|
10342
|
+
findProjectRoot as findProjectRoot38,
|
|
10191
10343
|
getUsage as getUsage18,
|
|
10192
10344
|
loadMemoriesFromDir as loadMemoriesFromDir31,
|
|
10193
10345
|
loadUsageIndex as loadUsageIndex24,
|
|
@@ -10199,9 +10351,9 @@ function registerMemoryArchive(memory2) {
|
|
|
10199
10351
|
memory2.command("archive").description(
|
|
10200
10352
|
"Archive obsolete memories: marks status='deprecated' for memories not read in N days\n whose anchored paths have all disappeared (or have no anchor at all).\n\n Defaults to a DRY RUN \u2014 pass --apply to actually rewrite files.\n Targets `attempt` memories by default since they age the fastest.\n\n Recover later with `haive memory edit <id>` to set status back to validated."
|
|
10201
10353
|
).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10202
|
-
const root =
|
|
10354
|
+
const root = findProjectRoot38(opts.dir);
|
|
10203
10355
|
const paths = resolveHaivePaths34(root);
|
|
10204
|
-
if (!
|
|
10356
|
+
if (!existsSync59(paths.memoriesDir)) {
|
|
10205
10357
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
10206
10358
|
process.exitCode = 1;
|
|
10207
10359
|
return;
|
|
@@ -10222,7 +10374,7 @@ function registerMemoryArchive(memory2) {
|
|
|
10222
10374
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
10223
10375
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
10224
10376
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
10225
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
10377
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync59(path39.join(paths.root, p)));
|
|
10226
10378
|
const isAnchorless = !hasAnyAnchor;
|
|
10227
10379
|
if (!isAnchorless && !allPathsGone) continue;
|
|
10228
10380
|
const u = getUsage18(usage, fm.id);
|
|
@@ -10270,7 +10422,7 @@ function registerMemoryArchive(memory2) {
|
|
|
10270
10422
|
if (!found) continue;
|
|
10271
10423
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
10272
10424
|
try {
|
|
10273
|
-
await
|
|
10425
|
+
await writeFile30(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
10274
10426
|
archived++;
|
|
10275
10427
|
} catch (err) {
|
|
10276
10428
|
if (!opts.json) {
|
|
@@ -10296,14 +10448,14 @@ function parseDays(input) {
|
|
|
10296
10448
|
}
|
|
10297
10449
|
|
|
10298
10450
|
// src/commands/doctor.ts
|
|
10299
|
-
import { existsSync as
|
|
10451
|
+
import { existsSync as existsSync60 } from "fs";
|
|
10300
10452
|
import { stat } from "fs/promises";
|
|
10301
|
-
import
|
|
10453
|
+
import path40 from "path";
|
|
10302
10454
|
import { execSync as execSync3 } from "child_process";
|
|
10303
10455
|
import "commander";
|
|
10304
10456
|
import {
|
|
10305
10457
|
codeMapPath as codeMapPath2,
|
|
10306
|
-
findProjectRoot as
|
|
10458
|
+
findProjectRoot as findProjectRoot39,
|
|
10307
10459
|
getUsage as getUsage19,
|
|
10308
10460
|
loadCodeMap as loadCodeMap5,
|
|
10309
10461
|
loadConfig as loadConfig7,
|
|
@@ -10317,10 +10469,10 @@ function registerDoctor(program2) {
|
|
|
10317
10469
|
program2.command("doctor").description(
|
|
10318
10470
|
"Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
|
|
10319
10471
|
).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10320
|
-
const root =
|
|
10472
|
+
const root = findProjectRoot39(opts.dir);
|
|
10321
10473
|
const paths = resolveHaivePaths35(root);
|
|
10322
10474
|
const findings = [];
|
|
10323
|
-
if (!
|
|
10475
|
+
if (!existsSync60(paths.haiveDir)) {
|
|
10324
10476
|
findings.push({
|
|
10325
10477
|
severity: "error",
|
|
10326
10478
|
code: "not-initialized",
|
|
@@ -10329,7 +10481,7 @@ function registerDoctor(program2) {
|
|
|
10329
10481
|
});
|
|
10330
10482
|
return emit(findings, opts);
|
|
10331
10483
|
}
|
|
10332
|
-
if (!
|
|
10484
|
+
if (!existsSync60(paths.projectContext)) {
|
|
10333
10485
|
findings.push({
|
|
10334
10486
|
severity: "warn",
|
|
10335
10487
|
code: "no-project-context",
|
|
@@ -10337,8 +10489,8 @@ function registerDoctor(program2) {
|
|
|
10337
10489
|
fix: "haive init"
|
|
10338
10490
|
});
|
|
10339
10491
|
} else {
|
|
10340
|
-
const { readFile:
|
|
10341
|
-
const content = await
|
|
10492
|
+
const { readFile: readFile18 } = await import("fs/promises");
|
|
10493
|
+
const content = await readFile18(paths.projectContext, "utf8");
|
|
10342
10494
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10343
10495
|
if (isTemplate) {
|
|
10344
10496
|
findings.push({
|
|
@@ -10349,7 +10501,7 @@ function registerDoctor(program2) {
|
|
|
10349
10501
|
});
|
|
10350
10502
|
}
|
|
10351
10503
|
}
|
|
10352
|
-
const memories =
|
|
10504
|
+
const memories = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
|
|
10353
10505
|
const now = Date.now();
|
|
10354
10506
|
if (memories.length === 0) {
|
|
10355
10507
|
findings.push({
|
|
@@ -10460,12 +10612,12 @@ function registerDoctor(program2) {
|
|
|
10460
10612
|
}
|
|
10461
10613
|
const config = await loadConfig7(paths);
|
|
10462
10614
|
if (config.enforcement?.requireBriefingFirst) {
|
|
10463
|
-
const claudeSettings =
|
|
10615
|
+
const claudeSettings = path40.join(root, ".claude", "settings.local.json");
|
|
10464
10616
|
let hasClaudeEnforcement = false;
|
|
10465
|
-
if (
|
|
10617
|
+
if (existsSync60(claudeSettings)) {
|
|
10466
10618
|
try {
|
|
10467
|
-
const { readFile:
|
|
10468
|
-
const raw = await
|
|
10619
|
+
const { readFile: readFile18 } = await import("fs/promises");
|
|
10620
|
+
const raw = await readFile18(claudeSettings, "utf8");
|
|
10469
10621
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10470
10622
|
} catch {
|
|
10471
10623
|
hasClaudeEnforcement = false;
|
|
@@ -10494,7 +10646,7 @@ function registerDoctor(program2) {
|
|
|
10494
10646
|
timeout: 3e3,
|
|
10495
10647
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10496
10648
|
}).trim();
|
|
10497
|
-
const cliVersion = "0.9.
|
|
10649
|
+
const cliVersion = "0.9.11";
|
|
10498
10650
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10499
10651
|
findings.push({
|
|
10500
10652
|
severity: "warn",
|
|
@@ -10543,10 +10695,10 @@ function isSearchTool(name) {
|
|
|
10543
10695
|
}
|
|
10544
10696
|
|
|
10545
10697
|
// src/commands/playback.ts
|
|
10546
|
-
import { existsSync as
|
|
10698
|
+
import { existsSync as existsSync61 } from "fs";
|
|
10547
10699
|
import "commander";
|
|
10548
10700
|
import {
|
|
10549
|
-
findProjectRoot as
|
|
10701
|
+
findProjectRoot as findProjectRoot40,
|
|
10550
10702
|
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
10551
10703
|
parseSince as parseSince3,
|
|
10552
10704
|
readUsageEvents as readUsageEvents5,
|
|
@@ -10557,7 +10709,7 @@ function registerPlayback(program2) {
|
|
|
10557
10709
|
program2.command("playback").description(
|
|
10558
10710
|
"Replay past sessions from the usage log. For each session, show:\n - tool calls (what kind, how many)\n - briefing tasks asked\n - memories that have been created since then (that the session didn't have)\n\n Useful to ask 'would today's haive have helped past me on this task?'"
|
|
10559
10711
|
).option("--since <window>", "limit to events in this window (e.g. '7d')", "30d").option("--session-gap <minutes>", "minutes of inactivity that splits a session", "30").option("--limit <n>", "show at most this many sessions (newest first)", "10").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10560
|
-
const root =
|
|
10712
|
+
const root = findProjectRoot40(opts.dir);
|
|
10561
10713
|
const paths = resolveHaivePaths36(root);
|
|
10562
10714
|
const events = await readUsageEvents5(paths);
|
|
10563
10715
|
if (events.length === 0) {
|
|
@@ -10573,7 +10725,7 @@ function registerPlayback(program2) {
|
|
|
10573
10725
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
10574
10726
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
10575
10727
|
const sessions = bucketSessions(filtered, gapMs);
|
|
10576
|
-
const all =
|
|
10728
|
+
const all = existsSync61(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
|
|
10577
10729
|
const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
|
|
10578
10730
|
const enriched = sessions.map((s, i) => {
|
|
10579
10731
|
const startMs = Date.parse(s.start);
|
|
@@ -10663,7 +10815,7 @@ function truncate3(text, max) {
|
|
|
10663
10815
|
import { spawn as spawn4 } from "child_process";
|
|
10664
10816
|
import "commander";
|
|
10665
10817
|
import {
|
|
10666
|
-
findProjectRoot as
|
|
10818
|
+
findProjectRoot as findProjectRoot41,
|
|
10667
10819
|
resolveHaivePaths as resolveHaivePaths37
|
|
10668
10820
|
} from "@hiveai/core";
|
|
10669
10821
|
function registerPrecommit(program2) {
|
|
@@ -10674,7 +10826,7 @@ function registerPrecommit(program2) {
|
|
|
10674
10826
|
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
10675
10827
|
"high-confidence"
|
|
10676
10828
|
).option("--no-semantic", "disable semantic search in anti-patterns matching").option("--json", "emit JSON instead of human-readable output", false).option("--paths <paths...>", "explicit paths to check (skips git diff)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10677
|
-
const root =
|
|
10829
|
+
const root = findProjectRoot41(opts.dir);
|
|
10678
10830
|
const paths = resolveHaivePaths37(root);
|
|
10679
10831
|
const ctx = { paths };
|
|
10680
10832
|
let diff = "";
|
|
@@ -10767,10 +10919,10 @@ function runCommand3(cmd, args, cwd) {
|
|
|
10767
10919
|
}
|
|
10768
10920
|
|
|
10769
10921
|
// src/commands/welcome.ts
|
|
10770
|
-
import { existsSync as
|
|
10922
|
+
import { existsSync as existsSync63 } from "fs";
|
|
10771
10923
|
import "commander";
|
|
10772
10924
|
import {
|
|
10773
|
-
findProjectRoot as
|
|
10925
|
+
findProjectRoot as findProjectRoot42,
|
|
10774
10926
|
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
10775
10927
|
resolveHaivePaths as resolveHaivePaths38
|
|
10776
10928
|
} from "@hiveai/core";
|
|
@@ -10786,9 +10938,9 @@ function registerWelcome(program2) {
|
|
|
10786
10938
|
program2.command("welcome").description(
|
|
10787
10939
|
"Onboarding checklist: ranks validated/proposed **team** memories by type.\nUse after `haive init` so new devs skim institutional knowledge quickly.\n\n haive welcome\n haive welcome --limit 15\n"
|
|
10788
10940
|
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10789
|
-
const root =
|
|
10941
|
+
const root = findProjectRoot42(opts.dir);
|
|
10790
10942
|
const paths = resolveHaivePaths38(root);
|
|
10791
|
-
if (!
|
|
10943
|
+
if (!existsSync63(paths.memoriesDir)) {
|
|
10792
10944
|
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
10793
10945
|
process.exitCode = 1;
|
|
10794
10946
|
return;
|
|
@@ -10829,17 +10981,17 @@ function registerWelcome(program2) {
|
|
|
10829
10981
|
}
|
|
10830
10982
|
|
|
10831
10983
|
// src/commands/memory-lint.ts
|
|
10832
|
-
import { existsSync as
|
|
10984
|
+
import { existsSync as existsSync64 } from "fs";
|
|
10833
10985
|
import "commander";
|
|
10834
10986
|
import {
|
|
10835
|
-
findProjectRoot as
|
|
10987
|
+
findProjectRoot as findProjectRoot43,
|
|
10836
10988
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
10837
10989
|
resolveHaivePaths as resolveHaivePaths39
|
|
10838
10990
|
} from "@hiveai/core";
|
|
10839
10991
|
async function lintMemoriesAsync(root) {
|
|
10840
10992
|
const paths = resolveHaivePaths39(root);
|
|
10841
10993
|
const out = [];
|
|
10842
|
-
if (!
|
|
10994
|
+
if (!existsSync64(paths.memoriesDir)) return out;
|
|
10843
10995
|
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
10844
10996
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
10845
10997
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
@@ -10900,7 +11052,7 @@ function registerMemoryLint(parent) {
|
|
|
10900
11052
|
parent.command("lint").description(
|
|
10901
11053
|
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
10902
11054
|
).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10903
|
-
const root =
|
|
11055
|
+
const root = findProjectRoot43(opts.dir);
|
|
10904
11056
|
const findings = await lintMemoriesAsync(root);
|
|
10905
11057
|
if (opts.json) {
|
|
10906
11058
|
console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
|
|
@@ -10946,25 +11098,25 @@ function registerMemorySuggestTopic(memory2) {
|
|
|
10946
11098
|
}
|
|
10947
11099
|
|
|
10948
11100
|
// src/commands/resolve-project.ts
|
|
10949
|
-
import
|
|
11101
|
+
import path41 from "path";
|
|
10950
11102
|
import "commander";
|
|
10951
11103
|
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
10952
11104
|
function registerResolveProject(program2) {
|
|
10953
11105
|
program2.command("resolve-project").description(
|
|
10954
11106
|
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
10955
11107
|
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
10956
|
-
const info = resolveProjectInfo2({ cwd:
|
|
11108
|
+
const info = resolveProjectInfo2({ cwd: path41.resolve(opts.dir) });
|
|
10957
11109
|
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
10958
11110
|
});
|
|
10959
11111
|
}
|
|
10960
11112
|
|
|
10961
11113
|
// src/commands/runtime-journal.ts
|
|
10962
|
-
import { existsSync as
|
|
10963
|
-
import
|
|
11114
|
+
import { existsSync as existsSync65 } from "fs";
|
|
11115
|
+
import path43 from "path";
|
|
10964
11116
|
import "commander";
|
|
10965
11117
|
import {
|
|
10966
11118
|
appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
|
|
10967
|
-
findProjectRoot as
|
|
11119
|
+
findProjectRoot as findProjectRoot44,
|
|
10968
11120
|
readRuntimeJournalTail as readRuntimeJournalTail2,
|
|
10969
11121
|
resolveHaivePaths as resolveHaivePaths40
|
|
10970
11122
|
} from "@hiveai/core";
|
|
@@ -10974,18 +11126,18 @@ function registerRuntime(program2) {
|
|
|
10974
11126
|
);
|
|
10975
11127
|
const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
|
|
10976
11128
|
journal.command("append").description("Append one JSON line to .ai/.runtime/session-journal.ndjson").argument("<message>", "short text to log").option("-k, --kind <kind>", "note | session_end | mcp", "note").option("-d, --dir <dir>", "project root", process.cwd()).action(async (message, opts) => {
|
|
10977
|
-
const root =
|
|
10978
|
-
const paths = resolveHaivePaths40(
|
|
11129
|
+
const root = path43.resolve(opts.dir ?? process.cwd());
|
|
11130
|
+
const paths = resolveHaivePaths40(findProjectRoot44(root));
|
|
10979
11131
|
const raw = opts.kind ?? "note";
|
|
10980
11132
|
const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
|
|
10981
11133
|
await appendRuntimeJournalEntry3(paths, { kind, message });
|
|
10982
|
-
ui.success(`Appended to ${
|
|
11134
|
+
ui.success(`Appended to ${path43.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
|
|
10983
11135
|
});
|
|
10984
11136
|
journal.command("tail").description("Print the last N entries from the runtime session journal as JSON").option("-n, --limit <n>", "number of lines", "30").option("-d, --dir <dir>", "project root", process.cwd()).action(async (opts) => {
|
|
10985
|
-
const root =
|
|
10986
|
-
const paths = resolveHaivePaths40(
|
|
11137
|
+
const root = path43.resolve(opts.dir ?? process.cwd());
|
|
11138
|
+
const paths = resolveHaivePaths40(findProjectRoot44(root));
|
|
10987
11139
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
10988
|
-
if (!
|
|
11140
|
+
if (!existsSync65(paths.haiveDir)) {
|
|
10989
11141
|
ui.error("No .ai/ \u2014 run `haive init` first.");
|
|
10990
11142
|
process.exitCode = 1;
|
|
10991
11143
|
return;
|
|
@@ -11000,12 +11152,12 @@ function registerRuntime(program2) {
|
|
|
11000
11152
|
}
|
|
11001
11153
|
|
|
11002
11154
|
// src/commands/memory-timeline.ts
|
|
11003
|
-
import { existsSync as
|
|
11004
|
-
import
|
|
11155
|
+
import { existsSync as existsSync66 } from "fs";
|
|
11156
|
+
import path44 from "path";
|
|
11005
11157
|
import "commander";
|
|
11006
11158
|
import {
|
|
11007
11159
|
collectTimelineEntries as collectTimelineEntries2,
|
|
11008
|
-
findProjectRoot as
|
|
11160
|
+
findProjectRoot as findProjectRoot45,
|
|
11009
11161
|
resolveHaivePaths as resolveHaivePaths41
|
|
11010
11162
|
} from "@hiveai/core";
|
|
11011
11163
|
function registerMemoryTimeline(memory2) {
|
|
@@ -11017,9 +11169,9 @@ function registerMemoryTimeline(memory2) {
|
|
|
11017
11169
|
process.exitCode = 1;
|
|
11018
11170
|
return;
|
|
11019
11171
|
}
|
|
11020
|
-
const root =
|
|
11021
|
-
const paths = resolveHaivePaths41(
|
|
11022
|
-
if (!
|
|
11172
|
+
const root = path44.resolve(opts.dir ?? process.cwd());
|
|
11173
|
+
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11174
|
+
if (!existsSync66(paths.memoriesDir)) {
|
|
11023
11175
|
ui.error("No memories \u2014 run `haive init`.");
|
|
11024
11176
|
process.exitCode = 1;
|
|
11025
11177
|
return;
|
|
@@ -11037,13 +11189,13 @@ function registerMemoryTimeline(memory2) {
|
|
|
11037
11189
|
}
|
|
11038
11190
|
|
|
11039
11191
|
// src/commands/memory-conflict-candidates.ts
|
|
11040
|
-
import { existsSync as
|
|
11041
|
-
import
|
|
11192
|
+
import { existsSync as existsSync67 } from "fs";
|
|
11193
|
+
import path45 from "path";
|
|
11042
11194
|
import "commander";
|
|
11043
11195
|
import {
|
|
11044
11196
|
findLexicalConflictPairs as findLexicalConflictPairs2,
|
|
11045
11197
|
findTopicStatusConflictPairs as findTopicStatusConflictPairs2,
|
|
11046
|
-
findProjectRoot as
|
|
11198
|
+
findProjectRoot as findProjectRoot46,
|
|
11047
11199
|
resolveHaivePaths as resolveHaivePaths42
|
|
11048
11200
|
} from "@hiveai/core";
|
|
11049
11201
|
function parseTypes(csv) {
|
|
@@ -11060,9 +11212,9 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11060
11212
|
"decision,architecture,convention,gotcha (lexical scan)",
|
|
11061
11213
|
"decision,architecture"
|
|
11062
11214
|
).option("--min-jaccard <x>", "minimum Jaccard for lexical pairs", "0.45").option("--max-pairs <n>", "cap lexical pairs", "20").option("--max-scan <n>", "max memories scanned (lexical)", "500").option("--max-topic-pairs <n>", "cap topic/status pairs", "20").action(async (opts) => {
|
|
11063
|
-
const root =
|
|
11064
|
-
const paths = resolveHaivePaths42(
|
|
11065
|
-
if (!
|
|
11215
|
+
const root = path45.resolve(opts.dir ?? process.cwd());
|
|
11216
|
+
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
11217
|
+
if (!existsSync67(paths.memoriesDir)) {
|
|
11066
11218
|
ui.error("No memories \u2014 run `haive init`.");
|
|
11067
11219
|
process.exitCode = 1;
|
|
11068
11220
|
return;
|
|
@@ -11098,16 +11250,18 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11098
11250
|
|
|
11099
11251
|
// src/commands/enforce.ts
|
|
11100
11252
|
import { spawn as spawn5 } from "child_process";
|
|
11101
|
-
import { existsSync as
|
|
11102
|
-
import { chmod as chmod2, mkdir as mkdir18, readFile as
|
|
11103
|
-
import
|
|
11253
|
+
import { existsSync as existsSync68 } from "fs";
|
|
11254
|
+
import { chmod as chmod2, mkdir as mkdir18, readFile as readFile17, rm as rm3, writeFile as writeFile31 } from "fs/promises";
|
|
11255
|
+
import path46 from "path";
|
|
11104
11256
|
import "commander";
|
|
11105
11257
|
import {
|
|
11106
|
-
findProjectRoot as
|
|
11258
|
+
findProjectRoot as findProjectRoot47,
|
|
11107
11259
|
hasRecentBriefingMarker,
|
|
11108
11260
|
isFreshIsoDate,
|
|
11109
11261
|
loadConfig as loadConfig8,
|
|
11110
11262
|
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
11263
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
|
|
11264
|
+
readRecentBriefingMarker,
|
|
11111
11265
|
resolveBriefingBudget as resolveBriefingBudget3,
|
|
11112
11266
|
resolveHaivePaths as resolveHaivePaths43,
|
|
11113
11267
|
saveConfig as saveConfig3,
|
|
@@ -11122,7 +11276,7 @@ function registerEnforce(program2) {
|
|
|
11122
11276
|
"Agent-agnostic enforcement helpers: install policy gates, report status, and block unsafe workflows."
|
|
11123
11277
|
);
|
|
11124
11278
|
enforce.command("install").description("Install hAIve enforcement across MCP config, git hooks, CI template, and supported client hooks.").option("-d, --dir <dir>", "project root").option("--no-git", "skip git pre-commit/pre-push enforcement hooks").option("--no-claude", "skip Claude Code hooks").option("--no-ci", "skip GitHub Actions enforcement workflow").action(async (opts) => {
|
|
11125
|
-
const root =
|
|
11279
|
+
const root = findProjectRoot47(opts.dir);
|
|
11126
11280
|
const paths = resolveHaivePaths43(root);
|
|
11127
11281
|
await mkdir18(paths.haiveDir, { recursive: true });
|
|
11128
11282
|
const current = await loadConfig8(paths);
|
|
@@ -11135,7 +11289,11 @@ function registerEnforce(program2) {
|
|
|
11135
11289
|
requireSessionRecap: true,
|
|
11136
11290
|
requireMemoryVerify: true,
|
|
11137
11291
|
blockStaleDecisionChanges: true,
|
|
11138
|
-
|
|
11292
|
+
requireDecisionCoverage: true,
|
|
11293
|
+
scoreThreshold: 85,
|
|
11294
|
+
cleanupGeneratedArtifacts: true,
|
|
11295
|
+
toolProfile: "enforcement",
|
|
11296
|
+
policyPacks: ["architecture", "gotchas", "security", "domain", "release"]
|
|
11139
11297
|
}
|
|
11140
11298
|
});
|
|
11141
11299
|
ui.success("hAIve strict enforcement enabled in .ai/haive.config.json");
|
|
@@ -11144,7 +11302,7 @@ function registerEnforce(program2) {
|
|
|
11144
11302
|
if (opts.claude !== false) {
|
|
11145
11303
|
try {
|
|
11146
11304
|
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
11147
|
-
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${
|
|
11305
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path46.relative(root, result.settingsPath)})`);
|
|
11148
11306
|
} catch (err) {
|
|
11149
11307
|
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11150
11308
|
}
|
|
@@ -11162,6 +11320,23 @@ function registerEnforce(program2) {
|
|
|
11162
11320
|
printReport(report, Boolean(opts.json));
|
|
11163
11321
|
if (report.should_block) process.exit(2);
|
|
11164
11322
|
});
|
|
11323
|
+
enforce.command("cleanup").description("Remove generated hAIve runtime/cache artifacts that should not appear in commits.").option("-d, --dir <dir>", "project root").option("--dry-run", "print what would be removed without deleting", false).action(async (opts) => {
|
|
11324
|
+
const root = findProjectRoot47(opts.dir);
|
|
11325
|
+
const paths = resolveHaivePaths43(root);
|
|
11326
|
+
const targets = [
|
|
11327
|
+
path46.join(paths.haiveDir, ".cache"),
|
|
11328
|
+
path46.join(paths.haiveDir, ".runtime")
|
|
11329
|
+
];
|
|
11330
|
+
for (const target of targets) {
|
|
11331
|
+
if (!existsSync68(target)) continue;
|
|
11332
|
+
const rel = path46.relative(root, target);
|
|
11333
|
+
if (opts.dryRun) ui.info(`would remove ${rel}`);
|
|
11334
|
+
else {
|
|
11335
|
+
await rm3(target, { recursive: true, force: true });
|
|
11336
|
+
ui.success(`removed ${rel}`);
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11339
|
+
});
|
|
11165
11340
|
enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
11166
11341
|
const report = await buildEnforcementReport(opts.dir, "ci");
|
|
11167
11342
|
printReport(report, Boolean(opts.json));
|
|
@@ -11172,15 +11347,10 @@ function registerEnforce(program2) {
|
|
|
11172
11347
|
const root = resolveRoot(opts.dir, payload);
|
|
11173
11348
|
if (!root) return;
|
|
11174
11349
|
const paths = resolveHaivePaths43(root);
|
|
11175
|
-
if (!
|
|
11350
|
+
if (!existsSync68(paths.haiveDir)) return;
|
|
11176
11351
|
await mkdir18(paths.runtimeDir, { recursive: true });
|
|
11177
11352
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
11178
11353
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
11179
|
-
await writeBriefingMarker2(paths, {
|
|
11180
|
-
sessionId,
|
|
11181
|
-
task,
|
|
11182
|
-
source: opts.source ?? "claude-session-start"
|
|
11183
|
-
});
|
|
11184
11354
|
const budget = resolveBriefingBudget3("quick", {
|
|
11185
11355
|
max_tokens: 2500,
|
|
11186
11356
|
max_memories: 5,
|
|
@@ -11204,6 +11374,12 @@ function registerEnforce(program2) {
|
|
|
11204
11374
|
},
|
|
11205
11375
|
{ paths }
|
|
11206
11376
|
);
|
|
11377
|
+
await writeBriefingMarker2(paths, {
|
|
11378
|
+
sessionId,
|
|
11379
|
+
task,
|
|
11380
|
+
source: opts.source ?? "claude-session-start",
|
|
11381
|
+
memoryIds: briefing.memories.map((m) => m.id)
|
|
11382
|
+
});
|
|
11207
11383
|
console.log("hAIve briefing loaded. Agents must consult this before editing.");
|
|
11208
11384
|
if (briefing.last_session) {
|
|
11209
11385
|
console.log(`
|
|
@@ -11233,7 +11409,7 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11233
11409
|
const root = resolveRoot(opts.dir, payload);
|
|
11234
11410
|
if (!root) return;
|
|
11235
11411
|
const paths = resolveHaivePaths43(root);
|
|
11236
|
-
if (!
|
|
11412
|
+
if (!existsSync68(paths.haiveDir)) return;
|
|
11237
11413
|
if (!isWriteLikeTool(payload)) return;
|
|
11238
11414
|
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
11239
11415
|
if (ok) return;
|
|
@@ -11255,9 +11431,9 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11255
11431
|
});
|
|
11256
11432
|
}
|
|
11257
11433
|
async function runWithEnforcement(command, args, opts) {
|
|
11258
|
-
const root =
|
|
11434
|
+
const root = findProjectRoot47(opts.dir);
|
|
11259
11435
|
const paths = resolveHaivePaths43(root);
|
|
11260
|
-
if (!
|
|
11436
|
+
if (!existsSync68(paths.haiveDir)) {
|
|
11261
11437
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
11262
11438
|
process.exit(1);
|
|
11263
11439
|
}
|
|
@@ -11276,7 +11452,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
11276
11452
|
process.exit(2);
|
|
11277
11453
|
}
|
|
11278
11454
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
11279
|
-
ui.info(`Briefing written to ${
|
|
11455
|
+
ui.info(`Briefing written to ${path46.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
11280
11456
|
const child = spawn5(command, args, {
|
|
11281
11457
|
cwd: root,
|
|
11282
11458
|
stdio: "inherit",
|
|
@@ -11319,9 +11495,15 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11319
11495
|
min_semantic_score: 0.25,
|
|
11320
11496
|
budget_preset: "quick"
|
|
11321
11497
|
}, { paths });
|
|
11322
|
-
|
|
11498
|
+
await writeBriefingMarker2(paths, {
|
|
11499
|
+
sessionId,
|
|
11500
|
+
task,
|
|
11501
|
+
source: "haive-run",
|
|
11502
|
+
memoryIds: briefing.memories.map((m) => m.id)
|
|
11503
|
+
});
|
|
11504
|
+
const dir = path46.join(paths.runtimeDir, "enforcement", "briefings");
|
|
11323
11505
|
await mkdir18(dir, { recursive: true });
|
|
11324
|
-
const file =
|
|
11506
|
+
const file = path46.join(dir, `${sessionId}.md`);
|
|
11325
11507
|
const parts = [
|
|
11326
11508
|
"# hAIve Briefing",
|
|
11327
11509
|
"",
|
|
@@ -11339,13 +11521,13 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11339
11521
|
if (briefing.setup_warnings.length > 0) {
|
|
11340
11522
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
11341
11523
|
}
|
|
11342
|
-
await
|
|
11524
|
+
await writeFile31(file, parts.join("\n") + "\n", "utf8");
|
|
11343
11525
|
return file;
|
|
11344
11526
|
}
|
|
11345
11527
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
11346
|
-
const root =
|
|
11528
|
+
const root = findProjectRoot47(dir);
|
|
11347
11529
|
const paths = resolveHaivePaths43(root);
|
|
11348
|
-
const initialized =
|
|
11530
|
+
const initialized = existsSync68(paths.haiveDir);
|
|
11349
11531
|
const config = initialized ? await loadConfig8(paths) : {};
|
|
11350
11532
|
const mode = config.enforcement?.mode ?? "strict";
|
|
11351
11533
|
const findings = [];
|
|
@@ -11354,12 +11536,14 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11354
11536
|
root,
|
|
11355
11537
|
initialized,
|
|
11356
11538
|
mode,
|
|
11539
|
+
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
11357
11540
|
should_block: true,
|
|
11358
11541
|
findings: [{
|
|
11359
11542
|
severity: "error",
|
|
11360
11543
|
code: "not-initialized",
|
|
11361
11544
|
message: "This repository is not initialized with hAIve.",
|
|
11362
|
-
fix: "Run `haive init` or `haive enforce install`."
|
|
11545
|
+
fix: "Run `haive init` or `haive enforce install`.",
|
|
11546
|
+
impact: 100
|
|
11363
11547
|
}]
|
|
11364
11548
|
};
|
|
11365
11549
|
}
|
|
@@ -11368,6 +11552,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11368
11552
|
root,
|
|
11369
11553
|
initialized,
|
|
11370
11554
|
mode,
|
|
11555
|
+
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
11371
11556
|
should_block: false,
|
|
11372
11557
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
11373
11558
|
};
|
|
@@ -11378,42 +11563,67 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11378
11563
|
severity: "error",
|
|
11379
11564
|
code: "briefing-missing",
|
|
11380
11565
|
message: "No recent hAIve briefing marker was found for this workflow.",
|
|
11381
|
-
fix: 'Run `haive briefing --task "..."`, `haive enforce session-start`, or wrap the agent with `haive run -- <agent>`.'
|
|
11566
|
+
fix: 'Run `haive briefing --task "..."`, `haive enforce session-start`, or wrap the agent with `haive run -- <agent>`.',
|
|
11567
|
+
impact: 35
|
|
11382
11568
|
});
|
|
11383
11569
|
}
|
|
11384
11570
|
if (config.enforcement?.requireSessionRecap !== false && (stage === "pre-push" || stage === "ci")) {
|
|
11385
11571
|
const hasRecap = await hasRecentSessionRecap(paths);
|
|
11386
|
-
findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : {
|
|
11572
|
+
findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : stage === "ci" ? {
|
|
11573
|
+
severity: "warn",
|
|
11574
|
+
code: "session-recap-missing",
|
|
11575
|
+
message: "No recent session_recap memory was found. CI reports this as a warning because personal recaps are usually not committed.",
|
|
11576
|
+
fix: "Run `haive session end --scope team --goal ... --accomplished ...` if you want a team recap visible in CI.",
|
|
11577
|
+
impact: 5
|
|
11578
|
+
} : {
|
|
11387
11579
|
severity: "error",
|
|
11388
11580
|
code: "session-recap-missing",
|
|
11389
11581
|
message: "No recent session_recap memory was found.",
|
|
11390
|
-
fix: "Run `haive session end --goal ... --accomplished ...` before pushing."
|
|
11582
|
+
fix: "Run `haive session end --goal ... --accomplished ...` before pushing.",
|
|
11583
|
+
impact: 20
|
|
11391
11584
|
});
|
|
11392
11585
|
}
|
|
11393
11586
|
if (config.enforcement?.requireMemoryVerify !== false) {
|
|
11394
11587
|
findings.push(...await verifyMemoryPolicy(paths, config));
|
|
11395
11588
|
}
|
|
11589
|
+
if (config.enforcement?.requireDecisionCoverage !== false) {
|
|
11590
|
+
findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
|
|
11591
|
+
}
|
|
11396
11592
|
if (stage === "pre-commit" || stage === "ci") {
|
|
11397
11593
|
findings.push(...await runPrecommitPolicy(paths));
|
|
11398
11594
|
}
|
|
11595
|
+
if (config.enforcement?.cleanupGeneratedArtifacts !== false) {
|
|
11596
|
+
findings.push(...await findGeneratedArtifacts(paths));
|
|
11597
|
+
}
|
|
11598
|
+
const score = buildScore(findings, config.enforcement?.scoreThreshold);
|
|
11599
|
+
if (score.score < score.threshold) {
|
|
11600
|
+
findings.push({
|
|
11601
|
+
severity: "error",
|
|
11602
|
+
code: "enforcement-score-below-threshold",
|
|
11603
|
+
message: `Enforcement score ${score.score}% is below required threshold ${score.threshold}%.`,
|
|
11604
|
+
fix: "Load the relevant briefing, address policy findings, then rerun `haive enforce check`.",
|
|
11605
|
+
impact: 0
|
|
11606
|
+
});
|
|
11607
|
+
}
|
|
11399
11608
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
11400
11609
|
return {
|
|
11401
11610
|
root,
|
|
11402
11611
|
initialized,
|
|
11403
11612
|
mode,
|
|
11613
|
+
score: buildScore(findings, config.enforcement?.scoreThreshold),
|
|
11404
11614
|
should_block: mode === "strict" && hasErrors,
|
|
11405
11615
|
findings
|
|
11406
11616
|
};
|
|
11407
11617
|
}
|
|
11408
11618
|
async function hasRecentSessionRecap(paths) {
|
|
11409
|
-
if (!
|
|
11619
|
+
if (!existsSync68(paths.memoriesDir)) return false;
|
|
11410
11620
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11411
11621
|
return all.some(
|
|
11412
11622
|
({ memory: memory2 }) => memory2.frontmatter.type === "session_recap" && memory2.frontmatter.status !== "rejected" && isFreshIsoDate(memory2.frontmatter.created_at, SESSION_RECAP_TTL_MS)
|
|
11413
11623
|
);
|
|
11414
11624
|
}
|
|
11415
11625
|
async function verifyMemoryPolicy(paths, config) {
|
|
11416
|
-
if (!
|
|
11626
|
+
if (!existsSync68(paths.memoriesDir)) return [];
|
|
11417
11627
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11418
11628
|
const findings = [];
|
|
11419
11629
|
const staleImportant = [];
|
|
@@ -11444,11 +11654,51 @@ async function verifyMemoryPolicy(paths, config) {
|
|
|
11444
11654
|
severity: "error",
|
|
11445
11655
|
code: "stale-important-memories",
|
|
11446
11656
|
message: `${staleImportant.length} important anchored memories are stale: ${staleImportant.slice(0, 8).join(", ")}`,
|
|
11447
|
-
fix: "Run `haive memory verify --update`, then update or delete stale decisions/gotchas before merging."
|
|
11657
|
+
fix: "Run `haive memory verify --update`, then update or delete stale decisions/gotchas before merging.",
|
|
11658
|
+
impact: 40
|
|
11448
11659
|
});
|
|
11449
11660
|
}
|
|
11450
11661
|
return findings;
|
|
11451
11662
|
}
|
|
11663
|
+
async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
11664
|
+
if (!existsSync68(paths.memoriesDir)) return [];
|
|
11665
|
+
const changedFiles = await getChangedFiles(paths.root, stage);
|
|
11666
|
+
if (changedFiles.length === 0) {
|
|
11667
|
+
return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
|
|
11668
|
+
}
|
|
11669
|
+
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11670
|
+
const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention"]);
|
|
11671
|
+
const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
|
|
11672
|
+
const fm = memory2.frontmatter;
|
|
11673
|
+
if (!policyTypes.has(fm.type)) return false;
|
|
11674
|
+
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
11675
|
+
return memoryMatchesAnchorPaths6(memory2, changedFiles);
|
|
11676
|
+
});
|
|
11677
|
+
if (relevant.length === 0) {
|
|
11678
|
+
return [{
|
|
11679
|
+
severity: "ok",
|
|
11680
|
+
code: "decision-coverage-none-required",
|
|
11681
|
+
message: `No anchored decisions or policies matched ${changedFiles.length} changed file(s).`
|
|
11682
|
+
}];
|
|
11683
|
+
}
|
|
11684
|
+
const marker = await readRecentBriefingMarker(paths, sessionId);
|
|
11685
|
+
const consulted = new Set(marker?.memory_ids ?? []);
|
|
11686
|
+
const missing = relevant.filter((memory2) => !consulted.has(memory2.frontmatter.id));
|
|
11687
|
+
if (missing.length === 0) {
|
|
11688
|
+
return [{
|
|
11689
|
+
severity: "ok",
|
|
11690
|
+
code: "decision-coverage-pass",
|
|
11691
|
+
message: `Relevant decisions/policies were surfaced for ${changedFiles.length} changed file(s): ${relevant.length}/${relevant.length}.`
|
|
11692
|
+
}];
|
|
11693
|
+
}
|
|
11694
|
+
return [{
|
|
11695
|
+
severity: stage === "local" ? "warn" : "error",
|
|
11696
|
+
code: "decision-coverage-missing",
|
|
11697
|
+
message: `${missing.length}/${relevant.length} relevant anchored decisions/policies were not present in the latest briefing: ${missing.slice(0, 6).map((m) => m.frontmatter.id).join(", ")}`,
|
|
11698
|
+
fix: `Run \`haive briefing --files "${changedFiles.slice(0, 10).join(",")}" --task "..."\` before committing.`,
|
|
11699
|
+
impact: Math.min(35, 10 + missing.length * 5)
|
|
11700
|
+
}];
|
|
11701
|
+
}
|
|
11452
11702
|
async function runPrecommitPolicy(paths) {
|
|
11453
11703
|
const staged = await runCommand4("git", ["diff", "--cached", "--name-only"], paths.root).catch(() => "");
|
|
11454
11704
|
const touchedPaths = staged.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -11473,12 +11723,62 @@ async function runPrecommitPolicy(paths) {
|
|
|
11473
11723
|
severity: "error",
|
|
11474
11724
|
code: "precommit-policy-block",
|
|
11475
11725
|
message: `Pre-commit policy matched ${result.summary.anti_patterns} anti-pattern(s), ${result.summary.stale_anchors} stale anchor(s).`,
|
|
11476
|
-
fix: "Review the hAIve warnings, then update the code or the relevant memories."
|
|
11726
|
+
fix: "Review the hAIve warnings, then update the code or the relevant memories.",
|
|
11727
|
+
impact: 45
|
|
11477
11728
|
}];
|
|
11478
11729
|
}
|
|
11730
|
+
async function findGeneratedArtifacts(paths) {
|
|
11731
|
+
const dirty = await runCommand4("git", ["status", "--short", "--untracked-files=all"], paths.root).catch(() => "");
|
|
11732
|
+
const generated = dirty.split("\n").map((line) => line.trim()).filter(Boolean).filter(
|
|
11733
|
+
(line) => line.includes(".ai/.cache/") || line.includes(".ai/.runtime/") || line.includes("__pycache__/") || line.endsWith(".pyc")
|
|
11734
|
+
);
|
|
11735
|
+
if (generated.length === 0) {
|
|
11736
|
+
return [{ severity: "ok", code: "generated-artifacts-clean", message: "No generated runtime/cache artifacts are visible to git." }];
|
|
11737
|
+
}
|
|
11738
|
+
return [{
|
|
11739
|
+
severity: "warn",
|
|
11740
|
+
code: "generated-artifacts-visible",
|
|
11741
|
+
message: `${generated.length} generated artifact(s) are visible in git status.`,
|
|
11742
|
+
fix: "Run `haive enforce cleanup`, update .gitignore, or remove test/runtime outputs before committing.",
|
|
11743
|
+
impact: 10
|
|
11744
|
+
}];
|
|
11745
|
+
}
|
|
11746
|
+
async function getChangedFiles(root, stage) {
|
|
11747
|
+
const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
|
|
11748
|
+
["diff", "--cached", "--name-only"],
|
|
11749
|
+
["diff", "--name-only"]
|
|
11750
|
+
];
|
|
11751
|
+
const files = /* @__PURE__ */ new Set();
|
|
11752
|
+
for (const args of commands) {
|
|
11753
|
+
const out = await runCommand4("git", args, root).catch(() => "");
|
|
11754
|
+
for (const line of out.split("\n")) {
|
|
11755
|
+
const file = line.trim();
|
|
11756
|
+
if (file) files.add(file);
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
11759
|
+
return [...files].filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"));
|
|
11760
|
+
}
|
|
11761
|
+
function buildScore(findings, threshold = 80) {
|
|
11762
|
+
const checks = {
|
|
11763
|
+
total: findings.length,
|
|
11764
|
+
ok: findings.filter((f) => f.severity === "ok").length,
|
|
11765
|
+
warn: findings.filter((f) => f.severity === "warn").length,
|
|
11766
|
+
error: findings.filter((f) => f.severity === "error").length
|
|
11767
|
+
};
|
|
11768
|
+
const penalty = findings.reduce((sum, f) => {
|
|
11769
|
+
if (f.severity === "error") return sum + (f.impact ?? 25);
|
|
11770
|
+
if (f.severity === "warn") return sum + (f.impact ?? 8);
|
|
11771
|
+
return sum;
|
|
11772
|
+
}, 0);
|
|
11773
|
+
return {
|
|
11774
|
+
score: Math.max(0, Math.min(100, 100 - penalty)),
|
|
11775
|
+
threshold,
|
|
11776
|
+
checks
|
|
11777
|
+
};
|
|
11778
|
+
}
|
|
11479
11779
|
async function installGitEnforcement(root) {
|
|
11480
|
-
const hooksDir =
|
|
11481
|
-
if (!
|
|
11780
|
+
const hooksDir = path46.join(root, ".git", "hooks");
|
|
11781
|
+
if (!existsSync68(path46.join(root, ".git"))) {
|
|
11482
11782
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
11483
11783
|
return;
|
|
11484
11784
|
}
|
|
@@ -11500,31 +11800,31 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
11500
11800
|
}
|
|
11501
11801
|
];
|
|
11502
11802
|
for (const hook of hooks) {
|
|
11503
|
-
const file =
|
|
11504
|
-
if (
|
|
11505
|
-
const current = await
|
|
11803
|
+
const file = path46.join(hooksDir, hook.name);
|
|
11804
|
+
if (existsSync68(file)) {
|
|
11805
|
+
const current = await readFile17(file, "utf8").catch(() => "");
|
|
11506
11806
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
11507
|
-
await
|
|
11807
|
+
await writeFile31(file, hook.body, "utf8");
|
|
11508
11808
|
} else {
|
|
11509
|
-
await
|
|
11809
|
+
await writeFile31(file, `${current.trimEnd()}
|
|
11510
11810
|
|
|
11511
11811
|
${hook.body}`, "utf8");
|
|
11512
11812
|
}
|
|
11513
11813
|
} else {
|
|
11514
|
-
await
|
|
11814
|
+
await writeFile31(file, hook.body, "utf8");
|
|
11515
11815
|
}
|
|
11516
11816
|
await chmod2(file, 493);
|
|
11517
11817
|
}
|
|
11518
11818
|
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
11519
11819
|
}
|
|
11520
11820
|
async function installCiEnforcement(root) {
|
|
11521
|
-
const workflowPath =
|
|
11522
|
-
await mkdir18(
|
|
11523
|
-
if (
|
|
11821
|
+
const workflowPath = path46.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
11822
|
+
await mkdir18(path46.dirname(workflowPath), { recursive: true });
|
|
11823
|
+
if (existsSync68(workflowPath)) {
|
|
11524
11824
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
11525
11825
|
return;
|
|
11526
11826
|
}
|
|
11527
|
-
await
|
|
11827
|
+
await writeFile31(workflowPath, `name: haive-enforcement
|
|
11528
11828
|
|
|
11529
11829
|
on:
|
|
11530
11830
|
pull_request:
|
|
@@ -11548,7 +11848,7 @@ jobs:
|
|
|
11548
11848
|
- name: Enforce hAIve policy
|
|
11549
11849
|
run: haive enforce ci
|
|
11550
11850
|
`, "utf8");
|
|
11551
|
-
ui.success(`Created ${
|
|
11851
|
+
ui.success(`Created ${path46.relative(root, workflowPath)}`);
|
|
11552
11852
|
}
|
|
11553
11853
|
function printReport(report, json) {
|
|
11554
11854
|
if (json) {
|
|
@@ -11557,6 +11857,7 @@ function printReport(report, json) {
|
|
|
11557
11857
|
}
|
|
11558
11858
|
console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
|
|
11559
11859
|
console.log(ui.dim(` root: ${report.root}`));
|
|
11860
|
+
console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
|
|
11560
11861
|
for (const finding of report.findings) {
|
|
11561
11862
|
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
11562
11863
|
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
@@ -11576,7 +11877,7 @@ async function readHookPayload() {
|
|
|
11576
11877
|
}
|
|
11577
11878
|
function resolveRoot(dir, payload) {
|
|
11578
11879
|
try {
|
|
11579
|
-
return
|
|
11880
|
+
return findProjectRoot47(dir ?? payload.cwd);
|
|
11580
11881
|
} catch {
|
|
11581
11882
|
return null;
|
|
11582
11883
|
}
|
|
@@ -11645,8 +11946,8 @@ function registerRun(program2) {
|
|
|
11645
11946
|
}
|
|
11646
11947
|
|
|
11647
11948
|
// src/index.ts
|
|
11648
|
-
var program = new
|
|
11649
|
-
program.name("haive").description("hAIve \u2014
|
|
11949
|
+
var program = new Command50();
|
|
11950
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.11");
|
|
11650
11951
|
registerInit(program);
|
|
11651
11952
|
registerWelcome(program);
|
|
11652
11953
|
registerResolveProject(program);
|
|
@@ -11694,6 +11995,7 @@ registerSnapshot(program);
|
|
|
11694
11995
|
registerHub(program);
|
|
11695
11996
|
registerStats(program);
|
|
11696
11997
|
registerBench(program);
|
|
11998
|
+
registerBenchmark(program);
|
|
11697
11999
|
registerDoctor(program);
|
|
11698
12000
|
registerPlayback(program);
|
|
11699
12001
|
registerPrecommit(program);
|