@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/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@main
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((p) => !p.startsWith(".ai/.usage/"));
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 firstPath = paths[0];
6904
- return firstPath ? `haive briefing --files "${firstPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
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
- When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
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.8";
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 anchorless = memories.filter(
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
- if (anchorless.length / Math.max(memories.length, 1) > 0.3) {
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}/${memories.length} memories have no anchor path/symbol \u2014 staleness undetectable.`,
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.8"));
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.8";
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.8"));
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 staged = await runCommand4("git", ["diff", "--cached", "--name-only"], paths.root).catch(() => "");
14006
- const touchedPaths = staged.split("\n").map((s) => s.trim()).filter(Boolean);
14256
+ const snapshot = await getPolicyDiffSnapshot(paths.root, stage);
14257
+ const touchedPaths = snapshot.paths;
14007
14258
  if (touchedPaths.length === 0) {
14008
- return [{ severity: "info", code: "no-staged-changes", message: "No staged changes found for pre-commit policy." }];
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: `Pre-commit policy passed for ${touchedPaths.length} staged file(s).`
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
- const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
14175
- ["diff", "--cached", "--name-only"],
14176
- ["diff", "--name-only"]
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 commands) {
14180
- const out = await runCommand4("git", args, root).catch(() => "");
14181
- for (const line of out.split("\n")) {
14182
- const file = line.trim();
14183
- if (file) files.add(file);
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
- return [...files].filter(
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.8").option("--advanced", "show maintenance and experimental commands in help");
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);