@hiveai/cli 0.12.9 → 0.13.1

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
@@ -2789,6 +2789,111 @@ Use \`select_related\` (FK / one-to-one, SQL JOIN) and \`prefetch_related\` (M2M
2789
2789
  \`\`\`py
2790
2790
  for order in Order.objects.select_related("customer").all():
2791
2791
  order.customer.name # no extra query
2792
+ \`\`\``
2793
+ }
2794
+ ],
2795
+ flask: [
2796
+ {
2797
+ slug: "flask-no-debug-in-prod",
2798
+ type: "gotcha",
2799
+ tags: ["flask", "python", "security", "deployment"],
2800
+ body: `\`app.run(debug=True)\` enables the Werkzeug debugger \u2014 remote code execution if exposed.
2801
+
2802
+ Never ship debug mode. Run behind a real WSGI server (gunicorn/uwsgi) in production and
2803
+ drive debug from the environment for local dev only.`,
2804
+ sensor: {
2805
+ pattern: "app\\.run\\([^)]*debug\\s*=\\s*True",
2806
+ message: "Flask debug=True exposes the Werkzeug console (RCE) \u2014 never run it in production."
2807
+ }
2808
+ },
2809
+ {
2810
+ slug: "flask-secret-key-from-env",
2811
+ type: "convention",
2812
+ tags: ["flask", "python", "security"],
2813
+ body: `Load \`SECRET_KEY\` from the environment \u2014 never commit a literal.
2814
+
2815
+ \`\`\`py
2816
+ app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
2817
+ \`\`\`
2818
+ A committed key lets anyone forge sessions and CSRF tokens.`
2819
+ },
2820
+ {
2821
+ slug: "flask-no-sql-string-interpolation",
2822
+ type: "gotcha",
2823
+ tags: ["flask", "python", "security", "sql-injection"],
2824
+ body: `Never build SQL with f-strings/%-formatting \u2014 use parameterized queries.
2825
+
2826
+ \`\`\`py
2827
+ # \u274C SQL injection
2828
+ db.execute(f"SELECT * FROM users WHERE id = {uid}")
2829
+ # \u2705
2830
+ db.execute("SELECT * FROM users WHERE id = %s", (uid,))
2831
+ \`\`\``
2832
+ }
2833
+ ],
2834
+ vue: [
2835
+ {
2836
+ slug: "vue-v-html-xss",
2837
+ type: "gotcha",
2838
+ tags: ["vue", "security", "xss"],
2839
+ body: `\`v-html\` renders raw HTML and bypasses Vue's escaping \u2014 an XSS sink for user content.
2840
+
2841
+ Only use it on trusted/sanitized content. Prefer text interpolation ({{ }}) or sanitize
2842
+ with DOMPurify before binding.`,
2843
+ sensor: {
2844
+ pattern: "v-html",
2845
+ message: "v-html renders unescaped HTML (XSS risk) \u2014 sanitize the value or use text interpolation."
2846
+ }
2847
+ },
2848
+ {
2849
+ slug: "vue-key-in-v-for",
2850
+ type: "convention",
2851
+ tags: ["vue", "performance"],
2852
+ body: `Always bind a stable \`:key\` on \`v-for\` \u2014 and never the loop index.
2853
+
2854
+ Index keys corrupt component state on reorder/insert, exactly like React. Use a stable id.`
2855
+ },
2856
+ {
2857
+ slug: "vue-props-are-readonly",
2858
+ type: "gotcha",
2859
+ tags: ["vue", "reactivity"],
2860
+ body: `Never mutate a prop inside a child component \u2014 props are one-way (parent \u2192 child).
2861
+
2862
+ Mutating a prop breaks the data flow and warns in dev. Emit an event (\`update:modelValue\`)
2863
+ or copy the prop into local state, depending on intent.`
2864
+ }
2865
+ ],
2866
+ spring: [
2867
+ {
2868
+ slug: "spring-constructor-injection",
2869
+ type: "convention",
2870
+ tags: ["spring", "java", "di", "testing"],
2871
+ body: `Prefer constructor injection over \`@Autowired\` field injection.
2872
+
2873
+ Constructor injection makes dependencies explicit, allows \`final\` fields, and lets you
2874
+ instantiate the class in tests without a Spring context. Field injection hides dependencies
2875
+ and forces reflection-based test setup.`
2876
+ },
2877
+ {
2878
+ slug: "spring-no-cors-wildcard",
2879
+ type: "gotcha",
2880
+ tags: ["spring", "java", "security", "cors"],
2881
+ body: `\`@CrossOrigin(origins = "*")\` (or wildcard CORS config) allows any site to call your API.
2882
+
2883
+ Combined with credentials it leaks authenticated data cross-origin. Whitelist explicit origins.`,
2884
+ sensor: {
2885
+ pattern: "@CrossOrigin\\([^)]*\\*",
2886
+ message: 'Wildcard CORS (@CrossOrigin origins="*") lets any site call your API \u2014 whitelist explicit origins.'
2887
+ }
2888
+ },
2889
+ {
2890
+ slug: "spring-no-field-secrets",
2891
+ type: "convention",
2892
+ tags: ["spring", "java", "security", "config"],
2893
+ body: `Keep secrets in externalized config (env / vault / application.yml placeholders), not in source.
2894
+
2895
+ \`\`\`java
2896
+ @Value("\${app.api-key}") private String apiKey; // resolved from env, not hardcoded
2792
2897
  \`\`\``
2793
2898
  }
2794
2899
  ],
@@ -2907,7 +3012,7 @@ ${SEED_FOOTER(stack)}` });
2907
3012
  }
2908
3013
 
2909
3014
  // src/commands/init.ts
2910
- var HAIVE_GITHUB_ACTION_REF = `v${"0.12.9"}`;
3015
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.13.1"}`;
2911
3016
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
2912
3017
 
2913
3018
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
@@ -3071,6 +3176,27 @@ jobs:
3071
3176
  # post-if-empty: 'true' # uncomment to always post (even when no memories found)
3072
3177
  # max-memories: '5' # limit memories per file in the comment
3073
3178
 
3179
+ # On pull request: fail if the harness quality score regressed vs the committed baseline.
3180
+ # Measures whether the right memories still surface and the right sensors still fire.
3181
+ # No-op (passes) when no .ai/eval/baseline.json exists \u2014 safe to keep enabled before you
3182
+ # ever create one. To turn the gate on: run \`haive eval --baseline\` locally and commit
3183
+ # .ai/eval/baseline.json. Needs nothing external \u2014 no secrets, no services.
3184
+ pr-eval-gate:
3185
+ if: github.event_name == 'pull_request'
3186
+ runs-on: ubuntu-latest
3187
+ steps:
3188
+ - uses: actions/checkout@v4
3189
+
3190
+ - uses: actions/setup-node@v4
3191
+ with:
3192
+ node-version: '20'
3193
+
3194
+ - name: install haive
3195
+ run: npm install -g @hiveai/cli
3196
+
3197
+ - name: harness quality regression gate
3198
+ run: haive eval --regression-gate
3199
+
3074
3200
  # On push to main: push shared memories to the hub (if hubPath is configured)
3075
3201
  # Uncomment and configure hubPath in .ai/haive.config.json to enable.
3076
3202
  # hub-push:
@@ -7786,7 +7912,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7786
7912
  };
7787
7913
  }
7788
7914
  var SERVER_NAME = "haive";
7789
- var SERVER_VERSION = "0.12.9";
7915
+ var SERVER_VERSION = "0.13.1";
7790
7916
  function jsonResult(data) {
7791
7917
  return {
7792
7918
  content: [
@@ -12546,7 +12672,7 @@ import {
12546
12672
  function registerEval(program2) {
12547
12673
  program2.command("eval").description(
12548
12674
  "Rigorous, repeatable quality eval: do the right memories surface (retrieval) and do the right sensors fire (catch-rate)? Emits a chiffr\xE9 0\u2013100 score. Uses .ai/eval cases via --spec, or auto-synthesizes cases from anchored memories."
12549
- ).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
12675
+ ).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
12550
12676
  const root = findProjectRoot42(opts.dir);
12551
12677
  const paths = resolveHaivePaths38(root);
12552
12678
  if (!existsSync65(paths.memoriesDir)) {
@@ -12594,14 +12720,19 @@ function registerEval(program2) {
12594
12720
  if (!opts.json) ui.success(`Saved baseline (score ${report.score}/100) \u2192 ${path43.relative(root, baselineFile)}`);
12595
12721
  }
12596
12722
  let delta = null;
12597
- if (opts.compare) {
12723
+ if (opts.compare || opts.regressionGate) {
12598
12724
  if (!existsSync65(baselineFile)) {
12599
- ui.error(`No baseline at ${path43.relative(root, baselineFile)}. Run \`haive eval --baseline\` first.`);
12600
- process.exitCode = 1;
12601
- return;
12725
+ if (opts.regressionGate) {
12726
+ if (!opts.json) ui.info(`No baseline at ${path43.relative(root, baselineFile)} \u2014 regression gate skipped. Run \`haive eval --baseline\` to enable it.`);
12727
+ } else {
12728
+ ui.error(`No baseline at ${path43.relative(root, baselineFile)}. Run \`haive eval --baseline\` first.`);
12729
+ process.exitCode = 1;
12730
+ return;
12731
+ }
12732
+ } else {
12733
+ const snapshot = JSON.parse(await readFile20(baselineFile, "utf8"));
12734
+ delta = compareEvalReports(snapshot.report, report);
12602
12735
  }
12603
- const snapshot = JSON.parse(await readFile20(baselineFile, "utf8"));
12604
- delta = compareEvalReports(snapshot.report, report);
12605
12736
  }
12606
12737
  if (opts.json) {
12607
12738
  console.log(JSON.stringify({ root, k, spec_source: resolvedSpec.source, report, ...delta ? { delta } : {} }, null, 2));
@@ -12633,7 +12764,7 @@ function applyExitGates(opts, report, delta) {
12633
12764
  process.exitCode = 1;
12634
12765
  }
12635
12766
  }
12636
- if (opts.failOnRegression && delta?.regressed) {
12767
+ if ((opts.failOnRegression || opts.regressionGate) && delta?.regressed) {
12637
12768
  ui.error(`eval score regressed ${delta.score.baseline} \u2192 ${delta.score.current} (\u0394 ${delta.score.delta}) vs baseline`);
12638
12769
  process.exitCode = 1;
12639
12770
  }
@@ -13350,7 +13481,7 @@ function registerDoctor(program2) {
13350
13481
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
13351
13482
  });
13352
13483
  }
13353
- findings.push(...await collectInstallFindings(root, "0.12.9"));
13484
+ findings.push(...await collectInstallFindings(root, "0.13.1"));
13354
13485
  findings.push(...await collectToolchainFindings(root));
13355
13486
  try {
13356
13487
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -13358,7 +13489,7 @@ function registerDoctor(program2) {
13358
13489
  timeout: 3e3,
13359
13490
  stdio: ["ignore", "pipe", "ignore"]
13360
13491
  }).trim();
13361
- const cliVersion = "0.12.9";
13492
+ const cliVersion = "0.13.1";
13362
13493
  if (legacyRaw && legacyRaw !== cliVersion) {
13363
13494
  findings.push({
13364
13495
  severity: "warn",
@@ -14951,7 +15082,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
14951
15082
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
14952
15083
  });
14953
15084
  }
14954
- findings.push(...await inspectIntegrationVersions(root, "0.12.9"));
15085
+ findings.push(...await inspectIntegrationVersions(root, "0.13.1"));
14955
15086
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
14956
15087
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
14957
15088
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -16077,11 +16208,11 @@ import {
16077
16208
  var SEVERITIES = ["info", "minor", "major", "critical", "blocker"];
16078
16209
  function registerIngest(program2) {
16079
16210
  program2.command("ingest").description(
16080
- "Ingest scanner findings (SonarQube / SARIF) as proposed, anchored memories with sensors.\n\n Closes the review\u2194memory loop: a real defect a scanner found becomes a `gotcha`/`convention`\n memory anchored to the file, pre-filled with a conservative `warn` sensor, so the next agent\n is steered away from it. Drafts are status=proposed; a human validates/promotes them.\n\n Example:\n haive ingest --from sarif eslint.sarif --dry-run\n haive ingest --from sonar sonar-issues.json --scope team --min-severity major\n"
16081
- ).argument("<file>", "path to the findings report (JSON)").requiredOption("--from <format>", "report format: sarif | sonar").option("--dry-run", "show what would be created without writing", false).option("--scope <scope>", "memory scope: personal | team | module", "team").option("--module <name>", "module name (required when scope=module)").option("--type <type>", "memory type: gotcha | convention", "gotcha").option("--min-severity <severity>", "ignore findings below this severity (info|minor|major|critical|blocker)").option("--limit <n>", "cap the number of memories created").option("--author <author>", "author email or handle").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (file, opts) => {
16211
+ "Ingest scanner findings (SonarQube / SARIF) as proposed, anchored memories with sensors.\n\n Closes the review\u2194memory loop: a real defect a scanner found becomes a `gotcha`/`convention`\n memory anchored to the file, pre-filled with a conservative `warn` sensor, so the next agent\n is steered away from it. Drafts are status=proposed; a human validates/promotes them.\n\n `sonar-api` fetches issues live over plain HTTPS from any SonarQube/SonarCloud instance \u2014\n no MCP or special setup required, just a URL + token you provide (or SONAR_HOST_URL /\n SONAR_TOKEN env). If you don't use it, file-based ingest works exactly the same.\n\n Example:\n haive ingest --from sarif eslint.sarif --dry-run\n haive ingest --from sonar sonar-issues.json --scope team --min-severity major\n haive ingest --from sonar-api --sonar-component my_project --min-severity major\n"
16212
+ ).argument("[file]", "path to the findings report JSON (required for --from sarif|sonar)").requiredOption("--from <format>", "report format: sarif | sonar | sonar-api").option("--dry-run", "show what would be created without writing", false).option("--scope <scope>", "memory scope: personal | team | module", "team").option("--module <name>", "module name (required when scope=module)").option("--type <type>", "memory type: gotcha | convention", "gotcha").option("--min-severity <severity>", "ignore findings below this severity (info|minor|major|critical|blocker)").option("--limit <n>", "cap the number of memories created").option("--author <author>", "author email or handle").option("--json", "emit JSON", false).option("--sonar-url <url>", "SonarQube base URL for --from sonar-api (or env SONAR_HOST_URL)").option("--sonar-token <token>", "SonarQube token for --from sonar-api (or env SONAR_TOKEN)").option("--sonar-component <key>", "SonarQube project/component key for --from sonar-api").option("--sonar-branch <branch>", "optional SonarQube branch for --from sonar-api").option("-d, --dir <dir>", "project root").action(async (file, opts) => {
16082
16213
  const format = opts.from;
16083
- if (format !== "sarif" && format !== "sonar") {
16084
- ui.error("--from must be sarif or sonar");
16214
+ if (format !== "sarif" && format !== "sonar" && format !== "sonar-api") {
16215
+ ui.error("--from must be sarif, sonar, or sonar-api");
16085
16216
  process.exitCode = 1;
16086
16217
  return;
16087
16218
  }
@@ -16102,23 +16233,39 @@ function registerIngest(program2) {
16102
16233
  process.exitCode = 1;
16103
16234
  return;
16104
16235
  }
16105
- const reportPath = path54.resolve(root, file);
16106
- if (!existsSync77(reportPath)) {
16107
- ui.error(`Report file not found: ${reportPath}`);
16108
- process.exitCode = 1;
16109
- return;
16110
- }
16236
+ const parseFormat = format === "sarif" ? "sarif" : "sonar";
16111
16237
  let raw;
16112
- try {
16113
- raw = await readFile25(reportPath, "utf8");
16114
- } catch (err) {
16115
- ui.error(`Could not read ${reportPath}: ${err instanceof Error ? err.message : String(err)}`);
16116
- process.exitCode = 1;
16117
- return;
16238
+ if (format === "sonar-api") {
16239
+ const fetched = await fetchSonarIssues(opts);
16240
+ if (!fetched.ok) {
16241
+ ui.error(fetched.error);
16242
+ process.exitCode = 1;
16243
+ return;
16244
+ }
16245
+ raw = fetched.json;
16246
+ } else {
16247
+ if (!file) {
16248
+ ui.error(`--from ${format} needs a report file argument, e.g. \`haive ingest --from ${format} report.json\`.`);
16249
+ process.exitCode = 1;
16250
+ return;
16251
+ }
16252
+ const reportPath = path54.resolve(root, file);
16253
+ if (!existsSync77(reportPath)) {
16254
+ ui.error(`Report file not found: ${reportPath}`);
16255
+ process.exitCode = 1;
16256
+ return;
16257
+ }
16258
+ try {
16259
+ raw = await readFile25(reportPath, "utf8");
16260
+ } catch (err) {
16261
+ ui.error(`Could not read ${reportPath}: ${err instanceof Error ? err.message : String(err)}`);
16262
+ process.exitCode = 1;
16263
+ return;
16264
+ }
16118
16265
  }
16119
16266
  let drafts;
16120
16267
  try {
16121
- const findings = parseFindings2(format, raw);
16268
+ const findings = parseFindings2(parseFormat, raw);
16122
16269
  drafts = draftsFromFindings2(findings, {
16123
16270
  type: opts.type ?? "gotcha",
16124
16271
  scope: opts.scope ?? "team",
@@ -16201,6 +16348,42 @@ async function writeDraft2(paths, draft) {
16201
16348
  await writeFile37(file, serializeMemory28({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
16202
16349
  return file;
16203
16350
  }
16351
+ async function fetchSonarIssues(opts) {
16352
+ const baseUrl = (opts.sonarUrl ?? process.env.SONAR_HOST_URL ?? "").trim().replace(/\/+$/, "");
16353
+ const token = (opts.sonarToken ?? process.env.SONAR_TOKEN ?? "").trim();
16354
+ const component = (opts.sonarComponent ?? "").trim();
16355
+ if (!baseUrl) {
16356
+ return { ok: false, error: "--from sonar-api needs --sonar-url (or env SONAR_HOST_URL)." };
16357
+ }
16358
+ if (!token) {
16359
+ return { ok: false, error: "--from sonar-api needs --sonar-token (or env SONAR_TOKEN)." };
16360
+ }
16361
+ if (!component) {
16362
+ return { ok: false, error: "--from sonar-api needs --sonar-component <projectKey>." };
16363
+ }
16364
+ if (typeof fetch !== "function") {
16365
+ return { ok: false, error: "global fetch is unavailable \u2014 Node 18+ is required for --from sonar-api." };
16366
+ }
16367
+ const params = new URLSearchParams({ componentKeys: component, resolved: "false", ps: "500" });
16368
+ if (opts.sonarBranch) params.set("branch", opts.sonarBranch);
16369
+ const url = `${baseUrl}/api/issues/search?${params.toString()}`;
16370
+ try {
16371
+ const res = await fetch(url, {
16372
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
16373
+ });
16374
+ if (!res.ok) {
16375
+ const hint = res.status === 401 || res.status === 403 ? " (check the token and its permissions)" : "";
16376
+ return { ok: false, error: `SonarQube API returned ${res.status} ${res.statusText}${hint}.` };
16377
+ }
16378
+ const json = await res.text();
16379
+ return { ok: true, json };
16380
+ } catch (err) {
16381
+ return {
16382
+ ok: false,
16383
+ error: `Could not reach SonarQube at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. File-based ingest (--from sonar) still works.`
16384
+ };
16385
+ }
16386
+ }
16204
16387
 
16205
16388
  // src/commands/dashboard.ts
16206
16389
  import { existsSync as existsSync78 } from "fs";
@@ -16305,7 +16488,7 @@ function warnNum(n) {
16305
16488
 
16306
16489
  // src/index.ts
16307
16490
  var program = new Command58();
16308
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.12.9").option("--advanced", "show maintenance and experimental commands in help");
16491
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.13.1").option("--advanced", "show maintenance and experimental commands in help");
16309
16492
  registerInit(program);
16310
16493
  registerWelcome(program);
16311
16494
  registerResolveProject(program);