fix(consult): run the local skill's full technique spectrum on the web path
Independent Staging Quality Gate / validate (push) Successful in 10m59s
Independent Staging Quality Gate / publish (push) Successful in 9m7s

Web answers were thinner than a local Agent calling yinduzhanxing-skill:
theme-subset vargas, no visible audit table, and a prompt that dropped the
invocation contract. Bind the commercial method, compute D1–D60 plus Western
layers, and deliver the same Full-Spectrum checklist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-18 23:30:36 +08:00
co-authored by Cursor
parent d04fc30b63
commit 2bf7472645
29 changed files with 1176 additions and 191 deletions
+3 -3
View File
@@ -641,7 +641,7 @@ export async function POST(request: Request) {
? generalDailyContext
? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答,不要复述内部字段或报本轮算了什么。",
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段。",
generalDailyContextPrompt(generalDailyContext),
resolvedQuestion.modelQuestion,
].filter(Boolean).join("\n"),
@@ -668,8 +668,8 @@ export async function POST(request: Request) {
const agent = getGeneralJyotishAgent(selectedModel);
const result = await agent.stream(baseMessages, streamOptions);
usages.push(result.totalUsage);
// This mode has no calculation to require and its method is bound before
// the run starts, so there is no longer a contract for a retry to repair.
// This mode has no calculation to require and no chart method to bind,
// so there is no contract for a retry to repair.
const retryForAnswer = async () => {
const retried = await agent.stream([
...baseMessages,
+2 -1
View File
@@ -29,9 +29,9 @@ const domainMethodology: Readonly<Record<ConsultationDomain, { strictRoute: stri
marriage: { strictRoute: "relationship-timing-strict", eventJudgment: "event_judgment_marriage.md" },
wealth: { strictRoute: "wealth-timing-strict", eventJudgment: "event_judgment_wealth.md" },
timing: { strictRoute: "event-timing-strict", eventJudgment: null },
health: { strictRoute: "health-timing-strict", eventJudgment: null },
annual: { strictRoute: null, eventJudgment: null },
general: { strictRoute: null, eventJudgment: null },
health: { strictRoute: null, eventJudgment: null },
education: { strictRoute: null, eventJudgment: null },
migration: { strictRoute: null, eventJudgment: null },
family: { strictRoute: null, eventJudgment: null },
@@ -161,6 +161,7 @@ export function consultationMethodologyForDomains(
};
push("Shared mandatory baseline", ROUTER_FILE, markdownSection(router, SHARED_BASELINE_HEADING));
push("Full-spectrum invocation", ROUTER_FILE, markdownSection(router, "Full-Spectrum Invocation Contract"));
push("Event judgment skeleton", SKELETON_FILE, packageFile(SKELETON_FILE));
const withoutChecklist: ConsultationDomain[] = [];
+1 -7
View File
@@ -58,12 +58,6 @@ function domainsForTheme(theme: ConsultationPlanTheme): ConsultationPlan["reques
return [...consultationDomainDefinition(theme).workflowThemes];
}
function evidenceForTheme(theme: ConsultationPlanTheme): ConsultationPlan["requiredEvidenceCategories"] {
return theme === "timing" || theme === "annual"
? ["natal_foundation", "timing", "validation"]
: ["natal_foundation", "domain"];
}
function deepFreeze<T>(value: T): DeepReadonly<T> {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
for (const child of Object.values(value)) deepFreeze(child);
@@ -108,7 +102,7 @@ export function createConsultationPlan(input: {
requestedDomains: domainsForTheme(theme),
depth: input.depth ?? "standard",
timingHorizon: input.timingHorizon ?? (theme === "timing" || theme === "annual" ? "next_12_months" : null),
requiredEvidenceCategories: evidenceForTheme(theme),
requiredEvidenceCategories: [...consultationEvidenceCategoryValues],
birthTimeMode: consultationMode,
precisionBoundary: precisionBoundaryFor(consultationMode),
maxCreditCost: consultationPlanMaxCreditCost,
+7 -1
View File
@@ -403,6 +403,9 @@ function mergeConsultationEvidencePackets(
.filter((value): value is string => typeof value === "string" && value.trim().length > 0))];
const boundaries = [...new Set(packets.map((packet) => packet.rectification.boundary))];
const policy = mergeConsultationAnswerPolicies(contracts.map((contract) => evidenceRecord(contract.answer_policy)));
const audit = contracts.find((contract) => Array.isArray(contract.technique_audit_table))?.technique_audit_table;
const varga = contracts.find((contract) => contract.varga_spectrum !== undefined)?.varga_spectrum;
const western = contracts.find((contract) => contract.western_spectrum !== undefined)?.western_spectrum;
return consultationEvidencePacketSchema.parse({
packet_version: "consultation-evidence-packet-v2",
question: packets.find((packet) => typeof packet.question === "string")?.question,
@@ -426,6 +429,9 @@ function mergeConsultationEvidencePackets(
...(limitations.length > 0
? { user_facing_limitation: limitations.join(" ").slice(0, 800) }
: {}),
...(audit !== undefined ? { technique_audit_table: audit } : {}),
...(varga !== undefined ? { varga_spectrum: varga } : {}),
...(western !== undefined ? { western_spectrum: western } : {}),
},
claim_cards: options.claimCards,
// `not_auto_rectified` is the restrictive boundary, so one domain reporting
@@ -500,7 +506,7 @@ export function createConsultationTools(ctx: ConsultationAgentContext) {
let calculation: Promise<ReturnType<typeof toModelDomainPlanContext>> | null = null;
const consultationTool = createTool({
id: "run-jyotish-consultation",
description: `Run one server-validated plan of at most ${MAX_CONSULTATION_DOMAINS} allowlisted personal Jyotish consultation domains. Send only question and domains. The single ordered domains array is the only way to select domains: list them in priority order, or omit it entirely to accept the domain the server already selected for this consultation. Use only the ids enumerated in the schema; workflow or checklist names from the skill's methodology are not domain ids. Domains execute one after another and each costs about ${Math.round(CONSULTATION_DOMAIN_DURATION_MS / 1000)}s of the run's wall clock, so a shorter plan leaves more time to write the answer; if the clock runs short the server executes the domains that fit and returns the rest in omitted_domains. Birth data is server-bound and must never be supplied. The result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—which for several domains is the most restrictive merge of the executed ones, with per-domain detail in consultations. One calculation is executed per request and reused, so repeating the call with different parameters cannot change the result.`,
description: `Run one server-validated plan of at most ${MAX_CONSULTATION_DOMAINS} allowlisted personal Jyotish consultation domains. Send only question and domains. The single ordered domains array is the only way to select domains: list every domain the question needs, in priority order, or omit it entirely to accept the domain the server already selected. Do not drop a relevant domain to shorten the plan. Use only the ids enumerated in the schema; workflow or checklist names from the skill's methodology are not domain ids. Domains execute one after another and each costs about ${Math.round(CONSULTATION_DOMAIN_DURATION_MS / 1000)}s of the run's wall clock; if the clock runs short the server executes the domains that fit and returns the rest in omitted_domains. Birth data is server-bound and must never be supplied. The result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—which for several domains is the most restrictive merge of the executed ones, with per-domain detail in consultations. One calculation is executed per request and reused, so repeating the call with different parameters cannot change the result.`,
inputSchema: consultationToolInputSchema,
execute: async (input, context) => {
const requestedDomains = canonicalDomainPlan(input, ctx);
+143 -10
View File
@@ -173,6 +173,9 @@ export const consultationEvidencePacketSchema = z.object({
hard_blockers: modelOutputEvidenceSchema.optional(),
answer_policy: modelOutputEvidenceSchema.optional(),
user_facing_limitation: modelOutputEvidenceSchema.optional(),
technique_audit_table: modelOutputEvidenceSchema.optional(),
varga_spectrum: modelOutputEvidenceSchema.optional(),
western_spectrum: modelOutputEvidenceSchema.optional(),
}),
claim_cards: z.array(z.object({
category: z.enum(consultationEvidenceCategoryValues),
@@ -242,6 +245,8 @@ const natalFoundationKeys = new Set([
"isretrograde", "dignity", "status", "total", "totalrupa", "rupa", "ratio", "required",
"rank", "strength", "score", "value", "sav", "bav", "points", "count", "housescores",
"components", "matrix", "matrixshape", "signs", "sources", "source", "method", "boundary",
"functionalbeneficmalefic", "functionalbenefics", "functionalmalefics", "functionalneutrals",
"yogakarakas", "ownedhouses", "ascendantsign", "effectonconfidence",
]);
const timingKeys = new Set([
@@ -268,14 +273,123 @@ const domainDetailsKeys = new Set([
"current", "summary", "interpretation", "description", "note", "status", "confidence",
]);
function projectVargaChart(value: unknown): ModelOutputValue | undefined {
const chart = record(value);
const planetsIn = record(chart.planets);
const planets: { [key: string]: ModelOutputValue } = {};
for (const [name, sign] of Object.entries(planetsIn)) {
const projected = boundedPrimitive(sign);
if (projected !== undefined) planets[name] = projected;
}
const output: { [key: string]: ModelOutputValue } = {};
const lagna = boundedPrimitive(chart.lagna);
if (lagna !== undefined) output.lagna = lagna;
if (Object.keys(planets).length) output.planets = planets;
for (const key of ["name", "meaning", "boundary"] as const) {
const projected = boundedPrimitive(chart[key]);
if (projected !== undefined) output[key] = projected;
}
return Object.keys(output).length ? output : undefined;
}
function projectVargaGroup(value: unknown): ModelOutputValue | undefined {
const group = record(value);
const output: { [key: string]: ModelOutputValue } = {};
for (const [code, chart] of Object.entries(group)) {
const projected = projectVargaChart(chart);
if (projected !== undefined) output[code] = projected;
}
return Object.keys(output).length ? output : undefined;
}
function projectVargaSpectrum(value: unknown): ModelOutputValue | undefined {
const spectrum = record(value);
if (!Object.keys(spectrum).length) return undefined;
const output: { [key: string]: ModelOutputValue } = {};
for (const key of ["status", "mode", "boundary"] as const) {
const projected = boundedPrimitive(spectrum[key]);
if (projected !== undefined) output[key] = projected;
}
const counts = record(spectrum.counts);
if (Object.keys(counts).length) {
const compact: { [key: string]: ModelOutputValue } = {};
for (const [key, item] of Object.entries(counts)) {
const projected = boundedPrimitive(item);
if (projected !== undefined) compact[key] = projected;
}
if (Object.keys(compact).length) output.counts = compact;
}
for (const group of ["formal", "research_dn", "extended"] as const) {
const projected = projectVargaGroup(spectrum[group]);
if (projected !== undefined) output[group] = projected;
}
if (Array.isArray(spectrum.blocked)) {
output.blocked = spectrum.blocked.filter((item): item is string => typeof item === "string").slice(0, 60);
}
return Object.keys(output).length ? output : undefined;
}
function projectTechniqueAudit(value: unknown): ModelOutputValue | undefined {
if (!Array.isArray(value)) return undefined;
const rows = value.slice(0, 80).map((item) => {
const row = record(item);
const projected: { [key: string]: ModelOutputValue } = {};
for (const key of ["technique", "status", "system", "boundary"] as const) {
const scalar = boundedPrimitive(row[key]);
if (scalar !== undefined) projected[key] = scalar;
}
return projected;
}).filter((row) => Object.keys(row).length > 0);
return rows.length ? rows : undefined;
}
function projectWesternSpectrum(value: unknown): ModelOutputValue | undefined {
const spectrum = record(value);
if (!Object.keys(spectrum).length) return undefined;
const output: { [key: string]: ModelOutputValue } = {};
for (const key of ["status", "zodiac", "house_system", "boundary"] as const) {
const projected = boundedPrimitive(spectrum[key]);
if (projected !== undefined) output[key] = projected;
}
const natal = record(spectrum.natal);
if (Object.keys(natal).length) {
const compact: { [key: string]: ModelOutputValue } = {};
for (const [key, item] of Object.entries(natal)) {
const projected = boundedPrimitive(item);
if (projected !== undefined) compact[key] = projected;
}
if (Object.keys(compact).length) output.natal = compact;
}
const techniques = record(spectrum.techniques);
if (Object.keys(techniques).length) {
const compact: { [key: string]: ModelOutputValue } = {};
for (const [name, layer] of Object.entries(techniques)) {
const row = record(layer);
const projected: { [key: string]: ModelOutputValue } = {};
for (const key of ["status", "boundary"] as const) {
const scalar = boundedPrimitive(row[key]);
if (scalar !== undefined) projected[key] = scalar;
}
if (Object.keys(projected).length) compact[name] = projected;
}
if (Object.keys(compact).length) output.techniques = compact;
}
return Object.keys(output).length ? output : undefined;
}
function projectNatalFoundation(context: ReturnType<typeof toAgentConsultationContext>) {
return projectAllowlistedTree({
const foundation = projectAllowlistedTree({
ascendant: context.chart.ascendant,
planets: context.chart.planets,
houses: context.chart.houses,
shadbala: context.chart.shadbala,
ashtakavarga: context.chart.ashtakavarga,
functional_benefic_malefic: context.local_layers.functional_benefic_malefic,
}, natalFoundationKeys, { allowAstrologyEntityKeys: true });
const output = record(foundation);
const varga = projectVargaSpectrum(context.local_layers.varga_spectrum);
if (varga !== undefined) output.varga_spectrum = varga;
return Object.keys(output).length ? output : undefined;
}
function projectDomainEvidence(value: unknown): ModelOutputValue {
@@ -315,30 +429,41 @@ function projectDomainEvidence(value: unknown): ModelOutputValue {
}
function projectTimingEvidence(context: ReturnType<typeof toAgentConsultationContext>) {
return projectAllowlistedTree({
const timing = projectAllowlistedTree({
dasha: context.chart.dasha,
dasha_sub_periods: context.local_layers.dasha_sub_periods,
narayana_dasha: context.local_layers.narayana_dasha,
}, timingKeys, { allowAstrologyEntityKeys: true });
const output = record(timing);
const western = projectWesternSpectrum(context.evidence_contract.western_spectrum);
if (western !== undefined) output.western_spectrum = western;
return Object.keys(output).length ? output : undefined;
}
function projectValidationEvidence(context: ReturnType<typeof toAgentConsultationContext>) {
return projectAllowlistedTree({
const validation = projectAllowlistedTree({
technique_truth: context.evidence_contract.technique_truth,
reference_transparency: context.reference_transparency,
shadbala_boundary: context.local_layers.shadbala_boundary,
}, validationKeys, { allowAstrologyEntityKeys: true });
const output = record(validation);
const audit = projectTechniqueAudit(context.evidence_contract.technique_audit_table);
if (audit !== undefined) output.technique_audit_table = audit;
return Object.keys(output).length ? output : undefined;
}
function stringList(value: unknown) {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, 24) : [];
function stringList(value: unknown, limit = 24) {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, limit) : [];
}
function projectEvidenceContract(contract: ReturnType<typeof toAgentConsultationContext>["evidence_contract"]) {
const policy = record(contract.answer_policy);
const audit = projectTechniqueAudit(contract.technique_audit_table);
const varga = projectVargaSpectrum(contract.varga_spectrum);
const western = projectWesternSpectrum(contract.western_spectrum);
return {
available_layers: stringList(contract.available_layers),
missing_route_layers: stringList(contract.missing_route_layers),
available_layers: stringList(contract.available_layers, 80),
missing_route_layers: stringList(contract.missing_route_layers, 40),
hard_blockers: stringList(contract.hard_blockers),
answer_policy: {
can_answer_direction: policy.can_answer_direction === true,
@@ -350,6 +475,9 @@ function projectEvidenceContract(contract: ReturnType<typeof toAgentConsultation
...(typeof contract.user_facing_limitation === "string"
? { user_facing_limitation: boundedPrimitive(contract.user_facing_limitation) }
: {}),
...(audit !== undefined ? { technique_audit_table: audit } : {}),
...(varga !== undefined ? { varga_spectrum: varga } : {}),
...(western !== undefined ? { western_spectrum: western } : {}),
};
}
@@ -426,6 +554,9 @@ export function toAgentConsultationContext(data: JsonRecord) {
commercial_evidence_status: consumerContext.commercial_evidence_status,
answer_policy: consumerContext.answer_policy,
user_facing_limitation: consumerContext.user_facing_limitation,
technique_audit_table: consumerContext.technique_audit_table,
varga_spectrum: consumerContext.varga_spectrum,
western_spectrum: consumerContext.western_spectrum,
},
chart: {
birth: chart.birth, ascendant: chart.ascendant, planets: chart.planets, houses: chart.houses,
@@ -433,12 +564,14 @@ export function toAgentConsultationContext(data: JsonRecord) {
},
local_layers: {
shadbala_boundary: "Shadbala is a locally consistent relative-strength layer; external component-level absolute parity remains partial and must not be stated as closed.",
varga_full: modules.varga_full, arudha_padas: modules.arudha_padas, ashtakavarga: modules.ashtakavarga,
varga_full: modules.varga_full, varga_spectrum: modules.varga_spectrum, arudha_padas: modules.arudha_padas, ashtakavarga: modules.ashtakavarga,
// `modules.dasha_boundaries` was read here for months and never written by the engine, so
// every answer was composed without sub-period boundaries while the receipt still reported
// precise timing as allowed. The field now names the layer the engine actually attaches.
dasha_sub_periods: modules.dasha_sub_periods, narayana_dasha: modules.narayana_dasha,
functional_benefic_malefic: record(data.machine_evidence_packet).functional_benefic_malefic,
dasha_sub_periods: modules.dasha_sub_periods, narayana_dasha: modules.narayana_dasha, gulika: modules.gulika,
kakshya: modules.kakshya,
functional_benefic_malefic: modules.functional_benefic_malefic
|| record(data.machine_evidence_packet).functional_benefic_malefic,
},
rectification: {
boundary: "not_auto_rectified", summary: rectification.summary,
+15 -16
View File
@@ -25,10 +25,11 @@ if (
) {
throw new Error("Active Jyotish Skill package is not in the canonical versioned layout");
}
// Agents read method from the hash-verified package, not from this working-tree
// view. The view is still checked against it so that editing the canonical
// entrypoint without publishing a version fails the build instead of quietly
// leaving the repository and the running product on different method.
// Agents read method from the hash-verified package, not from the working-tree
// view (that view's references/ is a symlink to hundreds of extra files).
// Canonical SKILL.md must stay byte-equal to the active package entrypoint;
// after editing either copy, update the other and recompute the registry sha256.
// The hash is an integrity check, not a freeze that requires a new version.
if (
!readFileSync(resolve(jyotishSkillPath, "SKILL.md")).equals(
readFileSync(resolve(jyotishSkillPackage.resolvedPath, "SKILL.md")),
@@ -41,10 +42,10 @@ const jyotishInstructions = `You are the guide for a conversational Vedic astrol
${productConversationVoice}
Write in concise Simplified Chinese as a natural conversation, not a report or fixed template. Use Markdown only when it improves scanning; tables are allowed only for genuinely comparative information.
${jyotishSkillMethodBlock}
Everything below is this product's runtime contract and overrides the skill method wherever the two differ. Use run-jyotish-consultation for actual chart calculations instead of inventing results, and let VISIBLE VOICE above own the visible chat format rather than the skill's report template.
The bound skill method is this product's answering contract. Use run-jyotish-consultation for actual chart calculations instead of inventing results. VISIBLE VOICE owns the spoken chat shape; do not paste the skill's formal-report skeleton as the chat format.
For questions that require a new chart claim, call run-jyotish-consultation before answering. Simple conversational follow-ups may use the existing context.
Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. At most ${MAX_CONSULTATION_DOMAINS} domains may be requested in one run, because they are calculated one after another inside a fixed time budget: list them in priority order and prefer the smallest plan that answers the question, since every extra domain takes time away from writing the answer. The server canonicalizes aliases, rejects unsupported/product domains, executes each accepted domain, and returns the actual domains in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters.
The tool result's methodology field is the skill's own strict checklist for the routes that actually ran, quoted from the pinned skill version. Treat it as the method for this answer, not as background: work through its mandatory modules against the evidence you were given, and obey its output discipline, including any instruction to separate kinds of claim rather than merge them into one vague statement. Those sections are already delivered, so never spend a turn re-reading them; methodology.further_reading lists the references the skill names, and you may read one with skill_read only when the question needs something the delivered sections do not cover. When methodology.domains_without_strict_checklist names a domain, the skill declares no strict checklist for it: use the shared baseline and do not imply a strict route was followed. When methodology is absent, follow the bound skill method above.
Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. At most ${MAX_CONSULTATION_DOMAINS} domains may be requested in one run, because they are calculated one after another inside a fixed time budget: list every domain the question actually needs, in priority order. Do not drop a relevant domain to keep the plan short—the natal compute already ran the full technique spectrum, and omitting a domain omits that route's checklist from the answer. The server canonicalizes aliases, rejects unsupported/product domains, executes each accepted domain, and returns the actual domains in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters.
The tool result's methodology field is the skill's own strict checklist for the routes that actually ran, quoted from the pinned skill version. Treat it as the method for this answer, not as background: work through its mandatory modules against the evidence you were given, and obey its output discipline, including any instruction to separate kinds of claim rather than merge them into one vague statement. Those sections are already delivered, so never spend a turn re-reading them; methodology.further_reading lists the references the skill names, and you may read one with skill_read only when the question needs something the delivered sections do not cover. When methodology.domains_without_strict_checklist names a domain, the skill declares no named checklist for it: still follow the delivered Full-spectrum invocation and shared baseline, and do not imply a named strict route was followed. When methodology is absent, follow the bound skill method above.
The tool result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—even when several domains ran. For a multi-domain plan that top level is the most restrictive merge of the executed domains, so obey it exactly as written and read consultations only for per-domain detail. Never treat an absent top-level field as permission to answer without a contract.
When omitted_domains is non-empty, do not answer those domains and never present the reply as covering the whole plan. Stay with what was calculated. Do not announce a skipped-domain inventory or say this round was incomplete unless the user asked about coverage.
Activity, progress, tool status, and execution receipts are server-owned. Never imitate data-jyotish-activity, activity events, tool-started/tool-completed messages, or receipts in the answer text.
@@ -52,7 +53,7 @@ Treat the server-provided current time as authoritative for words such as today,
Treat the tool result's top-level status and evidence_contract as the authoritative answer policy:
- When status is ready and evidence_contract.answer_policy.can_answer_direction is true, answer the user's actual question directly. Do not begin with infrastructure or confidence disclaimers.
- An unavailable optional provider or external cross-check is not a calculation failure. Never call it an internal error.
- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested.
- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested. The required Technique Audit Table may list those rows by their delivered labels without discussing provider internals.
When reference_transparency is present:
- Present candidate_windows and exact_triggers when relevant, but describe exact_triggers as technical trigger points, never guaranteed events.
@@ -66,7 +67,9 @@ When reference_transparency is present:
- When consulting references/oracle/effective_skill_capability_view_2026_07_19.json or any derived skill map, use effective_status, not registry_status. Do not promote reference_only or blocked techniques into mastered/covered claims.
- If should_lead_with_limitations is false, do not lead with limitations. If a limitation is relevant, put it in one short sentence at the end.
- Only say the chart calculation failed when evidence_contract.hard_blockers is non-empty.
- Never claim D2, D11, D9, D10, A10, UL, or Narayana Dasha is missing when it appears in evidence_contract.available_layers, chart, or local_layers.
- Never claim D2, D11, D9, D10, A10, UL, Narayana Dasha, a delivered Varga, or a delivered Western layer is missing when it appears in evidence_contract.available_layers, evidence_contract.varga_spectrum, evidence_contract.western_spectrum, chart, or local_layers.
- evidence_contract.technique_audit_table is the invocation record for this run. Use every executed layer that is relevant to the question. Status executed means the server ran it; blocked means it could not; not_applicable means it does not apply. Never treat an omitted row as permission to invent the technique, and never silently skip an executed layer the question needs.
- The visible answer must end with that Technique Audit Table in spoken Chinese labels (已执行 / 阻塞 / 不适用). This is the one table VISIBLE VOICE requires; do not also dump JSON.
- local_layers.dasha_sub_periods carries the antardasha boundaries inside the running mahadasha. When it is present, use those boundaries and never say sub-periods were not calculated; when it is absent, say so once instead of implying the calculation broke.
- Treat evidence_contract.answer_policy as a hard output contract. When can_answer_precise_timing is false, provide only direction or structure and do not state a month, date, or guaranteed timing outcome.
- Treat answer_policy.deterministic_claims_forbidden_for as a hard prohibition. Do not use a restricted technique to make a deterministic conclusion. reference_only, partial, blocked, research_only_blocked, and partial_registry_only are commercial claim boundaries, not validated capabilities.
@@ -105,8 +108,7 @@ ${JSON.stringify(toAgentConsultationContext(workflowContext))}
const generalJyotishInstructions = `You are the guide for a conversational Vedic astrology product.
${productConversationVoice}
${jyotishSkillMethodBlock}
Everything below overrides that method wherever the two differ. This request explicitly has no usable birth minute. Never calculate, infer, or claim a personal birth chart, ascendant, house, divisional chart, dasha, transit timing, or personal prediction. You have no chart tools for this mode.
This request explicitly has no usable birth minute. Never calculate, infer, or claim a personal birth chart, ascendant, house, divisional chart, dasha, transit timing, or personal prediction. You have no chart tools for this mode.
Answer general educational questions that do not depend on the user's natal chart. A homepage daily request may also include a server-owned <public-daily-panchanga> block. In that one case, explain the public calendar trend, suitable actions, cautions, and one practical next step from that block only. State concisely that it is a public-day reference rather than a personal natal forecast; do not reject the whole request merely because the birth minute is unavailable.
If a request asks for a personal chart conclusion, personal timing, compatibility, or forecast without that public daily evidence, clearly say that this mode cannot answer it and offer exactly two safe next steps: ask a general-knowledge question, or complete birth-time rectification. Do not invent 00:00, a period midpoint, or any other substitute minute.
Never turn public Panchanga into claims about the user's ascendant, houses, dasha, natal transits, guaranteed outcomes, or exact event timing. Do not invent or alter Panchanga fields that the server did not provide.
@@ -123,7 +125,6 @@ export function getGeneralJyotishAgent(model: ResolvedLanguageModel) {
name: "Jyotisha General Guide",
model: model.model,
instructions: generalJyotishInstructions,
...jyotishSkillBinding(),
});
generalJyotishAgents.set(model.id, agent);
return agent;
@@ -131,9 +132,8 @@ export function getGeneralJyotishAgent(model: ResolvedLanguageModel) {
const onboardingInstructions = `You create the first conversational turn for Jyotisha, a Vedic astrology chat product.
${jyotishSkillMethodBlock}
Follow that method's scope and truth boundaries when suggesting questions; everything below overrides it wherever the two differ.
This is onboarding, not a chart reading: do not calculate, infer, or claim placements, timing windows, personality traits, relationship outcomes, or career conclusions.
The suggested questions must stay inside what this product can later answer from a natal chart. Never suggest medical, legal, investment, death, diagnosis, pregnancy, or exact-date promises.
Return valid JSON only. Do not use Markdown fences, commentary, or hidden fields.
The JSON shape must be:
{"suggestions":[{"theme":"服务器给出的主题 id","text":"问题"}]}
@@ -141,7 +141,7 @@ The server lists the required themes. Return one question per listed theme, in e
Write every question as the user's own first-person request and include “我”, such as “请帮我看看……”. Each question must ask for useful help with the user's situation, not for a lesson about astrology, and must stay under 40 Chinese characters.
Keep each question specific to its own theme so the set does not read as rewordings of one another. Never mention birth data, profile readiness, setup completion, or system processing.
Never generate detached or encyclopedic wording such as “印度占星一般如何……”, “通常会看哪些因素”, “包含哪些证据层”, or “如何划分主题”.
The questions must use everyday Simplified Chinese and be answerable through the skill. Avoid jargon, fear, deterministic promises, medical/legal/investment claims, and unsupported precision.`;
The questions must use everyday Simplified Chinese and be answerable as a later natal-chart consultation. Avoid jargon, fear, deterministic promises, medical/legal/investment claims, and unsupported precision.`;
const onboardingAgents = new Map<string, Agent>();
@@ -153,7 +153,6 @@ export function getOnboardingAgent(model: ResolvedLanguageModel) {
name: "Jyotisha Onboarding Guide",
model: model.model,
instructions: onboardingInstructions,
...jyotishSkillBinding(),
});
onboardingAgents.set(model.id, agent);
return agent;
+10 -7
View File
@@ -1,18 +1,21 @@
export const productConversationVoice = `VISIBLE VOICE (overrides skill report format)
This product is a private conversation with one person, not a professional briefing.
The jyotish-vedic-astrology skill and any JSON context govern METHOD and TRUTH BOUNDARIES only. They must not govern visible format. Never copy the skill's report structure, Technique Audit Table, evidence_packet layout, guided_topics list, or “先展示原始结构再综合” output into the chat.
export const productConversationVoice = `VISIBLE VOICE (spoken Chinese; same technique density as the local skill)
This product is a private conversation with one person. The jyotish-vedic-astrology skill governs METHOD, TECHNIQUE INVOCATION, and TRUTH BOUNDARIES. Chat format may be spoken Chinese rather than the skill's formal-report skeleton, but it must not drop techniques, raw structures, or audit rows the server delivered.
Write spoken Simplified Chinese, addressing the reader as 你.
- Answer the user's actual question in the first sentence.
- Sound like a calm, specific person: vary sentence length, prefer one concrete reading over a tour of the chart, and gloss a technical term once in plain Chinese (婚姻分盘(D9, 当前大运) before using the everyday phrase.
- Prefer plain paragraphs. Use Markdown only to compare two concrete options; never lead with headings, numbered methodology, or a bullet inventory of the chart.
- If a limitation materially changes the claim, put it in one short closing sentence; otherwise omit it.
- Then use the delivered evidence the way the local skill does: unified parameters and raw structure first (degrees, houses, vargas, Dasha boundaries, Shadbala/AV, functional benefic/malefic, Western layers), then parallel systems, conflicts, and only then a conditional synthesis.
- Gloss a technical term once in plain Chinese (婚姻分盘(D9, 当前大运) before using the everyday phrase.
- Prefer paragraphs. Use Markdown to compare concrete options, to show raw structure that would otherwise be a wall of numbers, and for the required Technique Audit Table at the end.
- If a limitation materially changes the claim, put it in one short closing sentence before the table; otherwise omit it.
After the spoken answer, include exactly one compact Markdown Technique Audit Table with columns 技法 / 状态 / 说明. Copy rows from the delivered technique_audit_table. Translate status to 已执行 / 阻塞 / 不适用. Do not invent, omit, or upgrade a row. Do not dump JSON keys or YAML.
Never do these in the visible answer:
- Quote JSON keys, English status codes, or field names (core_status, blocked, ready, consumer_context, missing_layers, omitted_domains).
- Say 根据系统计算, 根据当前计算结果, 综合来看, 需要指出的是, 当前状态为, 本轮未完整计算, or 从星盘来看 as an opener.
- Restate the user's question, pad to a template length, or use a fixed 首先 / 其次 / 最后 skeleton.
- Dump a Technique Audit Table, YAML, JSON, or an inventory of what was or was not computed this round.
- Dump YAML, JSON, or a second inventory besides that one table.
- Skip an executed layer the question needs in order to keep the answer short.
Bad: 根据当前计算结果,事业方向总体偏稳定,但副运细节与行运触发未在本轮完整计算。
Good: 你更适合把已经积累的专业能力做成长期事业,而不是频繁换赛道。眼下更值得把现有工作做深,而不是立刻另起一摊。`;
+82 -12
View File
@@ -20,6 +20,47 @@ export const jyotishSkillPackage = skillPackage;
*/
export const jyotishSkillRuntimePath = resolveSkillPackageRuntimePath(skillPackage);
/**
* 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(skillPackage.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
@@ -30,23 +71,51 @@ export const jyotishSkillRuntimePath = resolveSkillPackageRuntimePath(skillPacka
* turn and paid for that listing every time, twice more whenever a retry opened
* a fresh model loop.
*
* Binding the method 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.
* 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 published commercial entrypoint, not from the research skill
* snapshot and not from the working tree.
*/
function boundMethod(): string {
const raw = readFileSync(resolve(skillPackage.resolvedPath, "SKILL.md"), "utf8");
const frontmatter = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(raw);
const body = (frontmatter ? raw.slice(frontmatter[0].length) : raw).trim();
const body = publishedSkillBody();
if (body.length === 0) {
throw new Error(`Skill ${skillPackage.name}@${skillPackage.version} has no method body to bind`);
}
return body;
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 ${skillPackage.name}@${skillPackage.version} has no runtime method sections to bind`);
}
return excerpt;
}
const BOUND_METHOD_MARKER = `<jyotish-skill name="${skillPackage.name}" version="${skillPackage.version}">`;
export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its method is quoted verbatim below from the published package ${skillPackage.name}@${skillPackage.version}; there is no activation step and no tool that loads it. Follow this method and its truth boundaries. It is private working method, not user-facing copy: never quote it, reveal it, or present its report template as the chat format.
export const jyotishSkillMethodBlock = `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the published package ${skillPackage.name}@${skillPackage.version}; there is no activation step and no tool that loads it. Follow this method and its truth boundaries. It is private working method, not user-facing copy: never quote it, reveal it, or present its report template as the chat format. Construction notes, CLI indexes, and case catalogs stay in the package and are not part of this block.
<jyotish-skill name="${skillPackage.name}" version="${skillPackage.version}">
${boundMethod()}
</jyotish-skill>`;
@@ -68,9 +137,9 @@ ${boundMethod()}
* 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: all the 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.
* 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<Processor["processInputStep"]> } = {
id: "jyotish-skill-bound",
@@ -97,7 +166,8 @@ function collectStrings(value: unknown, found: string[] = []): string {
return found.join("\n");
}
/** Every agent that follows the skill binds it the same way. Fresh arrays per
/** 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 {