Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-09-22
skip_specs: true
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### Requirement: Check and save faces MUST skip dot-prefixed paths by default

相对项目根,任一路径段以 `.` 开头(`.`、`..` 段除外)的目录与文件为点前缀路径,默认忽略:不扫描、不检查、不报告。适用于 PostToolUse 保存面、提交门禁 delta 面、语言发现面,以及全量扫描的 ruff 与 `find` 型 gate 通道。项目根本身位于点前缀父目录下(如 `~/.config/proj/`)不构成点前缀命中。
点前缀路径的默认忽略 MUST 作用于全部检查面。相对项目根,任一路径段以 `.` 开头(`.`、`..` 段除外)的目录与文件为点前缀路径,默认忽略:不扫描、不检查、不报告。适用于 PostToolUse 保存面、提交门禁 delta 面、语言发现面,以及全量扫描的 ruff 与 `find` 型 gate 通道。项目根本身位于点前缀父目录下(如 `~/.config/proj/`)不构成点前缀命中。

#### Scenario: PostToolUse skips dot-prefixed files

Expand Down Expand Up @@ -37,7 +37,7 @@

### Requirement: Commit safety face MUST NOT be weakened by dot-prefix skipping

点前缀默认忽略只作用于检查面。入库安全检查独立收集拟入库路径:密钥/凭据类文件模式(`.env`、`*.pem`、`.DS_Store` 等)无论点前缀与否照常拦截;点前缀目录(宿主插件清单与第一方配置)照常可入库。
入库安全检查 MUST NOT 因点前缀忽略而放宽。点前缀默认忽略只作用于检查面。入库安全检查独立收集拟入库路径:密钥/凭据类文件模式(`.env`、`*.pem`、`.DS_Store` 等)无论点前缀与否照常拦截;点前缀目录(宿主插件清单与第一方配置)照常可入库。

#### Scenario: Secret files remain blocked

Expand All @@ -51,7 +51,7 @@

### Requirement: Config discovery MUST keep matching dot-prefixed config files

linter 配置发现(`requiresConfig`、`linter_config_files` 项目级匹配)不受点前缀忽略影响:点前缀配置文件照常使语言判定为已接入。
配置发现 MUST NOT 受点前缀忽略影响。linter 配置发现(`requiresConfig`、`linter_config_files` 项目级匹配)不受点前缀忽略影响:点前缀配置文件照常使语言判定为已接入。

#### Scenario: requiresConfig matches dot files

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Context

v0.15.0 发版实测:bump-plugin 的两个 plain manifest 没被更新(catalog 0.14.14 vs manifest 0.14.12
漂移 → replace 静默 no-op),且 `node scripts/bump-plugin.mjs` 被 git 意图门禁不可绕过地拦住
(UNVERIFIED)。后者的机制:`_indirect_body` 取到 .mjs 正文后,`split_shell_segments` 按 shell
语法切段,帮助文本模板字符串里的 `git add && git commit && git push` 被认成真实副作用;resolve 阶段因
「非 Shell 正文不可建模」抛 GitIntentError——而 skipGate/内联豁免都在解析成功之后才评估,故不可绕过。

## Goals / Non-Goals

**Goals:**
- 版本漂移时发版工具 fail-loud,写后回读校验,消除半程假成功。
- 非 Shell 正文的 git 归因只认 subprocess/exec 调用形态,消除帮助文本误报;
真实间接 git 调用保持「UNVERIFIED 阻断」的既有保守语义。

**Non-Goals:**
- 不建模 Node/Python 的动态拼接 subprocess(设计既有边界)。
- 不改 Shell 正文的切段解析与 skipGate 链语义。
- 不改 bump 的版本计算规则(仍以 catalog 为 oldVersion 源)。
- Bash heredoc/命令文本数据段的 git 样例误报不在本次范围(需完整 shell 解析,另立 change)。

## Decisions

**fail-loud 而非 regex 自愈。** 替换失败时按任意版本强行覆盖会把「上游误升/误降」也静默吞掉;
显式抛错并报告 manifest 实际版本与 catalog 版本,把漂移暴露给人,符合 verdict-integrity
「不把缺失证据标为通过」。写后回读校验五文件版本一致,崩溃在 sync 步之前也能被拦住。

**非 Shell 正文按调用形态识别,不按文本切段。** 高精度信号是 `execFileSync("git", …)`、
`subprocess.run(["git", …])`、`os.system("…")` 这类调用点;帮助文本不会以调用形态出现
(bump 帮助文本前是 `console.log(`,不匹配调用词表)。命中调用形态 → 沿用既有阻断
(不可建模 UNVERIFIED),保持「宁可拦住不放行不可证明的 git」的方向安全。
误报面收窄到「帮助文本里恰好写成调用形态的样例」,可接受。

## Risks / Trade-offs

- 调用词表是启发式:`eval(...)`、动态拼接命令等仍漏检——与既有「不承诺分析动态拼接」
边界一致,不新增风险敞口。
- fail-loud 会让漂移状态的发版直接失败:这是特性而非代价(此前是假成功)。
- bump 自身的发版即 dogfood 验证(工具修好后直跑它自己发版)。

## Verification

- 新增回归测试全绿 + 全量 unittest / run_all 零新增失败 + ruff 干净。
- 直跑 `node scripts/bump-plugin.mjs`(不带 sh -c 包装)通过门禁 = 误报修复的活证据。
- bump 在制造的漂移 fixture 上抛错 = fail-loud 活证据。
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 2026-09-23-fix-release-chain-and-gate-attribution

## Why

v0.15.0 发版过程实测出两个缺陷(用户指令 2026-09-23:修复):

1. **bump-plugin.mjs 的 plain manifest 替换静默 no-op**:`bumpPlain` 按 catalog 版本字符串
精确替换,catalog 与 manifest 漂移时(实测 catalog 0.14.14 vs manifest 0.14.12)replace
不匹配、无报错地写回原文,发版计划照常打印「-> 0.15.0」——半程假成功,版本链断裂还伴随
后续 sync 校验崩溃(同根因)。
2. **git 意图门禁的间接正文误报**:非 Shell 解释器(node/python)脚本正文被按 shell 切段扫描,
JS 模板字符串里的 git commit/git add/git push **样例文本**(bump-plugin.mjs:164-165 帮助文本
`cd … && git add && git commit && git push`)被当成真实 git 副作用 →
`node scripts/bump-plugin.mjs` 被判「间接 Git 操作不能可靠建模」**不可绕过地阻断**,
发版工具被自家门禁锁死。

## What Changes

- bump-plugin.mjs:plain manifest 替换失败(版本漂移)改为**显式抛错**,不再静默跳过;
全部写入后回读校验版本一致,杜绝半程假成功。
- git 意图归因:非 Shell 脚本正文不再 shell 切段,改按**subprocess/exec 调用形态**高精度识别
git 副作用(`execFileSync("git", …)`、`os.system("…")` 等);帮助文本/模板字符串里的
git 字面量不再触发门禁。命中真实调用时仍按既有语义「不可建模 → UNVERIFIED 阻断」。
- Shell 脚本正文的行为完全不变(可建模,保留切段解析)。

## Capabilities

### New Capabilities

无。

### Modified Capabilities

无(行为修复归入既有 gate-trigger-policy 语义:误报消除、触发面不变;按 fix-gate-trigger-and-mcp
先例以 proposal+design+tasks 收口)。

## Impact

- `scripts/bump-plugin.mjs`、`scripts/codeguard/git_syntax.py`、`scripts/codeguard/git_context.py`。
- 发版链路行为:漂移时 fail-loud(此前静默);`node scripts/bump-plugin.mjs` 恢复可直跑。
- 测试:新增间接归因回归(JS 帮助文本不触发 / node、python 真实 git 调用仍触发并阻断 / Shell 行为不变)。
- 已知边界(非目标):Bash heredoc/命令文本数据段里的 git 样例仍可能被外层切段误伤(写作侧规避)。
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# hook-protocol(增量):非 Shell 间接正文按调用形态归因

## MODIFIED Requirements

### Requirement: The guard SHALL treat one-level interpreter indirection as guarded

`is_guarded()` MUST 覆盖直接命令与一层解释器间接。**Shell 解释器(bash/sh/zsh)**:脚本文件文本或 `-c` 内联代码按与直接命令相同的分隔符切段规则扫描到 `git commit|git push` 段首命令词时 MUST 判为拦截;切段扫描前 MUST 先展开 `$(...)` 与反引号内层文本(shell 语义下它们会被真实执行;`ro=$(git push …)` 曾静默放行),段首归一化 MUST 覆盖 shell 控制引导词(`if`/`then`/`else`/`elif`/`while`/`until`/`do`/`!`——`if git push; then`、`for x; do git push; done` 曾静默放行)。**非 Shell 解释器(python/node 等)**:正文 MUST NOT 按 shell 切段归因——模板字符串/帮助文本里的 git 命令字面量样例不是调用(bump-plugin.mjs:164-165 帮助文本实测误报、发版工具被不可绕过地锁死);归因 MUST 只认 subprocess/exec 调用形态(`execFileSync("git", …)`、`subprocess.run(["git", …])`、`os.system("…")` 等调用点的 git 参数位),命中调用形态时 MUST 判为拦截并在 resolve 阶段按「不可建模」UNVERIFIED 阻断。`resolve_project_roots()` MUST 用同一套归一化判定收集仓库边界(含 `git -C <path>` 显式仓边界)——判定不同源时会出现"命中拦截但 roots=[] → 静默放行"的击穿(0.8.2 实测)。更深的动态构造(如 subprocess 参数拼接)MUST 在文档中声明为能力边界而非承诺。

#### Scenario: A wrapper script performs the commit

- **WHEN** 调用形如 `bash runner.sh` 且脚本体内含 `git commit` 或 `git push`
- **THEN** 判定命中,硬门禁按正常流程跑 linter 并可能 exit 2

#### Scenario: Command substitution or shell control structure hides the git call

- **WHEN** 调用形如 `ro=$(git push origin b 2>&1)`、`` x=`git commit` ``、`for x; do git push; done` 或 `if git push; then …; fi`
- **THEN** 展开/归一化后命中拦截,且 `resolve_project_roots` 解析出正确仓库边界(不为 [])

#### Scenario: Text mentioning git push in an echo is not guarded

- **WHEN** 调用为 `echo "git push 是危险命令"` 或脚本内容仅为 `echo hi`
- **THEN** 判定不命中,钩子静默放行

#### Scenario: Help text samples in a non-shell script are not guarded

- **WHEN** `node scripts/tool.mjs` 的正文含模板字符串帮助文本 `cd <root> && git add -A && git commit -m "x" && git push`
- **THEN** 判定不命中(字面量样例不是调用),钩子放行

#### Scenario: A non-shell script really invokes git via subprocess

- **WHEN** `python3 work.py` 的正文含 `subprocess.run(["git", "push"])` 或 `node run.mjs` 的正文含 `execFileSync("git", ["git commit" …])` 形态的调用
- **THEN** 判定命中,且 resolve 阶段按「间接 Git 操作不能可靠建模」UNVERIFIED 阻断(不静默放行)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Tasks: 2026-09-23-fix-release-chain-and-gate-attribution

## 1. bump-plugin fail-loud

- [x] 1.1 `bumpPlain` 替换无命中时抛错,报告 manifest 实际版本与 catalog 版本
- [x] 1.2 全部写入后回读校验 catalog + 4 manifest 版本与计划一致

## 2. git 意图归因(非 Shell 正文)

- [x] 2.1 `git_syntax` 新增调用形态检测(exec/spawn/subprocess/os.system 等 + git 参数位),返回 commit|push|add|config 与 skipGate 变更
- [x] 2.2 `git_context` resolve 路径:非 Shell 正文改走调用形态检测;Shell 正文行为不变
- [x] 2.3 `_guarded_mode`/`_command_indirect` 同步按 is_shell 分派

## 3. 回归测试

- [x] 3.1 JS 帮助文本(模板字符串 git 样例)不触发门禁
- [x] 3.2 node `execFileSync("git", ["…"])` / python `subprocess.run(["git", …])` 仍触发并 UNVERIFIED 阻断
- [x] 3.3 Shell 脚本真实 git commit 仍可建模(行为不变)
- [x] 3.4 非 Shell 帮助文本里的 `git config codeguard.skipGate` 不构成豁免变更
- [x] 3.5 bump 漂移 fixture 抛错(fail-loud)

## 4. 发版

- [ ] 4.1 全量回归 + ruff 干净
- [ ] 4.2 dogfood:直跑 `node scripts/bump-plugin.mjs codeguard patch`(不带包装)过门禁并发版
- [ ] 4.3 市场仓同步 + PR/CI/合并 + 两仓推送
2 changes: 1 addition & 1 deletion openspec/specs/asset-canonicalization/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# asset-canonicalization Specification

## Purpose
TBD - created by archiving change consolidate-marketing-assets. Update Purpose after archive.
定义营销资产的正典化契约:零引用资产必须移除、保留资产必须在 assets/README.md 逐个登记,使资产面始终可审计、可追溯,不积累无主文件。
## Requirements
### Requirement: Zero-reference marketing assets SHALL be removed

Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/bilingual-docs-consistency/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# bilingual-docs-consistency Specification

## Purpose
TBD - created by archiving change add-readme-parity-gate. Update Purpose after archive.
定义 README.md 与 README.zh-CN.md 的结构一致性契约:标题层级、本地链接目标与版本串必须镜像,由 tests/test_readme_parity.py 门禁强制,保证双语用户入口同步演进、任一边漏改即失败。
## Requirements
### Requirement: README structural parity SHALL be enforced by tests/test_readme_parity.py

Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/gate-trigger-policy/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# gate-trigger-policy Specification

## Purpose
TBD - created by archiving change 2026-09-22-fix-gate-trigger-and-mcp. Update Purpose after archive.
定义 UserPromptSubmit 门禁的触发策略:只有真实提交意图才启动 lint 门禁的精确判据(词边界、疑问句识别、否定式排除、可识别语言收窄),消除日常对话的误触发与无关全量检查。
## Requirements
### Requirement: The UserPromptSubmit hook SHALL run the lint gate only when intent is commit-like
The `UserPromptSubmit` hook MUST evaluate the user's prompt for commit intent using word-boundary matching of trigger words (`commit|push|deploy|发布|部署`) and a separate action-verb check. The hook SHALL exit silently when either (a) no trigger word is present, (b) the only trigger occurrence is followed by `?`, or (c) a trigger word appears in an interrogative sentence identified by `QUESTION_MARKERS` (`?`, `?`, `么`, `吗`, `如何`, `怎么`, `有没有`, `是不是`, `什么是`, `哪些`).
Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/idiomatic-runner/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# idiomatic-runner Specification

## Purpose
TBD - created by archiving change add-run-per-language. Update Purpose after archive.
定义按语言执行的运行器契约:per-language 子进程执行收口在 scripts/run_per_language.py,入口(run_check.py/fix.py)不再自建 subprocess 运行器,退出码统一归一化,避免同一执行语义多处漂移。
## Requirements
### Requirement: Per-language execution SHALL live in scripts/run_per_language.py

Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/languages-registry-contract/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# languages-registry-contract Specification

## Purpose
TBD - created by archiving change add-languages-schema-validator. Update Purpose after archive.
定义 languages.json 注册表契约:语言清单的单一事实源由 scripts/validate_languages_json.py 的结构与一致性规则校验并在 CI 强制,任何字段缺失、命令形态错误或命名冲突在入库前被拦截。
## Requirements
### Requirement: languages.json SHALL be validated by scripts/validate_languages_json.py

Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/plugin-manifest-contracts/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# plugin-manifest-contracts Specification

## Purpose
TBD - created by archiving change add-manifest-bundle-dynamic. Update Purpose after archive.
定义插件清单族的一致性契约:四端 manifest 与市场清单的版本链一致、发布物中的 skills bundle 必须等于 skills.lock 与 plugin-local 声明的并集,安装面不出现缺技能或多技能。
## Requirements
### Requirement: Released manifest skills bundle SHALL equal the lock+local union

Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/registry-driven-config/spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# registry-driven-config Specification

## Purpose
TBD - created by archiving change add-linter-config-from-registry. Update Purpose after archive.
定义注册表驱动的 linter 配置契约:各语言 linter 配置文件清单只在 languages.json 声明,env_check 等消费方从注册表派生配置事实而非硬编码字典,保证新增语言只改一处。
## Requirements
### Requirement: Per-language linter config files SHALL live in scripts/languages.json

Expand Down
24 changes: 22 additions & 2 deletions scripts/bump-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,19 @@ catalogText = catalogText.slice(0, segStart) + `"version": "${newVersion}"` + ca
fs.writeFileSync(catalogPath, catalogText);

// 2) 各仓 manifest
const bumpPlain = (text) => text.replace(`"version": "${oldVersion}"`, `"version": "${newVersion}"`);
const bumpPlain = (text, rel) => {
const updated = text.replace(`"version": "${oldVersion}"`, `"version": "${newVersion}"`);
if (updated === text) {
const actual = /"version": "([^"]+)"/.exec(text)?.[1] ?? "(未找到)";
throw new Error(`${rel}: manifest 版本 ${actual} 与 catalog ${oldVersion} 漂移;拒绝静默跳过,请先对齐版本再发版`);
}
return updated;
};
const bumpCodex = (text) => text.replace(/"version": "\d+\.\d+\.\d+(?:\+codex\.\d+)?"/, `"version": "${newVersion}+codex.${today}"`);

for (const rel of plainManifestRels) {
const manifest = path.join(repoDir, rel);
fs.writeFileSync(manifest, bumpPlain(fs.readFileSync(manifest, "utf8")));
fs.writeFileSync(manifest, bumpPlain(fs.readFileSync(manifest, "utf8"), rel));
}
const repositoryMarketplace = path.join(repoDir, ".agents/plugins/marketplace.json");
const marketplace = JSON.parse(fs.readFileSync(repositoryMarketplace, "utf8"));
Expand All @@ -153,6 +160,19 @@ fs.writeFileSync(repositoryMarketplace, `${JSON.stringify(marketplace, null, 2)}
const codexManifest = path.join(repoDir, ".codex-plugin/plugin.json");
fs.writeFileSync(codexManifest, bumpCodex(fs.readFileSync(codexManifest, "utf8")));

// 写后回读:五文件版本必须与计划一致——写后不验等于没写(防半程假成功)
for (const rel of plainManifestRels) {
const readback = JSON.parse(fs.readFileSync(path.join(repoDir, rel), "utf8")).version;
if (readback !== newVersion) throw new Error(`${rel}: 写后回读 ${readback} != ${newVersion}`);
}
const codexReadback = fs.readFileSync(codexManifest, "utf8");
if (!codexReadback.includes(`"version": "${newVersion}+codex.${today}"`)) {
throw new Error(`.codex-plugin/plugin.json: 写后回读版本不符`);
}
if (!catalogText.includes(`"version": "${newVersion}"`)) {
throw new Error(`catalog.json: 写后回读版本不符`);
}

// 3) 重新生成三平台清单 + 全量校验
const pluginFilter = `--plugin=${pluginId}`;
execFileSync(process.execPath, [path.join(root, "scripts/sync-marketplaces.mjs"), "--write", pluginFilter], { stdio: "inherit" });
Expand Down
Loading
Loading