@hiveai/cli 0.9.25 → 0.9.27
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/dist/index.js +343 -135
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -4815,7 +4815,8 @@ var SessionTracker = class {
|
|
|
4815
4815
|
const ranPostTask = this.events.some(
|
|
4816
4816
|
(e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
|
|
4817
4817
|
);
|
|
4818
|
-
|
|
4818
|
+
const isSubstantialSession = totalCalls >= 3 || writingTools.length > 0;
|
|
4819
|
+
if (!ranPostTask && isSubstantialSession && existsSync16(this.ctx.paths.haiveDir)) {
|
|
4819
4820
|
try {
|
|
4820
4821
|
const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
|
|
4821
4822
|
const payload = {
|
|
@@ -4907,7 +4908,12 @@ async function memSessionEnd(input, ctx) {
|
|
|
4907
4908
|
}
|
|
4908
4909
|
const body = buildBody(input);
|
|
4909
4910
|
const topic = recapTopic(input.scope, input.module);
|
|
4910
|
-
const
|
|
4911
|
+
const normalizedFiles = input.files_touched.map((p) => {
|
|
4912
|
+
if (!p || !path82.isAbsolute(p)) return p;
|
|
4913
|
+
const rel = path82.relative(ctx.paths.root, p);
|
|
4914
|
+
return rel.startsWith("..") ? p : rel;
|
|
4915
|
+
});
|
|
4916
|
+
const invalidPaths = normalizedFiles.filter(
|
|
4911
4917
|
(p) => !existsSync17(path82.resolve(ctx.paths.root, p))
|
|
4912
4918
|
);
|
|
4913
4919
|
if (invalidPaths.length > 0) {
|
|
@@ -4926,7 +4932,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
4926
4932
|
revision_count: revisionCount,
|
|
4927
4933
|
anchor: {
|
|
4928
4934
|
...fm.anchor,
|
|
4929
|
-
paths:
|
|
4935
|
+
paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
|
|
4930
4936
|
}
|
|
4931
4937
|
};
|
|
4932
4938
|
await writeFile10(
|
|
@@ -4949,7 +4955,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
4949
4955
|
scope: input.scope,
|
|
4950
4956
|
module: input.module,
|
|
4951
4957
|
tags: ["session", "recap"],
|
|
4952
|
-
paths:
|
|
4958
|
+
paths: normalizedFiles,
|
|
4953
4959
|
topic,
|
|
4954
4960
|
status: "validated"
|
|
4955
4961
|
});
|
|
@@ -6558,7 +6564,36 @@ function isDocLikePath(file) {
|
|
|
6558
6564
|
}
|
|
6559
6565
|
function isPackageOrConfigPath(file) {
|
|
6560
6566
|
const lower = file.toLowerCase();
|
|
6561
|
-
|
|
6567
|
+
const base = lower.split("/").pop() ?? lower;
|
|
6568
|
+
return lower.endsWith("package.json") || lower.endsWith("package-lock.json") || lower.endsWith("pnpm-lock.yaml") || lower.endsWith("yarn.lock") || lower.endsWith("bun.lockb") || lower.endsWith(".config.ts") || lower.endsWith(".config.js") || isJsonConfigFile(base) || lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.endsWith(".toml") || lower.startsWith(".github/workflows/") || lower.startsWith(".github/") && lower.endsWith(".yml") || // Dotfiles that are pure configuration/tooling — never trigger runtime gotchas
|
|
6569
|
+
base === ".gitignore" || base === ".gitattributes" || base === ".gitmodules" || base === ".editorconfig" || base === ".nvmrc" || base === ".node-version" || base === ".npmrc" || base === ".yarnrc" || base === ".yarnrc.yml" || base === ".dockerignore" || base === "dockerfile" || base.startsWith("dockerfile.") || base === ".env.example" || base === ".env.template" || lower.endsWith(".prettierrc") || lower.endsWith(".eslintrc") || lower.endsWith(".eslintignore") || lower.endsWith(".prettierignore") || lower.endsWith(".stylelintrc") || lower.endsWith(".browserslistrc");
|
|
6570
|
+
}
|
|
6571
|
+
function isJsonConfigFile(base) {
|
|
6572
|
+
const knownConfigs = /* @__PURE__ */ new Set([
|
|
6573
|
+
"tsconfig.json",
|
|
6574
|
+
"jsconfig.json",
|
|
6575
|
+
"deno.json",
|
|
6576
|
+
"deno.jsonc",
|
|
6577
|
+
"nx.json",
|
|
6578
|
+
"turbo.json",
|
|
6579
|
+
"lerna.json",
|
|
6580
|
+
"rush.json",
|
|
6581
|
+
"jest.config.json",
|
|
6582
|
+
"vitest.config.json",
|
|
6583
|
+
"babel.config.json",
|
|
6584
|
+
".babelrc.json",
|
|
6585
|
+
".swcrc",
|
|
6586
|
+
".mocharc.json",
|
|
6587
|
+
"renovate.json",
|
|
6588
|
+
"dependabot.json",
|
|
6589
|
+
".prettierrc.json",
|
|
6590
|
+
".eslintrc.json",
|
|
6591
|
+
".stylelintrc.json"
|
|
6592
|
+
]);
|
|
6593
|
+
if (knownConfigs.has(base)) return true;
|
|
6594
|
+
if (/^tsconfig\..+\.json$/.test(base)) return true;
|
|
6595
|
+
if (/^\.[a-z]+rc\.json$/.test(base)) return true;
|
|
6596
|
+
return false;
|
|
6562
6597
|
}
|
|
6563
6598
|
function repairCommandForWarning(warning, paths) {
|
|
6564
6599
|
const firstPath = paths[0];
|
|
@@ -7095,7 +7130,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7095
7130
|
};
|
|
7096
7131
|
}
|
|
7097
7132
|
var SERVER_NAME = "haive";
|
|
7098
|
-
var SERVER_VERSION = "0.9.
|
|
7133
|
+
var SERVER_VERSION = "0.9.27";
|
|
7099
7134
|
function jsonResult(data) {
|
|
7100
7135
|
return {
|
|
7101
7136
|
content: [
|
|
@@ -8092,14 +8127,14 @@ var BRIDGE_START = "<!-- haive:memories-start -->";
|
|
|
8092
8127
|
var BRIDGE_END = "<!-- haive:memories-end -->";
|
|
8093
8128
|
function registerSync(program2) {
|
|
8094
8129
|
program2.command("sync").description(
|
|
8095
|
-
"Refresh memory state after a git pull or merge.\n What it does:\n 1. Verifies anchor paths \u2014 marks stale if files/symbols moved or deleted\n 2. Re-validates previously stale memories that are now fresh\n 3. Auto-promotes proposed memories (by usage count or time delay in autopilot)\n 4. Auto-refreshes code-map if source files changed\n 5. Reports decay warnings for memories unused >90 days\n\n Install git hooks to run sync automatically: haive install-hooks\n\n Examples:\n haive sync\n haive sync --since main # also report memories changed since main\n haive sync --embed # also rebuild embeddings index\n"
|
|
8130
|
+
"Refresh memory state after a git pull or merge.\n What it does:\n 1. Verifies anchor paths \u2014 marks stale if files/symbols moved or deleted\n 2. Re-validates previously stale memories that are now fresh\n 3. Auto-promotes proposed memories (by usage count or time delay in autopilot)\n 4. Auto-refreshes code-map if source files changed\n 5. Reports decay warnings for memories unused >90 days\n\n Install git hooks to run sync automatically: haive install-hooks\n\n Examples:\n haive sync\n haive sync --dry-run # preview what would change without writing\n haive sync --since main # also report memories changed since main\n haive sync --embed # also rebuild embeddings index\n"
|
|
8096
8131
|
).option("-d, --dir <dir>", "project root").option("--quiet", "minimal output (suitable for git hooks)").option(
|
|
8097
8132
|
"--since <ref>",
|
|
8098
8133
|
"git ref/commit to compare against; report memories added/modified/removed since"
|
|
8099
8134
|
).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
|
|
8100
8135
|
"--inject-bridge",
|
|
8101
8136
|
"inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
|
|
8102
|
-
).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").action(async (opts) => {
|
|
8137
|
+
).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").option("--dry-run", "report what would change without writing any files").action(async (opts) => {
|
|
8103
8138
|
const root = findProjectRoot12(opts.dir);
|
|
8104
8139
|
const paths = resolveHaivePaths9(root);
|
|
8105
8140
|
if (!existsSync29(paths.memoriesDir)) {
|
|
@@ -8114,6 +8149,8 @@ function registerSync(program2) {
|
|
|
8114
8149
|
const autoApproveDelayHours = config.autoApproveDelayHours ?? null;
|
|
8115
8150
|
const autoPromoteMinReads = config.autoPromoteMinReads ?? DEFAULT_AUTO_PROMOTE_RULE2.minReads;
|
|
8116
8151
|
const autoRepair = config.autoRepair ?? {};
|
|
8152
|
+
const dryRun = opts.dryRun === true;
|
|
8153
|
+
if (dryRun) log(ui.yellow("(dry run \u2014 no files will be written)"));
|
|
8117
8154
|
let staleMarked = 0;
|
|
8118
8155
|
let revalidated = 0;
|
|
8119
8156
|
let promoted = 0;
|
|
@@ -8123,19 +8160,21 @@ function registerSync(program2) {
|
|
|
8123
8160
|
for (const { memory: memory2, filePath } of memories) {
|
|
8124
8161
|
if (memory2.frontmatter.type === "session_recap") {
|
|
8125
8162
|
if (memory2.frontmatter.status === "stale") {
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8163
|
+
if (!dryRun) {
|
|
8164
|
+
await writeFile13(
|
|
8165
|
+
filePath,
|
|
8166
|
+
serializeMemory11({
|
|
8167
|
+
frontmatter: {
|
|
8168
|
+
...memory2.frontmatter,
|
|
8169
|
+
status: "validated",
|
|
8170
|
+
stale_reason: null,
|
|
8171
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8172
|
+
},
|
|
8173
|
+
body: memory2.body
|
|
8174
|
+
}),
|
|
8175
|
+
"utf8"
|
|
8176
|
+
);
|
|
8177
|
+
}
|
|
8139
8178
|
revalidated++;
|
|
8140
8179
|
}
|
|
8141
8180
|
continue;
|
|
@@ -8146,35 +8185,39 @@ function registerSync(program2) {
|
|
|
8146
8185
|
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8147
8186
|
if (result.stale) {
|
|
8148
8187
|
if (memory2.frontmatter.status !== "stale") {
|
|
8188
|
+
if (!dryRun) {
|
|
8189
|
+
await writeFile13(
|
|
8190
|
+
filePath,
|
|
8191
|
+
serializeMemory11({
|
|
8192
|
+
frontmatter: {
|
|
8193
|
+
...memory2.frontmatter,
|
|
8194
|
+
status: "stale",
|
|
8195
|
+
verified_at: verifiedAt,
|
|
8196
|
+
stale_reason: result.reason
|
|
8197
|
+
},
|
|
8198
|
+
body: memory2.body
|
|
8199
|
+
}),
|
|
8200
|
+
"utf8"
|
|
8201
|
+
);
|
|
8202
|
+
}
|
|
8203
|
+
staleMarked++;
|
|
8204
|
+
}
|
|
8205
|
+
} else if (memory2.frontmatter.status === "stale") {
|
|
8206
|
+
if (!dryRun) {
|
|
8149
8207
|
await writeFile13(
|
|
8150
8208
|
filePath,
|
|
8151
8209
|
serializeMemory11({
|
|
8152
8210
|
frontmatter: {
|
|
8153
8211
|
...memory2.frontmatter,
|
|
8154
|
-
status: "
|
|
8212
|
+
status: "validated",
|
|
8155
8213
|
verified_at: verifiedAt,
|
|
8156
|
-
stale_reason:
|
|
8214
|
+
stale_reason: null
|
|
8157
8215
|
},
|
|
8158
8216
|
body: memory2.body
|
|
8159
8217
|
}),
|
|
8160
8218
|
"utf8"
|
|
8161
8219
|
);
|
|
8162
|
-
staleMarked++;
|
|
8163
8220
|
}
|
|
8164
|
-
} else if (memory2.frontmatter.status === "stale") {
|
|
8165
|
-
await writeFile13(
|
|
8166
|
-
filePath,
|
|
8167
|
-
serializeMemory11({
|
|
8168
|
-
frontmatter: {
|
|
8169
|
-
...memory2.frontmatter,
|
|
8170
|
-
status: "validated",
|
|
8171
|
-
verified_at: verifiedAt,
|
|
8172
|
-
stale_reason: null
|
|
8173
|
-
},
|
|
8174
|
-
body: memory2.body
|
|
8175
|
-
}),
|
|
8176
|
-
"utf8"
|
|
8177
|
-
);
|
|
8178
8221
|
revalidated++;
|
|
8179
8222
|
}
|
|
8180
8223
|
}
|
|
@@ -8190,35 +8233,39 @@ function registerSync(program2) {
|
|
|
8190
8233
|
minReads: autoPromoteMinReads,
|
|
8191
8234
|
maxRejections: DEFAULT_AUTO_PROMOTE_RULE2.maxRejections
|
|
8192
8235
|
})) {
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8236
|
+
if (!dryRun) {
|
|
8237
|
+
await writeFile13(
|
|
8238
|
+
filePath,
|
|
8239
|
+
serializeMemory11({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
|
|
8240
|
+
"utf8"
|
|
8241
|
+
);
|
|
8242
|
+
}
|
|
8198
8243
|
promoted++;
|
|
8199
8244
|
continue;
|
|
8200
8245
|
}
|
|
8201
8246
|
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
8202
8247
|
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
8203
8248
|
if (ageHours >= autoApproveDelayHours) {
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8249
|
+
if (!dryRun) {
|
|
8250
|
+
await writeFile13(
|
|
8251
|
+
filePath,
|
|
8252
|
+
serializeMemory11({
|
|
8253
|
+
frontmatter: {
|
|
8254
|
+
...fm,
|
|
8255
|
+
status: "validated",
|
|
8256
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8257
|
+
},
|
|
8258
|
+
body: memory2.body
|
|
8259
|
+
}),
|
|
8260
|
+
"utf8"
|
|
8261
|
+
);
|
|
8262
|
+
}
|
|
8216
8263
|
autoApproved++;
|
|
8217
8264
|
}
|
|
8218
8265
|
}
|
|
8219
8266
|
}
|
|
8220
8267
|
}
|
|
8221
|
-
if (config.autopilot || autoRepair.context || autoRepair.corpus) {
|
|
8268
|
+
if (!dryRun && (config.autopilot || autoRepair.context || autoRepair.corpus)) {
|
|
8222
8269
|
const repairs = await applyAutopilotRepairs(root, paths, {
|
|
8223
8270
|
applyContext: autoRepair.context ?? config.autopilot,
|
|
8224
8271
|
applyCorpus: autoRepair.corpus ?? config.autopilot,
|
|
@@ -8350,14 +8397,16 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
8350
8397
|
paths: [result.file],
|
|
8351
8398
|
topic: `dep-bump-${slugParts}`
|
|
8352
8399
|
});
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8400
|
+
if (!dryRun) {
|
|
8401
|
+
const teamDir = path15.join(paths.memoriesDir, "team");
|
|
8402
|
+
await mkdir10(teamDir, { recursive: true });
|
|
8403
|
+
await writeFile13(
|
|
8404
|
+
path15.join(teamDir, `${fm.id}.md`),
|
|
8405
|
+
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
8406
|
+
"utf8"
|
|
8407
|
+
);
|
|
8408
|
+
}
|
|
8409
|
+
log(ui.yellow(` \u2192 memory${dryRun ? " would be" : ""} created: ${fm.id}`));
|
|
8361
8410
|
}
|
|
8362
8411
|
}
|
|
8363
8412
|
}
|
|
@@ -8417,14 +8466,16 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
8417
8466
|
paths: [diff.file],
|
|
8418
8467
|
topic: `contract-breaking-${diff.contract}`
|
|
8419
8468
|
});
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8469
|
+
if (!dryRun) {
|
|
8470
|
+
const teamDir = path15.join(paths.memoriesDir, "team");
|
|
8471
|
+
await mkdir10(teamDir, { recursive: true });
|
|
8472
|
+
await writeFile13(
|
|
8473
|
+
path15.join(teamDir, `${fm.id}.md`),
|
|
8474
|
+
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
8475
|
+
"utf8"
|
|
8476
|
+
);
|
|
8477
|
+
}
|
|
8478
|
+
log(ui.yellow(` \u2192 memory${dryRun ? " would be" : ""} created: ${fm.id}`));
|
|
8428
8479
|
}
|
|
8429
8480
|
}
|
|
8430
8481
|
} catch (err) {
|
|
@@ -8432,7 +8483,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
8432
8483
|
}
|
|
8433
8484
|
}
|
|
8434
8485
|
const existingMap = await loadCodeMap5(paths);
|
|
8435
|
-
if (!existingMap && (config.autopilot || autoRepair.codeMap)) {
|
|
8486
|
+
if (!dryRun && !existingMap && (config.autopilot || autoRepair.codeMap)) {
|
|
8436
8487
|
try {
|
|
8437
8488
|
const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
|
|
8438
8489
|
log(ui.dim("code-map: missing \u2014 building index\u2026"));
|
|
@@ -8467,17 +8518,21 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
8467
8518
|
);
|
|
8468
8519
|
const changedSourceFiles = (gitResult.stdout ?? "").trim();
|
|
8469
8520
|
if (changedSourceFiles.length > 0) {
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8521
|
+
if (!dryRun) {
|
|
8522
|
+
try {
|
|
8523
|
+
const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
|
|
8524
|
+
log(ui.dim("code-map: source files changed \u2014 refreshing index\u2026"));
|
|
8525
|
+
const newMap = await buildCodeMap4(root);
|
|
8526
|
+
await saveCodeMap4(paths, newMap);
|
|
8527
|
+
log(ui.dim(`code-map: refreshed (${Object.keys(newMap.files).length} files)`));
|
|
8528
|
+
} catch {
|
|
8529
|
+
}
|
|
8530
|
+
} else {
|
|
8531
|
+
log(ui.dim("code-map: source files changed \u2014 would refresh index (skipped in dry-run)"));
|
|
8477
8532
|
}
|
|
8478
8533
|
}
|
|
8479
8534
|
}
|
|
8480
|
-
if (opts.embed || autoRepair.codeSearch) {
|
|
8535
|
+
if (!dryRun && (opts.embed || autoRepair.codeSearch)) {
|
|
8481
8536
|
try {
|
|
8482
8537
|
const { Embedder, rebuildCodeIndex, rebuildIndex } = await import("@hiveai/embeddings");
|
|
8483
8538
|
log(ui.dim("embed: rebuilding index\u2026"));
|
|
@@ -8817,7 +8872,7 @@ import {
|
|
|
8817
8872
|
|
|
8818
8873
|
// src/commands/memory-list.ts
|
|
8819
8874
|
function registerMemoryList(memory2) {
|
|
8820
|
-
memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8875
|
+
memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("--limit <n>", "max memories to display").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8821
8876
|
const root = findProjectRoot14(opts.dir);
|
|
8822
8877
|
const paths = resolveHaivePaths11(root);
|
|
8823
8878
|
if (!existsSync31(paths.memoriesDir)) {
|
|
@@ -8827,6 +8882,7 @@ function registerMemoryList(memory2) {
|
|
|
8827
8882
|
}
|
|
8828
8883
|
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8829
8884
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
8885
|
+
const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10)) : void 0;
|
|
8830
8886
|
const filtered = all.filter((m) => {
|
|
8831
8887
|
if (!matchesFilters(m, opts)) return false;
|
|
8832
8888
|
const status = m.memory.frontmatter.status;
|
|
@@ -8844,7 +8900,9 @@ function registerMemoryList(memory2) {
|
|
|
8844
8900
|
}
|
|
8845
8901
|
return;
|
|
8846
8902
|
}
|
|
8847
|
-
|
|
8903
|
+
const displayed = limit !== void 0 ? filtered.slice(0, limit) : filtered;
|
|
8904
|
+
const clipped = filtered.length - displayed.length;
|
|
8905
|
+
for (const { memory: mem, filePath } of displayed) {
|
|
8848
8906
|
const fm = mem.frontmatter;
|
|
8849
8907
|
const tagStr = fm.tags.length ? ui.dim(` [${fm.tags.join(", ")}]`) : "";
|
|
8850
8908
|
const moduleStr = fm.module ? ui.dim(` (${fm.module})`) : "";
|
|
@@ -8856,8 +8914,10 @@ function registerMemoryList(memory2) {
|
|
|
8856
8914
|
if (title && title !== fm.id) console.log(` ${title}`);
|
|
8857
8915
|
console.log(` ${ui.dim(path17.relative(root, filePath))}`);
|
|
8858
8916
|
}
|
|
8859
|
-
|
|
8860
|
-
${
|
|
8917
|
+
const totalLabel = clipped > 0 ? `
|
|
8918
|
+
${displayed.length} of ${filtered.length} memories shown (use --limit to adjust)` : `
|
|
8919
|
+
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`;
|
|
8920
|
+
console.log(ui.dim(totalLabel));
|
|
8861
8921
|
if (hiddenRejectedCount > 0) {
|
|
8862
8922
|
console.log(
|
|
8863
8923
|
ui.dim(`(${hiddenRejectedCount} rejected hidden \u2014 use --show-rejected to include)`)
|
|
@@ -9373,7 +9433,9 @@ import {
|
|
|
9373
9433
|
resolveHaivePaths as resolveHaivePaths18
|
|
9374
9434
|
} from "@hiveai/core";
|
|
9375
9435
|
function registerMemoryHot(memory2) {
|
|
9376
|
-
memory2.command("hot").description(
|
|
9436
|
+
memory2.command("hot").description(
|
|
9437
|
+
"List unvalidated memories with high read_count \u2014 proven-useful promotion candidates.\n\n Unlike `haive memory pending` (which lists ALL draft/proposed by status),\n `hot` filters by usage: only memories read \u2265N times qualify.\n Use it to quickly find memories that agents are already relying on\n but that haven't been formally validated yet."
|
|
9438
|
+
).option("--threshold <n>", "minimum read_count to qualify (default: 3)", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9377
9439
|
const root = findProjectRoot21(opts.dir);
|
|
9378
9440
|
const paths = resolveHaivePaths18(root);
|
|
9379
9441
|
if (!existsSync39(paths.memoriesDir)) {
|
|
@@ -9407,7 +9469,8 @@ function registerMemoryHot(memory2) {
|
|
|
9407
9469
|
console.log(` ${ui.dim(path25.relative(root, filePath))}`);
|
|
9408
9470
|
}
|
|
9409
9471
|
ui.info(
|
|
9410
|
-
`${candidates.length} hot \u2014
|
|
9472
|
+
`${candidates.length} hot (read \u2265${threshold}\xD7) \u2014 agents rely on these; promote with \`haive memory promote <id>\`.
|
|
9473
|
+
Tip: \`haive memory pending\` lists ALL unvalidated memories regardless of read count.`
|
|
9411
9474
|
);
|
|
9412
9475
|
});
|
|
9413
9476
|
}
|
|
@@ -10285,18 +10348,37 @@ async function buildAutoRecap(paths) {
|
|
|
10285
10348
|
}
|
|
10286
10349
|
if (obs.length === 0) return await buildGitAutoRecap(paths);
|
|
10287
10350
|
const toolCounts = /* @__PURE__ */ new Map();
|
|
10288
|
-
const
|
|
10289
|
-
const
|
|
10351
|
+
const writeFiles = /* @__PURE__ */ new Set();
|
|
10352
|
+
const readFiles = /* @__PURE__ */ new Set();
|
|
10290
10353
|
for (const o of obs) {
|
|
10291
10354
|
toolCounts.set(o.tool, (toolCounts.get(o.tool) ?? 0) + 1);
|
|
10292
|
-
|
|
10293
|
-
|
|
10355
|
+
const isWrite = ["Edit", "Write", "NotebookEdit"].includes(o.tool);
|
|
10356
|
+
for (const f of o.files ?? []) {
|
|
10357
|
+
const rel = normalizeAnchorPath(paths.root, f);
|
|
10358
|
+
if (isWrite) writeFiles.add(rel);
|
|
10359
|
+
else readFiles.add(rel);
|
|
10360
|
+
}
|
|
10294
10361
|
}
|
|
10362
|
+
for (const f of writeFiles) readFiles.delete(f);
|
|
10295
10363
|
const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, c]) => `${t} \xD7${c}`).join(", ");
|
|
10296
|
-
const
|
|
10297
|
-
const
|
|
10298
|
-
|
|
10299
|
-
|
|
10364
|
+
const recentCommits = await runGit(paths.root, ["log", "--oneline", "-5"]).catch(() => "");
|
|
10365
|
+
const accomplishedParts = [];
|
|
10366
|
+
if (writeFiles.size > 0) {
|
|
10367
|
+
accomplishedParts.push(
|
|
10368
|
+
`**Files modified (${writeFiles.size}):**`,
|
|
10369
|
+
...[...writeFiles].slice(0, 10).map((f) => `- \`${f}\``),
|
|
10370
|
+
...writeFiles.size > 10 ? [`- ...and ${writeFiles.size - 10} more`] : []
|
|
10371
|
+
);
|
|
10372
|
+
}
|
|
10373
|
+
if (recentCommits.trim()) {
|
|
10374
|
+
accomplishedParts.push("", "**Recent commits:**");
|
|
10375
|
+
for (const line of recentCommits.trim().split("\n").slice(0, 5)) {
|
|
10376
|
+
accomplishedParts.push(`- ${line}`);
|
|
10377
|
+
}
|
|
10378
|
+
}
|
|
10379
|
+
if (accomplishedParts.length === 0) {
|
|
10380
|
+
accomplishedParts.push(`${obs.length} tool calls (${topTools}) \u2014 no file writes detected.`);
|
|
10381
|
+
}
|
|
10300
10382
|
const failures = obs.filter((o) => o.failure_hint);
|
|
10301
10383
|
const discoveriesParts = [];
|
|
10302
10384
|
if (failures.length > 0) {
|
|
@@ -10305,37 +10387,75 @@ ${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
|
|
|
10305
10387
|
...failures.slice(0, 8).map((o) => `- ${o.summary.slice(0, 180)}`)
|
|
10306
10388
|
);
|
|
10307
10389
|
}
|
|
10390
|
+
const goal = writeFiles.size > 0 ? `Edited ${writeFiles.size} file${writeFiles.size === 1 ? "" : "s"} across ${obs.length} tool calls` : `Session with ${obs.length} tool calls (${topTools}) \u2014 read-only or no writes captured`;
|
|
10308
10391
|
return {
|
|
10309
10392
|
goal,
|
|
10310
|
-
accomplished,
|
|
10393
|
+
accomplished: accomplishedParts.join("\n"),
|
|
10311
10394
|
...discoveriesParts.length > 0 ? { discoveries: discoveriesParts.join("\n") } : {},
|
|
10312
|
-
files:
|
|
10395
|
+
files: [...writeFiles].slice(0, 12),
|
|
10313
10396
|
rawCount: obs.length
|
|
10314
10397
|
};
|
|
10315
10398
|
}
|
|
10316
10399
|
async function buildGitAutoRecap(paths) {
|
|
10317
10400
|
const changed = await runGit(paths.root, ["diff", "--name-only"]).catch(() => "");
|
|
10318
10401
|
const staged = await runGit(paths.root, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
10319
|
-
const
|
|
10402
|
+
const statusRaw = await runGit(paths.root, ["status", "--porcelain"]).catch(() => "");
|
|
10403
|
+
const recentLog = await runGit(paths.root, ["log", "--oneline", "-5"]).catch(() => "");
|
|
10404
|
+
const diffStat = await runGit(paths.root, ["diff", "--stat", "HEAD"]).catch(() => "");
|
|
10320
10405
|
const files = Array.from(new Set(
|
|
10321
10406
|
[
|
|
10322
10407
|
...changed.split("\n"),
|
|
10323
10408
|
...staged.split("\n"),
|
|
10324
|
-
...
|
|
10409
|
+
...statusRaw.split("\n").map((line) => line.replace(/^[ MADRCU?!]{1,2}\s+/, ""))
|
|
10325
10410
|
].map((s) => s.trim()).filter(Boolean).filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"))
|
|
10326
10411
|
)).sort();
|
|
10327
|
-
|
|
10328
|
-
const
|
|
10412
|
+
const modified = [];
|
|
10413
|
+
const added = [];
|
|
10414
|
+
const deleted = [];
|
|
10415
|
+
for (const line of statusRaw.split("\n")) {
|
|
10416
|
+
const code = line.substring(0, 2).trim();
|
|
10417
|
+
const file = line.substring(3).trim().replace(/".+"/g, (m) => m.slice(1, -1));
|
|
10418
|
+
if (!file || file.startsWith(".ai/.runtime/") || file.startsWith(".ai/.cache/")) continue;
|
|
10419
|
+
if (code === "D" || code === "DD") deleted.push(file);
|
|
10420
|
+
else if (code === "A" || code === "??") added.push(file);
|
|
10421
|
+
else if (file) modified.push(file);
|
|
10422
|
+
}
|
|
10423
|
+
const accomplishedParts = [];
|
|
10424
|
+
if (modified.length > 0) {
|
|
10425
|
+
accomplishedParts.push(`**Modified (${modified.length}):**`);
|
|
10426
|
+
for (const f of modified.slice(0, 8)) accomplishedParts.push(`- \`${f}\``);
|
|
10427
|
+
if (modified.length > 8) accomplishedParts.push(`- ...and ${modified.length - 8} more`);
|
|
10428
|
+
}
|
|
10429
|
+
if (added.length > 0) {
|
|
10430
|
+
accomplishedParts.push(`
|
|
10431
|
+
**Added (${added.length}):**`);
|
|
10432
|
+
for (const f of added.slice(0, 5)) accomplishedParts.push(`- \`${f}\``);
|
|
10433
|
+
if (added.length > 5) accomplishedParts.push(`- ...and ${added.length - 5} more`);
|
|
10434
|
+
}
|
|
10435
|
+
if (deleted.length > 0) {
|
|
10436
|
+
accomplishedParts.push(`
|
|
10437
|
+
**Deleted (${deleted.length}):**`);
|
|
10438
|
+
for (const f of deleted.slice(0, 5)) accomplishedParts.push(`- \`${f}\``);
|
|
10439
|
+
}
|
|
10440
|
+
if (recentLog.trim()) {
|
|
10441
|
+
accomplishedParts.push("\n**Recent commits:**");
|
|
10442
|
+
for (const line of recentLog.trim().split("\n").slice(0, 5)) {
|
|
10443
|
+
accomplishedParts.push(`- ${line}`);
|
|
10444
|
+
}
|
|
10445
|
+
}
|
|
10446
|
+
if (accomplishedParts.length === 0 && files.length === 0) return null;
|
|
10447
|
+
if (accomplishedParts.length === 0) {
|
|
10448
|
+
accomplishedParts.push(...files.slice(0, 12).map((f) => `- \`${f}\``));
|
|
10449
|
+
if (files.length > 12) accomplishedParts.push(`- ...and ${files.length - 12} more`);
|
|
10450
|
+
}
|
|
10329
10451
|
return {
|
|
10330
|
-
goal: `
|
|
10331
|
-
accomplished:
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
${diffStat.trim()}` : void 0,
|
|
10338
|
-
files,
|
|
10452
|
+
goal: files.length > 0 ? `Session with ${files.length} changed file${files.length === 1 ? "" : "s"}` : `Session with recent commits (no uncommitted changes)`,
|
|
10453
|
+
accomplished: accomplishedParts.join("\n"),
|
|
10454
|
+
discoveries: diffStat.trim() ? `Git diff stat:
|
|
10455
|
+
\`\`\`
|
|
10456
|
+
${diffStat.trim()}
|
|
10457
|
+
\`\`\`` : void 0,
|
|
10458
|
+
files: files.slice(0, 12),
|
|
10339
10459
|
rawCount: files.length
|
|
10340
10460
|
};
|
|
10341
10461
|
}
|
|
@@ -10438,7 +10558,7 @@ function registerSessionEnd(session2) {
|
|
|
10438
10558
|
next: opts.next
|
|
10439
10559
|
});
|
|
10440
10560
|
const topic = recapTopic2(scope, opts.module);
|
|
10441
|
-
const filesTouched = parseCsv5(resolvedFiles);
|
|
10561
|
+
const filesTouched = parseCsv5(resolvedFiles).map((p) => normalizeAnchorPath(root, p));
|
|
10442
10562
|
const missingPaths = filesTouched.filter((p) => !existsSync53(path36.resolve(root, p)));
|
|
10443
10563
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
10444
10564
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
@@ -10503,6 +10623,13 @@ function parseCsv5(value) {
|
|
|
10503
10623
|
if (!value) return [];
|
|
10504
10624
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
10505
10625
|
}
|
|
10626
|
+
function normalizeAnchorPath(root, filePath) {
|
|
10627
|
+
if (!filePath) return filePath;
|
|
10628
|
+
if (!path36.isAbsolute(filePath)) return filePath;
|
|
10629
|
+
const rel = path36.relative(root, filePath);
|
|
10630
|
+
if (rel.startsWith("..")) return filePath;
|
|
10631
|
+
return rel;
|
|
10632
|
+
}
|
|
10506
10633
|
|
|
10507
10634
|
// src/commands/snapshot.ts
|
|
10508
10635
|
import { existsSync as existsSync54 } from "fs";
|
|
@@ -11691,8 +11818,8 @@ function parseDays(input) {
|
|
|
11691
11818
|
}
|
|
11692
11819
|
|
|
11693
11820
|
// src/commands/doctor.ts
|
|
11694
|
-
import { existsSync as existsSync60 } from "fs";
|
|
11695
|
-
import { readFile as readFile19, stat } from "fs/promises";
|
|
11821
|
+
import { existsSync as existsSync60, statSync } from "fs";
|
|
11822
|
+
import { readFile as readFile19, stat, writeFile as writeFile31 } from "fs/promises";
|
|
11696
11823
|
import path44 from "path";
|
|
11697
11824
|
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
11698
11825
|
import "commander";
|
|
@@ -11795,6 +11922,22 @@ function registerDoctor(program2) {
|
|
|
11795
11922
|
fix: "haive memory pending # list them\nhaive memory auto-promote # promote those with high read_count"
|
|
11796
11923
|
});
|
|
11797
11924
|
}
|
|
11925
|
+
const OLD_DRAFT_DAYS = 30;
|
|
11926
|
+
const oldDrafts = memories.filter((m) => {
|
|
11927
|
+
if (m.memory.frontmatter.status !== "draft") return false;
|
|
11928
|
+
const age = (now - Date.parse(m.memory.frontmatter.created_at)) / MS_PER_DAY3;
|
|
11929
|
+
return age > OLD_DRAFT_DAYS;
|
|
11930
|
+
});
|
|
11931
|
+
if (oldDrafts.length > 0) {
|
|
11932
|
+
const ids = oldDrafts.slice(0, 4).map((m) => m.memory.frontmatter.id).join(", ");
|
|
11933
|
+
const more = oldDrafts.length > 4 ? ` (+${oldDrafts.length - 4} more)` : "";
|
|
11934
|
+
findings.push({
|
|
11935
|
+
severity: "warn",
|
|
11936
|
+
code: "stale-draft-memories",
|
|
11937
|
+
message: `${oldDrafts.length} draft memor${oldDrafts.length === 1 ? "y has" : "ies have"} been in draft status for 30+ days: ${ids}${more}`,
|
|
11938
|
+
fix: "haive memory approve <id> # activate\nhaive memory rm <id> # or delete if obsolete"
|
|
11939
|
+
});
|
|
11940
|
+
}
|
|
11798
11941
|
const anchorless = memories.filter(
|
|
11799
11942
|
(m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary" && m.memory.frontmatter.type !== "skill"
|
|
11800
11943
|
);
|
|
@@ -11920,26 +12063,58 @@ function registerDoctor(program2) {
|
|
|
11920
12063
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
11921
12064
|
});
|
|
11922
12065
|
}
|
|
11923
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
12066
|
+
findings.push(...await collectInstallFindings(root, "0.9.27"));
|
|
11924
12067
|
try {
|
|
11925
12068
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
11926
12069
|
encoding: "utf8",
|
|
11927
12070
|
timeout: 3e3,
|
|
11928
12071
|
stdio: ["ignore", "pipe", "ignore"]
|
|
11929
12072
|
}).trim();
|
|
11930
|
-
const cliVersion = "0.9.
|
|
12073
|
+
const cliVersion = "0.9.27";
|
|
11931
12074
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
11932
12075
|
findings.push({
|
|
11933
12076
|
severity: "warn",
|
|
11934
12077
|
code: "legacy-haive-mcp-stale",
|
|
11935
|
-
message: `Standalone haive-mcp on PATH is v${legacyRaw} but haive CLI is v${cliVersion}.
|
|
11936
|
-
fix:
|
|
11937
|
-
|
|
12078
|
+
message: `Standalone haive-mcp on PATH is v${legacyRaw} but haive CLI is v${cliVersion}. MCP is now bundled in haive itself \u2014 switch your client configs to command "haive" + args ["mcp", "--stdio"], then uninstall @hiveai/mcp.`,
|
|
12079
|
+
fix: `# 1. Run haive init to regenerate MCP configs pointing to bundled server:
|
|
12080
|
+
haive init
|
|
12081
|
+
# 2. Optionally remove the now-redundant standalone package:
|
|
11938
12082
|
npm uninstall -g @hiveai/mcp`
|
|
11939
12083
|
});
|
|
11940
12084
|
}
|
|
11941
12085
|
} catch {
|
|
11942
12086
|
}
|
|
12087
|
+
{
|
|
12088
|
+
const configPaths = [
|
|
12089
|
+
path44.join(root, ".mcp.json"),
|
|
12090
|
+
path44.join(root, ".cursor", "mcp.json"),
|
|
12091
|
+
path44.join(root, ".vscode", "mcp.json")
|
|
12092
|
+
];
|
|
12093
|
+
const staleConfigs = [];
|
|
12094
|
+
for (const cfgPath of configPaths) {
|
|
12095
|
+
if (!existsSync60(cfgPath)) continue;
|
|
12096
|
+
try {
|
|
12097
|
+
const raw = await readFile19(cfgPath, "utf8");
|
|
12098
|
+
if (raw.includes('"haive-mcp"') || raw.includes("'haive-mcp'")) {
|
|
12099
|
+
staleConfigs.push(path44.relative(root, cfgPath));
|
|
12100
|
+
if (opts.fix && !opts.dryRun) {
|
|
12101
|
+
const updated = raw.replace(/"command"\s*:\s*"haive-mcp"/g, '"command": "haive"').replace(/"args"\s*:\s*\[\]/g, '"args": ["mcp", "--stdio"]');
|
|
12102
|
+
await writeFile31(cfgPath, updated, "utf8");
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
} catch {
|
|
12106
|
+
}
|
|
12107
|
+
}
|
|
12108
|
+
if (staleConfigs.length > 0) {
|
|
12109
|
+
findings.push({
|
|
12110
|
+
severity: "warn",
|
|
12111
|
+
code: "legacy-mcp-config",
|
|
12112
|
+
message: `${staleConfigs.length} MCP config file${staleConfigs.length === 1 ? "" : "s"} still reference the old "haive-mcp" command: ` + staleConfigs.join(", ") + `. Run \`haive doctor --fix\` to auto-migrate to the bundled server.`,
|
|
12113
|
+
fix: "haive doctor --fix",
|
|
12114
|
+
section: "Protection"
|
|
12115
|
+
});
|
|
12116
|
+
}
|
|
12117
|
+
}
|
|
11943
12118
|
if (repairs.length > 0) {
|
|
11944
12119
|
findings.push({
|
|
11945
12120
|
severity: "info",
|
|
@@ -12063,9 +12238,21 @@ function groupBySection(findings) {
|
|
|
12063
12238
|
function nextActions(findings) {
|
|
12064
12239
|
return [...new Set(findings.flatMap((finding) => finding.fix ? finding.fix.split("\n") : []))].filter(Boolean);
|
|
12065
12240
|
}
|
|
12241
|
+
function isLowValueCoverageFile(file) {
|
|
12242
|
+
const lower = file.toLowerCase();
|
|
12243
|
+
const base = lower.split("/").pop() ?? lower;
|
|
12244
|
+
if (lower.includes(".test.") || lower.includes(".spec.")) return true;
|
|
12245
|
+
if (lower.includes("/__tests__/") || lower.includes("/test/") || lower.includes("/tests/")) return true;
|
|
12246
|
+
if (lower.endsWith(".d.ts")) return true;
|
|
12247
|
+
if (base === "tsup.config.ts" || base === "vitest.config.ts" || base === "jest.config.ts") return true;
|
|
12248
|
+
if (base.endsWith(".config.ts") || base.endsWith(".config.js")) return true;
|
|
12249
|
+
if (base === "vite.config.ts" || base === "vite.config.js") return true;
|
|
12250
|
+
return false;
|
|
12251
|
+
}
|
|
12066
12252
|
async function collectHarnessCoverageFindings(codeMap, memories) {
|
|
12067
12253
|
if (!codeMap) return [];
|
|
12068
|
-
const
|
|
12254
|
+
const allFiles = Object.keys(codeMap.files);
|
|
12255
|
+
const codeFiles = allFiles.filter((f) => !isLowValueCoverageFile(f));
|
|
12069
12256
|
const total = codeFiles.length;
|
|
12070
12257
|
if (total === 0) return [];
|
|
12071
12258
|
const validatedWithAnchors = memories.filter(
|
|
@@ -12084,13 +12271,22 @@ async function collectHarnessCoverageFindings(codeMap, memories) {
|
|
|
12084
12271
|
}
|
|
12085
12272
|
const covered = coveredFiles.size;
|
|
12086
12273
|
const pct = Math.round(covered / total * 100);
|
|
12274
|
+
const uncovered = codeFiles.filter((f) => !coveredFiles.has(f)).sort((a, b) => {
|
|
12275
|
+
const depthA = a.split("/").length;
|
|
12276
|
+
const depthB = b.split("/").length;
|
|
12277
|
+
if (depthA !== depthB) return depthA - depthB;
|
|
12278
|
+
return a.localeCompare(b);
|
|
12279
|
+
}).slice(0, 5);
|
|
12280
|
+
const coverageDesc = pct < 10 && total > 10 ? "Low coverage \u2014 add memory anchors on key modules to improve harness enforcement." : pct < 50 ? "Partial coverage \u2014 useful but not yet broad enough to call the harness mature." : pct < 80 ? "Good coverage \u2014 critical modules are increasingly protected." : "Good harness coverage.";
|
|
12281
|
+
const uncoveredHint = uncovered.length > 0 ? `
|
|
12282
|
+
Top uncovered: ${uncovered.map((f) => `\`${f}\``).join(", ")}` : "";
|
|
12087
12283
|
const findings = [];
|
|
12088
12284
|
findings.push({
|
|
12089
12285
|
severity: "info",
|
|
12090
12286
|
code: "harness-coverage",
|
|
12091
12287
|
coverage_percent: pct,
|
|
12092
|
-
message: `${covered}/${total} code-map files have validated memory anchors (${pct}%). ` +
|
|
12093
|
-
fix: pct < 50 && total > 10 ?
|
|
12288
|
+
message: `${covered}/${total} code-map files have validated memory anchors (${pct}%). ` + coverageDesc + uncoveredHint,
|
|
12289
|
+
fix: pct < 50 && total > 10 ? `haive memory add --type gotcha|convention|architecture --paths <key-file> --scope team` : void 0,
|
|
12094
12290
|
section: "Harness coverage"
|
|
12095
12291
|
});
|
|
12096
12292
|
return findings;
|
|
@@ -12322,7 +12518,13 @@ function extractAbsoluteHaiveBins(text) {
|
|
|
12322
12518
|
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
12323
12519
|
let match;
|
|
12324
12520
|
while (match = re.exec(text)) {
|
|
12325
|
-
|
|
12521
|
+
const p = match[2];
|
|
12522
|
+
if (!p) continue;
|
|
12523
|
+
try {
|
|
12524
|
+
if (statSync(p).isDirectory()) continue;
|
|
12525
|
+
} catch {
|
|
12526
|
+
}
|
|
12527
|
+
out.add(p);
|
|
12326
12528
|
}
|
|
12327
12529
|
return [...out].sort();
|
|
12328
12530
|
}
|
|
@@ -12823,8 +13025,8 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
12823
13025
|
|
|
12824
13026
|
// src/commands/enforce.ts
|
|
12825
13027
|
import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
|
|
12826
|
-
import { existsSync as existsSync67 } from "fs";
|
|
12827
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile20, readdir as readdir6, rm as rm3, writeFile as
|
|
13028
|
+
import { existsSync as existsSync67, statSync as statSync2 } from "fs";
|
|
13029
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile20, readdir as readdir6, rm as rm3, writeFile as writeFile33 } from "fs/promises";
|
|
12828
13030
|
import path49 from "path";
|
|
12829
13031
|
import "commander";
|
|
12830
13032
|
import {
|
|
@@ -13118,7 +13320,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
13118
13320
|
if (briefing.setup_warnings.length > 0) {
|
|
13119
13321
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
13120
13322
|
}
|
|
13121
|
-
await
|
|
13323
|
+
await writeFile33(file, parts.join("\n") + "\n", "utf8");
|
|
13122
13324
|
return file;
|
|
13123
13325
|
}
|
|
13124
13326
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
@@ -13155,7 +13357,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13155
13357
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
13156
13358
|
});
|
|
13157
13359
|
}
|
|
13158
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
13360
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.27"));
|
|
13159
13361
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
13160
13362
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
13161
13363
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -13377,9 +13579,9 @@ async function cleanupRuntimeDir(runtimeDir) {
|
|
|
13377
13579
|
await rm3(path49.join(runtimeDir, entry.name), { recursive: true, force: true });
|
|
13378
13580
|
removed++;
|
|
13379
13581
|
}
|
|
13380
|
-
await
|
|
13582
|
+
await writeFile33(path49.join(runtimeDir, ".gitignore"), "*\n!.gitignore\n!README.md\n", "utf8");
|
|
13381
13583
|
if (!existsSync67(path49.join(runtimeDir, "README.md"))) {
|
|
13382
|
-
await
|
|
13584
|
+
await writeFile33(
|
|
13383
13585
|
path49.join(runtimeDir, "README.md"),
|
|
13384
13586
|
"# .ai/.runtime \u2014 disposable local layer\n\nRuntime data is local. hAIve cleanup preserves briefing markers so enforcement state remains valid.\n",
|
|
13385
13587
|
"utf8"
|
|
@@ -13396,7 +13598,7 @@ async function cleanupCacheDir(cacheDir) {
|
|
|
13396
13598
|
await rm3(path49.join(cacheDir, entry.name), { recursive: true, force: true });
|
|
13397
13599
|
removed++;
|
|
13398
13600
|
}
|
|
13399
|
-
await
|
|
13601
|
+
await writeFile33(path49.join(cacheDir, ".gitignore"), "*\n!.gitignore\n", "utf8");
|
|
13400
13602
|
return removed;
|
|
13401
13603
|
}
|
|
13402
13604
|
async function cleanupEnforcementDir(enforcementDir) {
|
|
@@ -13458,7 +13660,13 @@ function extractAbsoluteHaiveBins2(text) {
|
|
|
13458
13660
|
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
13459
13661
|
let match;
|
|
13460
13662
|
while (match = re.exec(text)) {
|
|
13461
|
-
|
|
13663
|
+
const p = match[2];
|
|
13664
|
+
if (!p) continue;
|
|
13665
|
+
try {
|
|
13666
|
+
if (statSync2(p).isDirectory()) continue;
|
|
13667
|
+
} catch {
|
|
13668
|
+
}
|
|
13669
|
+
out.add(p);
|
|
13462
13670
|
}
|
|
13463
13671
|
return [...out].sort();
|
|
13464
13672
|
}
|
|
@@ -13537,14 +13745,14 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
13537
13745
|
if (existsSync67(file)) {
|
|
13538
13746
|
const current = await readFile20(file, "utf8").catch(() => "");
|
|
13539
13747
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
13540
|
-
await
|
|
13748
|
+
await writeFile33(file, hook.body, "utf8");
|
|
13541
13749
|
} else {
|
|
13542
|
-
await
|
|
13750
|
+
await writeFile33(file, `${current.trimEnd()}
|
|
13543
13751
|
|
|
13544
13752
|
${hook.body}`, "utf8");
|
|
13545
13753
|
}
|
|
13546
13754
|
} else {
|
|
13547
|
-
await
|
|
13755
|
+
await writeFile33(file, hook.body, "utf8");
|
|
13548
13756
|
}
|
|
13549
13757
|
await chmod2(file, 493);
|
|
13550
13758
|
}
|
|
@@ -13557,7 +13765,7 @@ async function installCiEnforcement(root) {
|
|
|
13557
13765
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
13558
13766
|
return;
|
|
13559
13767
|
}
|
|
13560
|
-
await
|
|
13768
|
+
await writeFile33(workflowPath, `name: haive-enforcement
|
|
13561
13769
|
|
|
13562
13770
|
on:
|
|
13563
13771
|
pull_request:
|
|
@@ -13756,7 +13964,7 @@ function registerRun(program2) {
|
|
|
13756
13964
|
|
|
13757
13965
|
// src/index.ts
|
|
13758
13966
|
var program = new Command51();
|
|
13759
|
-
program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.
|
|
13967
|
+
program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.27").option("--advanced", "show maintenance and experimental commands in help");
|
|
13760
13968
|
registerInit(program);
|
|
13761
13969
|
registerWelcome(program);
|
|
13762
13970
|
registerResolveProject(program);
|