fix(consult): read the live skill tree instead of a hash-pinned snapshot
Independent Staging Quality Gate / validate (push) Successful in 9m18s
Independent Staging Quality Gate / publish (push) Has been cancelled

Manual SKILL.md updates were blocked by registry sha256 and a byte-equal versions/ gate. Consult now loads the operator-maintained tree; rectification and personal-report stay hashed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 10:25:55 +08:00
parent 233c728176
commit d35828e76d
14 changed files with 315 additions and 109 deletions
+7 -11
View File
@@ -6,7 +6,7 @@ import {
consultationDomainIds,
type ConsultationDomain,
} from "./consultation-domain-registry.ts";
import { resolveActiveSkillPackage } from "./skill-package-registry.ts";
import { resolveLiveJyotishSkill } from "./skill-package-registry.ts";
/**
* The strict checklist each consultation domain must be read against, named in
@@ -66,9 +66,9 @@ export const consultationMethodologySchema = z.object({
export type ConsultationMethodology = z.infer<typeof consultationMethodologySchema>;
const skillPackage = resolveActiveSkillPackage("jyotish-vedic-astrology");
const skill = resolveLiveJyotishSkill();
/** A published version's files never change, so each one is read from disk once per process. */
/** Live skill files are reread only after process restart; consult requests share one cache. */
const fileCache = new Map<string, string | null>();
function packageFile(relativePath: string): string | null {
@@ -76,7 +76,7 @@ function packageFile(relativePath: string): string | null {
if (hit !== undefined) return hit;
let content: string | null = null;
try {
content = readFileSync(resolve(skillPackage.resolvedPath, relativePath), "utf8");
content = readFileSync(resolve(skill.resolvedPath, relativePath), "utf8");
} catch {
content = null;
}
@@ -127,11 +127,7 @@ function namedReferences(instructions: string): string[] {
const planCache = new Map<string, ConsultationMethodology>();
/**
* The strict method for one domain plan, read from the hash-pinned package.
*
* Reading the pinned version rather than the live skill directory means the
* method that shaped an answer is the method a published version states, and
* the answer stays reproducible from the registry entry alone.
* The strict method for one domain plan, read from the live skill tree.
*/
export function consultationMethodologyForDomains(
domains: readonly ConsultationDomain[],
@@ -183,8 +179,8 @@ export function consultationMethodologyForDomains(
}
const methodology = consultationMethodologySchema.parse({
skill: skillPackage.name,
version: skillPackage.version,
skill: skill.name,
version: skill.version,
domains_without_strict_checklist: withoutChecklist,
sections,
further_reading: namedReferences(instructions),
+168 -1
View File
@@ -589,12 +589,179 @@ export class SkillPackageRegistry {
}
}
export const LIVE_JYOTISH_SKILL_NAME = "jyotish-vedic-astrology";
export type LiveJyotishSkill = {
name: typeof LIVE_JYOTISH_SKILL_NAME;
version: string;
resolvedPath: string;
};
function parseSkillFrontmatter(raw: string): { name?: string; version?: string } {
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
if (!block) return {};
const name = /^name:\s*["']?([a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)["']?\s*$/im.exec(block[1]);
const version = /^version:\s*["']?([^\s"']+)["']?\s*$/im.exec(block[1]);
return {
...(name ? { name: name[1] } : {}),
...(version ? { version: version[1] } : {}),
};
}
function resolveSkillProjectRoot(options: SkillPackageRegistryOptions = {}): string {
const requestedProjectRoot = resolve(
options.projectRoot ?? process.env.JYOTISHA_PROJECT_ROOT ?? SOURCE_TREE_PROJECT_ROOT,
);
const projectRoot = resolveExistingPath(
requestedProjectRoot,
"Skill registry project root",
);
assertDirectory(projectRoot, "Skill registry project root");
return projectRoot;
}
const LIVE_SKILL_RUNTIME_ENTRIES = [
"SKILL.md",
"references",
"scripts",
"assets",
] as const;
export type LiveJyotishSkillOptions = SkillPackageRegistryOptions & {
/** Overrides `JYOTISH_SKILL_PATH` when tests or callers pass an explicit tree. */
skillPath?: string;
};
/**
* The natal consult skill is the live tree the operator updates by hand.
* It is not a hashed registry package: copy SKILL.md / references / scripts
* and restart, without bumping a version or recomputing sha256.
*/
export function resolveLiveJyotishSkill(
options: LiveJyotishSkillOptions = {},
): LiveJyotishSkill {
const projectRoot = resolveSkillProjectRoot(options);
const configured = options.skillPath?.trim() || process.env.JYOTISH_SKILL_PATH?.trim();
const requested = configured
? (isAbsolute(configured) ? resolve(configured) : resolve(projectRoot, configured))
: resolve(projectRoot, "skills", LIVE_JYOTISH_SKILL_NAME);
if (!isInside(projectRoot, requested)) {
let realRequested: string;
try {
realRequested = realpathSync(requested);
} catch {
fail("Live Jyotish skill path escapes the project root");
}
if (!isInside(projectRoot, realRequested)) {
fail("Live Jyotish skill path escapes the project root");
}
}
const resolvedPath = resolveExistingPath(requested, "Live Jyotish skill directory");
if (!isInside(projectRoot, resolvedPath)) {
fail("Live Jyotish skill directory resolves outside the project root");
}
assertDirectory(resolvedPath, "Live Jyotish skill directory");
if (basename(resolvedPath) !== LIVE_JYOTISH_SKILL_NAME) {
fail(`Live Jyotish skill directory must be named ${LIVE_JYOTISH_SKILL_NAME}`);
}
const skillPath = resolve(resolvedPath, "SKILL.md");
const skillFile = resolveExistingPath(skillPath, `SKILL.md for ${LIVE_JYOTISH_SKILL_NAME}`);
if (!isInside(projectRoot, skillFile)) {
fail(`SKILL.md for ${LIVE_JYOTISH_SKILL_NAME} resolves outside the project root`);
}
let raw: string;
try {
raw = readFileSync(skillFile, "utf8");
} catch (error) {
fail(
`SKILL.md for ${LIVE_JYOTISH_SKILL_NAME} cannot be read: ${
error instanceof Error ? error.message : "unknown filesystem error"
}`,
);
}
const frontmatter = parseSkillFrontmatter(raw);
if (frontmatter.name && frontmatter.name !== LIVE_JYOTISH_SKILL_NAME) {
fail(
`Live Jyotish skill frontmatter name must be ${LIVE_JYOTISH_SKILL_NAME}`,
);
}
for (const name of LIVE_SKILL_RUNTIME_ENTRIES) {
const entry = resolve(resolvedPath, name);
if (!existsSync(entry)) continue;
const real = resolveExistingPath(entry, `Live Jyotish skill ${name}`);
if (!isInside(projectRoot, real)) {
fail(`Live Jyotish skill ${name} resolves outside the project root`);
}
}
return Object.freeze({
name: LIVE_JYOTISH_SKILL_NAME,
version: frontmatter.version || "live",
resolvedPath,
});
}
/**
* Mastra-compatible view of the live consult skill.
*
* The git-tracked live directory also contains leftover hashed
* `versions/` snapshots. Those must not be on the loader path: Mastra walks
* every real subdirectory, and the operator's updates live in the four
* top-level entries (often git-tracked symlinks into the repo root).
*/
export function resolveLiveJyotishSkillRuntimePath(
skill: LiveJyotishSkill,
): string {
const token = createHash("sha256").update(skill.resolvedPath).digest("hex").slice(0, 12);
const runtimeParent = join(RUNTIME_SKILL_ROOT, "live", token);
mkdirSync(runtimeParent, { recursive: true });
const runtimePath = join(runtimeParent, skill.name);
if (basename(runtimePath) !== skill.name) {
fail(`Live runtime skill path must end with ${skill.name}`);
}
mkdirSync(runtimePath, { recursive: true });
for (const name of LIVE_SKILL_RUNTIME_ENTRIES) {
const source = resolve(skill.resolvedPath, name);
if (!existsSync(source)) continue;
const target = resolveExistingPath(source, `Live Jyotish skill ${name}`);
const dest = join(runtimePath, name);
try {
const existing = lstatSync(dest);
if (!existing.isSymbolicLink()) {
fail(`Live runtime entry ${name} is not a symbolic-link alias`);
}
const linked = resolveExistingPath(dest, `Live runtime alias for ${name}`);
if (linked !== target) {
fail(`Live runtime entry ${name} points at an unexpected path`);
}
} catch (error) {
if (error instanceof SkillPackageRegistryError) throw error;
try {
const linkType = statSync(target).isDirectory() ? "dir" : "file";
symlinkSync(target, dest, linkType);
} catch (linkError) {
fail(
`Live runtime alias for ${name} could not be created: ${
linkError instanceof Error ? linkError.message : "unknown filesystem error"
}`,
);
}
}
}
return runtimePath;
}
/**
* Return a Mastra-compatible read-only alias for a verified package.
*
* Mastra validates that the configured skill directory basename matches the
* frontmatter name. Versioned registry packages intentionally live under a
* numeric directory (for example, `versions/6.9.14`), so loading that
* numeric directory (for example, `versions/10.0.2`), so loading that
* directory directly fails validation. The alias keeps the verified package
* bytes and identity unchanged while exposing the canonical skill name at the
* loader boundary.