@hiveai/cli 0.10.8 → 0.10.9
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 +2 -0
- package/dist/index.js +483 -33
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2743,6 +2743,7 @@ ${SEED_FOOTER(stack)}` });
|
|
|
2743
2743
|
}
|
|
2744
2744
|
|
|
2745
2745
|
// src/commands/init.ts
|
|
2746
|
+
var HAIVE_GITHUB_ACTION_REF = `v${"0.10.9"}`;
|
|
2746
2747
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
2747
2748
|
|
|
2748
2749
|
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
@@ -2777,6 +2778,7 @@ This repo uses **hAIve** for shared context. The map:
|
|
|
2777
2778
|
1. **Before editing** for a goal, call \`get_briefing\` (task + files/symbols) to load ranked context \u2014 or \`mem_relevant_to\` if project context is already loaded this session.
|
|
2778
2779
|
2. **When an approach fails**, call \`mem_tried\` right away so the next agent skips the dead end.
|
|
2779
2780
|
3. **Before closing** a substantive session, run the \`post_task\` prompt to capture what was learned.
|
|
2781
|
+
4. **Before final response**, run \`haive enforce finish\`. If it blocks, commit/push, bump/tag shippable releases, then rerun it.
|
|
2780
2782
|
|
|
2781
2783
|
If the haive MCP server is not available, tell the developer rather than silently skipping it.
|
|
2782
2784
|
|
|
@@ -2803,6 +2805,7 @@ This repository uses **hAIve**. Running \`haive init\` means the team expects ag
|
|
|
2803
2805
|
|
|
2804
2806
|
- On failure: **\`mem_tried\`** immediately.
|
|
2805
2807
|
- Before closing a substantive session: MCP prompt **\`post_task\`** when there is something worth capturing.
|
|
2808
|
+
- Before final response: **\`haive enforce finish\`** must pass; it checks commit/push and release version/tag protocol.
|
|
2806
2809
|
|
|
2807
2810
|
## If haive MCP is missing
|
|
2808
2811
|
|
|
@@ -2898,7 +2901,7 @@ jobs:
|
|
|
2898
2901
|
steps:
|
|
2899
2902
|
- uses: actions/checkout@v4
|
|
2900
2903
|
|
|
2901
|
-
- uses: Doucs91/hAIve/packages/github-action
|
|
2904
|
+
- uses: Doucs91/hAIve/packages/github-action@${HAIVE_GITHUB_ACTION_REF}
|
|
2902
2905
|
with:
|
|
2903
2906
|
github-token: \${{ secrets.GITHUB_TOKEN }}
|
|
2904
2907
|
# post-if-empty: 'true' # uncomment to always post (even when no memories found)
|
|
@@ -6715,7 +6718,9 @@ async function preCommitCheck(input, ctx) {
|
|
|
6715
6718
|
};
|
|
6716
6719
|
}
|
|
6717
6720
|
function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
6718
|
-
const affectedFiles = paths.filter(
|
|
6721
|
+
const affectedFiles = paths.filter(
|
|
6722
|
+
(p) => !p.startsWith(".ai/.usage/") && !p.startsWith(".ai/.cache/") && !p.startsWith(".ai/.runtime/")
|
|
6723
|
+
);
|
|
6719
6724
|
const repairCommand = repairCommandForWarning(warning, affectedFiles);
|
|
6720
6725
|
const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
|
|
6721
6726
|
if (fileDowngrade) {
|
|
@@ -6746,6 +6751,15 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
|
6746
6751
|
};
|
|
6747
6752
|
}
|
|
6748
6753
|
if (isBlockingWarning(warning)) {
|
|
6754
|
+
if (warning.scope === "personal") {
|
|
6755
|
+
return {
|
|
6756
|
+
...warning,
|
|
6757
|
+
level: "review",
|
|
6758
|
+
rationale: "personal anti-pattern memories are review guidance unless a deterministic block-severity sensor fires",
|
|
6759
|
+
affected_files: affectedFiles,
|
|
6760
|
+
repair_command: repairCommand
|
|
6761
|
+
};
|
|
6762
|
+
}
|
|
6749
6763
|
if (warning.has_sensor && !warning.reasons.includes("sensor")) {
|
|
6750
6764
|
return {
|
|
6751
6765
|
...warning,
|
|
@@ -6766,7 +6780,7 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
|
6766
6780
|
const hasSemantic = warning.reasons.includes("semantic");
|
|
6767
6781
|
const semanticScore = warning.semantic_score ?? 0;
|
|
6768
6782
|
const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
|
|
6769
|
-
if (anchoredBlocks && highConfidence && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
|
|
6783
|
+
if (anchoredBlocks && highConfidence && warning.scope !== "personal" && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
|
|
6770
6784
|
if (warning.has_sensor && !warning.reasons.includes("sensor")) {
|
|
6771
6785
|
return {
|
|
6772
6786
|
...warning,
|
|
@@ -6900,8 +6914,20 @@ function isJsonConfigFile(base) {
|
|
|
6900
6914
|
return false;
|
|
6901
6915
|
}
|
|
6902
6916
|
function repairCommandForWarning(warning, paths) {
|
|
6903
|
-
const
|
|
6904
|
-
return
|
|
6917
|
+
const targetPath = repairTargetPathForWarning(warning, paths);
|
|
6918
|
+
return targetPath ? `haive briefing --files "${targetPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
|
|
6919
|
+
}
|
|
6920
|
+
function repairTargetPathForWarning(warning, paths) {
|
|
6921
|
+
const usablePaths = paths.filter(
|
|
6922
|
+
(p) => !p.startsWith(".ai/.usage/") && !p.startsWith(".ai/.cache/") && !p.startsWith(".ai/.runtime/")
|
|
6923
|
+
);
|
|
6924
|
+
const anchors = warning.anchor_paths ?? [];
|
|
6925
|
+
for (const file of usablePaths) {
|
|
6926
|
+
if (anchors.some((anchor) => anchor === file || file.startsWith(`${anchor}/`) || anchor.startsWith(`${file}/`))) {
|
|
6927
|
+
return file;
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
return usablePaths[0];
|
|
6905
6931
|
}
|
|
6906
6932
|
var CONFIG_PATTERNS = [
|
|
6907
6933
|
".eslintrc",
|
|
@@ -7355,7 +7381,19 @@ This creates/updates a single rolling recap that **get_briefing automatically su
|
|
|
7355
7381
|
|
|
7356
7382
|
Calling \`mem_session_end\` also **clears the pending-distill marker** (if any), confirming that this session's learnings have been properly captured rather than left as an auto-recap skeleton.
|
|
7357
7383
|
|
|
7358
|
-
|
|
7384
|
+
### 7. Verify the git/release exit protocol \u2014 always
|
|
7385
|
+
Run **\`haive enforce finish\`** before your final response.
|
|
7386
|
+
|
|
7387
|
+
This executable gate checks the multi-agent git-sync decision:
|
|
7388
|
+
- no completed work is left as an uncommitted local diff
|
|
7389
|
+
- shippable package changes have a lockstep version bump
|
|
7390
|
+
- the release tag \`vX.Y.Z\` exists when a version was bumped
|
|
7391
|
+
- commits and tags have been pushed
|
|
7392
|
+
- agents never run \`npm publish\` (publication remains human-owned)
|
|
7393
|
+
|
|
7394
|
+
If it blocks, fix the reported Git/version/tag/push issue before telling the developer the task is done.
|
|
7395
|
+
|
|
7396
|
+
When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved. hAIve finish gate passed."
|
|
7359
7397
|
`;
|
|
7360
7398
|
return {
|
|
7361
7399
|
description: "Post-task reflection: capture what you learned before closing the session",
|
|
@@ -7434,7 +7472,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7434
7472
|
};
|
|
7435
7473
|
}
|
|
7436
7474
|
var SERVER_NAME = "haive";
|
|
7437
|
-
var SERVER_VERSION = "0.10.
|
|
7475
|
+
var SERVER_VERSION = "0.10.9";
|
|
7438
7476
|
function jsonResult(data) {
|
|
7439
7477
|
return {
|
|
7440
7478
|
content: [
|
|
@@ -12299,6 +12337,7 @@ import {
|
|
|
12299
12337
|
codeMapPath as codeMapPath2,
|
|
12300
12338
|
findProjectRoot as findProjectRoot42,
|
|
12301
12339
|
getUsage as getUsage19,
|
|
12340
|
+
isStackPackSeed as isStackPackSeed4,
|
|
12302
12341
|
loadCodeMap as loadCodeMap7,
|
|
12303
12342
|
loadConfig as loadConfig11,
|
|
12304
12343
|
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
@@ -12410,16 +12449,25 @@ function registerDoctor(program2) {
|
|
|
12410
12449
|
fix: "haive memory approve <id> # activate\nhaive memory rm <id> # or delete if obsolete"
|
|
12411
12450
|
});
|
|
12412
12451
|
}
|
|
12413
|
-
const
|
|
12452
|
+
const policyMemories = memories.filter((m) => !isStackPackSeed4(m.memory.frontmatter));
|
|
12453
|
+
const anchorless = policyMemories.filter(
|
|
12414
12454
|
(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"
|
|
12415
12455
|
);
|
|
12416
|
-
|
|
12456
|
+
const stackSeeds = memories.filter((m) => isStackPackSeed4(m.memory.frontmatter));
|
|
12457
|
+
if (anchorless.length / Math.max(policyMemories.length, 1) > 0.3) {
|
|
12417
12458
|
findings.push({
|
|
12418
12459
|
severity: "warn",
|
|
12419
12460
|
code: "anchorless-majority",
|
|
12420
|
-
message: `${anchorless.length}/${
|
|
12461
|
+
message: `${anchorless.length}/${policyMemories.length} repo-specific memories have no anchor path/symbol \u2014 staleness undetectable.`,
|
|
12421
12462
|
fix: "Add `paths:` + `symbols:` to mem_save calls to enable haive memory verify."
|
|
12422
12463
|
});
|
|
12464
|
+
} else if (stackSeeds.length > 0 && policyMemories.length === 0) {
|
|
12465
|
+
findings.push({
|
|
12466
|
+
severity: "info",
|
|
12467
|
+
code: "stack-pack-seeds",
|
|
12468
|
+
message: `${stackSeeds.length} starter stack memor${stackSeeds.length === 1 ? "y is" : "ies are"} present as generic background guidance.`,
|
|
12469
|
+
fix: "Replace or anchor stack-pack seeds when they become repo-specific policy."
|
|
12470
|
+
});
|
|
12423
12471
|
}
|
|
12424
12472
|
const decayCandidates = memories.filter((m) => {
|
|
12425
12473
|
if (m.memory.frontmatter.status !== "validated") return false;
|
|
@@ -12535,14 +12583,14 @@ function registerDoctor(program2) {
|
|
|
12535
12583
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
12536
12584
|
});
|
|
12537
12585
|
}
|
|
12538
|
-
findings.push(...await collectInstallFindings(root, "0.10.
|
|
12586
|
+
findings.push(...await collectInstallFindings(root, "0.10.9"));
|
|
12539
12587
|
try {
|
|
12540
12588
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
12541
12589
|
encoding: "utf8",
|
|
12542
12590
|
timeout: 3e3,
|
|
12543
12591
|
stdio: ["ignore", "pipe", "ignore"]
|
|
12544
12592
|
}).trim();
|
|
12545
|
-
const cliVersion = "0.10.
|
|
12593
|
+
const cliVersion = "0.10.9";
|
|
12546
12594
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
12547
12595
|
findings.push({
|
|
12548
12596
|
severity: "warn",
|
|
@@ -13602,6 +13650,13 @@ function registerEnforce(program2) {
|
|
|
13602
13650
|
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
13603
13651
|
if (report.should_block) process.exit(2);
|
|
13604
13652
|
});
|
|
13653
|
+
enforce.command("finish").alias("completion").description(
|
|
13654
|
+
"Final agent-exit gate: verify the git sync/release protocol before reporting a task done."
|
|
13655
|
+
).option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
|
|
13656
|
+
const report = await buildFinishReport(opts.dir);
|
|
13657
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
13658
|
+
if (report.should_block) process.exit(2);
|
|
13659
|
+
});
|
|
13605
13660
|
enforce.command("session-start").description("Claude Code SessionStart hook: inject briefing and write a local briefing marker.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text to rank memories").option("--source <name>", "marker source", "claude-session-start").option("--session-id <id>", "agent session id").action(async (opts) => {
|
|
13606
13661
|
const payload = await readHookPayload();
|
|
13607
13662
|
const root = resolveRoot(opts.dir, payload);
|
|
@@ -13711,6 +13766,202 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
13711
13766
|
process.exit(2);
|
|
13712
13767
|
});
|
|
13713
13768
|
}
|
|
13769
|
+
async function buildFinishReport(dir) {
|
|
13770
|
+
const root = findProjectRoot49(dir);
|
|
13771
|
+
const paths = resolveHaivePaths45(root);
|
|
13772
|
+
const initialized = existsSync69(paths.haiveDir);
|
|
13773
|
+
const config = initialized ? await loadConfig13(paths) : {};
|
|
13774
|
+
const mode = config.enforcement?.mode ?? "strict";
|
|
13775
|
+
const findings = [];
|
|
13776
|
+
if (!initialized) {
|
|
13777
|
+
return withCategories({
|
|
13778
|
+
root,
|
|
13779
|
+
initialized,
|
|
13780
|
+
mode,
|
|
13781
|
+
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
13782
|
+
should_block: true,
|
|
13783
|
+
findings: [{
|
|
13784
|
+
severity: "error",
|
|
13785
|
+
code: "not-initialized",
|
|
13786
|
+
message: "This repository is not initialized with hAIve.",
|
|
13787
|
+
fix: "Run `haive init` or `haive enforce install`.",
|
|
13788
|
+
impact: 100
|
|
13789
|
+
}]
|
|
13790
|
+
});
|
|
13791
|
+
}
|
|
13792
|
+
const status = await getGitSyncStatus(root);
|
|
13793
|
+
if (!status.available) {
|
|
13794
|
+
findings.push({
|
|
13795
|
+
severity: "error",
|
|
13796
|
+
code: "git-unavailable",
|
|
13797
|
+
message: "Git status could not be inspected, so hAIve cannot verify the exit protocol.",
|
|
13798
|
+
fix: "Run `git status` manually, then commit/push according to the hAIve git-sync protocol.",
|
|
13799
|
+
impact: 100
|
|
13800
|
+
});
|
|
13801
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13802
|
+
}
|
|
13803
|
+
const shippableDirty = status.dirtyFiles.filter(isShippablePath);
|
|
13804
|
+
if (status.dirtyFiles.length > 0) {
|
|
13805
|
+
findings.push({
|
|
13806
|
+
severity: "error",
|
|
13807
|
+
code: shippableDirty.length > 0 ? "git-sync-uncommitted-shippable" : "git-sync-uncommitted-changes",
|
|
13808
|
+
message: shippableDirty.length > 0 ? `${shippableDirty.length} shippable file(s) are modified but not committed.` : `${status.dirtyFiles.length} file(s) are modified but not committed.`,
|
|
13809
|
+
fix: shippableDirty.length > 0 ? "Bump the lockstep package version if needed, then `git add`, `git commit`, `git tag vX.Y.Z`, `git push && git push --tags`." : "Commit and push these changes before reporting the task done.",
|
|
13810
|
+
reason: "The multi-agent git-sync decision requires agents to leave completed work committed and pushed, not as a local diff.",
|
|
13811
|
+
affected_files: status.dirtyFiles.slice(0, 12),
|
|
13812
|
+
impact: 100
|
|
13813
|
+
});
|
|
13814
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13815
|
+
}
|
|
13816
|
+
findings.push({
|
|
13817
|
+
severity: "ok",
|
|
13818
|
+
code: "git-worktree-clean",
|
|
13819
|
+
message: "No uncommitted worktree changes remain."
|
|
13820
|
+
});
|
|
13821
|
+
if (!status.upstream) {
|
|
13822
|
+
findings.push({
|
|
13823
|
+
severity: "warn",
|
|
13824
|
+
code: "git-sync-no-upstream",
|
|
13825
|
+
message: "This branch has no upstream, so hAIve cannot verify that commits/tags were pushed.",
|
|
13826
|
+
fix: "Set an upstream with `git push -u origin <branch>`.",
|
|
13827
|
+
impact: 15
|
|
13828
|
+
});
|
|
13829
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13830
|
+
}
|
|
13831
|
+
if (status.behind > 0) {
|
|
13832
|
+
findings.push({
|
|
13833
|
+
severity: "error",
|
|
13834
|
+
code: "git-sync-behind-upstream",
|
|
13835
|
+
message: `This branch is ${status.behind} commit(s) behind ${status.upstream}.`,
|
|
13836
|
+
fix: "Run `git pull --ff-only` and resolve any conflicts before finishing.",
|
|
13837
|
+
impact: 40
|
|
13838
|
+
});
|
|
13839
|
+
}
|
|
13840
|
+
if (status.ahead > 0) {
|
|
13841
|
+
findings.push({
|
|
13842
|
+
severity: "error",
|
|
13843
|
+
code: "git-sync-unpushed-commits",
|
|
13844
|
+
message: `This branch is ${status.ahead} commit(s) ahead of ${status.upstream}.`,
|
|
13845
|
+
fix: "Run `git push` before reporting the task done.",
|
|
13846
|
+
reason: "The multi-agent git-sync decision requires agents to push completed commits.",
|
|
13847
|
+
impact: 60
|
|
13848
|
+
});
|
|
13849
|
+
} else {
|
|
13850
|
+
findings.push({
|
|
13851
|
+
severity: "ok",
|
|
13852
|
+
code: "git-sync-pushed",
|
|
13853
|
+
message: `Branch is not ahead of ${status.upstream}.`
|
|
13854
|
+
});
|
|
13855
|
+
}
|
|
13856
|
+
const releaseChangedFiles = status.releaseChangedFiles ?? status.changedSinceUpstream;
|
|
13857
|
+
const releaseBaseRef = status.releaseBaseRef ?? status.upstream;
|
|
13858
|
+
const shippableChanged = releaseChangedFiles.filter(isShippablePath);
|
|
13859
|
+
if (shippableChanged.length === 0) {
|
|
13860
|
+
findings.push({
|
|
13861
|
+
severity: "ok",
|
|
13862
|
+
code: "release-version-not-required",
|
|
13863
|
+
message: "No shippable package code changed since upstream; no version/tag required."
|
|
13864
|
+
});
|
|
13865
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13866
|
+
}
|
|
13867
|
+
findings.push({
|
|
13868
|
+
severity: "info",
|
|
13869
|
+
code: "release-shippable-changes",
|
|
13870
|
+
message: `${shippableChanged.length} shippable file(s) changed since ${releaseBaseRef}.`,
|
|
13871
|
+
affected_files: shippableChanged.slice(0, 12)
|
|
13872
|
+
});
|
|
13873
|
+
const versionState = await inspectReleaseVersionState(root, releaseBaseRef);
|
|
13874
|
+
if (!versionState.lockstep) {
|
|
13875
|
+
findings.push({
|
|
13876
|
+
severity: "error",
|
|
13877
|
+
code: "release-version-not-lockstep",
|
|
13878
|
+
message: `Publishable package versions are not in lockstep: ${versionState.localVersionsLabel}.`,
|
|
13879
|
+
fix: "Set root, core, cli, mcp, and embeddings package.json versions to the same X.Y.Z.",
|
|
13880
|
+
impact: 60
|
|
13881
|
+
});
|
|
13882
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13883
|
+
}
|
|
13884
|
+
const version = versionState.version;
|
|
13885
|
+
if (!version) {
|
|
13886
|
+
findings.push({
|
|
13887
|
+
severity: "error",
|
|
13888
|
+
code: "release-version-unreadable",
|
|
13889
|
+
message: "Could not read the lockstep package version.",
|
|
13890
|
+
fix: "Verify package.json files are valid JSON.",
|
|
13891
|
+
impact: 60
|
|
13892
|
+
});
|
|
13893
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13894
|
+
}
|
|
13895
|
+
if (versionState.baseVersion && compareSemver(version, versionState.baseVersion) <= 0) {
|
|
13896
|
+
findings.push({
|
|
13897
|
+
severity: "error",
|
|
13898
|
+
code: "release-version-missing",
|
|
13899
|
+
message: `Shippable code changed, but version stayed at ${version} (base: ${versionState.baseVersion}).`,
|
|
13900
|
+
fix: "Bump the lockstep package version (patch by default), commit the bump, tag it, then push code and tags.",
|
|
13901
|
+
impact: 70
|
|
13902
|
+
});
|
|
13903
|
+
} else {
|
|
13904
|
+
findings.push({
|
|
13905
|
+
severity: "ok",
|
|
13906
|
+
code: "release-version-bumped",
|
|
13907
|
+
message: versionState.baseVersion ? `Lockstep version bumped from ${versionState.baseVersion} to ${version}.` : `Lockstep version is ${version}.`
|
|
13908
|
+
});
|
|
13909
|
+
}
|
|
13910
|
+
const tag = `v${version}`;
|
|
13911
|
+
const localTagAtHead = await tagPointsAtHead(root, tag);
|
|
13912
|
+
if (!localTagAtHead) {
|
|
13913
|
+
findings.push({
|
|
13914
|
+
severity: "error",
|
|
13915
|
+
code: "release-tag-missing",
|
|
13916
|
+
message: `Expected git tag ${tag} to point at HEAD.`,
|
|
13917
|
+
fix: `Run \`git tag ${tag}\` after committing the version bump.`,
|
|
13918
|
+
impact: 50
|
|
13919
|
+
});
|
|
13920
|
+
} else {
|
|
13921
|
+
findings.push({
|
|
13922
|
+
severity: "ok",
|
|
13923
|
+
code: "release-tag-present",
|
|
13924
|
+
message: `Tag ${tag} points at HEAD.`
|
|
13925
|
+
});
|
|
13926
|
+
}
|
|
13927
|
+
const remoteTag = await remoteTagExists(root, tag);
|
|
13928
|
+
if (remoteTag === false) {
|
|
13929
|
+
findings.push({
|
|
13930
|
+
severity: "error",
|
|
13931
|
+
code: "release-tag-unpushed",
|
|
13932
|
+
message: `Tag ${tag} is not present on the remote.`,
|
|
13933
|
+
fix: "Run `git push --tags`.",
|
|
13934
|
+
impact: 50
|
|
13935
|
+
});
|
|
13936
|
+
} else if (remoteTag === true) {
|
|
13937
|
+
findings.push({
|
|
13938
|
+
severity: "ok",
|
|
13939
|
+
code: "release-tag-pushed",
|
|
13940
|
+
message: `Tag ${tag} exists on the remote.`
|
|
13941
|
+
});
|
|
13942
|
+
} else {
|
|
13943
|
+
findings.push({
|
|
13944
|
+
severity: "warn",
|
|
13945
|
+
code: "release-tag-remote-unverified",
|
|
13946
|
+
message: `Could not verify whether tag ${tag} exists on the remote.`,
|
|
13947
|
+
fix: "Run `git push --tags` if you have not already.",
|
|
13948
|
+
impact: 10
|
|
13949
|
+
});
|
|
13950
|
+
}
|
|
13951
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13952
|
+
}
|
|
13953
|
+
function finishReport(root, initialized, mode, findings, config) {
|
|
13954
|
+
const score = buildScore(findings, config.enforcement?.scoreThreshold);
|
|
13955
|
+
const hasErrors = findings.some((f) => f.severity === "error");
|
|
13956
|
+
return withCategories({
|
|
13957
|
+
root,
|
|
13958
|
+
initialized,
|
|
13959
|
+
mode,
|
|
13960
|
+
score,
|
|
13961
|
+
should_block: mode === "strict" && hasErrors,
|
|
13962
|
+
findings
|
|
13963
|
+
});
|
|
13964
|
+
}
|
|
13714
13965
|
async function runWithEnforcement(command, args, opts) {
|
|
13715
13966
|
const root = findProjectRoot49(opts.dir);
|
|
13716
13967
|
const paths = resolveHaivePaths45(root);
|
|
@@ -13840,7 +14091,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13840
14091
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
13841
14092
|
});
|
|
13842
14093
|
}
|
|
13843
|
-
findings.push(...await inspectIntegrationVersions(root, "0.10.
|
|
14094
|
+
findings.push(...await inspectIntegrationVersions(root, "0.10.9"));
|
|
13844
14095
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
13845
14096
|
const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
|
|
13846
14097
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -13874,7 +14125,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13874
14125
|
findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
|
|
13875
14126
|
}
|
|
13876
14127
|
if (stage === "pre-commit" || stage === "ci") {
|
|
13877
|
-
findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored"));
|
|
14128
|
+
findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored", stage));
|
|
13878
14129
|
}
|
|
13879
14130
|
if (config.enforcement?.cleanupGeneratedArtifacts !== false) {
|
|
13880
14131
|
findings.push(...await findGeneratedArtifacts(paths));
|
|
@@ -13998,19 +14249,20 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
13998
14249
|
impact: Math.min(35, 10 + missing.length * 5)
|
|
13999
14250
|
}];
|
|
14000
14251
|
}
|
|
14001
|
-
async function runPrecommitPolicy(paths, gate) {
|
|
14252
|
+
async function runPrecommitPolicy(paths, gate, stage) {
|
|
14002
14253
|
if (gate === "off") {
|
|
14003
14254
|
return [{ severity: "info", code: "precommit-policy-off", message: "Anti-pattern gate is disabled (enforcement.antiPatternGate=off)." }];
|
|
14004
14255
|
}
|
|
14005
|
-
const
|
|
14006
|
-
const touchedPaths =
|
|
14256
|
+
const snapshot = await getPolicyDiffSnapshot(paths.root, stage);
|
|
14257
|
+
const touchedPaths = snapshot.paths;
|
|
14007
14258
|
if (touchedPaths.length === 0) {
|
|
14008
|
-
|
|
14259
|
+
const code = stage === "ci" ? "no-ci-diff-changes" : "no-staged-changes";
|
|
14260
|
+
const message = stage === "ci" ? "No changed files found for CI policy diff." : "No staged changes found for pre-commit policy.";
|
|
14261
|
+
return [{ severity: "info", code, message }];
|
|
14009
14262
|
}
|
|
14010
|
-
const diff = await runCommand4("git", ["diff", "--cached"], paths.root).catch(() => "");
|
|
14011
14263
|
const { block_on, anchored_blocks } = antiPatternGateParams2(gate);
|
|
14012
14264
|
const result = await preCommitCheck({
|
|
14013
|
-
diff,
|
|
14265
|
+
diff: snapshot.diff,
|
|
14014
14266
|
paths: touchedPaths,
|
|
14015
14267
|
block_on,
|
|
14016
14268
|
anchored_blocks,
|
|
@@ -14020,7 +14272,7 @@ async function runPrecommitPolicy(paths, gate) {
|
|
|
14020
14272
|
return [{
|
|
14021
14273
|
severity: "ok",
|
|
14022
14274
|
code: "precommit-policy-pass",
|
|
14023
|
-
message:
|
|
14275
|
+
message: `${stage === "ci" ? "CI" : "Pre-commit"} policy passed for ${touchedPaths.length} changed file(s).`
|
|
14024
14276
|
}];
|
|
14025
14277
|
}
|
|
14026
14278
|
return [{
|
|
@@ -14171,22 +14423,217 @@ function versionForBinary2(bin) {
|
|
|
14171
14423
|
}
|
|
14172
14424
|
}
|
|
14173
14425
|
async function getChangedFiles(root, stage) {
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14426
|
+
if (stage === "ci") {
|
|
14427
|
+
return (await getPolicyDiffSnapshot(root, "ci")).paths;
|
|
14428
|
+
}
|
|
14429
|
+
if (stage === "pre-commit") {
|
|
14430
|
+
return normalizeChangedFileList(
|
|
14431
|
+
await runCommand4("git", ["diff", "--cached", "--name-only"], root).catch(() => "")
|
|
14432
|
+
);
|
|
14433
|
+
}
|
|
14178
14434
|
const files = /* @__PURE__ */ new Set();
|
|
14179
|
-
for (const args of
|
|
14180
|
-
const
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14435
|
+
for (const args of [["diff", "--cached", "--name-only"], ["diff", "--name-only"]]) {
|
|
14436
|
+
for (const file of normalizeChangedFileList(await runCommand4("git", args, root).catch(() => ""))) {
|
|
14437
|
+
files.add(file);
|
|
14438
|
+
}
|
|
14439
|
+
}
|
|
14440
|
+
return [...files];
|
|
14441
|
+
}
|
|
14442
|
+
async function getPolicyDiffSnapshot(root, stage) {
|
|
14443
|
+
if (stage === "pre-commit") {
|
|
14444
|
+
const diff = await runCommand4("git", ["diff", "--cached"], root).catch(() => "");
|
|
14445
|
+
const names = await runCommand4("git", ["diff", "--cached", "--name-only"], root).catch(() => "");
|
|
14446
|
+
return { diff, paths: normalizeChangedFileList(names), source: "staged" };
|
|
14447
|
+
}
|
|
14448
|
+
const range = await resolveCiDiffRange(root);
|
|
14449
|
+
if (range) {
|
|
14450
|
+
const diff = await runCommand4("git", ["diff", range], root).catch(() => "");
|
|
14451
|
+
const names = await runCommand4("git", ["diff", "--name-only", range], root).catch(() => "");
|
|
14452
|
+
return { diff, paths: normalizeChangedFileList(names), source: range };
|
|
14453
|
+
}
|
|
14454
|
+
return { diff: "", paths: [], source: "none" };
|
|
14455
|
+
}
|
|
14456
|
+
async function resolveCiDiffRange(root) {
|
|
14457
|
+
const explicitBase = cleanGitSha(process.env.HAIVE_BASE_SHA ?? process.env.HAIVE_BASE_REF);
|
|
14458
|
+
const explicitHead = cleanGitSha(process.env.HAIVE_HEAD_SHA ?? process.env.GITHUB_SHA) ?? "HEAD";
|
|
14459
|
+
if (explicitBase && await gitCommitExists(root, explicitBase)) {
|
|
14460
|
+
return `${explicitBase}...${explicitHead}`;
|
|
14461
|
+
}
|
|
14462
|
+
const eventRange = await resolveGithubEventRange(root);
|
|
14463
|
+
if (eventRange) return eventRange;
|
|
14464
|
+
const baseRef = process.env.GITHUB_BASE_REF?.trim();
|
|
14465
|
+
if (baseRef) {
|
|
14466
|
+
const remoteRef = `origin/${baseRef}`;
|
|
14467
|
+
if (await gitCommitExists(root, remoteRef)) return `${remoteRef}...${explicitHead}`;
|
|
14468
|
+
}
|
|
14469
|
+
if (await gitCommitExists(root, "origin/main")) return `origin/main...${explicitHead}`;
|
|
14470
|
+
if (await gitCommitExists(root, "origin/master")) return `origin/master...${explicitHead}`;
|
|
14471
|
+
if (await gitCommitExists(root, "HEAD^")) return `HEAD^..${explicitHead}`;
|
|
14472
|
+
return null;
|
|
14473
|
+
}
|
|
14474
|
+
async function resolveGithubEventRange(root) {
|
|
14475
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
14476
|
+
if (!eventPath || !existsSync69(eventPath)) return null;
|
|
14477
|
+
try {
|
|
14478
|
+
const event = JSON.parse(await readFile21(eventPath, "utf8"));
|
|
14479
|
+
const prBase = cleanGitSha(event.pull_request?.base?.sha);
|
|
14480
|
+
const prHead = cleanGitSha(event.pull_request?.head?.sha ?? event.after ?? process.env.GITHUB_SHA) ?? "HEAD";
|
|
14481
|
+
if (prBase && await gitCommitExists(root, prBase)) return `${prBase}...${prHead}`;
|
|
14482
|
+
const pushBase = cleanGitSha(event.before);
|
|
14483
|
+
const pushHead = cleanGitSha(event.after ?? process.env.GITHUB_SHA) ?? "HEAD";
|
|
14484
|
+
if (pushBase && await gitCommitExists(root, pushBase)) return `${pushBase}..${pushHead}`;
|
|
14485
|
+
} catch {
|
|
14486
|
+
return null;
|
|
14487
|
+
}
|
|
14488
|
+
return null;
|
|
14489
|
+
}
|
|
14490
|
+
function cleanGitSha(value) {
|
|
14491
|
+
const trimmed = value?.trim();
|
|
14492
|
+
if (!trimmed || /^0+$/.test(trimmed)) return null;
|
|
14493
|
+
return trimmed;
|
|
14494
|
+
}
|
|
14495
|
+
async function gitCommitExists(root, ref) {
|
|
14496
|
+
try {
|
|
14497
|
+
await runCommand4("git", ["rev-parse", "--verify", `${ref}^{commit}`], root);
|
|
14498
|
+
return true;
|
|
14499
|
+
} catch {
|
|
14500
|
+
return false;
|
|
14185
14501
|
}
|
|
14186
|
-
|
|
14502
|
+
}
|
|
14503
|
+
function normalizeChangedFileList(raw) {
|
|
14504
|
+
return raw.split("\n").map((s) => s.trim()).filter(Boolean).filter(
|
|
14187
14505
|
(file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/") && !file.startsWith(".ai/.usage/") && file !== ".ai/.usage/tool-usage.jsonl"
|
|
14188
14506
|
);
|
|
14189
14507
|
}
|
|
14508
|
+
async function getGitSyncStatus(root) {
|
|
14509
|
+
const dirty = (await runCommand4("git", ["status", "--short", "--untracked-files=all"], root).catch(() => "")).split("\n").map((line) => statusLineToPath(line.trim())).filter(Boolean).filter((file) => normalizeChangedFileList(file).length > 0);
|
|
14510
|
+
const branch = (await runCommand4("git", ["branch", "--show-current"], root).catch(() => "")).trim() || void 0;
|
|
14511
|
+
const upstream = (await runCommand4("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root).catch(() => "")).trim() || void 0;
|
|
14512
|
+
if (!branch && !upstream) {
|
|
14513
|
+
const inside = (await runCommand4("git", ["rev-parse", "--is-inside-work-tree"], root).catch(() => "")).trim();
|
|
14514
|
+
if (inside !== "true") return { available: false, ahead: 0, behind: 0, dirtyFiles: [], changedSinceUpstream: [] };
|
|
14515
|
+
}
|
|
14516
|
+
let ahead = 0;
|
|
14517
|
+
let behind = 0;
|
|
14518
|
+
let changedSinceUpstream = [];
|
|
14519
|
+
let releaseBaseRef;
|
|
14520
|
+
let releaseChangedFiles;
|
|
14521
|
+
if (upstream) {
|
|
14522
|
+
const counts = (await runCommand4("git", ["rev-list", "--left-right", "--count", `${upstream}...HEAD`], root).catch(() => "")).trim();
|
|
14523
|
+
const [behindRaw, aheadRaw] = counts.split(/\s+/);
|
|
14524
|
+
behind = Number.parseInt(behindRaw ?? "0", 10) || 0;
|
|
14525
|
+
ahead = Number.parseInt(aheadRaw ?? "0", 10) || 0;
|
|
14526
|
+
changedSinceUpstream = normalizeChangedFileList(
|
|
14527
|
+
await runCommand4("git", ["diff", "--name-only", `${upstream}...HEAD`], root).catch(() => "")
|
|
14528
|
+
);
|
|
14529
|
+
if (changedSinceUpstream.length > 0) {
|
|
14530
|
+
releaseBaseRef = upstream;
|
|
14531
|
+
releaseChangedFiles = changedSinceUpstream;
|
|
14532
|
+
}
|
|
14533
|
+
}
|
|
14534
|
+
if (!releaseChangedFiles || releaseChangedFiles.length === 0) {
|
|
14535
|
+
const hasParent = (await runCommand4("git", ["rev-parse", "--verify", "--quiet", "HEAD^"], root).catch(() => "")).trim().length > 0;
|
|
14536
|
+
if (hasParent) {
|
|
14537
|
+
const changedSinceParent = normalizeChangedFileList(
|
|
14538
|
+
await runCommand4("git", ["diff", "--name-only", "HEAD^..HEAD"], root).catch(() => "")
|
|
14539
|
+
);
|
|
14540
|
+
if (changedSinceParent.length > 0) {
|
|
14541
|
+
releaseBaseRef = "HEAD^";
|
|
14542
|
+
releaseChangedFiles = changedSinceParent;
|
|
14543
|
+
}
|
|
14544
|
+
}
|
|
14545
|
+
}
|
|
14546
|
+
return {
|
|
14547
|
+
available: true,
|
|
14548
|
+
branch,
|
|
14549
|
+
upstream,
|
|
14550
|
+
ahead,
|
|
14551
|
+
behind,
|
|
14552
|
+
dirtyFiles: dirty,
|
|
14553
|
+
changedSinceUpstream,
|
|
14554
|
+
...releaseBaseRef ? { releaseBaseRef } : {},
|
|
14555
|
+
...releaseChangedFiles ? { releaseChangedFiles } : {}
|
|
14556
|
+
};
|
|
14557
|
+
}
|
|
14558
|
+
function statusLineToPath(line) {
|
|
14559
|
+
const body = line.replace(/^[ MADRCU?!]{1,2}\s+/, "").trim();
|
|
14560
|
+
const renamed = body.match(/.+ -> (.+)$/);
|
|
14561
|
+
return renamed?.[1]?.trim() ?? body;
|
|
14562
|
+
}
|
|
14563
|
+
var VERSION_FILES = [
|
|
14564
|
+
"package.json",
|
|
14565
|
+
"packages/core/package.json",
|
|
14566
|
+
"packages/cli/package.json",
|
|
14567
|
+
"packages/mcp/package.json",
|
|
14568
|
+
"packages/embeddings/package.json"
|
|
14569
|
+
];
|
|
14570
|
+
var SHIPPABLE_PATH_PREFIXES = [
|
|
14571
|
+
"packages/core/src/",
|
|
14572
|
+
"packages/cli/src/",
|
|
14573
|
+
"packages/mcp/src/",
|
|
14574
|
+
"packages/embeddings/src/"
|
|
14575
|
+
];
|
|
14576
|
+
function isShippablePath(file) {
|
|
14577
|
+
return SHIPPABLE_PATH_PREFIXES.some((prefix) => file.startsWith(prefix)) || VERSION_FILES.includes(file);
|
|
14578
|
+
}
|
|
14579
|
+
async function inspectReleaseVersionState(root, upstream) {
|
|
14580
|
+
const localEntries = await Promise.all(VERSION_FILES.map(async (file) => [file, await readPackageVersion(root, file)]));
|
|
14581
|
+
const localVersions = new Map(localEntries);
|
|
14582
|
+
const unique = new Set([...localVersions.values()].filter(Boolean));
|
|
14583
|
+
const version = unique.size === 1 ? [...unique][0] : void 0;
|
|
14584
|
+
const localVersionsLabel = VERSION_FILES.map((file) => `${file}=${localVersions.get(file) ?? "unreadable"}`).join(", ");
|
|
14585
|
+
const baseVersion = await readPackageVersionAtRef(root, upstream, "package.json");
|
|
14586
|
+
return {
|
|
14587
|
+
lockstep: unique.size === 1 && localVersions.size === VERSION_FILES.length,
|
|
14588
|
+
...version ? { version } : {},
|
|
14589
|
+
...baseVersion ? { baseVersion } : {},
|
|
14590
|
+
localVersionsLabel
|
|
14591
|
+
};
|
|
14592
|
+
}
|
|
14593
|
+
async function readPackageVersion(root, relPath) {
|
|
14594
|
+
try {
|
|
14595
|
+
const data = JSON.parse(await readFile21(path50.join(root, relPath), "utf8"));
|
|
14596
|
+
return typeof data.version === "string" ? data.version : void 0;
|
|
14597
|
+
} catch {
|
|
14598
|
+
return void 0;
|
|
14599
|
+
}
|
|
14600
|
+
}
|
|
14601
|
+
async function readPackageVersionAtRef(root, ref, relPath) {
|
|
14602
|
+
try {
|
|
14603
|
+
const raw = await runCommand4("git", ["show", `${ref}:${relPath}`], root);
|
|
14604
|
+
const data = JSON.parse(raw);
|
|
14605
|
+
return typeof data.version === "string" ? data.version : void 0;
|
|
14606
|
+
} catch {
|
|
14607
|
+
return void 0;
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
function compareSemver(a, b) {
|
|
14611
|
+
const pa = a.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
14612
|
+
const pb = b.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
14613
|
+
const len = Math.max(pa.length, pb.length, 3);
|
|
14614
|
+
for (let i = 0; i < len; i++) {
|
|
14615
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
14616
|
+
if (diff !== 0) return diff;
|
|
14617
|
+
}
|
|
14618
|
+
return 0;
|
|
14619
|
+
}
|
|
14620
|
+
async function tagPointsAtHead(root, tag) {
|
|
14621
|
+
const tags = await runCommand4("git", ["tag", "--points-at", "HEAD"], root).catch(() => "");
|
|
14622
|
+
return tags.split("\n").map((line) => line.trim()).includes(tag);
|
|
14623
|
+
}
|
|
14624
|
+
async function remoteTagExists(root, tag) {
|
|
14625
|
+
const branch = (await runCommand4("git", ["branch", "--show-current"], root).catch(() => "")).trim();
|
|
14626
|
+
const branchRemote = branch ? (await runCommand4("git", ["config", "--get", `branch.${branch}.remote`], root).catch(() => "")).trim() : "";
|
|
14627
|
+
const hasOrigin = (await runCommand4("git", ["config", "--get", "remote.origin.url"], root).catch(() => "")).trim().length > 0;
|
|
14628
|
+
const remote = branchRemote || (hasOrigin ? "origin" : "");
|
|
14629
|
+
if (!remote) return null;
|
|
14630
|
+
try {
|
|
14631
|
+
const out = await runCommand4("git", ["ls-remote", "--tags", remote, `refs/tags/${tag}`], root);
|
|
14632
|
+
return out.trim().length > 0;
|
|
14633
|
+
} catch {
|
|
14634
|
+
return null;
|
|
14635
|
+
}
|
|
14636
|
+
}
|
|
14190
14637
|
function buildScore(findings, threshold = 80) {
|
|
14191
14638
|
const checks = {
|
|
14192
14639
|
total: findings.length,
|
|
@@ -14275,6 +14722,9 @@ jobs:
|
|
|
14275
14722
|
- name: Install hAIve
|
|
14276
14723
|
run: npm install -g @hiveai/cli
|
|
14277
14724
|
- name: Enforce hAIve policy
|
|
14725
|
+
env:
|
|
14726
|
+
HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
|
|
14727
|
+
HAIVE_HEAD_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
|
|
14278
14728
|
run: haive enforce ci
|
|
14279
14729
|
`, "utf8");
|
|
14280
14730
|
ui.success(`Created ${path50.relative(root, workflowPath)}`);
|
|
@@ -14640,7 +15090,7 @@ function shellQuote(value) {
|
|
|
14640
15090
|
|
|
14641
15091
|
// src/index.ts
|
|
14642
15092
|
var program = new Command53();
|
|
14643
|
-
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.
|
|
15093
|
+
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.9").option("--advanced", "show maintenance and experimental commands in help");
|
|
14644
15094
|
registerInit(program);
|
|
14645
15095
|
registerWelcome(program);
|
|
14646
15096
|
registerResolveProject(program);
|