fix: support rolling commander brief profile
This commit is contained in:
@@ -127,6 +127,52 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
].join("\n"),
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/60",
|
||||
};
|
||||
const legacyCommanderBriefIssue = {
|
||||
...issue,
|
||||
id: 2024,
|
||||
number: 24,
|
||||
title: "指挥简报",
|
||||
body: [
|
||||
"# 指挥简报",
|
||||
"",
|
||||
"## 常驻观察与长期建议",
|
||||
"",
|
||||
"- 维持 Code Queue 指挥态势。",
|
||||
"",
|
||||
"## 更新 2026-05-20 17:28 北京时间",
|
||||
"",
|
||||
"- 已完成历史简报入口维护。",
|
||||
"",
|
||||
].join("\n"),
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/24",
|
||||
};
|
||||
const dailyCommanderBriefIssue = {
|
||||
...issue,
|
||||
id: 2046,
|
||||
number: 46,
|
||||
title: "2026-05-21 指挥简报(北京时间)",
|
||||
body: [
|
||||
"# 2026-05-21 指挥简报(北京时间)",
|
||||
"",
|
||||
"## 常驻观察与长期建议",
|
||||
"",
|
||||
"- 今日滚动简报使用每日 issue 主体维护。",
|
||||
"",
|
||||
"## 更新 2026-05-21 09:00 北京时间",
|
||||
"",
|
||||
"- 启动当日队列监督。",
|
||||
"",
|
||||
].join("\n"),
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/46",
|
||||
};
|
||||
const nonBriefIssue = {
|
||||
...issue,
|
||||
id: 2047,
|
||||
number: 47,
|
||||
title: "普通任务 issue",
|
||||
body: "# 普通任务\n\n## 背景\n\n- 不是指挥简报。\n",
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/47",
|
||||
};
|
||||
const issueList = [
|
||||
{
|
||||
id: 2001,
|
||||
@@ -351,6 +397,18 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, boardIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/24") {
|
||||
sendJson(res, 200, legacyCommanderBriefIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/46") {
|
||||
sendJson(res, 200, dailyCommanderBriefIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/47") {
|
||||
sendJson(res, 200, nonBriefIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/60") {
|
||||
sendJson(res, 200, legacyBoardIssue);
|
||||
return;
|
||||
@@ -474,7 +532,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const mock = await startMockGitHub();
|
||||
const tmp = mkdtempSync(join(tmpdir(), "unidesk-gh-issue-guard-"));
|
||||
const env = {
|
||||
GH_TOKEN: "contract-token",
|
||||
GH_TOKEN: "contract-token-should-not-print",
|
||||
UNIDESK_GITHUB_API_URL: mock.baseUrl,
|
||||
};
|
||||
try {
|
||||
@@ -707,9 +765,56 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const briefWrongData = failedDataOf(briefWrongProfile.json ?? {});
|
||||
const briefWrongGuard = briefWrongData.guard as JsonRecord;
|
||||
assertCondition(Array.isArray(briefWrongGuard.failures) && briefWrongGuard.failures.includes("profile-issue-mismatch"), "explicit profile should check issue number", briefWrongGuard);
|
||||
assertCondition(!briefWrongProfile.stdout.includes(env.GH_TOKEN) && !briefWrongProfile.stderr.includes(env.GH_TOKEN), "failed profile output must not print GH_TOKEN", {
|
||||
stdout: briefWrongProfile.stdout,
|
||||
stderr: briefWrongProfile.stderr,
|
||||
});
|
||||
|
||||
const safeFile = join(tmp, "safe.md");
|
||||
writeFileSync(safeFile, "# Code Queue\n\n## 看板(OPEN)\n\n- multiline Markdown keeps `code` intact.\n- real newline follows.\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n", "utf8");
|
||||
const validBriefFile = join(tmp, "valid-commander-brief.md");
|
||||
writeFileSync(validBriefFile, [
|
||||
"# 2026-05-21 指挥简报(北京时间)",
|
||||
"",
|
||||
"## 常驻观察与长期建议",
|
||||
"",
|
||||
"- 保持滚动简报正文只通过 body-file 更新。",
|
||||
"",
|
||||
"## 更新 2026-05-21 15:18 北京时间",
|
||||
"",
|
||||
"- 今日新增进展包含 `code` 和表格。",
|
||||
"",
|
||||
"| 项 | 状态 |",
|
||||
"| --- | --- |",
|
||||
"| CLI | guarded |",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
|
||||
const legacyBriefDryRun = await runCli(["gh", "issue", "update", "24", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", validBriefFile, "--body-profile", "commander-brief", "--dry-run"], env);
|
||||
assertCondition(legacyBriefDryRun.status === 0, "#24 explicit commander-brief profile should remain compatible", legacyBriefDryRun.json ?? { stdout: legacyBriefDryRun.stdout });
|
||||
const legacyBriefData = dataOf(legacyBriefDryRun.json ?? {});
|
||||
const legacyBriefGuard = legacyBriefData.guard as JsonRecord;
|
||||
const legacyBriefProfile = legacyBriefGuard.profile as JsonRecord;
|
||||
assertCondition(legacyBriefGuard.ok === true && legacyBriefProfile.issueMatchesProfile === true && legacyBriefProfile.issueMatchReason === "legacy-issue-number", "#24 commander-brief profile should pass by legacy issue number", legacyBriefGuard);
|
||||
|
||||
const dailyBriefDryRun = await runCli(["gh", "issue", "update", "46", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", validBriefFile, "--body-profile", "commander-brief", "--dry-run"], env);
|
||||
assertCondition(dailyBriefDryRun.status === 0, "daily commander brief issue should pass commander-brief dry-run guard", dailyBriefDryRun.json ?? { stdout: dailyBriefDryRun.stdout });
|
||||
const dailyBriefData = dataOf(dailyBriefDryRun.json ?? {});
|
||||
const dailyBriefGuard = dailyBriefData.guard as JsonRecord;
|
||||
const dailyBriefProfile = dailyBriefGuard.profile as JsonRecord;
|
||||
assertCondition(dailyBriefGuard.ok === true && dailyBriefProfile.issueMatchesProfile === true && dailyBriefProfile.issueMatchReason === "daily-title", "daily commander brief profile should match by title", dailyBriefGuard);
|
||||
assertCondition(dailyBriefData.containsLiteralBackslashN === false && dailyBriefData.containsMarkdownTable === true, "daily brief dry-run should preserve markdown safety signals", dailyBriefData);
|
||||
|
||||
const nonBriefDryRun = await runCli(["gh", "issue", "update", "47", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", validBriefFile, "--body-profile", "commander-brief", "--dry-run"], env);
|
||||
assertCondition(nonBriefDryRun.status !== 0, "non-brief issue should fail commander-brief profile", nonBriefDryRun.json ?? { stdout: nonBriefDryRun.stdout });
|
||||
const nonBriefData = failedDataOf(nonBriefDryRun.json ?? {});
|
||||
const nonBriefGuard = nonBriefData.guard as JsonRecord;
|
||||
assertCondition(Array.isArray(nonBriefGuard.failures) && nonBriefGuard.failures.includes("profile-issue-mismatch"), "non-brief issue should report profile-issue-mismatch", nonBriefGuard);
|
||||
assertCondition(!nonBriefDryRun.stdout.includes(env.GH_TOKEN) && !nonBriefDryRun.stderr.includes(env.GH_TOKEN), "non-brief failure must not print GH_TOKEN", {
|
||||
stdout: nonBriefDryRun.stdout,
|
||||
stderr: nonBriefDryRun.stderr,
|
||||
});
|
||||
|
||||
const requestCountBeforeDryRun = mock.requests.length;
|
||||
const safeDryRun = await runCli(["gh", "issue", "edit", "20", "--repo", "pikasTech/unidesk", "--body-file", safeFile, "--dry-run"], env);
|
||||
assertCondition(safeDryRun.status === 0, "safe issue edit dry-run should succeed", safeDryRun.json ?? { stdout: safeDryRun.stdout });
|
||||
@@ -825,6 +930,9 @@ 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",
|
||||
"#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",
|
||||
"dry-run reports old/new body safety and does not PATCH",
|
||||
"multiline Markdown and backticks are not polluted",
|
||||
"expect-updated-at stale write protection blocks PATCH",
|
||||
|
||||
+88
-20
@@ -30,11 +30,14 @@ const ISSUE_BODY_PROFILES = {
|
||||
requiredHeadings: ["## 看板(OPEN)"],
|
||||
},
|
||||
"commander-brief": {
|
||||
label: "Commander brief body-only issue #24",
|
||||
issueNumber: COMMANDER_BRIEF_TARGET_ISSUE,
|
||||
label: "Commander brief body-only issue (#24 legacy or daily rolling brief)",
|
||||
legacyIssueNumber: COMMANDER_BRIEF_TARGET_ISSUE,
|
||||
requiredHeadings: ["## 常驻观察与长期建议"],
|
||||
},
|
||||
} as const;
|
||||
const DAILY_COMMANDER_BRIEF_TITLE_PATTERN = /^\d{4}-\d{2}-\d{2}\s+指挥简报(北京时间)$/u;
|
||||
const DAILY_COMMANDER_BRIEF_BODY_HEADING_PATTERN = /^#\s+\d{4}-\d{2}-\d{2}\s+指挥简报(北京时间)\s*$/u;
|
||||
const LEGACY_COMMANDER_BRIEF_BODY_HEADING_PATTERN = /^#\s+指挥简报\s*$/u;
|
||||
|
||||
type IssueViewJsonField = typeof ISSUE_VIEW_JSON_FIELDS[number];
|
||||
type IssueListJsonField = typeof ISSUE_LIST_JSON_FIELDS[number];
|
||||
@@ -289,6 +292,12 @@ interface GitHubOptions {
|
||||
boardRowValue?: string;
|
||||
}
|
||||
|
||||
interface IssueProfileValidationContext {
|
||||
issueTitle?: string | null;
|
||||
issueBody?: string | null;
|
||||
issueMetadataFetched?: boolean;
|
||||
}
|
||||
|
||||
interface GitHubErrorPayload {
|
||||
ok: false;
|
||||
degradedReason: GitHubDegradedReason;
|
||||
@@ -1890,21 +1899,67 @@ function issueProfileFor(issueNumber: number, requested: IssueBodyProfileOption)
|
||||
return null;
|
||||
}
|
||||
|
||||
function issueProfileValidation(issueNumber: number, body: string, requested: IssueBodyProfileOption): Record<string, unknown> {
|
||||
function firstMarkdownLine(body: string | null | undefined): string {
|
||||
return normalizeNewlines(body ?? "").split("\n").find((line) => line.trim().length > 0)?.trim() ?? "";
|
||||
}
|
||||
|
||||
function isDailyCommanderBriefTitle(title: string | null | undefined): boolean {
|
||||
return DAILY_COMMANDER_BRIEF_TITLE_PATTERN.test((title ?? "").trim());
|
||||
}
|
||||
|
||||
function isDailyCommanderBriefBodyHeading(line: string): boolean {
|
||||
return DAILY_COMMANDER_BRIEF_BODY_HEADING_PATTERN.test(line.trim());
|
||||
}
|
||||
|
||||
function isLegacyCommanderBriefBodyHeading(line: string): boolean {
|
||||
return LEGACY_COMMANDER_BRIEF_BODY_HEADING_PATTERN.test(line.trim());
|
||||
}
|
||||
|
||||
function currentBodyLooksLikeCommanderBrief(body: string | null | undefined): boolean {
|
||||
const normalized = normalizeNewlines(body ?? "");
|
||||
const firstLine = firstMarkdownLine(normalized);
|
||||
if (isDailyCommanderBriefBodyHeading(firstLine)) return true;
|
||||
return isLegacyCommanderBriefBodyHeading(firstLine)
|
||||
&& normalized.includes("## 常驻观察与长期建议")
|
||||
&& extractTimelineSections(normalized).length > 0;
|
||||
}
|
||||
|
||||
function commanderBriefIssueProfileMatch(issueNumber: number, context: IssueProfileValidationContext): { ok: boolean; reason: string } {
|
||||
if (issueNumber === COMMANDER_BRIEF_TARGET_ISSUE) return { ok: true, reason: "legacy-issue-number" };
|
||||
if (isDailyCommanderBriefTitle(context.issueTitle)) return { ok: true, reason: "daily-title" };
|
||||
if (currentBodyLooksLikeCommanderBrief(context.issueBody)) return { ok: true, reason: "current-body-heading" };
|
||||
return {
|
||||
ok: false,
|
||||
reason: context.issueMetadataFetched === true ? "not-commander-brief" : "issue-metadata-unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function issueProfileNeedsMetadata(issueNumber: number, requested: IssueBodyProfileOption): boolean {
|
||||
return requested === "commander-brief" && issueNumber !== COMMANDER_BRIEF_TARGET_ISSUE;
|
||||
}
|
||||
|
||||
function issueProfileValidation(issueNumber: number, body: string, requested: IssueBodyProfileOption, context: IssueProfileValidationContext = {}): Record<string, unknown> {
|
||||
const profileName = issueProfileFor(issueNumber, requested);
|
||||
if (profileName === null) return { profile: null, applied: false, requiredHeadings: [], missingHeadings: [], ok: true };
|
||||
const profile = ISSUE_BODY_PROFILES[profileName];
|
||||
const issueMatchesProfile = issueNumber === profile.issueNumber;
|
||||
const match = profileName === "commander-brief"
|
||||
? commanderBriefIssueProfileMatch(issueNumber, context)
|
||||
: { ok: issueNumber === ISSUE_BODY_PROFILES["code-queue-board"].issueNumber, reason: issueNumber === ISSUE_BODY_PROFILES["code-queue-board"].issueNumber ? "issue-number" : "wrong-issue-number" };
|
||||
const missingHeadings = profile.requiredHeadings.filter((heading) => !body.includes(heading));
|
||||
return {
|
||||
profile: profileName,
|
||||
label: profile.label,
|
||||
applied: true,
|
||||
expectedIssueNumber: profile.issueNumber,
|
||||
issueMatchesProfile,
|
||||
...(profileName === "code-queue-board"
|
||||
? { expectedIssueNumber: ISSUE_BODY_PROFILES["code-queue-board"].issueNumber }
|
||||
: { legacyIssueNumber: COMMANDER_BRIEF_TARGET_ISSUE }),
|
||||
issueMatchesProfile: match.ok,
|
||||
issueMatchReason: match.reason,
|
||||
issueMetadataFetched: context.issueMetadataFetched === true,
|
||||
issueTitle: context.issueTitle ?? null,
|
||||
requiredHeadings: profile.requiredHeadings,
|
||||
missingHeadings,
|
||||
ok: issueMatchesProfile && missingHeadings.length === 0,
|
||||
ok: match.ok && missingHeadings.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1917,12 +1972,12 @@ function validationStringArray(record: Record<string, unknown>, key: string): st
|
||||
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||
}
|
||||
|
||||
function validateIssueBodyGuard(repo: string, issueNumber: number, body: string, options: GitHubOptions): GitHubCommandResult | null {
|
||||
function validateIssueBodyGuard(repo: string, issueNumber: number, body: string, options: GitHubOptions, profileContext: IssueProfileValidationContext = {}): GitHubCommandResult | null {
|
||||
const trimmed = body.trim();
|
||||
const isLiteralNull = trimmed.toLowerCase() === "null";
|
||||
const isBlank = trimmed.length === 0;
|
||||
const isShort = trimmed.length > 0 && trimmed.length < MIN_SAFE_ISSUE_BODY_CHARS;
|
||||
const profileValidation = issueProfileValidation(issueNumber, body, options.bodyProfile);
|
||||
const profileValidation = issueProfileValidation(issueNumber, body, options.bodyProfile, profileContext);
|
||||
const profileOk = validationBoolean(profileValidation, "ok");
|
||||
const failures: string[] = [];
|
||||
if (isLiteralNull) failures.push("literal-null-body");
|
||||
@@ -1958,9 +2013,9 @@ function validateIssueBodyGuard(repo: string, issueNumber: number, body: string,
|
||||
});
|
||||
}
|
||||
|
||||
function issueEditGuardSummary(issueNumber: number, body: string, options: GitHubOptions): Record<string, unknown> {
|
||||
function issueEditGuardSummary(issueNumber: number, body: string, options: GitHubOptions, profileContext: IssueProfileValidationContext = {}): Record<string, unknown> {
|
||||
const trimmed = body.trim();
|
||||
const profile = issueProfileValidation(issueNumber, body, options.bodyProfile);
|
||||
const profile = issueProfileValidation(issueNumber, body, options.bodyProfile, profileContext);
|
||||
const warnings: string[] = [];
|
||||
if (trimmed.length > 0 && trimmed.length < MIN_SAFE_ISSUE_BODY_CHARS) {
|
||||
warnings.push(options.allowShortBody ? "short-body-allowed" : "short-body-would-fail");
|
||||
@@ -2642,7 +2697,8 @@ async function issueCreate(repo: string, token: string, options: GitHubOptions):
|
||||
|
||||
async function issueEdit(repo: string, token: string, issueNumber: number, options: GitHubOptions, commandName = "issue edit"): Promise<GitHubCommandResult> {
|
||||
const body = readBodyFile(options.bodyFile, commandName);
|
||||
if (options.mode === "replace") {
|
||||
const needsProfileMetadata = issueProfileNeedsMetadata(issueNumber, options.bodyProfile);
|
||||
if (options.mode === "replace" && !needsProfileMetadata) {
|
||||
const bodyGuard = validateIssueBodyGuard(repo, issueNumber, body, options);
|
||||
if (bodyGuard !== null) return bodyGuard;
|
||||
}
|
||||
@@ -2650,27 +2706,31 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
if (concurrencyOptionError !== null) return concurrencyOptionError;
|
||||
let oldIssue: GitHubIssue | null = null;
|
||||
let briefDiff: CommanderBriefDiff | null = null;
|
||||
let profileContext: IssueProfileValidationContext = {};
|
||||
const claudeQqConfig = commanderBriefClaudeQqConfig();
|
||||
if (options.notifyClaudeQqBriefDiff && issueNumber !== COMMANDER_BRIEF_TARGET_ISSUE) {
|
||||
return validationError(commandName, repo, "--notify-claudeqq-brief-diff is only supported for commander brief issue #24", { issueNumber });
|
||||
}
|
||||
const needsReadBeforeEdit = options.mode === "append" || !options.dryRun && (options.notifyClaudeQqBriefDiff || options.expectUpdatedAt !== undefined || options.expectBodySha !== undefined);
|
||||
const needsReadBeforeEdit = options.mode === "append"
|
||||
|| needsProfileMetadata && token.length > 0
|
||||
|| !options.dryRun && (options.notifyClaudeQqBriefDiff || options.expectUpdatedAt !== undefined || options.expectBodySha !== undefined);
|
||||
if (needsReadBeforeEdit) {
|
||||
const issue = await getIssue(token, repo, issueNumber);
|
||||
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber, phase: "read-before-update" });
|
||||
oldIssue = issue;
|
||||
profileContext = { issueTitle: issue.title, issueBody: issue.body, issueMetadataFetched: true };
|
||||
const concurrencyError = validateIssueConcurrency(repo, issueNumber, issue, options);
|
||||
if (concurrencyError !== null) return concurrencyError;
|
||||
if (options.notifyClaudeQqBriefDiff) briefDiff = commanderBriefDiff(issue.body ?? "", options.mode === "append" ? `${issue.body ?? ""}${body}` : body);
|
||||
}
|
||||
const finalBody = options.mode === "append" ? `${oldIssue?.body ?? ""}${body}` : body;
|
||||
if (options.mode === "append") {
|
||||
const bodyGuard = validateIssueBodyGuard(repo, issueNumber, finalBody, options);
|
||||
if (options.mode === "append" || needsProfileMetadata) {
|
||||
const bodyGuard = validateIssueBodyGuard(repo, issueNumber, finalBody, options, profileContext);
|
||||
if (bodyGuard !== null) return bodyGuard;
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const dryRunDiff = options.notifyClaudeQqBriefDiff ? commanderBriefDiff("", finalBody) : null;
|
||||
const guard = issueEditGuardSummary(issueNumber, finalBody, options);
|
||||
const guard = issueEditGuardSummary(issueNumber, finalBody, options, profileContext);
|
||||
let dryRunOldBody: Record<string, unknown> = {
|
||||
fetched: false,
|
||||
bodyChars: null,
|
||||
@@ -2678,7 +2738,15 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
updatedAt: null,
|
||||
reason: token.length > 0 ? "not-requested" : "no token supplied to dry-run",
|
||||
};
|
||||
if (token.length > 0) {
|
||||
if (oldIssue !== null) {
|
||||
const oldBody = oldIssue.body ?? "";
|
||||
dryRunOldBody = {
|
||||
fetched: true,
|
||||
bodyChars: oldBody.length,
|
||||
bodySha: bodySha(oldBody),
|
||||
updatedAt: oldIssue.updated_at ?? null,
|
||||
};
|
||||
} else if (token.length > 0) {
|
||||
const issue = await getIssue(token, repo, issueNumber);
|
||||
if (isGitHubError(issue)) {
|
||||
dryRunOldBody = {
|
||||
@@ -2743,7 +2811,7 @@ async function issueEdit(repo: string, token: string, issueNumber: number, optio
|
||||
if (options.title !== undefined) payload.title = options.title;
|
||||
const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, payload);
|
||||
if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
|
||||
const guard = issueEditGuardSummary(issueNumber, finalBody, options);
|
||||
const guard = issueEditGuardSummary(issueNumber, finalBody, options, profileContext);
|
||||
const concurrency = oldIssue === null
|
||||
? { checked: false, expectUpdatedAt: options.expectUpdatedAt ?? null, expectBodySha: options.expectBodySha ?? null }
|
||||
: { checked: true, oldIssueUpdatedAt: oldIssue.updated_at ?? null, oldBodySha: bodySha(oldIssue.body ?? ""), expectUpdatedAt: options.expectUpdatedAt ?? null, expectBodySha: options.expectBodySha ?? null };
|
||||
@@ -3351,7 +3419,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 and #24 body-only profiles require their stable headings.",
|
||||
"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 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.",
|
||||
@@ -3360,7 +3428,7 @@ export function ghHelp(): unknown {
|
||||
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It compares GitHub open/closed issue lists with the board OPEN/CLOSED tables and reports missingOpenIssues, closedInOpenRows, missingClosedRows, rowValidationWarnings, ignoredIssues, and recommendedActions. When an Issue column exists, row.issueNumber is taken from that column; #20 and #24 are known meta issues by default.",
|
||||
"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 move/delete are structured unsupported in this phase; open/closed relocation and row removal need explicit table placement and duplicate-row semantics before they write.",
|
||||
"issue edit 24 --notify-claudeqq-brief-diff reads the old issue body, PATCHes the new body, and sends only newly added '## 更新 ... 北京时间' sections to ClaudeQQ; ClaudeQQ failure does not roll back GitHub.",
|
||||
"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.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR create/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
|
||||
|
||||
Reference in New Issue
Block a user