fix: guard hwlab product routing from issue 20
This commit is contained in:
@@ -737,7 +737,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(Array.isArray(boardRowGetRow.cells) && boardRowGetRow.cells[1] === "OPEN", "board-row get should expose the GitHub status column", boardRowGetRow);
|
||||
assertCondition(boardRowGetFields.branch === "master" && boardRowGetFields.status === "pass" && boardRowGetFields.validation === "pass" && boardRowGetFields.tasks === "cq-35" && String(boardRowGetFields.focus ?? "").includes("当前关注点"), "board-row get should expose canonical field aliases", boardRowGetFields);
|
||||
const boardRowGetHint = boardRowGetData.codeQueueBoardHint as JsonRecord;
|
||||
assertCondition(boardRowGetHint.detected === false && String(boardRowGetHint.warning ?? "").includes("#20 is the long-term board only"), "board-row get should remind callers not to put daily briefs into #20", boardRowGetHint);
|
||||
assertCondition(boardRowGetHint.detected === false && String(boardRowGetHint.warning ?? "").includes("governance board only"), "board-row get should remind callers that #20 is governance-only", boardRowGetHint);
|
||||
|
||||
const boardRowUpsertUpdateRequestCountBefore = mock.requests.length;
|
||||
const boardRowUpsertUpdate = await runCli([
|
||||
@@ -968,7 +968,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const boardRowAddNoGuardData = dataOf(boardRowAddNoGuard.json ?? {});
|
||||
assertCondition(boardRowAddNoGuardData.dryRun === true && boardRowAddNoGuardData.planned === true, "board-row add without guard should not PATCH GitHub", boardRowAddNoGuardData);
|
||||
const boardRowAddNoGuardPlan = boardRowAddNoGuardData.add as JsonRecord;
|
||||
assertCondition(boardRowAddNoGuardPlan.section === "open" && boardRowAddNoGuardPlan.insertAfterLine > 0, "board-row add without guard should still return an insertion plan", boardRowAddNoGuardPlan);
|
||||
assertCondition(boardRowAddNoGuardPlan.section === "open" && Number(boardRowAddNoGuardPlan.insertAfterLine ?? 0) > 0, "board-row add without guard should still return an insertion plan", boardRowAddNoGuardPlan);
|
||||
const boardRowAddNoGuardWriteCount = mock.requests.slice(boardRowAddNoGuardRequestCountBefore).filter((request) => request.method === "PATCH").length;
|
||||
assertCondition(boardRowAddNoGuardWriteCount === 0, "board-row add without guard must not PATCH GitHub", { requests: mock.requests.slice(boardRowAddNoGuardRequestCountBefore) });
|
||||
|
||||
@@ -978,7 +978,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const boardRowAddDryRunData = dataOf(boardRowAddDryRun.json ?? {});
|
||||
assertCondition(boardRowAddDryRunData.command === "issue board-row add" && boardRowAddDryRunData.dryRun === true && boardRowAddDryRunData.planned === true, "board-row add should default to dry-run", boardRowAddDryRunData);
|
||||
const boardRowAddDryRunPlan = boardRowAddDryRunData.add as JsonRecord;
|
||||
assertCondition(boardRowAddDryRunPlan.section === "open" && boardRowAddDryRunPlan.validation.actualStatus === "OPEN", "board-row add dry-run should validate the target section and GitHub status", boardRowAddDryRunPlan);
|
||||
const boardRowAddDryRunValidation = boardRowAddDryRunPlan.validation as JsonRecord;
|
||||
assertCondition(boardRowAddDryRunPlan.section === "open" && boardRowAddDryRunValidation.actualStatus === "OPEN", "board-row add dry-run should validate the target section and GitHub status", boardRowAddDryRunPlan);
|
||||
const boardRowAddDryRunPatchCount = mock.requests.slice(boardRowAddDryRunRequestCountBefore).filter((request) => request.method === "PATCH").length;
|
||||
assertCondition(boardRowAddDryRunPatchCount === 0, "board-row add dry-run must not PATCH GitHub", { requests: mock.requests.slice(boardRowAddDryRunRequestCountBefore) });
|
||||
|
||||
@@ -992,7 +993,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const boardRowAddData = dataOf(boardRowAdd.json ?? {});
|
||||
assertCondition(boardRowAddData.dryRun === false && boardRowAddData.rest === true, "board-row add should report a real REST update", boardRowAddData);
|
||||
const boardRowAddPlan = boardRowAddData.add as JsonRecord;
|
||||
assertCondition(boardRowAddPlan.section === "open" && boardRowAddPlan.validation.expectedStatus === "OPEN" && boardRowAddPlan.validation.actualStatus === "OPEN", "board-row add should validate section/status alignment", boardRowAddPlan);
|
||||
const boardRowAddValidation = boardRowAddPlan.validation as JsonRecord;
|
||||
assertCondition(boardRowAddPlan.section === "open" && boardRowAddValidation.expectedStatus === "OPEN" && boardRowAddValidation.actualStatus === "OPEN", "board-row add should validate section/status alignment", boardRowAddPlan);
|
||||
const boardRowAddRequests = mock.requests.slice(boardRowAddRequestCountBefore).filter((request) => request.method === "PATCH" && request.url === "/repos/pikasTech/unidesk/issues/20");
|
||||
assertCondition(boardRowAddRequests.length === 1, "board-row add should send exactly one PATCH", { requests: mock.requests.slice(boardRowAddRequestCountBefore) });
|
||||
const boardRowAddPayload = JSON.parse(boardRowAddRequests[0]?.body ?? "{}") as JsonRecord;
|
||||
@@ -1066,7 +1068,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const boardRowMoveDryRunData = dataOf(boardRowMoveDryRun.json ?? {});
|
||||
assertCondition(boardRowMoveDryRunData.command === "issue board-row move" && boardRowMoveDryRunData.dryRun === true && boardRowMoveDryRunData.planned === true, "board-row move should default to dry-run", boardRowMoveDryRunData);
|
||||
const boardRowMoveDryRunPlan = boardRowMoveDryRunData.move as JsonRecord;
|
||||
assertCondition(boardRowMoveDryRunPlan.from === "open" && boardRowMoveDryRunPlan.to === "closed" && boardRowMoveDryRunPlan.status.requested === "CLOSED", "board-row move dry-run should plan the cross-section migration", boardRowMoveDryRunPlan);
|
||||
const boardRowMoveDryRunStatus = boardRowMoveDryRunPlan.status as JsonRecord;
|
||||
assertCondition(boardRowMoveDryRunPlan.from === "open" && boardRowMoveDryRunPlan.to === "closed" && boardRowMoveDryRunStatus.requested === "CLOSED", "board-row move dry-run should plan the cross-section migration", boardRowMoveDryRunPlan);
|
||||
|
||||
const boardRowMoveConflict = await runCli(["gh", "issue", "board-row", "move", "35", "--repo", "pikasTech/unidesk", "--board-issue", "20", "--to", "closed", "--status", "OPEN", "--dry-run"], env);
|
||||
assertCondition(boardRowMoveConflict.status !== 0, "board-row move status conflict should fail structurally", boardRowMoveConflict.json ?? { stdout: boardRowMoveConflict.stdout, stderr: boardRowMoveConflict.stderr });
|
||||
@@ -1082,7 +1085,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const boardRowMoveData = dataOf(boardRowMove.json ?? {});
|
||||
assertCondition(boardRowMoveData.dryRun === false && boardRowMoveData.rest === true, "board-row move should report a real REST update", boardRowMoveData);
|
||||
const boardRowMovePlan = boardRowMoveData.move as JsonRecord;
|
||||
assertCondition(boardRowMovePlan.from === "open" && boardRowMovePlan.to === "closed" && boardRowMovePlan.status.new === "CLOSED", "board-row move should update the GitHub status column", boardRowMovePlan);
|
||||
const boardRowMoveStatus = boardRowMovePlan.status as JsonRecord;
|
||||
assertCondition(boardRowMovePlan.from === "open" && boardRowMovePlan.to === "closed" && boardRowMoveStatus.new === "CLOSED", "board-row move should update the GitHub status column", boardRowMovePlan);
|
||||
const boardRowMoveLinePlan = boardRowMovePlan.linePlan as JsonRecord;
|
||||
const boardRowMoveSectionPlan = boardRowMovePlan.sectionPlan as JsonRecord;
|
||||
assertCondition(boardRowMoveLinePlan.action === "move" && Number(boardRowMoveLinePlan.sourceLineNumber ?? 0) > 0 && Number(boardRowMoveLinePlan.newLineNumber ?? 0) > 0, "board-row move should expose a line plan", boardRowMoveLinePlan);
|
||||
@@ -1123,7 +1127,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const readIssue = readBodyData.issue as JsonRecord;
|
||||
assertCondition(typeof readIssue.body === "string" && readIssue.body.includes("## 看板(OPEN)"), ".data.issue.body should remain readable", readBodyData);
|
||||
const readBodyHint = readBodyData.codeQueueBoardHint as JsonRecord;
|
||||
assertCondition(readBodyHint.detected === false && String(readBodyHint.warning ?? "").includes("#20 is the long-term board only"), "issue read #20 should remind callers not to put daily briefs into #20", readBodyHint);
|
||||
assertCondition(readBodyHint.detected === false && String(readBodyHint.warning ?? "").includes("governance board only"), "issue read #20 should remind callers that #20 is governance-only", readBodyHint);
|
||||
const selectedJson = readBodyData.json as JsonRecord;
|
||||
assertCondition(typeof selectedJson.body === "string" && selectedJson.body === readIssue.body, "selected json body should match issue body", readBodyData);
|
||||
assertCondition(!("comments" in selectedJson), "--json body should not imply comments field", selectedJson);
|
||||
@@ -1216,6 +1220,88 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const pollutedBoardHint = pollutedBoardGuard.codeQueueBoardHint as JsonRecord;
|
||||
assertCondition(pollutedBoardHint.detected === true && String(pollutedBoardHint.route ?? "").includes("daily rolling commander brief"), "#20 guard should hint to move updates to the daily brief issue", pollutedBoardHint);
|
||||
|
||||
const hwlabProductBoardFile = join(tmp, "hwlab-product-board.md");
|
||||
writeFileSync(hwlabProductBoardFile, [
|
||||
"# Code Queue",
|
||||
"",
|
||||
"## 看板(OPEN)",
|
||||
"",
|
||||
"| Issue | GitHub 状态 | Branch | 验收状态 | 相关 Code Queue 任务 | 当前关注点 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- |",
|
||||
"| [pikasTech/HWLAB#108](https://github.com/pikasTech/HWLAB/issues/108) HWLAB user feedback | OPEN | main | product | cq-hwlab | Cloud Workbench 用户反馈 | doing |",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
const hwlabProductBoard = await runCli(["gh", "issue", "update", "20", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", hwlabProductBoardFile, "--dry-run"], env);
|
||||
assertCondition(hwlabProductBoard.status !== 0, "#20 body guard should reject HWLAB product issue rows", hwlabProductBoard.json ?? { stdout: hwlabProductBoard.stdout });
|
||||
const hwlabProductBoardData = failedDataOf(hwlabProductBoard.json ?? {});
|
||||
const hwlabProductBoardGuard = hwlabProductBoardData.guard as JsonRecord;
|
||||
assertCondition(Array.isArray(hwlabProductBoardGuard.failures) && hwlabProductBoardGuard.failures.includes("code-queue-board-contains-hwlab-product-work"), "#20 guard should report HWLAB product routing pollution", hwlabProductBoardGuard);
|
||||
const hwlabProductHint = hwlabProductBoardGuard.codeQueueBoardHint as JsonRecord;
|
||||
const hwlabProductRouting = hwlabProductHint.hwlabProductRouting as JsonRecord;
|
||||
assertCondition(hwlabProductRouting.detected === true && String(hwlabProductRouting.route ?? "").includes("pikasTech/HWLAB"), "#20 guard should route HWLAB product issues to the HWLAB repo", hwlabProductRouting);
|
||||
|
||||
const hwlabProductUpsert = await runCli([
|
||||
"gh",
|
||||
"issue",
|
||||
"board-row",
|
||||
"upsert",
|
||||
"108",
|
||||
"--repo",
|
||||
"pikasTech/unidesk",
|
||||
"--board-issue",
|
||||
"20",
|
||||
"--section",
|
||||
"open",
|
||||
"--branch",
|
||||
"main",
|
||||
"--tasks",
|
||||
"cq-hwlab",
|
||||
"--summary",
|
||||
"pikasTech/HWLAB#108 HWLAB user feedback",
|
||||
"--focus",
|
||||
"Cloud Workbench 用户反馈",
|
||||
"--validation",
|
||||
"product",
|
||||
"--progress",
|
||||
"doing",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(hwlabProductUpsert.status !== 0, "#20 board-row upsert should reject HWLAB product issue rows", hwlabProductUpsert.json ?? { stdout: hwlabProductUpsert.stdout });
|
||||
const hwlabProductUpsertData = failedDataOf(hwlabProductUpsert.json ?? {});
|
||||
const hwlabProductUpsertGuard = hwlabProductUpsertData.guard as JsonRecord;
|
||||
assertCondition(Array.isArray(hwlabProductUpsertGuard.warnings) && hwlabProductUpsertGuard.warnings.includes("code-queue-board-contains-hwlab-product-work"), "board-row upsert guard should report HWLAB product routing pollution", hwlabProductUpsertGuard);
|
||||
|
||||
const hwlabGovernanceGuard = await runCli([
|
||||
"gh",
|
||||
"issue",
|
||||
"board-row",
|
||||
"upsert",
|
||||
"109",
|
||||
"--repo",
|
||||
"pikasTech/unidesk",
|
||||
"--board-issue",
|
||||
"20",
|
||||
"--section",
|
||||
"open",
|
||||
"--branch",
|
||||
"master",
|
||||
"--tasks",
|
||||
"cq-guard",
|
||||
"--summary",
|
||||
"UniDesk CLI guard for HWLAB#108 routing",
|
||||
"--focus",
|
||||
"commander governance guard prevents HWLAB product misfile",
|
||||
"--validation",
|
||||
"dry-run guard",
|
||||
"--progress",
|
||||
"ready",
|
||||
"--dry-run",
|
||||
], env);
|
||||
assertCondition(hwlabGovernanceGuard.status === 0, "#20 board-row upsert should allow UniDesk governance rows that mention HWLAB as routing context", hwlabGovernanceGuard.json ?? { stdout: hwlabGovernanceGuard.stdout });
|
||||
const hwlabGovernanceGuardData = dataOf(hwlabGovernanceGuard.json ?? {});
|
||||
const hwlabGovernanceGuardSummary = hwlabGovernanceGuardData.guard as JsonRecord;
|
||||
assertCondition(hwlabGovernanceGuardSummary.ok === true && !(hwlabGovernanceGuardSummary.warnings as unknown[]).includes("code-queue-board-contains-hwlab-product-work"), "governance rows mentioning HWLAB should not be classified as HWLAB product work", hwlabGovernanceGuardSummary);
|
||||
|
||||
const commanderBriefBlocked = await runCli(["gh", "issue", "edit", "24", "--repo", "pikasTech/unidesk", "--body-file", missingHeadingFile, "--dry-run"], env);
|
||||
assertCondition(commanderBriefBlocked.status !== 0, "#24 missing heading should fail", commanderBriefBlocked.json ?? { stdout: commanderBriefBlocked.stdout });
|
||||
const commanderBriefData = failedDataOf(commanderBriefBlocked.json ?? {});
|
||||
@@ -1397,6 +1483,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"unsupported --json fields fail structurally",
|
||||
"issue edit --body-file rejects literal null",
|
||||
"#20/#24 body profile guards reject missing headings or wrong profile",
|
||||
"#20 body and board-row guards reject HWLAB product issue routing and point to pikasTech/HWLAB",
|
||||
"#20 board-row guard allows UniDesk governance rows that mention HWLAB only as routing context",
|
||||
"#24 commander-brief profile remains compatible",
|
||||
"daily commander brief issues match commander-brief profile by title",
|
||||
"non-brief issues fail commander-brief profile without printing token",
|
||||
|
||||
+98
-8
@@ -993,16 +993,102 @@ function commanderBriefUpdateHeadings(body: string): string[] {
|
||||
.filter((line) => isCommanderBriefUpdateHeading(line));
|
||||
}
|
||||
|
||||
function codeQueueBoardHwlabProductRoutingFindings(body: string | undefined): Array<Record<string, unknown>> {
|
||||
if (body === undefined) return [];
|
||||
const findings: Array<Record<string, unknown>> = [];
|
||||
const directIssuePatterns = [
|
||||
{ kind: "hwlab-repo-issue-url", pattern: /(?:https?:\/\/github\.com\/)?pikasTech\/HWLAB\/issues\/\d+/iu },
|
||||
{ kind: "hwlab-repo-issue-short-ref", pattern: /\bpikasTech\/HWLAB#\d+\b/iu },
|
||||
{ kind: "hwlab-short-issue-ref", pattern: /\bHWLAB#\d+\b/iu },
|
||||
];
|
||||
const hwlabContextPattern = /\bHWLAB\b|pikasTech\/HWLAB|hwlab-/iu;
|
||||
const strongProductPattern = /hwlab-cloud-web|Cloud Workbench|DEV-LIVE|res_boxsimu|hwlab-patch-panel|patch panel|M3\s*(?:虚拟硬件|可信闭环)/iu;
|
||||
const genericProductPattern = /用户反馈|user feedback/iu;
|
||||
const governanceContextPattern = /\b(?:commander|Code Queue|CLI|infra|governance|guard|guardrail|routing|misfile|board)\b|治理|调度|基础设施|守卫|边界|分流|看板/iu;
|
||||
const lines = normalizeNewlines(body).split("\n");
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
const isTableRow = line.trim().startsWith("|") && !isMarkdownTableSeparator(line);
|
||||
const strongProductSignal = strongProductPattern.test(line);
|
||||
const genericProductSignal = genericProductPattern.test(line);
|
||||
const governanceContext = governanceContextPattern.test(line);
|
||||
for (const definition of directIssuePatterns) {
|
||||
const match = definition.pattern.exec(line);
|
||||
if (match !== null && !governanceContext && (isTableRow || strongProductSignal || (genericProductSignal && hwlabContextPattern.test(line)))) {
|
||||
findings.push({
|
||||
kind: definition.kind,
|
||||
lineNumber: index + 1,
|
||||
match: match[0],
|
||||
snippet: preview(line.trim()),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isTableRow && !governanceContext && (strongProductSignal || (genericProductSignal && hwlabContextPattern.test(line)))) {
|
||||
const match = strongProductSignal ? strongProductPattern.exec(line) : genericProductPattern.exec(line);
|
||||
findings.push({
|
||||
kind: "hwlab-product-signal",
|
||||
lineNumber: index + 1,
|
||||
match: match?.[0] ?? "HWLAB product signal",
|
||||
snippet: preview(line.trim()),
|
||||
});
|
||||
}
|
||||
if (findings.length >= 12) break;
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function codeQueueBoardHwlabProductRoutingHint(boardIssueNumber: number, body?: string): Record<string, unknown> | null {
|
||||
if (boardIssueNumber !== CODE_QUEUE_BOARD_TARGET_ISSUE) return null;
|
||||
const findings = codeQueueBoardHwlabProductRoutingFindings(body);
|
||||
return {
|
||||
warning: "#20 is only for UniDesk commander/Code Queue/CLI/infra governance; HWLAB user/product issues belong in pikasTech/HWLAB.",
|
||||
route: "Create or update the corresponding issue in pikasTech/HWLAB; keep #20 rows limited to UniDesk governance or infrastructure support work.",
|
||||
forbiddenPatterns: [
|
||||
"pikasTech/HWLAB#<N>",
|
||||
"github.com/pikasTech/HWLAB/issues/<N>",
|
||||
"HWLAB#<N>",
|
||||
"HWLAB product/live validation rows",
|
||||
],
|
||||
allowedScope: ["commander governance", "Code Queue supervision", "UniDesk CLI guardrails", "UniDesk infrastructure"],
|
||||
detected: findings.length > 0,
|
||||
findings,
|
||||
};
|
||||
}
|
||||
|
||||
function codeQueueBoardHintHasHwlabProductRouting(hint: Record<string, unknown> | null): boolean {
|
||||
if (hint === null) return false;
|
||||
const routing = hint.hwlabProductRouting;
|
||||
return typeof routing === "object" && routing !== null && (routing as { detected?: unknown }).detected === true;
|
||||
}
|
||||
|
||||
function codeQueueBoardHintHasCommanderBriefUpdates(hint: Record<string, unknown> | null): boolean {
|
||||
if (hint === null) return false;
|
||||
const commanderBrief = hint.commanderBrief;
|
||||
return typeof commanderBrief === "object" && commanderBrief !== null && (commanderBrief as { detected?: unknown }).detected === true;
|
||||
}
|
||||
|
||||
function codeQueueBoardCommanderBriefHint(boardIssueNumber: number, body?: string): Record<string, unknown> | null {
|
||||
if (boardIssueNumber !== CODE_QUEUE_BOARD_TARGET_ISSUE) return null;
|
||||
const headings = body === undefined ? [] : commanderBriefUpdateHeadings(body);
|
||||
const hwlabProductRouting = codeQueueBoardHwlabProductRoutingHint(boardIssueNumber, body);
|
||||
const commanderBriefDetected = headings.length > 0;
|
||||
const hwlabProductDetected = codeQueueBoardHintHasHwlabProductRouting({ hwlabProductRouting });
|
||||
return {
|
||||
warning: "#20 is the long-term board only; do not write daily commander brief update sections into #20.",
|
||||
route: "Move daily progress notes to the daily rolling commander brief issue referenced in #20's 指挥简报索引, using --body-profile commander-brief.",
|
||||
warning: "#20 is the long-term UniDesk commander/Code Queue/CLI/infra governance board only; do not write daily commander brief updates or HWLAB product/user issue tracking into #20.",
|
||||
route: commanderBriefDetected
|
||||
? "Move daily progress notes to the daily rolling commander brief issue referenced in #20's 指挥简报索引, using --body-profile commander-brief."
|
||||
: hwlabProductDetected
|
||||
? "Move HWLAB product/user issue tracking to pikasTech/HWLAB; keep #20 limited to UniDesk commander/Code Queue/CLI/infra governance."
|
||||
: "Use #20 only for UniDesk commander/Code Queue/CLI/infra governance rows.",
|
||||
forbiddenHeadings: ["## 更新 YYYY-MM-DD HH:mm 北京时间", "## YYYY-MM-DD HH:mm 北京时间指挥更新", "### YYYY-MM-DD HH:mm CST:..."],
|
||||
suggestedCommand: "bun scripts/cli.ts gh issue update <daily-brief-issue> --mode replace --body-file <file> --body-profile commander-brief --expect-body-sha <sha>",
|
||||
detected: headings.length > 0,
|
||||
detected: commanderBriefDetected || hwlabProductDetected,
|
||||
detectedHeadings: headings,
|
||||
commanderBrief: {
|
||||
detected: commanderBriefDetected,
|
||||
detectedHeadings: headings,
|
||||
},
|
||||
hwlabProductRouting,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3561,13 +3647,15 @@ function validateIssueBodyGuard(repo: string, issueNumber: number, body: string,
|
||||
const isShort = trimmed.length > 0 && trimmed.length < MIN_SAFE_ISSUE_BODY_CHARS;
|
||||
const profileValidation = issueProfileValidation(issueNumber, body, options.bodyProfile, profileContext);
|
||||
const boardBriefHint = codeQueueBoardCommanderBriefHint(issueNumber, body);
|
||||
const boardContainsCommanderBriefUpdates = boardBriefHint?.detected === true;
|
||||
const boardContainsCommanderBriefUpdates = codeQueueBoardHintHasCommanderBriefUpdates(boardBriefHint);
|
||||
const boardContainsHwlabProductWork = codeQueueBoardHintHasHwlabProductRouting(boardBriefHint);
|
||||
const profileOk = validationBoolean(profileValidation, "ok");
|
||||
const failures: string[] = [];
|
||||
if (isLiteralNull) failures.push("literal-null-body");
|
||||
if (isBlank) failures.push("blank-body");
|
||||
if (isShort && !options.allowShortBody) failures.push("short-body");
|
||||
if (boardContainsCommanderBriefUpdates) failures.push("code-queue-board-contains-commander-brief-updates");
|
||||
if (boardContainsHwlabProductWork) failures.push("code-queue-board-contains-hwlab-product-work");
|
||||
if (!profileOk) {
|
||||
if (profileValidation.issueMatchesProfile === false) failures.push("profile-issue-mismatch");
|
||||
const missingHeadings = validationStringArray(profileValidation, "missingHeadings");
|
||||
@@ -3609,7 +3697,8 @@ function issueEditGuardSummary(issueNumber: number, body: string, options: GitHu
|
||||
}
|
||||
if (options.allowShortBody) warnings.push("allow-short-body enabled; caller accepted short-body corruption risk");
|
||||
if (profile.ok === false) warnings.push("profile guard would fail");
|
||||
if (boardBriefHint?.detected === true) warnings.push("code-queue-board-contains-commander-brief-updates");
|
||||
if (codeQueueBoardHintHasCommanderBriefUpdates(boardBriefHint)) warnings.push("code-queue-board-contains-commander-brief-updates");
|
||||
if (codeQueueBoardHintHasHwlabProductRouting(boardBriefHint)) warnings.push("code-queue-board-contains-hwlab-product-work");
|
||||
const signals = bodySafetySignals(body);
|
||||
const shellPollution = signals.shellPollution as Record<string, unknown>;
|
||||
if (shellPollution.suspected === true) warnings.push("shell-pollution-suspected");
|
||||
@@ -3618,7 +3707,8 @@ function issueEditGuardSummary(issueNumber: number, body: string, options: GitHu
|
||||
&& trimmed.toLowerCase() !== "null"
|
||||
&& (trimmed.length >= MIN_SAFE_ISSUE_BODY_CHARS || options.allowShortBody)
|
||||
&& profile.ok === true
|
||||
&& boardBriefHint?.detected !== true,
|
||||
&& !codeQueueBoardHintHasCommanderBriefUpdates(boardBriefHint)
|
||||
&& !codeQueueBoardHintHasHwlabProductRouting(boardBriefHint),
|
||||
minSafeBodyChars: MIN_SAFE_ISSUE_BODY_CHARS,
|
||||
allowShortBody: options.allowShortBody,
|
||||
warnings,
|
||||
@@ -5173,7 +5263,7 @@ export function ghHelp(): unknown {
|
||||
"--body-file is the recommended source for Markdown bodies so real newlines, backticks, and tables are read as file bytes instead of shell arguments.",
|
||||
"update defaults to --mode replace; --mode append reads the current body and appends file bytes so real newlines, backticks, and Markdown tables are preserved.",
|
||||
"issue edit is a compatibility alias for issue update --mode replace.",
|
||||
"issue update --body-file refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading, and commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).",
|
||||
"issue update --body-file refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading and rejects HWLAB product/user issue rows in favor of pikasTech/HWLAB, while commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).",
|
||||
"issue update dry-run reports old/new body length slots, body SHA, required heading checks, literal \\n detection, and shell-pollution signals. Non-dry-run can use --expect-updated-at or --expect-body-sha for stale-cache protection.",
|
||||
"Issue body stdin is intentionally unsupported in this CLI; write generated Markdown to a file and pass --body-file.",
|
||||
"When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
|
||||
@@ -5182,7 +5272,7 @@ export function ghHelp(): unknown {
|
||||
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It reads only the board issue body, returns body size/SHA and parsed Markdown board sections, and no longer validates GitHub open/closed issue coverage against OPEN/CLOSED tables. The legacy coverage fields remain present as empty arrays/zero counts for compatibility.",
|
||||
"issue board-row list/get reuse the board-audit table parser and are read-only. board-row update changes one table cell by issue number, returns old/new row, body SHA, body guard and request plan, and defaults to dry-run unless --expect-updated-at or --expect-body-sha is supplied for the guarded PATCH. Field aliases map status and validation to the 验收状态 column, tasks to 相关 Code Queue 任务, and focus to 当前关注点.",
|
||||
"issue board-row upsert updates an existing row when the issue is already present, or generates a complete row in --section open|closed when missing. It returns operation=update or operation=add, defaults to dry-run, requires --expect-body-sha or --expect-updated-at before PATCH, and refuses section migration; use board-row move for OPEN/CLOSED migration.",
|
||||
"issue board-row add/move/delete are row-scoped #20 table mutations. add validates a one-line --row-file against the target table column count, Issue column, and GitHub 状态 column; move refuses duplicate/ambiguous rows and can update GitHub 状态 via --status; delete removes only the matched row. All three default to dry-run and require --expect-body-sha or --expect-updated-at before PATCH. add/move/delete return old/new row, body SHA, and line/section plan details for the parsed table mutation.",
|
||||
"issue board-row add/move/delete are row-scoped #20 table mutations. add validates a one-line --row-file against the target table column count, Issue column, and GitHub 状态 column; move refuses duplicate/ambiguous rows and can update GitHub 状态 via --status; delete removes only the matched row. All three default to dry-run and require --expect-body-sha or --expect-updated-at before PATCH. add/move/delete return old/new row, body SHA, and line/section plan details for the parsed table mutation, and the shared #20 guard rejects HWLAB product/user issue rows in favor of pikasTech/HWLAB.",
|
||||
"issue edit 24 --notify-claudeqq-brief-diff remains the legacy #24 notification helper: it reads the old issue body, PATCHes the new body, and sends only newly added '## 更新 ... 北京时间' sections to ClaudeQQ; ClaudeQQ failure does not roll back GitHub.",
|
||||
"Commander brief ClaudeQQ defaults to private target 645275593 through backend-core /api/microservices/claudeqq/proxy; UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_* env vars can override target, base URL, timeout, and enabled state.",
|
||||
"comment delete is supported because GitHub supports deleting issue comments; issue/pr hard delete is unsupported and close is the lifecycle alternative.",
|
||||
|
||||
Reference in New Issue
Block a user