- 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)
107 lines
3.9 KiB
TypeScript
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");
|
|
}
|
|
},
|
|
});
|
|
}
|