import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import type { Processor } from "@mastra/core/processors"; import { resolveLiveJyotishSkill, resolveLiveJyotishSkillRuntimePath, } from "../lib/skill-package-registry.ts"; import { sharedConsultationMethodMarkdown } from "../lib/consultation-methodology.ts"; import { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING } from "../lib/consultation-thinking-plan.ts"; const skill = resolveLiveJyotishSkill(); export const jyotishSkillPackage = skill; /** * Mastra-named alias of the live SKILL.md / references / scripts / assets. * Not a hashed snapshot: leftover `versions/` trees stay off this path. */ export const jyotishSkillRuntimePath = resolveLiveJyotishSkillRuntimePath(skill); /** * Headings from the commercial SKILL.md that govern answering a natal chart. * The rest of that file is a local-agent / maintainer manual (CLI indexes, * construction rules, celebrity case catalogs) and stays in the package for * skill_read rather than being stuffed into every consultation prompt. */ const RUNTIME_METHOD_HEADINGS = [ "商业运行时路由", "关联技法完整调取", "强制工作流", "五层硬约束", "强制规则", "当前最硬的未闭环点", "核心方法论", "强制规范速查", "注意事项", ] as const; const DROPPED_RUNTIME_SUBHEADINGS = ["开源复用边界冻结"] as const; function publishedSkillBody(): string { const raw = readFileSync(resolve(skill.resolvedPath, "SKILL.md"), "utf8"); const frontmatter = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(raw); return (frontmatter ? raw.slice(frontmatter[0].length) : raw).trim(); } function dropSubheadings(section: string, dropped: readonly string[]): string { const lines = section.split("\n"); const kept: string[] = []; let skipping = false; for (const line of lines) { if (line.startsWith("### ")) { skipping = dropped.some((heading) => line.includes(heading)); } else if (line.startsWith("## ")) { skipping = false; } if (!skipping) kept.push(line); } return kept.join("\n").trim(); } /** * Mastra answers a skill activation with the entrypoint *and* a flat listing of * every file in the package, which for this package measured 129,651 bytes: the * method is 30,507 of them and the remaining 99,144 are 1,592 bare paths with no * description. Nothing in a request carries over to the next one - the agent is * rebuilt per request, no thread memory is configured, and the replayed history * is plain question/answer text - so the model re-activated the skill on every * turn and paid for that listing every time, twice more whenever a retry opened * a fresh model loop. * * Binding the runtime excerpt into the system prompt is what the listing was * standing in for. It also removes an activation the model could forget, which * is what `runtime_contract_incomplete` was mostly reporting. The excerpt is * taken from the live commercial entrypoint the operator maintains. */ function boundMethod(): string { const body = publishedSkillBody(); if (body.length === 0) { throw new Error(`Skill ${skill.name} has no method body to bind`); } const lines = body.split("\n"); const preamble: string[] = []; const sections: string[][] = []; let current: string[] | null = null; for (const line of lines) { if (line.startsWith("## ")) { if (current) sections.push(current); current = [line]; } else if (current) { current.push(line); } else { preamble.push(line); } } if (current) sections.push(current); const kept = sections.flatMap((section) => { const heading = section[0] ?? ""; if (!RUNTIME_METHOD_HEADINGS.some((wanted) => heading.includes(wanted))) return []; const text = dropSubheadings(section.join("\n"), DROPPED_RUNTIME_SUBHEADINGS); return text.length > 0 ? [text] : []; }); const excerpt = [...(preamble.join("\n").trim() ? [preamble.join("\n").trim()] : []), ...kept].join("\n\n"); if (excerpt.length === 0) { throw new Error(`Skill ${skill.name} has no runtime method sections to bind`); } return excerpt; } export const BOUND_METHOD_MARKER = ``; const NATAL_REPORT_SKELETON = `For career, wealth, marriage, family, parents and children answers, present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself. ${NATAL_ANSWER_SHAPE_SUMMARY} Raw structure, six-step houses and the Yoga table go under ## ${REPORT_HEADING.support}; the actions appear once; the Technique Audit Table stays on the evidence panel, and a follow-up turn follows the shape instruction in the user turn instead. `; function methodBlock(reportSkeleton: string) { return `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the live skill the operator maintains; there is no activation step, no hashed package, and no tool that loads it. Follow this method and its truth boundaries. ${reportSkeleton}Construction notes, CLI indexes, and case catalogs stay in the skill tree and are not part of this block. ${BOUND_METHOD_MARKER} ${boundMethod()} ${sharedConsultationMethodMarkdown()} `; } /** Method body + marker, without the natal Level 2 report skeleton. Window agents use this. */ export const jyotishSkillMethodCoreBlock = methodBlock(""); /** Natal chart-answering block: core method plus the Level 2 report skeleton. */ export const jyotishSkillMethodBlock = methodBlock(NATAL_REPORT_SKELETON); /** * Withdraw the activation tools while keeping `skill_read`. * * `providesSkillDiscovery: "on-demand"` is Mastra's declaration that the caller * owns skill discovery and instruction loading, which is now literally true. * Measured against this package: `skills` alone yields skill, skill_search and * skill_read; with this marker present it yields skill_read only, so the long * tail of references the method names by path stays reachable. * * The declaration is the point, but a processor has to do something, so it does * the thing that only it is positioned to do: confirm the method really did * arrive in the system prompt. Withdrawing the activation tool means an agent * assembled without the method block would no longer fail loudly - it would * simply answer without method, which is the failure this change exists to * remove. A prompt that cannot be read is left alone; only one that can be read * and has no method in it stops the run. * * Every step is checked rather than the first one: the chart-answering agents * that bind this skill share this object, so remembering that one of them had * its method would stop the others from ever being asked. */ export const jyotishSkillBoundProcessor: Processor & { processInputStep: NonNullable } = { id: "jyotish-skill-bound", name: "Jyotish Skill Bound", providesSkillDiscovery: "on-demand", processInputStep({ messageList, abort }) { const system = messageList?.getAllSystemMessages?.(); if (!Array.isArray(system) || system.length === 0) return; if (!collectStrings(system).includes(BOUND_METHOD_MARKER)) { abort(`Jyotish skill method is not bound into the system prompt for ${skill.name}`); } }, }; /** * Every string anywhere in a system message, joined. A system message's content * is a string in some shapes and a list of parts in others, and serialising the * whole thing instead would escape the quotes in the marker and never match it. */ function collectStrings(value: unknown, found: string[] = []): string { if (typeof value === "string") found.push(value); else if (Array.isArray(value)) for (const item of value) collectStrings(item, found); else if (value && typeof value === "object") for (const item of Object.values(value)) collectStrings(item, found); return found.join("\n"); } /** Chart-answering agents bind the runtime excerpt this way. General and * onboarding agents do not: they cannot make natal claims. Fresh arrays per * agent, because the loader takes ownership of the ones it is handed. */ export function jyotishSkillBinding() { return { skills: [jyotishSkillRuntimePath], inputProcessors: [jyotishSkillBoundProcessor], }; }