TRELLIS1 迁移 Trellis 主导开发流程

这个提交包含在:
2026-05-26 09:28:51 +08:00
父节点 7c65d418f3
当前提交 e7baa6c234
共修改 114 个文件,包含 14684 行新增和 178 行删除
+257
查看文件
@@ -0,0 +1,257 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const repoRoot = process.cwd();
const requiredSpecs = [
".trellis/spec/hyb-oa/index.md",
".trellis/spec/hyb-oa/project-governance.md",
".trellis/spec/hyb-oa/backend-fastify-prisma.md",
".trellis/spec/hyb-oa/frontend-react-vite-design.md",
".trellis/spec/hyb-oa/approval-workflow.md",
".trellis/spec/hyb-oa/reimbursement-invoice-ocr.md",
".trellis/spec/hyb-oa/wecom-boundary.md",
".trellis/spec/hyb-oa/deployment-trialrun.md",
".trellis/spec/hyb-oa/docs-and-evidence.md",
".trellis/spec/hyb-oa/security-privacy.md",
];
const requiredDocMentions = [
"docs/Agent开发交付实践.md",
"docs/文档索引.md",
"docs/文档事实源状态矩阵.md",
".agents/skills/hyb-oa-delivery/SKILL.md",
];
const issueIdPattern = /^TRELLIS\d+-\d+$/;
function readText(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
}
function exists(relativePath) {
return fs.existsSync(path.join(repoRoot, relativePath));
}
function listFiles(relativeDir) {
const abs = path.join(repoRoot, relativeDir);
if (!fs.existsSync(abs)) return [];
const result = [];
for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
const relative = path.join(relativeDir, entry.name);
if (entry.isDirectory()) {
if (relative.includes(`${path.sep}__pycache__`)) continue;
result.push(...listFiles(relative));
} else {
result.push(relative);
}
}
return result;
}
function listTaskDirs() {
const tasksDir = path.join(repoRoot, ".trellis/tasks");
if (!fs.existsSync(tasksDir)) return [];
return fs
.readdirSync(tasksDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "archive")
.map((entry) => `.trellis/tasks/${entry.name}`);
}
function parseCsvLine(line) {
const cells = [];
let current = "";
let quoted = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
const next = line[index + 1];
if (quoted && char === "\"" && next === "\"") {
current += "\"";
index += 1;
continue;
}
if (char === "\"") {
quoted = !quoted;
continue;
}
if (!quoted && char === ",") {
cells.push(current);
current = "";
continue;
}
current += char;
}
cells.push(current);
return cells;
}
function issueIdsFromCsv(relativePath) {
const text = readText(relativePath);
const lines = text.split(/\r?\n/).filter((line) => line.trim());
if (lines.length < 2) return new Set();
const header = parseCsvLine(lines[0]);
const idIndex = header.indexOf("ID");
if (idIndex < 0) return new Set();
return new Set(lines.slice(1).map((line) => parseCsvLine(line)[idIndex]).filter(Boolean));
}
function normalizeIssueCsv(value) {
if (typeof value !== "string") return "";
return value.trim().replace(/^\.?\//, "");
}
function createResult() {
return {
status: "PASS",
checks: [],
warnings: [],
errors: [],
};
}
function pass(result, name, detail = "") {
result.checks.push({ name, status: "PASS", detail });
}
function warn(result, code, message) {
result.warnings.push({ code, message });
}
function fail(result, code, message) {
result.status = "FAIL";
result.errors.push({ code, message });
}
function checkCoreFiles(result) {
const requiredFiles = [
"AGENTS.md",
".trellis/workflow.md",
".trellis/config.yaml",
...requiredSpecs,
];
for (const file of requiredFiles) {
if (!exists(file)) fail(result, "missing_file", file);
}
if (result.status === "PASS") pass(result, "core_files", `${requiredFiles.length} required files exist`);
if (exists("AGENTS.md")) {
const agents = readText("AGENTS.md");
if (!agents.includes(".trellis/workflow.md")) {
fail(result, "agents.workflow_pointer", "AGENTS.md must point to .trellis/workflow.md");
} else {
pass(result, "agents.workflow_pointer", "AGENTS.md points to Trellis workflow");
}
}
if (exists(".trellis/config.yaml")) {
const config = readText(".trellis/config.yaml");
if (!/session_auto_commit:\s*false/.test(config)) {
fail(result, "config.auto_commit", "session_auto_commit must be false");
} else {
pass(result, "config.auto_commit", "session_auto_commit=false");
}
if (!/dispatch_mode:\s*inline/.test(config)) {
warn(result, "config.dispatch_mode", "Codex dispatch_mode is not explicitly inline");
}
}
}
function checkTaskBindings(result) {
const taskDirs = listTaskDirs();
if (taskDirs.length === 0) {
fail(result, "tasks.none", "no .trellis/tasks/* directories found");
return;
}
for (const dir of taskDirs) {
const taskJsonPath = `${dir}/task.json`;
const prdPath = `${dir}/prd.md`;
if (!exists(taskJsonPath)) {
fail(result, "task.missing_task_json", `${dir} missing task.json`);
continue;
}
const task = JSON.parse(readText(taskJsonPath));
const issueCsv = normalizeIssueCsv(task.meta?.issueCsv);
const issueIds = Array.isArray(task.meta?.issueIds) ? task.meta.issueIds : [];
const prdText = exists(prdPath) ? readText(prdPath) : "";
const csvFromPrd = prdText.match(/issues\/[A-Za-z0-9_.-]+\.csv/)?.[0] ?? "";
const boundCsv = issueCsv || csvFromPrd;
if (!boundCsv) {
fail(result, "task.issue_csv", `${dir} must bind an Issue CSV in task.json meta or prd.md`);
continue;
}
if (!exists(boundCsv)) {
fail(result, "task.issue_csv_missing", `${dir} references missing ${boundCsv}`);
continue;
}
const ids = issueIds.length > 0 ? issueIds : [...prdText.matchAll(/TRELLIS\d+-\d+/g)].map((match) => match[0]);
if (ids.length === 0) {
fail(result, "task.issue_ids", `${dir} must bind at least one TRELLIS Issue ID`);
continue;
}
const csvIds = issueIdsFromCsv(boundCsv);
for (const id of ids) {
if (!issueIdPattern.test(id)) {
fail(result, "task.issue_id_format", `${dir} has invalid Issue ID ${id}`);
} else if (!csvIds.has(id)) {
fail(result, "task.issue_id_missing", `${dir} references ${id}, not found in ${boundCsv}`);
}
}
}
if (result.status === "PASS") pass(result, "task_bindings", `${taskDirs.length} task directories are bound`);
}
function checkDocsRegistration(result) {
for (const file of requiredDocMentions) {
if (!exists(file)) {
fail(result, "docs.required_missing", file);
continue;
}
const text = readText(file);
const hasTrellis = text.includes("TRELLIS1") || text.includes(".trellis") || text.includes("Trellis");
if (!hasTrellis) {
fail(result, "docs.trellis_registration", `${file} must mention Trellis/TRELLIS1`);
}
}
if (result.status === "PASS") pass(result, "docs_registration", `${requiredDocMentions.length} fact sources mention Trellis`);
}
function checkSensitiveData(result) {
const scanFiles = [
"AGENTS.md",
...listFiles(".trellis").filter((file) => /\.(md|json|jsonl|ya?ml|toml|txt)$/i.test(file)),
];
const patterns = [
{ code: "private_key", pattern: /BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY/ },
{ code: "assigned_secret", pattern: /\b(secret|token|password|api[_-]?key)\b\s*[:=]\s*["']?[A-Za-z0-9_./+=:-]{16,}/i },
{ code: "live_smoke_password", pattern: /LIVE_SMOKE_[A-Z_]*PASSWORD\s*[:=]/ },
{ code: "complete_local_path", pattern: /\/home\/shunli\/Projects\/[^ \n)"']+/ },
{ code: "raw_wecom_id", pattern: /\b(sp_no|template_id|userid)\s*[:=]\s*["']?[A-Za-z0-9_-]{12,}/i },
];
for (const file of scanFiles) {
const text = readText(file);
for (const { code, pattern } of patterns) {
if (pattern.test(text)) {
fail(result, `sensitive.${code}`, `possible sensitive data in ${file}`);
}
}
}
if (result.status === "PASS") pass(result, "sensitive_scan", `${scanFiles.length} Trellis files scanned`);
}
const result = createResult();
try {
checkCoreFiles(result);
checkTaskBindings(result);
checkDocsRegistration(result);
checkSensitiveData(result);
} catch (error) {
fail(result, "exception", error instanceof Error ? error.message : String(error));
}
console.log(JSON.stringify(result, null, 2));
process.exit(result.status === "PASS" ? 0 : 1);