@hiveai/cli 0.12.9 → 0.13.0

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.0"}`;
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,
@@ -7786,7 +7891,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7786
7891
  };
7787
7892
  }
7788
7893
  var SERVER_NAME = "haive";
7789
- var SERVER_VERSION = "0.12.9";
7894
+ var SERVER_VERSION = "0.13.0";
7790
7895
  function jsonResult(data) {
7791
7896
  return {
7792
7897
  content: [
@@ -12546,7 +12651,7 @@ import {
12546
12651
  function registerEval(program2) {
12547
12652
  program2.command("eval").description(
12548
12653
  "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) => {
12654
+ ).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
12655
  const root = findProjectRoot42(opts.dir);
12551
12656
  const paths = resolveHaivePaths38(root);
12552
12657
  if (!existsSync65(paths.memoriesDir)) {
@@ -12594,14 +12699,19 @@ function registerEval(program2) {
12594
12699
  if (!opts.json) ui.success(`Saved baseline (score ${report.score}/100) \u2192 ${path43.relative(root, baselineFile)}`);
12595
12700
  }
12596
12701
  let delta = null;
12597
- if (opts.compare) {
12702
+ if (opts.compare || opts.regressionGate) {
12598
12703
  if (!existsSync65(baselineFile)) {
12599
- ui.error(`No baseline at ${path43.relative(root, baselineFile)}. Run \`haive eval --baseline\` first.`);
12600
- process.exitCode = 1;
12601
- return;
12704
+ if (opts.regressionGate) {
12705
+ if (!opts.json) ui.info(`No baseline at ${path43.relative(root, baselineFile)} \u2014 regression gate skipped. Run \`haive eval --baseline\` to enable it.`);
12706
+ } else {
12707
+ ui.error(`No baseline at ${path43.relative(root, baselineFile)}. Run \`haive eval --baseline\` first.`);
12708
+ process.exitCode = 1;
12709
+ return;
12710
+ }
12711
+ } else {
12712
+ const snapshot = JSON.parse(await readFile20(baselineFile, "utf8"));
12713
+ delta = compareEvalReports(snapshot.report, report);
12602
12714
  }
12603
- const snapshot = JSON.parse(await readFile20(baselineFile, "utf8"));
12604
- delta = compareEvalReports(snapshot.report, report);
12605
12715
  }
12606
12716
  if (opts.json) {
12607
12717
  console.log(JSON.stringify({ root, k, spec_source: resolvedSpec.source, report, ...delta ? { delta } : {} }, null, 2));
@@ -12633,7 +12743,7 @@ function applyExitGates(opts, report, delta) {
12633
12743
  process.exitCode = 1;
12634
12744
  }
12635
12745
  }
12636
- if (opts.failOnRegression && delta?.regressed) {
12746
+ if ((opts.failOnRegression || opts.regressionGate) && delta?.regressed) {
12637
12747
  ui.error(`eval score regressed ${delta.score.baseline} \u2192 ${delta.score.current} (\u0394 ${delta.score.delta}) vs baseline`);
12638
12748
  process.exitCode = 1;
12639
12749
  }
@@ -13350,7 +13460,7 @@ function registerDoctor(program2) {
13350
13460
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
13351
13461
  });
13352
13462
  }
13353
- findings.push(...await collectInstallFindings(root, "0.12.9"));
13463
+ findings.push(...await collectInstallFindings(root, "0.13.0"));
13354
13464
  findings.push(...await collectToolchainFindings(root));
13355
13465
  try {
13356
13466
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -13358,7 +13468,7 @@ function registerDoctor(program2) {
13358
13468
  timeout: 3e3,
13359
13469
  stdio: ["ignore", "pipe", "ignore"]
13360
13470
  }).trim();
13361
- const cliVersion = "0.12.9";
13471
+ const cliVersion = "0.13.0";
13362
13472
  if (legacyRaw && legacyRaw !== cliVersion) {
13363
13473
  findings.push({
13364
13474
  severity: "warn",
@@ -14951,7 +15061,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
14951
15061
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
14952
15062
  });
14953
15063
  }
14954
- findings.push(...await inspectIntegrationVersions(root, "0.12.9"));
15064
+ findings.push(...await inspectIntegrationVersions(root, "0.13.0"));
14955
15065
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
14956
15066
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
14957
15067
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -16077,11 +16187,11 @@ import {
16077
16187
  var SEVERITIES = ["info", "minor", "major", "critical", "blocker"];
16078
16188
  function registerIngest(program2) {
16079
16189
  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) => {
16190
+ "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"
16191
+ ).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
16192
  const format = opts.from;
16083
- if (format !== "sarif" && format !== "sonar") {
16084
- ui.error("--from must be sarif or sonar");
16193
+ if (format !== "sarif" && format !== "sonar" && format !== "sonar-api") {
16194
+ ui.error("--from must be sarif, sonar, or sonar-api");
16085
16195
  process.exitCode = 1;
16086
16196
  return;
16087
16197
  }
@@ -16102,23 +16212,39 @@ function registerIngest(program2) {
16102
16212
  process.exitCode = 1;
16103
16213
  return;
16104
16214
  }
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
- }
16215
+ const parseFormat = format === "sarif" ? "sarif" : "sonar";
16111
16216
  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;
16217
+ if (format === "sonar-api") {
16218
+ const fetched = await fetchSonarIssues(opts);
16219
+ if (!fetched.ok) {
16220
+ ui.error(fetched.error);
16221
+ process.exitCode = 1;
16222
+ return;
16223
+ }
16224
+ raw = fetched.json;
16225
+ } else {
16226
+ if (!file) {
16227
+ ui.error(`--from ${format} needs a report file argument, e.g. \`haive ingest --from ${format} report.json\`.`);
16228
+ process.exitCode = 1;
16229
+ return;
16230
+ }
16231
+ const reportPath = path54.resolve(root, file);
16232
+ if (!existsSync77(reportPath)) {
16233
+ ui.error(`Report file not found: ${reportPath}`);
16234
+ process.exitCode = 1;
16235
+ return;
16236
+ }
16237
+ try {
16238
+ raw = await readFile25(reportPath, "utf8");
16239
+ } catch (err) {
16240
+ ui.error(`Could not read ${reportPath}: ${err instanceof Error ? err.message : String(err)}`);
16241
+ process.exitCode = 1;
16242
+ return;
16243
+ }
16118
16244
  }
16119
16245
  let drafts;
16120
16246
  try {
16121
- const findings = parseFindings2(format, raw);
16247
+ const findings = parseFindings2(parseFormat, raw);
16122
16248
  drafts = draftsFromFindings2(findings, {
16123
16249
  type: opts.type ?? "gotcha",
16124
16250
  scope: opts.scope ?? "team",
@@ -16201,6 +16327,42 @@ async function writeDraft2(paths, draft) {
16201
16327
  await writeFile37(file, serializeMemory28({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
16202
16328
  return file;
16203
16329
  }
16330
+ async function fetchSonarIssues(opts) {
16331
+ const baseUrl = (opts.sonarUrl ?? process.env.SONAR_HOST_URL ?? "").trim().replace(/\/+$/, "");
16332
+ const token = (opts.sonarToken ?? process.env.SONAR_TOKEN ?? "").trim();
16333
+ const component = (opts.sonarComponent ?? "").trim();
16334
+ if (!baseUrl) {
16335
+ return { ok: false, error: "--from sonar-api needs --sonar-url (or env SONAR_HOST_URL)." };
16336
+ }
16337
+ if (!token) {
16338
+ return { ok: false, error: "--from sonar-api needs --sonar-token (or env SONAR_TOKEN)." };
16339
+ }
16340
+ if (!component) {
16341
+ return { ok: false, error: "--from sonar-api needs --sonar-component <projectKey>." };
16342
+ }
16343
+ if (typeof fetch !== "function") {
16344
+ return { ok: false, error: "global fetch is unavailable \u2014 Node 18+ is required for --from sonar-api." };
16345
+ }
16346
+ const params = new URLSearchParams({ componentKeys: component, resolved: "false", ps: "500" });
16347
+ if (opts.sonarBranch) params.set("branch", opts.sonarBranch);
16348
+ const url = `${baseUrl}/api/issues/search?${params.toString()}`;
16349
+ try {
16350
+ const res = await fetch(url, {
16351
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
16352
+ });
16353
+ if (!res.ok) {
16354
+ const hint = res.status === 401 || res.status === 403 ? " (check the token and its permissions)" : "";
16355
+ return { ok: false, error: `SonarQube API returned ${res.status} ${res.statusText}${hint}.` };
16356
+ }
16357
+ const json = await res.text();
16358
+ return { ok: true, json };
16359
+ } catch (err) {
16360
+ return {
16361
+ ok: false,
16362
+ error: `Could not reach SonarQube at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. File-based ingest (--from sonar) still works.`
16363
+ };
16364
+ }
16365
+ }
16204
16366
 
16205
16367
  // src/commands/dashboard.ts
16206
16368
  import { existsSync as existsSync78 } from "fs";
@@ -16305,7 +16467,7 @@ function warnNum(n) {
16305
16467
 
16306
16468
  // src/index.ts
16307
16469
  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");
16470
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.13.0").option("--advanced", "show maintenance and experimental commands in help");
16309
16471
  registerInit(program);
16310
16472
  registerWelcome(program);
16311
16473
  registerResolveProject(program);