From e4854b10926202909d8f21805e4d5357c1e42bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Thu, 17 Sep 2026 17:48:18 +0800 Subject: [PATCH] fix(cli): render missing required args from command catalog; add change list --- README.md | 3 +- cli.mjs | 3 +- lib/commands.mjs | 51 ++++++++++--------- lib/domains/change.mjs | 17 ++++++- lib/human.mjs | 12 +++-- lib/i18n.mjs | 2 +- .../20260917-fix-missing-arg-hints/brief.md | 15 ++++-- shadow-docs/knowledge/cli-output-contract.md | 5 +- test/cli.test.mjs | 32 ++++++++++++ 9 files changed, 102 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a3518d5..082c357 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Shadow dev workflow 的确定性脚手架 CLI。所有命令走 plan → execute | 命令 | 说明 | |------|------| | `repo inspect` | 查看仓库状态(分支、HEAD、脏文件) | -| `change create\|approve` | 创建/批准变更 brief | +| `change create\|approve\|list` | 创建/批准变更 brief;列出活动变更 | | `issue plan\|execute` | 创建 GitHub issue | | `branch plan\|execute` | 建功能分支 | | `sync plan\|execute` | fast-forward 同步上游 | @@ -33,6 +33,7 @@ CLI 在 stderr 渲染一层人类提示:进场横幅(命令+参数)、收 - 语言解析:`--lang zh|en` > `SHADOW_DEV_LANG` > 系统 locale 自动探测 > 默认 `zh`。非法取值报 `INVALID_LANG`(退出码 2)。 - 关闭提示:`SHADOW_DEV_QUIET=1`(或 `true`)时 stderr 零输出,适合日志管道。 - 语言只影响 stderr 文案;错误 code、JSON 结构、`nextStep` 模板均不本地化。 +- 缺必填参数报错时,stderr 逐行列出该命令在命令目录中的完整参数描述(`flag * 说明`,含示例值与来源位置),示例行的占位符与目录一致(如 `--name `)——提示与人用 help 共享同一事实源 `lib/commands.mjs`。 - `shadow-dev help` 概览默认只回最小面:`data.help`(命令一览字符串,约 350 字节);agent 需要结构化明细(usage/参数/必填/示例/nextStep)时用 `shadow-dev help --full`。`shadow-dev help <命令>` 查看单组详情,恒定结构化(组面小)。stderr 中文命令表不受 `--full` 影响。 ## 平台兼容 diff --git a/cli.mjs b/cli.mjs index 65f1ed0..be8c7e7 100755 --- a/cli.mjs +++ b/cli.mjs @@ -71,6 +71,7 @@ async function handle(r, p, o) { if (d === 'task' && a === 'set') return { ok: true, command: 'task.set', data: task.set(r, o) } if (d === 'change' && a === 'create') return { ok: true, command: 'change.create', data: change.create(r, o) } if (d === 'change' && a === 'approve') return { ok: true, command: 'change.approve', data: change.approve(r, o) } + if (d === 'change' && a === 'list') return { ok: true, command: 'change.list', data: change.list(r) } if (Object.hasOwn(DOMAINS, d)) { const verb = a === 'rebuild' ? s : a, c = a === 'rebuild' ? `${d}.rebuild` : d if (verb === 'plan') return await planDomain(c, DOMAINS[d], r, o) @@ -98,7 +99,7 @@ try { } if (jsonEnabled(o)) out(v) } catch (e) { - human.error(L, e, p) + human.error(L, e, p, o) if (jsonEnabled(o)) fail(e.code || e.message, e.message, e.status || 1) else process.exitCode = e.status || 1 } diff --git a/lib/commands.mjs b/lib/commands.mjs index d842f89..d13ccae 100644 --- a/lib/commands.mjs +++ b/lib/commands.mjs @@ -2,7 +2,7 @@ // summary/args.desc 提供 zh/en 双版(机器流不本地化,人用层按语言取用);next 为稳定英文命令模板,{x} 由执行结果填充。 const f = (flag, required, zh, en) => ({ flag, required, desc: { zh, en } }) const c = (usage, zh, en, args, example, next = null) => ({ usage, summary: { zh, en }, args, example, next }) -const N = f('--name', true, '变更名,如 20260917-feature-x', 'change name, e.g. 20260917-feature-x') +const N = f('--name', true, '变更名(shadow-docs/changes/ 下的子目录,如 20260917-feature-x)', 'change name (subdirectory under shadow-docs/changes/, e.g. 20260917-feature-x)') const CF = f('--confirm', true, '写操作显式确认', 'explicit confirmation for mutating commands') const PH = f('--plan-hash', false, '缺省时读取 brief 中持久化的 planHash', 'defaults to the planHash persisted in the brief') const T = f('--title', false, '标题,缺省为变更名', 'title, defaults to change name') @@ -12,30 +12,31 @@ const M = f('--message', false, '提交信息', 'commit message') export const COMMANDS = { 'repo.inspect': c('repo inspect', '查看仓库状态(分支、HEAD、脏文件)', 'show repository state (branch, HEAD, dirty files)', [], 'shadow-dev repo inspect'), - 'change.create': c('change create', '创建变更 brief', 'create a change brief', [N, f('--type', false, 'feature|fix|build|chore|docs|refactor|style|test,默认 feat', 'one of feature|fix|build|chore|docs|refactor|style|test, default feat'), f('--scope', false, '影响范围', 'scope'), f('--base-branch', false, '基线分支,默认 main', 'base branch, default main'), FI, f('--body-file', false, 'brief 正文来源文件', 'file supplying the brief body'), f('--repository', false, 'GitHub owner/repo', 'GitHub owner/repo'), CF], 'shadow-dev change create --name --type feature --confirm', 'change approve --name {name} --confirm'), - 'change.approve': c('change approve', '批准 brief(draft → proposed)', 'approve the brief (draft → proposed)', [N, CF], 'shadow-dev change approve --name --confirm', 'branch plan --name {name}'), - 'issue.plan': c('issue plan', '预览创建 GitHub issue', 'plan a GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels')], 'shadow-dev issue plan --name --title "标题" --labels feature', 'issue execute --name {name} --title "{title}" --body "{body}" --labels {labels} --confirm'), - 'issue.execute': c('issue execute', '创建 GitHub issue', 'create the GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels'), PH, CF], 'shadow-dev issue execute --name --confirm', null), - 'branch.plan': c('branch plan', '预览建功能分支', 'plan creating the feature branch', [N], 'shadow-dev branch plan --name ', 'branch execute --name {name} --confirm'), - 'branch.execute': c('branch execute', '从基线分支创建并切换', 'create and switch to the feature branch', [N, PH, CF], 'shadow-dev branch execute --name --confirm', null), - 'sync.plan': c('sync plan', '预览 fast-forward 同步上游', 'plan a fast-forward sync with upstream', [N], 'shadow-dev sync plan --name ', 'sync execute --name {name} --confirm'), - 'sync.execute': c('sync execute', 'fetch 后仅 fast-forward 合并上游', 'fetch then ff-only merge upstream', [N, PH, CF], 'shadow-dev sync execute --name --confirm', null), - 'conflict.inspect': c('conflict inspect', '检查与其他 active brief 的文件重叠', 'check file overlaps with other active briefs', [N], 'shadow-dev conflict inspect --name ', null), - 'task.list': c('task list', '列出 brief 任务清单', 'list the brief task checklist', [N], 'shadow-dev task list --name ', null), - 'task.set': c('task set', '勾选/取消任务', 'tick or untick a task', [N, f('--task', true, '任务 id,如 task-3', 'task id, e.g. task-3'), f('--state', true, 'todo|done', 'todo|done'), CF], 'shadow-dev task set --name --task task-1 --state done --confirm', null), - 'review.plan': c('review plan', '预览审查记录', 'plan the review record', [N], 'shadow-dev review plan --name ', 'review execute --name {name} --conclusion passed --confirm'), - 'review.execute': c('review execute', '写入审查结论与知识评估(任务未全部勾选会被拒绝)', 'persist review conclusion and knowledge action (blocked until all tasks are done)', [N, f('--conclusion', false, 'passed|blocked,默认 passed', 'passed|blocked, default passed'), f('--knowledge', false, '新增|更新|废弃|无需变更', 'knowledge action: 新增|更新|废弃|无需变更'), f('--target', false, '知识卡片路径', 'knowledge card path'), f('--reason', false, '知识动作理由', 'knowledge action reason'), PH, CF], 'shadow-dev review execute --name --conclusion passed --confirm', null), - 'commit.plan': c('commit plan', '预览按显式文件列表提交', 'plan an explicit-file-list commit', [N, f('--files', true, '逗号分隔文件列表', 'comma-separated file list'), f('--message', true, '提交信息', 'commit message')], 'shadow-dev commit plan --name --files a.mjs,b.mjs --message "fix: x"', 'commit execute --name {name} --files {files} --message "{message}" --confirm'), - 'commit.execute': c('commit execute', '提交并写 checkpoint(参数须与 plan 完全一致)', 'commit and write checkpoint (args must match the plan exactly)', [N, FI, M, PH, CF], 'shadow-dev commit execute --name --files a.mjs --message "fix: x" --confirm', null), - 'publish.plan': c('publish plan', '预览推送分支并创建/复用 PR', 'plan pushing the branch and creating/reusing the PR', [N, T, B], 'shadow-dev publish plan --name --title "标题"', 'publish execute --name {name} --title "{title}" --body "{body}" --confirm'), - 'publish.execute': c('publish execute', '推送分支并创建/复用 PR(参数须与 plan 完全一致)', 'push and create/reuse the PR (args must match the plan exactly)', [N, T, B, PH, CF], 'shadow-dev publish execute --name --confirm', 'archive plan --name {name}'), - 'release.plan': c('release plan', '预览提交+推送+PR 复合发布', 'plan the commit+push+PR composite release', [N, FI, M, T, B], 'shadow-dev release plan --name --files a.mjs --message "feat: x" --title "标题"', 'release execute --name {name} --confirm'), - 'release.execute': c('release execute', '执行复合发布(缺省参数回退 brief workflow.release)', 'run the composite release (params default to the stored workflow.release plan)', [N, FI, M, T, B, PH, CF], 'shadow-dev release execute --name --confirm', 'archive plan --name {name}'), - 'pr.inspect': c('pr inspect', '查看 brief 关联 PR', 'inspect the PR linked to the brief', [N], 'shadow-dev pr inspect --name ', null), - 'reconcile.plan': c('reconcile plan', '预览 brief 状态与实际进度对齐', 'plan reconciling brief state with actual progress', [N], 'shadow-dev reconcile plan --name ', 'reconcile execute --name {name} --confirm'), - 'reconcile.execute': c('reconcile execute', '回写对齐后的状态', 'persist the reconciled state', [N, PH, CF], 'shadow-dev reconcile execute --name --confirm', null), - 'archive.plan': c('archive plan', '预览归档(要求 review passed 且 PR merged)', 'plan archiving (requires review passed and PR merged)', [N], 'shadow-dev archive plan --name ', 'archive execute --name {name} --confirm'), - 'archive.execute': c('archive execute', '移入 archive 并重建 INDEX', 'move into archive and rebuild INDEX', [N, PH, CF], 'shadow-dev archive execute --name --confirm', null), + 'change.create': c('change create', '创建变更 brief', 'create a change brief', [N, f('--type', false, 'feature|fix|build|chore|docs|refactor|style|test,默认 feat', 'one of feature|fix|build|chore|docs|refactor|style|test, default feat'), f('--scope', false, '影响范围', 'scope'), f('--base-branch', false, '基线分支,默认 main', 'base branch, default main'), FI, f('--body-file', false, 'brief 正文来源文件', 'file supplying the brief body'), f('--repository', false, 'GitHub owner/repo', 'GitHub owner/repo'), CF], 'shadow-dev change create --name --type feature --confirm', 'change approve --name {name} --confirm'), + 'change.approve': c('change approve', '批准 brief(draft → proposed)', 'approve the brief (draft → proposed)', [N, CF], 'shadow-dev change approve --name --confirm', 'branch plan --name {name}'), + 'change.list': c('change list', '列出活动变更的名称、类型、状态与分支', 'list active changes with name, type, status and branch', [], 'shadow-dev change list'), + 'issue.plan': c('issue plan', '预览创建 GitHub issue', 'plan a GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels')], 'shadow-dev issue plan --name --title "标题" --labels feature', 'issue execute --name {name} --title "{title}" --body "{body}" --labels {labels} --confirm'), + 'issue.execute': c('issue execute', '创建 GitHub issue', 'create the GitHub issue', [N, T, B, f('--labels', false, '逗号分隔标签', 'comma-separated labels'), PH, CF], 'shadow-dev issue execute --name --confirm', null), + 'branch.plan': c('branch plan', '预览建功能分支', 'plan creating the feature branch', [N], 'shadow-dev branch plan --name ', 'branch execute --name {name} --confirm'), + 'branch.execute': c('branch execute', '从基线分支创建并切换', 'create and switch to the feature branch', [N, PH, CF], 'shadow-dev branch execute --name --confirm', null), + 'sync.plan': c('sync plan', '预览 fast-forward 同步上游', 'plan a fast-forward sync with upstream', [N], 'shadow-dev sync plan --name ', 'sync execute --name {name} --confirm'), + 'sync.execute': c('sync execute', 'fetch 后仅 fast-forward 合并上游', 'fetch then ff-only merge upstream', [N, PH, CF], 'shadow-dev sync execute --name --confirm', null), + 'conflict.inspect': c('conflict inspect', '检查与其他 active brief 的文件重叠', 'check file overlaps with other active briefs', [N], 'shadow-dev conflict inspect --name ', null), + 'task.list': c('task list', '列出 brief 任务清单', 'list the brief task checklist', [N], 'shadow-dev task list --name ', null), + 'task.set': c('task set', '勾选/取消任务', 'tick or untick a task', [N, f('--task', true, '任务 id,如 task-3', 'task id, e.g. task-3'), f('--state', true, 'todo|done', 'todo|done'), CF], 'shadow-dev task set --name --task task-1 --state done --confirm', null), + 'review.plan': c('review plan', '预览审查记录', 'plan the review record', [N], 'shadow-dev review plan --name ', 'review execute --name {name} --conclusion passed --confirm'), + 'review.execute': c('review execute', '写入审查结论与知识评估(任务未全部勾选会被拒绝)', 'persist review conclusion and knowledge action (blocked until all tasks are done)', [N, f('--conclusion', false, 'passed|blocked,默认 passed', 'passed|blocked, default passed'), f('--knowledge', false, '新增|更新|废弃|无需变更', 'knowledge action: 新增|更新|废弃|无需变更'), f('--target', false, '知识卡片路径', 'knowledge card path'), f('--reason', false, '知识动作理由', 'knowledge action reason'), PH, CF], 'shadow-dev review execute --name --conclusion passed --confirm', null), + 'commit.plan': c('commit plan', '预览按显式文件列表提交', 'plan an explicit-file-list commit', [N, f('--files', true, '逗号分隔文件列表', 'comma-separated file list'), f('--message', true, '提交信息', 'commit message')], 'shadow-dev commit plan --name --files a.mjs,b.mjs --message "fix: x"', 'commit execute --name {name} --files {files} --message "{message}" --confirm'), + 'commit.execute': c('commit execute', '提交并写 checkpoint(参数须与 plan 完全一致)', 'commit and write checkpoint (args must match the plan exactly)', [N, FI, M, PH, CF], 'shadow-dev commit execute --name --files a.mjs --message "fix: x" --confirm', null), + 'publish.plan': c('publish plan', '预览推送分支并创建/复用 PR', 'plan pushing the branch and creating/reusing the PR', [N, T, B], 'shadow-dev publish plan --name --title "标题"', 'publish execute --name {name} --title "{title}" --body "{body}" --confirm'), + 'publish.execute': c('publish execute', '推送分支并创建/复用 PR(参数须与 plan 完全一致)', 'push and create/reuse the PR (args must match the plan exactly)', [N, T, B, PH, CF], 'shadow-dev publish execute --name --confirm', 'archive plan --name {name}'), + 'release.plan': c('release plan', '预览提交+推送+PR 复合发布', 'plan the commit+push+PR composite release', [N, FI, M, T, B], 'shadow-dev release plan --name --files a.mjs --message "feat: x" --title "标题"', 'release execute --name {name} --confirm'), + 'release.execute': c('release execute', '执行复合发布(缺省参数回退 brief workflow.release)', 'run the composite release (params default to the stored workflow.release plan)', [N, FI, M, T, B, PH, CF], 'shadow-dev release execute --name --confirm', 'archive plan --name {name}'), + 'pr.inspect': c('pr inspect', '查看 brief 关联 PR', 'inspect the PR linked to the brief', [N], 'shadow-dev pr inspect --name ', null), + 'reconcile.plan': c('reconcile plan', '预览 brief 状态与实际进度对齐', 'plan reconciling brief state with actual progress', [N], 'shadow-dev reconcile plan --name ', 'reconcile execute --name {name} --confirm'), + 'reconcile.execute': c('reconcile execute', '回写对齐后的状态', 'persist the reconciled state', [N, PH, CF], 'shadow-dev reconcile execute --name --confirm', null), + 'archive.plan': c('archive plan', '预览归档(要求 review passed 且 PR merged)', 'plan archiving (requires review passed and PR merged)', [N], 'shadow-dev archive plan --name ', 'archive execute --name {name} --confirm'), + 'archive.execute': c('archive execute', '移入 archive 并重建 INDEX', 'move into archive and rebuild INDEX', [N, PH, CF], 'shadow-dev archive execute --name --confirm', null), 'index.rebuild.plan': c('index rebuild plan', '预览变更索引重建', 'plan rebuilding the change index', [], 'shadow-dev index rebuild plan', 'index rebuild execute --plan-hash {planHash} --confirm'), 'index.rebuild.execute': c('index rebuild execute', '重建 INDEX.md(无 brief 域,--plan-hash 为唯一凭证)', 'rebuild INDEX.md (briefless command: --plan-hash is the only credential)', [f('--plan-hash', true, 'index rebuild plan 的输出', 'hash from index rebuild plan'), CF], 'shadow-dev index rebuild execute --plan-hash --confirm', null), } diff --git a/lib/domains/change.mjs b/lib/domains/change.mjs index bc17c5e..284ff91 100644 --- a/lib/domains/change.mjs +++ b/lib/domains/change.mjs @@ -1,4 +1,5 @@ -import { existsSync } from 'node:fs' +import { existsSync, readdirSync } from 'node:fs' +import { join } from 'node:path' import { brief, write, ap } from '../brief.mjs' import { name, confirm, readBody, fileList } from '../input.mjs' import { err } from '../errors.mjs' @@ -28,6 +29,20 @@ export function create(r, o) { return { name: n, path: b.path } } +// 活动变更发现入口:变更名即 shadow-docs/changes/ 下子目录名;archive 与解析失败的目录跳过(与 indexer 同规则) +export function list(r) { + const base = join(r, 'shadow-docs', 'changes'), changes = [] + if (existsSync(base)) for (const e of readdirSync(base, { withFileTypes: true })) { + if (!e.isDirectory() || e.name === 'archive') continue + try { + const d = brief(r, e.name).data + changes.push({ name: d.name ?? e.name, type: d.type ?? null, status: d.status ?? null, branch: d.branch ?? null }) + } catch {} + } + changes.sort((a, b) => a.name.localeCompare(b.name)) + return { changes } +} + export function approve(r, o) { confirm(o) const b = brief(r, name(o)) diff --git a/lib/human.mjs b/lib/human.mjs index 8043846..9ce4e91 100644 --- a/lib/human.mjs +++ b/lib/human.mjs @@ -14,11 +14,17 @@ export function done(L, v, ms) { if (v.data?.nextStep) w(ui(L, 'next', { step: v.data.nextStep })) } -export function error(L, e, p) { +// 参数行单一格式源:help 详情与缺参错误共用,内容恒从命令目录派生 +const argLine = (L, a) => ` ${a.flag}${a.required ? ' *' : ' '} ${a.desc[L] ?? a.desc.zh}` + +export function error(L, e, p, o = {}) { const code = e.code || e.message w(ui(L, 'error', { code, hint: hint(L, code) })) const spec = lookup(p) - if (spec) w(ui(L, 'example', { example: spec.example })) + if (!spec) return + // 必填参数缺失时逐行列出目录中该参数的完整描述(flag → option key 即去掉 -- 前缀) + for (const a of spec.args) if (a.required && o[a.flag.slice(2)] === undefined) w(argLine(L, a)) + w(ui(L, 'example', { example: spec.example })) } export function printHelp(L, v) { @@ -34,7 +40,7 @@ export function printHelp(L, v) { } for (const e of Object.values(v.data.commands)) { w(` ${e.usage} — ${e.summary[L] ?? e.summary.zh}`) - for (const a of e.args) w(` ${a.flag}${a.required ? ' *' : ' '} ${a.desc[L] ?? a.desc.zh}`) + for (const a of e.args) w(argLine(L, a)) w(ui(L, 'example', { example: e.example })) } } diff --git a/lib/i18n.mjs b/lib/i18n.mjs index 459ea7e..9f76612 100644 --- a/lib/i18n.mjs +++ b/lib/i18n.mjs @@ -29,7 +29,7 @@ export const HINTS = { UNKNOWN_COMMAND: { zh: '未知命令;运行 shadow-dev help 查看命令目录', en: 'unknown command; run shadow-dev help for the command list' }, INVALID_LANG: { zh: '--lang 只支持 zh 或 en', en: '--lang accepts zh or en only' }, NOT_GIT_REPOSITORY: { zh: '当前目录不在 git 仓库内', en: 'not inside a git repository' }, - NAME_REQUIRED: { zh: '缺少必填参数 --name <变更名>', en: 'missing required flag --name ' }, + NAME_REQUIRED: { zh: '缺少必填参数(详见下列参数行与示例)', en: 'missing required argument (see flag lines below)' }, CHANGE_EXISTS: { zh: '同名变更已存在(shadow-docs/changes/)', en: 'a change with this name already exists under shadow-docs/changes' }, BRIEF_NOT_FOUND: { zh: 'brief 不存在;先运行 shadow-dev change create --name <变更名>', en: 'brief not found; run shadow-dev change create --name first' }, BRIEF_FRONTMATTER_REQUIRED: { zh: 'brief 缺少 --- 包裹的 frontmatter', en: 'brief is missing its --- frontmatter block' }, diff --git a/shadow-docs/changes/20260917-fix-missing-arg-hints/brief.md b/shadow-docs/changes/20260917-fix-missing-arg-hints/brief.md index 11c4f99..19f3e19 100644 --- a/shadow-docs/changes/20260917-fix-missing-arg-hints/brief.md +++ b/shadow-docs/changes/20260917-fix-missing-arg-hints/brief.md @@ -4,7 +4,7 @@ "name": "20260917-fix-missing-arg-hints", "type": "fix", "scope": "cli,lib/human,lib/i18n,lib/commands,lib/domains", - "status": "branched", + "status": "reviewed", "baseBranch": "main", "branch": "fix/20260917-fix-missing-arg-hints", "files": [ @@ -24,14 +24,14 @@ "pullRequestUrl": null }, "review": { - "conclusion": "pending", - "verifiedCommit": null, - "verifiedAt": null + "conclusion": "passed", + "verifiedCommit": "ffff1a47542d91708c304e96409c51179464bfa7", + "verifiedAt": "2026-09-17T09:40:49.587Z" }, "workflow": { "operation": null, "checkpoint": "issue:12", - "planHash": "8dd3e7cf972e43df8efc9ff4504599503e3e2c6e9609556b73a6d4e800b8f5f6", + "planHash": "d29e8d83e1938d0aba3729a1626d0490e77964b852425a77a11782eee2ae66f3", "updatedAt": null, "lastError": null, "issuePlan": { @@ -41,6 +41,11 @@ "fix" ] } + }, + "knowledge": { + "action": "更新", + "target": "shadow-docs/knowledge/cli-output-contract.md", + "reason": "缺参错误新增从命令目录 args 派生渲染的约束;example 占位符统一为 ;新增 change list 契约面" } } --- diff --git a/shadow-docs/knowledge/cli-output-contract.md b/shadow-docs/knowledge/cli-output-contract.md index a4d91f2..859c818 100644 --- a/shadow-docs/knowledge/cli-output-contract.md +++ b/shadow-docs/knowledge/cli-output-contract.md @@ -8,6 +8,7 @@ source: - changes/20260917-feature-human-cli-ux/brief.md - changes/20260917-feature-help-compact-noise/brief.md - changes/20260917-feature-tty-human-default/brief.md + - changes/20260917-fix-missing-arg-hints/brief.md verified: 2026-09-17 --- @@ -20,6 +21,8 @@ verified: 2026-09-17 ## 执行约束 - 任何新增输出必须二选一:进 stdout JSON 契约(视为公开 API,需测试钉住),或进 stderr 人用层;**禁止**向 stdout 写非 JSON 内容。 +- 缺必填参数报错时,stderr 必须从 `COMMANDS` 逐行列出缺失参数的目录描述(`flag * desc`,经 `human.argLine` 与 help 详情共用同一渲染),示例行占位符与目录 example 一致(变更名为 ``);`HINTS` 只保留 code 级短句兜底,不得重复目录中的参数说明。 +- `shadow-dev change list` 是变更名发现入口:stdout 契约 `data.changes:[{name,type,status,branch}]`(按 name 排序),只列 `shadow-docs/changes/` 活动目录,archive 与解析失败目录静默跳过(与 indexer 同规则)。 - 抑制 stdout 的分支必须仍然设置退出码;plan 的人用收场行必须透出 `planHash`(PTY 环境下的 agent 兜底)。`--json` 是跨环境逃生门,不得复用为其他语义。 - 错误 code 与 `data.nextStep` 模板永不本地化;语言链固定为 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,且只影响 stderr 文案。 - `nextStep` 为 additive 字段,写入发生在 planHash 持久化与计算之后,不得参与 hash 输入。 @@ -32,7 +35,7 @@ verified: 2026-09-17 ## 验证方式 -`node --test test/cli.test.mjs` 全绿即契约成立(关键用例:`jsonEnabled routes the JSON surface by environment and explicit flags`、`TTY suppresses stdout JSON; --json and env restore it; planHash surfaces on stderr`、`human layer: banners and hints on stderr, stdout contract language-invariant`、`SHADOW_DEV_QUIET silences the human channel`)。手工复验:管道中 `shadow-dev repo inspect | jq .` 有 JSON;TTY 终端里 `shadow-dev help` 只见中文表、`shadow-dev --json help` 恢复 JSON;`SHADOW_DEV_QUIET=1 shadow-dev repo inspect` 的 stderr 应为空。 +`node --test test/cli.test.mjs` 全绿即契约成立(关键用例:`jsonEnabled routes the JSON surface by environment and explicit flags`、`TTY suppresses stdout JSON; --json and env restore it; planHash surfaces on stderr`、`human layer: banners and hints on stderr, stdout contract language-invariant`、`SHADOW_DEV_QUIET silences the human channel`、`missing required args render from the command catalog on stderr`、`change list enumerates active briefs and skips archive and unreadable dirs`)。手工复验:管道中 `shadow-dev repo inspect | jq .` 有 JSON;TTY 终端里 `shadow-dev help` 只见中文表、`shadow-dev --json help` 恢复 JSON;`SHADOW_DEV_QUIET=1 shadow-dev repo inspect` 的 stderr 应为空;`shadow-dev task list`(不带参数)的 stderr 应含 `--name * 变更名(shadow-docs/changes/ 下的子目录…)` 参数行。 ## 关联知识 diff --git a/test/cli.test.mjs b/test/cli.test.mjs index f06bef4..0acbd75 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -169,6 +169,20 @@ test('change approve transitions draft to proposed', () => { assert.match(readFileSync(join(root, 'shadow-docs', 'changes', 'sample', 'brief.md'), 'utf8'), /"status": "proposed"/) }) +test('change list enumerates active briefs and skips archive and unreadable dirs', () => { + const root = fixture() + const result = run(['change', 'list'], root) + assert.equal(result.status, 0, result.stderr) + const payload = JSON.parse(result.stdout) + assert.equal(payload.command, 'change.list') + assert.deepEqual(payload.data.changes, [{ name: 'sample', type: 'feat', status: 'draft', branch: null }]) + mkdirSync(join(root, 'shadow-docs', 'changes', 'archive', 'old'), { recursive: true }) + writeFileSync(join(root, 'shadow-docs', 'changes', 'archive', 'old', 'brief.md'), '---\n{"schema":"shadow-dev/v1","name":"old","type":"feat","status":"archived"}\n---\nbody\n') + mkdirSync(join(root, 'shadow-docs', 'changes', 'broken'), { recursive: true }) + const second = JSON.parse(run(['change', 'list'], root).stdout) + assert.deepEqual(second.data.changes, [{ name: 'sample', type: 'feat', status: 'draft', branch: null }]) +}) + test('task list exposes stable task identifiers', () => { const root = fixture() const result = run(['task', 'list', '--name', 'sample', '--json'], root) @@ -222,6 +236,24 @@ test('validation errors carry localized usage hints on stderr', () => { assert.match(result.stderr, /--name/) }) +test('missing required args render from the command catalog on stderr', () => { + const root = fixture() + const zh = run(['task', 'list', '--lang', 'zh'], root) + assert.equal(zh.status, 1) + const machine = JSON.parse(zh.stdout).error + assert.equal(machine.code, 'NAME_REQUIRED') + assert.equal(machine.message, 'NAME_REQUIRED: pass --name , e.g. --name 20260917-feature-x', 'stdout machine message is frozen') + assert.match(zh.stderr, /--name \* 变更名(shadow-docs\/changes\/ 下的子目录,如 20260917-feature-x)/) + assert.match(zh.stderr, /示例: shadow-dev task list --name /) + const en = run(['task', 'list', '--lang', 'en'], root) + assert.equal(zh.stdout, en.stdout, 'machine contract stays language-invariant') + assert.match(en.stderr, /--name \* change name \(subdirectory under shadow-docs\/changes\/, e\.g\. 20260917-feature-x\)/) + const multi = run(['task', 'set', '--name', 'sample', '--lang', 'zh'], root) + assert.match(multi.stderr, /--task \* 任务 id,如 task-3/) + assert.match(multi.stderr, /--state \* todo\|done/) + assert.match(multi.stderr, /--confirm \* 写操作显式确认/) +}) + test('mutating results carry a stable untranslated nextStep in JSON', () => { const root = fixture() const zh = run(['change', 'approve', '--name', 'sample', '--confirm', '--lang', 'zh'], root)