fix: expose MDTODO safe stdin skill drift
This commit is contained in:
@@ -6091,6 +6091,9 @@ function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
|
||||
readonly: record.readonly ?? false,
|
||||
skillCount: record.skillCount ?? 0,
|
||||
version: record.version ?? null,
|
||||
capabilities: record.capabilities ?? null,
|
||||
warnings: Array.isArray(record.warnings) ? record.warnings.map(String) : [],
|
||||
next: record.next ?? null,
|
||||
sourceSkillCount: record.sourceSkillCount ?? null,
|
||||
targetSkillCount: record.targetSkillCount ?? null,
|
||||
sourceMissingSkills: Array.isArray(record.sourceMissingSkills) ? record.sourceMissingSkills.map(String) : [],
|
||||
@@ -6151,6 +6154,8 @@ function compactSkillsSyncStatus(value: unknown, full = false): Record<string, u
|
||||
expected: record.expected ?? null,
|
||||
counts: record.counts ?? null,
|
||||
version: record.version ?? null,
|
||||
capabilities: record.capabilities ?? null,
|
||||
warnings: Array.isArray(record.warnings) ? record.warnings.map(String) : [],
|
||||
missing: record.missing ?? null,
|
||||
permissionFailures: Array.isArray(record.permissionFailures) ? record.permissionFailures.slice(0, full ? undefined : 4) : [],
|
||||
permissionFailureCount: Array.isArray(record.permissionFailures) ? record.permissionFailures.length : 0,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
collectMdtodoSafeTitleStdinCapability,
|
||||
renderMdtodoSafeTitleCommand,
|
||||
} from "../../src/components/microservices/code-queue/src/skill-availability";
|
||||
|
||||
function writeFixture(root: string, stdinReady: boolean): void {
|
||||
const scripts = join(root, "mdtodo-edit", "scripts");
|
||||
mkdirSync(scripts, { recursive: true });
|
||||
const option = stdinReady ? " --stdin" : " title";
|
||||
const script = `#!/usr/bin/env python3\nimport sys\nprint("usage: mdtodo " + sys.argv[1] + "\\n${option}")\n`;
|
||||
const path = join(scripts, "mdtodo-edit-cli.py");
|
||||
writeFileSync(path, script);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
describe("MDTODO safe title stdin adoption", () => {
|
||||
test("reports stale and ready source/target capability without blocking", () => {
|
||||
const temporary = mkdtempSync("/tmp/unidesk-mdtodo-safe-input-");
|
||||
try {
|
||||
const source = join(temporary, "source");
|
||||
const staleTarget = join(temporary, "stale-target");
|
||||
const readyTarget = join(temporary, "ready-target");
|
||||
writeFixture(source, true);
|
||||
writeFixture(staleTarget, false);
|
||||
writeFixture(readyTarget, true);
|
||||
|
||||
expect(collectMdtodoSafeTitleStdinCapability(source, staleTarget)).toMatchObject({
|
||||
nonBlocking: true,
|
||||
ready: false,
|
||||
freshness: "target-stale",
|
||||
source: { ready: true },
|
||||
target: { ready: false },
|
||||
});
|
||||
expect(collectMdtodoSafeTitleStdinCapability(source, readyTarget)).toMatchObject({
|
||||
nonBlocking: true,
|
||||
ready: true,
|
||||
freshness: "ready",
|
||||
warning: null,
|
||||
});
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps Markdown and shell syntax in quoted heredoc stdin", () => {
|
||||
const temporary = mkdtempSync("/tmp/unidesk-mdtodo-command-");
|
||||
try {
|
||||
const home = join(temporary, "home");
|
||||
const scripts = join(home, ".agents", "skills", "mdtodo-edit", "scripts");
|
||||
const output = join(temporary, "title.txt");
|
||||
const forbidden = join(temporary, "forbidden");
|
||||
mkdirSync(scripts, { recursive: true });
|
||||
writeFileSync(join(scripts, "mdtodo-edit-cli.py"), [
|
||||
"import os, sys",
|
||||
"open(os.environ['TITLE_OUTPUT'], 'w', encoding='utf-8').write(sys.stdin.read())",
|
||||
].join("\n"));
|
||||
const title = `修复 \`path\` 与 $(touch ${forbidden}) 的 'quoted' 标题`;
|
||||
const command = renderMdtodoSafeTitleCommand("docs/MDTODO/demo file.md", "add").replace("<title>", title);
|
||||
|
||||
expect(command).toContain("'docs/MDTODO/demo file.md'");
|
||||
expect(command).toContain("--stdin <<'EOF'");
|
||||
expect(command.split("--stdin", 1)[0]).not.toContain(title);
|
||||
const result = Bun.spawnSync(["bash", "-c", command], {
|
||||
env: { ...process.env, HOME: home, TITLE_OUTPUT: output },
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(readFileSync(output, "utf8")).toBe(`${title}\n`);
|
||||
expect(existsSync(forbidden)).toBe(false);
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,14 @@ export interface SkillAvailabilityReport {
|
||||
sourceSampledSkillNames: string[];
|
||||
targetSampledSkillNames: string[];
|
||||
};
|
||||
capabilities: {
|
||||
mdtodoSafeTitleStdin: MdtodoSafeTitleStdinCapability;
|
||||
};
|
||||
warnings: string[];
|
||||
next: {
|
||||
inspect: string;
|
||||
safeTitleExample: string;
|
||||
};
|
||||
repairHint: string | null;
|
||||
error: string | null;
|
||||
valuesPrinted: false;
|
||||
@@ -164,6 +172,10 @@ export interface SkillSyncPreflightReport {
|
||||
sourceSampledSkillNames: string[];
|
||||
targetSampledSkillNames: string[];
|
||||
};
|
||||
capabilities: {
|
||||
mdtodoSafeTitleStdin: MdtodoSafeTitleStdinCapability;
|
||||
};
|
||||
warnings: string[];
|
||||
missing: {
|
||||
sourceSkills: string[];
|
||||
targetSkills: string[];
|
||||
@@ -185,10 +197,30 @@ export interface SkillSyncPreflightReport {
|
||||
health: string;
|
||||
runtimePreflight: string;
|
||||
contractTest: string;
|
||||
safeTitleExample: string;
|
||||
};
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface MdtodoSafeTitleStdinProbe {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
commands: Record<"add" | "add-sub" | "update", boolean>;
|
||||
ready: boolean;
|
||||
identity: string | null;
|
||||
}
|
||||
|
||||
export interface MdtodoSafeTitleStdinCapability {
|
||||
name: "mdtodo-safe-title-stdin";
|
||||
nonBlocking: true;
|
||||
ready: boolean;
|
||||
freshness: "ready" | "source-stale" | "target-stale" | "missing";
|
||||
source: MdtodoSafeTitleStdinProbe;
|
||||
target: MdtodoSafeTitleStdinProbe;
|
||||
warning: string | null;
|
||||
next: string;
|
||||
}
|
||||
|
||||
export const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"];
|
||||
export const defaultSource = "/home/ubuntu/.agents/skills";
|
||||
export const expectedTarget = "/root/.agents/skills";
|
||||
@@ -303,6 +335,64 @@ function skillSetVersion(path: string, readable: boolean): SkillSyncPathReport["
|
||||
}
|
||||
}
|
||||
|
||||
function mdtodoCliPath(skillsPath: string): string {
|
||||
return resolve(skillsPath, "mdtodo-edit", "scripts", "mdtodo-edit-cli.py");
|
||||
}
|
||||
|
||||
function probeMdtodoSafeTitleStdin(skillsPath: string): MdtodoSafeTitleStdinProbe {
|
||||
const path = mdtodoCliPath(skillsPath);
|
||||
const exists = existsSync(path);
|
||||
const commands = { add: false, "add-sub": false, update: false };
|
||||
if (exists) {
|
||||
for (const command of Object.keys(commands) as Array<keyof typeof commands>) {
|
||||
const result = spawnSync("python3", [path, command, "--help"], { encoding: "utf8", timeout: 2000 });
|
||||
commands[command] = result.status === 0 && `${result.stdout ?? ""}\n${result.stderr ?? ""}`.includes("--stdin");
|
||||
}
|
||||
}
|
||||
const ready = Object.values(commands).every(Boolean);
|
||||
let identity: string | null = null;
|
||||
if (exists) {
|
||||
try {
|
||||
identity = createHash("sha256").update(readFileSync(path)).digest("hex").slice(0, 16);
|
||||
} catch {
|
||||
identity = null;
|
||||
}
|
||||
}
|
||||
return { path, exists, commands, ready, identity };
|
||||
}
|
||||
|
||||
export function collectMdtodoSafeTitleStdinCapability(sourcePath: string, targetPath: string): MdtodoSafeTitleStdinCapability {
|
||||
const source = probeMdtodoSafeTitleStdin(sourcePath);
|
||||
const target = probeMdtodoSafeTitleStdin(targetPath);
|
||||
const freshness = target.ready
|
||||
? "ready"
|
||||
: source.ready
|
||||
? "target-stale"
|
||||
: source.exists || target.exists
|
||||
? "source-stale"
|
||||
: "missing";
|
||||
const warning = freshness === "ready"
|
||||
? null
|
||||
: freshness === "target-stale"
|
||||
? "MDTODO safe title stdin is available in the approved source but missing from the runner target projection."
|
||||
: "MDTODO safe title stdin is missing from the approved source; update the managed agent_skills source before refreshing projections.";
|
||||
return {
|
||||
name: "mdtodo-safe-title-stdin",
|
||||
nonBlocking: true,
|
||||
ready: target.ready,
|
||||
freshness,
|
||||
source,
|
||||
target,
|
||||
warning,
|
||||
next: "bun scripts/cli.ts codex skills-sync --dry-run --full",
|
||||
};
|
||||
}
|
||||
|
||||
export function renderMdtodoSafeTitleCommand(file: string, command: "add" | "add-sub" | "update", taskId?: string): string {
|
||||
const positional = command === "add" ? shellQuote(file) : `${shellQuote(file)} ${shellQuote(taskId ?? "<taskId>")}`;
|
||||
return `python3 "$HOME/.agents/skills/mdtodo-edit/scripts/mdtodo-edit-cli.py" ${command} ${positional} --stdin <<'EOF'\n<title>\nEOF`;
|
||||
}
|
||||
|
||||
function accessProbe(path: string, mode: number, operation: string): { ok: boolean; failure: { path: string; operation: string; error: string } | null } {
|
||||
try {
|
||||
accessSync(path, mode);
|
||||
@@ -450,6 +540,7 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
|
||||
: targetSymlinkToSource ? "target-symlink" : "target";
|
||||
const resolvedPath = target;
|
||||
const selectedReport = targetProbe;
|
||||
const mdtodoSafeTitleStdin = collectMdtodoSafeTitleStdinCapability(source, target);
|
||||
const runnerUsable = !forbiddenPathConfigured && targetReady && !forbiddenPathExists;
|
||||
const contractOk = runnerUsable;
|
||||
const blocker = forbiddenPathConfigured
|
||||
@@ -539,6 +630,12 @@ export function collectSkillAvailability(options: SkillAvailabilityOptions): Ski
|
||||
sourceSampledSkillNames: sourceProbe.version.sampledSkillNames,
|
||||
targetSampledSkillNames: targetProbe.version.sampledSkillNames,
|
||||
},
|
||||
capabilities: { mdtodoSafeTitleStdin },
|
||||
warnings: mdtodoSafeTitleStdin.warning === null ? [] : [mdtodoSafeTitleStdin.warning],
|
||||
next: {
|
||||
inspect: mdtodoSafeTitleStdin.next,
|
||||
safeTitleExample: renderMdtodoSafeTitleCommand("docs/MDTODO/example.md", "add"),
|
||||
},
|
||||
repairHint: contractOk
|
||||
? null
|
||||
: sourceHasRequiredSkills
|
||||
@@ -561,6 +658,7 @@ export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {
|
||||
const forbiddenPathConfigured = forbiddenSourceConfigured || forbiddenTargetConfigured;
|
||||
const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
|
||||
const permissionFailures = [...source.permissionFailures, ...target.permissionFailures];
|
||||
const mdtodoSafeTitleStdin = collectMdtodoSafeTitleStdinCapability(sourcePath, targetPath);
|
||||
const blocker = forbiddenPathConfigured
|
||||
? "forbidden-skills-path-configured"
|
||||
: !source.report.approved
|
||||
@@ -623,6 +721,8 @@ export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {
|
||||
sourceSampledSkillNames: source.report.version.sampledSkillNames,
|
||||
targetSampledSkillNames: target.report.version.sampledSkillNames,
|
||||
},
|
||||
capabilities: { mdtodoSafeTitleStdin },
|
||||
warnings: mdtodoSafeTitleStdin.warning === null ? [] : [mdtodoSafeTitleStdin.warning],
|
||||
missing: {
|
||||
sourceSkills: source.report.missingSkills,
|
||||
targetSkills: target.report.missingSkills,
|
||||
@@ -659,6 +759,7 @@ export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {
|
||||
health: "bun scripts/cli.ts microservice health code-queue",
|
||||
runtimePreflight: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||
contractTest: "bun scripts/code-queue-runner-skills-contract-test.ts",
|
||||
safeTitleExample: renderMdtodoSafeTitleCommand("docs/MDTODO/example.md", "add"),
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user