fix(consult): read the live skill tree instead of a hash-pinned snapshot
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:
@@ -7,6 +7,7 @@ import {
|
||||
METHODOLOGY_TOTAL_MAX_CHARS,
|
||||
} from "../src/lib/consultation-methodology.ts";
|
||||
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
|
||||
import { resolveLiveJyotishSkill } from "../src/lib/skill-package-registry.ts";
|
||||
|
||||
test("the route's own strict checklist reaches the answer, not just the package listing", () => {
|
||||
const methodology = consultationMethodologyForDomains(["career"]);
|
||||
@@ -104,11 +105,12 @@ test("further reading offers the references the skill names, and only ones that
|
||||
);
|
||||
});
|
||||
|
||||
test("the delivered method is quoted from the pinned skill version", () => {
|
||||
test("the delivered method is quoted from the live skill tree", () => {
|
||||
const methodology = consultationMethodologyForDomains(["career"]);
|
||||
assert.ok(methodology);
|
||||
assert.equal(methodology.skill, "jyotish-vedic-astrology");
|
||||
assert.match(methodology.version, /^\d+\.\d+\.\d+$/);
|
||||
assert.equal(methodology.version, resolveLiveJyotishSkill().version);
|
||||
assert.equal(methodology.version.includes("/"), false);
|
||||
});
|
||||
|
||||
test("a section is found by heading text so the router may be renumbered", () => {
|
||||
|
||||
@@ -534,9 +534,14 @@ test("instrumentation retains the Skill guard and starts a singleton Node worker
|
||||
const productionAdapter = readFileSync(new URL("../src/lib/personal-report-worker.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.match(instrumentation, /verifyAllActiveSkillPackages\(\)/);
|
||||
assert.match(instrumentation, /resolveLiveJyotishSkill\(\)/);
|
||||
assert.match(instrumentation, /startPersonalReportWorker\(\)/);
|
||||
assert.ok(
|
||||
instrumentation.indexOf("verifyAllActiveSkillPackages()")
|
||||
< instrumentation.indexOf("resolveLiveJyotishSkill()"),
|
||||
);
|
||||
assert.ok(
|
||||
instrumentation.indexOf("resolveLiveJyotishSkill()")
|
||||
< instrumentation.indexOf("startPersonalReportWorker()"),
|
||||
);
|
||||
assert.match(productionAdapter, /jyotishaPersonalReportWorker/);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
import { Agent } from "@mastra/core/agent";
|
||||
import {
|
||||
@@ -17,9 +17,9 @@ function probeAgent(options: Record<string, unknown>) {
|
||||
return new Agent({ id: "skill-binding-probe", name: "Probe", model, instructions: "x", ...options } as never);
|
||||
}
|
||||
|
||||
function publishedSkillBody() {
|
||||
const published = readFileSync(resolve(jyotishSkillPackage.resolvedPath, "SKILL.md"), "utf8");
|
||||
return published.slice(published.indexOf("---", 3) + 4).trim();
|
||||
function liveSkillBody() {
|
||||
const live = readFileSync(resolve(jyotishSkillPackage.resolvedPath, "SKILL.md"), "utf8");
|
||||
return live.slice(live.indexOf("---", 3) + 4).trim();
|
||||
}
|
||||
|
||||
function boundMethodText() {
|
||||
@@ -29,20 +29,23 @@ function boundMethodText() {
|
||||
return jyotishSkillMethodBlock.slice(jyotishSkillMethodBlock.indexOf("\n", opened) + 1, closed).trim();
|
||||
}
|
||||
|
||||
test("the method the model follows is the published one, not the working tree's", () => {
|
||||
const body = publishedSkillBody();
|
||||
test("the method the model follows is the live skill, not a hashed snapshot", () => {
|
||||
const body = liveSkillBody();
|
||||
const bound = boundMethodText();
|
||||
|
||||
assert.match(jyotishSkillMethodBlock, new RegExp(`version="${jyotishSkillPackage.version}"`));
|
||||
// Every bound line still comes from the hash-checked package. The route into
|
||||
// the loader is that same package, so skill_read can only reach what the
|
||||
// registry hash covers.
|
||||
assert.match(jyotishSkillMethodBlock, new RegExp(`name="${jyotishSkillPackage.name}"`));
|
||||
assert.doesNotMatch(jyotishSkillMethodBlock, /<jyotish-skill name="[^"]+" version="/);
|
||||
for (const line of bound.split("\n")) {
|
||||
if (line.length === 0) continue;
|
||||
assert.ok(body.includes(line), line.slice(0, 80));
|
||||
}
|
||||
assert.ok(jyotishSkillRuntimePath.includes(jyotishSkillPackage.sha256));
|
||||
assert.ok(jyotishSkillRuntimePath.endsWith(jyotishSkillPackage.name));
|
||||
assert.equal(jyotishSkillRuntimePath.includes("/versions/"), false);
|
||||
assert.equal(existsSync(join(jyotishSkillRuntimePath, "versions")), false);
|
||||
assert.equal(
|
||||
realpathSync(join(jyotishSkillRuntimePath, "SKILL.md")),
|
||||
realpathSync(join(jyotishSkillPackage.resolvedPath, "SKILL.md")),
|
||||
);
|
||||
});
|
||||
|
||||
test("the bound method is the runtime excerpt, not the maintainer manual", () => {
|
||||
@@ -50,7 +53,7 @@ test("the bound method is the runtime excerpt, not the maintainer manual", () =>
|
||||
// answering contract and the repo-construction notes. Binding the whole
|
||||
// file is what pushed Mastra past its 500-line warning; the excerpt keeps
|
||||
// the contract and drops the notes.
|
||||
const body = publishedSkillBody();
|
||||
const body = liveSkillBody();
|
||||
const bound = boundMethodText();
|
||||
|
||||
for (const heading of ["商业运行时路由", "关联技法完整调取", "强制工作流", "五层硬约束", "核心方法论", "注意事项", "commercial_skill_truth_overlay"]) {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
@@ -17,7 +19,8 @@ import {
|
||||
createSkillPackageRegistry,
|
||||
resolveActiveSkillPackage,
|
||||
resolveExactSkillPackage,
|
||||
resolveSkillPackageRuntimePath,
|
||||
resolveLiveJyotishSkill,
|
||||
resolveLiveJyotishSkillRuntimePath,
|
||||
resolveSkillPackageVersion,
|
||||
verifyAllActiveSkillPackages,
|
||||
type SkillPackageIdentity,
|
||||
@@ -75,16 +78,11 @@ function identity(
|
||||
};
|
||||
}
|
||||
|
||||
test("checked-in registry verifies all three active packages and returns absolute directories", () => {
|
||||
test("checked-in registry verifies hashed product packages and leaves consult on the live skill", () => {
|
||||
const packages = verifyAllActiveSkillPackages({ projectRoot });
|
||||
assert.deepEqual(
|
||||
packages.map(({ name, version, sha256 }) => ({ name, version, sha256 })),
|
||||
[
|
||||
{
|
||||
name: "jyotish-vedic-astrology",
|
||||
version: "6.9.14",
|
||||
sha256: "c44e6f500c671d18dd47d9714610b766fbeabe00ca5e495e5225bc2c97f5f105",
|
||||
},
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.2",
|
||||
@@ -106,21 +104,21 @@ test("checked-in registry verifies all three active packages and returns absolut
|
||||
}
|
||||
assert.equal(packages[0]?.sourceCommit, null);
|
||||
assert.equal(packages[1]?.sourceCommit, null);
|
||||
assert.equal(packages[2]?.sourceCommit, null);
|
||||
|
||||
const active = resolveActiveSkillPackage("jyotish-vedic-astrology", {
|
||||
projectRoot,
|
||||
});
|
||||
assert.equal(active.version, "6.9.14");
|
||||
const runtimePath = resolveSkillPackageRuntimePath(active);
|
||||
assert.equal(basename(runtimePath), active.name);
|
||||
assert.equal(realpathSync(runtimePath), active.resolvedPath);
|
||||
assert.notEqual(runtimePath, active.resolvedPath);
|
||||
assert.deepEqual(
|
||||
resolveExactSkillPackage(active.name, active.version, active.sha256, {
|
||||
projectRoot,
|
||||
}),
|
||||
active,
|
||||
assert.throws(
|
||||
() => resolveActiveSkillPackage("jyotish-vedic-astrology", { projectRoot }),
|
||||
/No active skill package found/,
|
||||
);
|
||||
const live = resolveLiveJyotishSkill({ projectRoot });
|
||||
assert.equal(live.name, "jyotish-vedic-astrology");
|
||||
assert.equal(basename(live.resolvedPath), "jyotish-vedic-astrology");
|
||||
assert.equal(live.resolvedPath, resolve(projectRoot, "skills/jyotish-vedic-astrology"));
|
||||
const liveRuntime = resolveLiveJyotishSkillRuntimePath(live);
|
||||
assert.equal(basename(liveRuntime), live.name);
|
||||
assert.equal(existsSync(join(liveRuntime, "versions")), false);
|
||||
assert.equal(
|
||||
realpathSync(join(liveRuntime, "SKILL.md")),
|
||||
realpathSync(join(live.resolvedPath, "SKILL.md")),
|
||||
);
|
||||
const deprecatedRectification = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
@@ -489,3 +487,49 @@ test("invalid hash, commit, and non-directory package paths fail closed", (t) =>
|
||||
/must be a directory/,
|
||||
);
|
||||
});
|
||||
|
||||
test("live consult skill reads a hand-updated tree without a registry hash", (t) => {
|
||||
const f = fixture(t);
|
||||
const dir = join(f.root, "skills", "jyotish-vedic-astrology");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "SKILL.md"),
|
||||
"---\nname: jyotish-vedic-astrology\nversion: 9.9.9\n---\n# live method\n",
|
||||
);
|
||||
mkdirSync(join(dir, "references"));
|
||||
writeFileSync(join(dir, "references", "note.md"), "live reference\n");
|
||||
mkdirSync(join(dir, "versions", "6.9.14"), { recursive: true });
|
||||
writeFileSync(join(dir, "versions", "6.9.14", "SKILL.md"), "# stale snapshot\n");
|
||||
|
||||
const live = resolveLiveJyotishSkill({ projectRoot: f.root, skillPath: dir });
|
||||
assert.equal(live.version, "9.9.9");
|
||||
assert.equal(live.resolvedPath, realpathSync(dir));
|
||||
const runtime = resolveLiveJyotishSkillRuntimePath(live);
|
||||
assert.equal(existsSync(join(runtime, "versions")), false);
|
||||
assert.match(readFileSync(join(runtime, "SKILL.md"), "utf8"), /live method/);
|
||||
assert.equal(readFileSync(join(runtime, "references", "note.md"), "utf8"), "live reference\n");
|
||||
});
|
||||
|
||||
test("live consult skill rejects a path outside the project", (t) => {
|
||||
const f = fixture(t);
|
||||
const dir = join(f.root, "skills", "jyotish-vedic-astrology");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, "SKILL.md"), "---\nname: jyotish-vedic-astrology\n---\n# live\n");
|
||||
const outside = mkdtempSync(join(tmpdir(), "jyotish-skill-outside-"));
|
||||
t.after(() => rmSync(outside, { recursive: true, force: true }));
|
||||
assert.throws(
|
||||
() => resolveLiveJyotishSkill({ projectRoot: f.root, skillPath: outside }),
|
||||
/escapes the project root/,
|
||||
);
|
||||
});
|
||||
|
||||
test("live consult skill rejects a directory that is not named jyotish-vedic-astrology", (t) => {
|
||||
const f = fixture(t);
|
||||
const dir = join(f.root, "skills", "other-skill");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, "SKILL.md"), "---\nname: jyotish-vedic-astrology\n---\n# live\n");
|
||||
assert.throws(
|
||||
() => resolveLiveJyotishSkill({ projectRoot: f.root, skillPath: dir }),
|
||||
/must be named jyotish-vedic-astrology/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -173,6 +173,7 @@ test("railway web image uses Next standalone runtime output", () => {
|
||||
assert.match(config, /outputFileTracingRoot: repositoryRoot/);
|
||||
assert.match(startupGuard, /process\.env\.NEXT_RUNTIME !== "nodejs"/);
|
||||
assert.match(startupGuard, /verifyAllActiveSkillPackages\(\)/);
|
||||
assert.match(startupGuard, /resolveLiveJyotishSkill\(\)/);
|
||||
assert.match(dockerfile, /ARG NEXT_DEPLOYMENT_ID\n/);
|
||||
assert.match(
|
||||
dockerfile,
|
||||
@@ -184,6 +185,10 @@ test("railway web image uses Next standalone runtime output", () => {
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/frontend\/\.next\/static \/app\/frontend\/\.next\/static/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/frontend\/public \/app\/frontend\/public/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/frontend\/db \/app\/frontend\/db/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/SKILL.md \/app\/SKILL.md/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/assets \/app\/assets/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/references \/app\/references/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/scripts \/app\/scripts/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/skills \/app\/skills/);
|
||||
assert.match(
|
||||
dockerfile,
|
||||
|
||||
Reference in New Issue
Block a user