Files
pi-kit/extensions/company-context.ts
T
Aleksey Shakhmatov c068dfae0c fix(review): address review findings (guardrails, prompts, tests, docs)
- permission-gate: block refs/tags/v* pushes, rm -r -f separated flags,
  git reset --hard, git clean (verified against actual bypasses)
- prompts: /bugfix /feature /review no longer hardcode go-standards —
  reference profile-gated <lang>-standards instead
- company-context: drop hardcoded Go stack, note TRACKER_URL priority,
  warn on context truncation instead of silently dropping rules
- repo-map/docs-map: graceful degradation when config values are TODO
- /kit-doctor: warn on unfilled config fields (repoMap/trackerUrl/docsUrl)
- audit: retry POSTs to endpoint (3 attempts, backoff), still best-effort
- install.sh: remove TODO course URL from cheat sheet
- tests: expand guardrails (43 node checks), add shell tests for create-mr.sh
  (scp/https origin parse, GITLAB_HOST override, protected branch refusal),
  cover company-context lib (normalize/fetch/truncation) and mcp-bridge
- commit package-lock.json for reproducible installs
- document npm test Node >= 22.6 requirement (type stripping)
2026-08-06 11:22:53 +03:00

107 lines
3.9 KiB
TypeScript

/**
* Company Context Extension
*
* WHAT: On session start, loads config/company.json (the single source of
* truth). If `remoteConfigUrl` is set, tries to fetch a fresh config
* from there with a short timeout and falls back silently to the local
* file on any error. Renders a short corporate context from
* `company-context.md` and injects it into the system prompt. Also
* registers the `/kit-config` command for debugging effective values.
*
* CONFIGURE: Edit config/company.json (gitHost, trackerUrl, docsUrl, repoMap,
* rules, remoteConfigUrl). Edit the wording in company-context.md —
* non-programmers can safely change text between the {{placeholders}}.
*
* DISABLE: Remove/rename this file from the package `extensions/` folder,
* or exclude it via the package manifest / settings.
*/
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import {
readLocalConfig,
fetchRemoteConfig,
loadTemplate,
renderContext,
DEFAULT_CONFIG,
type CompanyConfig,
} from "./lib/company-config.ts";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(HERE, "..", "config", "company.json");
const TEMPLATE_PATH = path.join(HERE, "company-context.md");
export default function companyContextExtension(pi: ExtensionAPI) {
const template = loadTemplate(TEMPLATE_PATH);
// Effective config and where it came from. Refreshed on every session start.
let effective: CompanyConfig = { ...DEFAULT_CONFIG };
let source: "local" | "remote" = "local";
// Rendered once per session; injected by before_agent_start.
let rendered: string | null = null;
pi.on("session_start", async (_event, ctx) => {
effective = readLocalConfig(CONFIG_PATH);
source = "local";
if (effective.remoteConfigUrl) {
const remote = await fetchRemoteConfig(effective.remoteConfigUrl);
if (remote) {
// Remote wins, but keep the local remoteConfigUrl so a bad remote
// value can't orphan future refreshes.
effective = { ...remote, remoteConfigUrl: effective.remoteConfigUrl };
source = "remote";
}
}
const { text, truncated } = renderContext(template, effective);
rendered = text;
if (ctx.hasUI) {
if (truncated) {
ctx.ui.notify(
`pi-kit: корпоративный контекст обрезан до ${text.split("\n").length} строк (лимит) — часть правил не попала в контекст.`,
"warning",
);
} else {
ctx.ui.notify(`pi-kit: корпоративный контекст загружен (источник: ${source})`, "info");
}
}
});
pi.on("before_agent_start", async (event) => {
// session_start may not have fired (rare); render on demand as a fallback.
const context = rendered ?? renderContext(template, effective).text;
if (!context) return undefined;
return { systemPrompt: `${event.systemPrompt}\n\n${context}` };
});
pi.registerCommand("kit-config", {
description: "Показать действующие корпоративные значения pi-kit и их источник",
handler: async (_args, ctx) => {
const origin =
source === "remote"
? `remote (${effective.remoteConfigUrl})`
: `local (${CONFIG_PATH})`;
const lines = [
`Источник: ${origin}`,
"",
`gitHost: ${effective.gitHost}`,
`trackerUrl: ${effective.trackerUrl}`,
`docsUrl: ${effective.docsUrl}`,
`repoMap: ${effective.repoMap}`,
`remoteConfigUrl: ${effective.remoteConfigUrl ?? "(не задан)"}`,
"rules:",
...(effective.rules.length > 0 ? effective.rules.map((r) => ` - ${r}`) : [" (нет)"]),
];
if (ctx.hasUI) {
await ctx.ui.select("Действующая конфигурация pi-kit", lines);
} else {
ctx.ui.notify(lines.join("\n"), "info");
}
},
});
}