AGENTS.md 骨架生成器

把项目命令、工作边界与验收条件整理成 AGENTS.md 草稿;使用前需核对仓库实际情况。

点击生成

用途与使用方法

适合把已经确认的仓库约定整理成便于审阅的 Markdown 起点。工具不会检查仓库、替你运行测试或证明命令有效。

  1. 依次填写项目名、技术栈、项目已有命令、禁止事项和完成定义;命令按行填写。
  2. 点击“生成”,检查下方 Markdown 的 Stack、Commands、Boundaries、Definition of done 与 Review。
  3. 复制结果,或使用“下载 AGENTS.md”入口保存后再审阅;已有同名文件时先做合并,避免覆盖团队约定。

默认输入与实际输出

以下为 2026-10-05 使用页面内置合成示例的浏览器核对记录;没有执行生成的命令、配置或技能。

proj: toolbox
stack: TanStack Start, Cloudflare Workers
cmds: npm run dev
npm test
npm run deploy
forbid: Do not commit .env or wrangler secrets.
Do not force-push main.
done: Tests pass. Diff is scoped. Production only after explicit deploy.
# AGENTS.md

This file is for coding agents working in **toolbox**.

## Stack
TanStack Start, Cloudflare Workers

## Commands
```bash
npm run dev
npm test
npm run deploy
```

## Boundaries
Do not commit .env or wrangler secrets.
Do not force-push main.

## Definition of done
Tests pass. Diff is scoped. Production only after explicit deploy.

## Review
- Inspect before editing.
- Prefer the smallest diff that solves the task.
- Stop before destructive git or production actions.

适用边界

  • 默认命令包含 npm run deploy,仅是文本示例;把命令写进文件不构成部署授权。
  • 不要填写真实密钥或敏感内部资料。工作区权限、审批与隔离须由运行环境落实,文字约定不能代替这些控制。
  • 多目录指令的放置、继承与冲突处理应按目标工具文档核对;本工具不扫描现有 AGENTS.md。

常见问题

会替我检查测试命令吗?

不会。生成结果照录输入,需在目标仓库单独确认命令与验收步骤。

可以直接覆盖已有 AGENTS.md 吗?

先比较差异,再保留仍适用的团队约定;不要直接覆盖。

生成文件就能阻止危险操作吗?

文件用于表达约定。实际权限、审批、沙箱与服务端校验仍须分别配置。

继续阅读

请只使用脱敏示例,不要粘贴密钥或保密资料。站点的数据处理声明见隐私政策;本次核对没有进行独立网络行为审计。

合并练习:用仓库证据改写一份已有 AGENTS.md

以下是一个完整的虚构小仓库,所有文件片段和维护者确认都属于教学设定。它展示如何人工审阅生成结果;本工具没有读取这些文件,也没有运行命令。先看事实,再做合并,不应把表单默认技术栈和部署命令直接带入陌生仓库。

输入:目录、包脚本与三条旧规则

本练习给定的目录清单如下。这里只列出确定存在的文件;没有提供部署配置、CI定义或其他子目录规则。

mini-notes/
  AGENTS.md
  package.json
  package-lock.json
  src/
    summarize.js
  test/
    summarize.test.js
  docs/
    usage.md
package.json
{
  "name": "mini-notes",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --test test/summarize.test.js",
    "check": "node --check src/summarize.js"
  }
}
已有 AGENTS.md 全文
# mini-notes 约定
- 不要改动 package-lock.json,除非任务明确要求调整依赖。
- 提交前运行 yarn test。
- 发布前请维护者确认目标环境。

补充事实F03:本练习的维护者明确确认“项目已改用 npm,旧文档里的 yarn test 应改为 npm test;本次只整理约定,不升级依赖”。这句话是本例替换旧命令的授权依据。只有 package-lock.json 的存在还不足以证明可以自行删除团队约定。

决定表:每项保留、替换或待确认都有出处

  • F01,package.json:确认项目名、ES模块声明、test与check脚本。不能据此推断框架、Node版本、覆盖率门槛或测试一定通过。
  • F02,目录清单:src/summarize.js与test/summarize.test.js存在,与脚本路径相符;docs/usage.md存在,允许在行为变更时检查文档是否需要同步。目录清单不证明文件实现正确。
  • 保留R01:旧规则禁止未经明确任务改锁文件,与此次仅整理约定的范围一致,原样保留。
  • 替换R02:依据F01脚本和F03迁移确认,把 yarn test 改为 npm test;新增 npm run check 是本练习建议的静态语法检查要求,不冒称旧文件已有这条。
  • 保留R03并标记未知:仍须维护者确认发布环境;由于没有发布脚本、环境或权限信息,不从工具默认值引入 npm run deploy,也不猜服务商。

人工合并后的完整参考文件

下面可以作为这个虚构仓库的待审合并稿。真实仓库应先验证同样的证据并审阅差异;这是文件内容参考,不代表已写入仓库或验证任何宿主的继承规则。

# mini-notes 约定

## 已确认的项目事实
- package.json 的 name 为 mini-notes,type 为 module。
- 源文件:src/summarize.js。
- 测试文件:test/summarize.test.js。
- 使用说明:docs/usage.md。
- 本项目使用 npm;不沿用旧文档的 yarn test。

## 工作范围
- 修改前阅读本文件及实际工作目录中适用的其他约定;发生冲突时先说明冲突,不静默覆盖。
- 先读取目标文件和对应测试,再做任务需要的最小改动。
- 行为发生变化时检查 docs/usage.md;不为无关内容扩大修改范围。
- 不要改动 package-lock.json,除非任务明确要求调整依赖。
- 不因缺依赖而自行安装、升级依赖或修改锁文件,先报告阻塞。

## 检查命令
在 mini-notes 仓库根目录运行:
- npm test:调用 node --test test/summarize.test.js,运行该文件的测试。
- npm run check:调用 node --check src/summarize.js,检查该源文件语法。
这两项不证明其他文件、集成环境或生产系统正常。

## 完成定义
- 在最终改动上执行 npm test 和 npm run check。
- 逐条报告命令、运行目录、退出码及实际输出中的通过/失败摘要。
- 未执行的命令标为“未执行”并说明原因;失败时报告失败项,不写“Tests pass”。
- 审阅最终差异,列出改动文件;确认未夹带无关修改、秘密或未经授权的锁文件变更。
- 说明新增或修改的行为由哪个测试覆盖;若没有覆盖,明确列为缺口。
- 用户原有且与本任务无关的修改保持不动。

## 发布边界与未确认事项
- 发布前请维护者确认目标环境。
- 当前资料没有确认发布命令、环境、账号权限、Node版本约束或CI门禁;需要时先核实。
- 本文件不授权部署、推送、安装依赖或执行破坏性操作。
- 不从通用模板补入 npm run deploy,不把本地检查通过当作发布批准。

用一个具体变更验收这份约定

假设任务是修正 summarize.js 的空输入处理,并同步对应测试。下面展示回报的填法,尖括号是需要用实际执行结果替换的字段,不是已有测试成绩。

修改范围:src/summarize.js;test/summarize.test.js
文档判断:<检查 docs/usage.md 后说明是否需要修改及原因>
检查1:npm test
运行目录:<实际仓库根目录>
退出码:<实际退出码,未运行则写未执行>
证据摘要:<测试实际输出及空输入用例对应名称>
检查2:npm run check
运行目录:<实际仓库根目录>
退出码:<实际退出码,未运行则写未执行>
证据摘要:<实际输出;若无输出,写无输出并保留退出码>
差异审阅:<实际改动文件及锁文件是否变化>
覆盖缺口:<实际未覆盖项,没有证据时不要填“无”>
发布:未执行;目标环境与发布步骤尚待确认

若 npm test 失败而语法检查成功,完成定义仍未满足;若根本没有执行,正确结论是“未验证”,不是“应该通过”。若下一项任务真的需要新增依赖,应先取得相应授权再决定锁文件变化,不能借本例中存在检查命令就推导出安装权限。

合并验收可逐项做:旧R01与R03仍在;旧R02已按确认替换;两个检查命令准确对应脚本;未知部署信息仍明确保留;没有把默认的TanStack、Cloudflare或部署命令带入新文件。这里没有测试下载按钮产生的文件,也没有实际提交这份参考稿。

填写仓库约定

输入项目名称和技术栈,再逐行填写实际存在的检查命令。操作边界用于记录需要特别处理的动作,完成标准说明怎样确认本次修改已完成。点击生成后,在输出框核对内容;下载按钮会保存为 AGENTS.md。

示例与维护

例如,项目名称填写 my-project,技术栈填写 TypeScript,检查命令填写 npm test 和 npm run build。生成的文件包含 Stack、Commands、Boundaries、Definition of done 和 Review 小节。将命令替换为仓库真实的脚本,并随项目变化维护规则。

把 npm test 写进约定前,展开它会带上的脚本

前面的合并练习确认了命令确实存在。下一步还要问:运行这个入口时,会不会顺便做了任务没有要求的事?例如维护者把发布接在 posttest 上,AGENTS.md 只写“提交前运行 npm test”,读起来像一次测试,实际入口却连到了发布流程。本节专门补这一处,不修改生成器,也不让检查器执行仓库脚本。

npm 的 Scripts 官方文档列出了 test 的 pretest、test、posttest 顺序,并说明自定义脚本也有对应的前后阶段。脚本非零退出会中断流程。因此下文输出是“假设前序成功时需要审阅的展开顺序”,不是执行日志,更不表示这些步骤都已经发生。

本例输入完全虚构。我们会静态展开 test → posttest → release,指出 npm publish 需要单独审阅,同时保留所有看不透的 Node 叶子脚本。只根据名字叫 test 就放行,或只搜索顶层有没有 deploy,都不能发现完整调用关系。

测试入口的潜在执行链:pretest、test、posttest,后者又进入 release 和 npm publish;图示不代表已执行
虚构示例:沿 posttest 继续追踪,才能看见发布入口。前序失败时,后续阶段不一定执行。

先固定输入和只读检查器的能力边界

把下面文件保存为 package.fixture.json,不要把它改名为 package.json 后执行 npm test。文件里的 npm publish 只是被检查的字符串,演示不会执行它;private 字段也不是这次检查的安全保证。

完整文件:package.fixture.json

{
  "name": "notes-audit-fixture",
  "private": true,
  "scripts": {
    "pretest": "node scripts/check-input.cjs",
    "test": "node --test test/summary.test.cjs",
    "posttest": "npm run release",
    "prerelease": "node scripts/check-release.cjs",
    "release": "npm publish",
    "postrelease": "node scripts/report.cjs"
  }
}

检查器只识别精确的 npm test 与 npm run 名称。它在每次显式调用时依次展开已定义的 pre、主体、post。自动进入 pretest 的正文不会仅因名称前缀再自动进入 prepretest;正文若显式调用另一个 npm run,才开始新的展开。它用当前调用栈发现环,而不用全局“看过就跳过”的集合,因为同一个 check 可以被两处合法调用。

分号、重定向、引号、变量、管道、&&、传参、workspace、别名以及其他可执行程序都作为完整不透明叶子保留。工具不会试图猜 shell 语法。opaque-leaf-review 表示必须阅读对应实现与实际环境,不表示安全;action-review 只是针对少数显眼 npm 操作的提醒,不是一份完整危险命令清单。无论有没有提醒,execution_authorized 永远为 false。

入口限定为显式存在的 scripts 项,不模拟 npm start 等内置回退;只描述单包、正常生命周期启用时的命令关系。解析失败、未知引用、调用环和展开上限都不能转成通过结论。输入应是你已取得并检查过的普通 JSON 文件;本例不是面对任意恶意大文件的解析沙箱。

保存完整展开器:只读 JSON,输出需要审阅的路径

完整文件:audit.py

"""Read-only npm script expansion for a deliberately narrow language."""
import json
import re
import sys

NAME = r"[A-Za-z0-9][A-Za-z0-9:_-]*"
CALL = re.compile(r"npm run (" + NAME + r")\Z")
# These are warning patterns, never a complete side-effect classifier.
ACTION = re.compile(r"npm (?:publish|unpublish|install|ci)(?: |$)")


def audit(package, entry="test", max_steps=100):
    scripts = package.get("scripts") if isinstance(package, dict) else None
    if not isinstance(scripts, dict) or any(
        not isinstance(k, str) or not re.fullmatch(NAME, k)
        or not isinstance(v, str) or not v.strip()
        for k, v in scripts.items()
    ):
        raise ValueError("invalid-scripts")
    if not isinstance(entry, str) or not re.fullmatch(NAME, entry):
        raise ValueError("invalid-entry")
    if type(max_steps) is not int or not 1 <= max_steps <= 100:
        raise ValueError("invalid-limit")
    steps, issues = [], []
    aborted = False

    def issue(code, path):
        issues.append({"code": code, "path": list(path)})

    def body(name, path, active):
        nonlocal aborted
        if aborted:
            return
        path = path + (name,)
        if len(steps) >= max_steps:
            issue("step-limit", path)
            aborted = True
            return
        command = scripts[name].strip()
        steps.append({"path": list(path), "command": command})
        match = CALL.fullmatch(command)
        if command == "npm test":
            invoke("test", path, active)
        elif match:
            invoke(match.group(1), path, active)
        elif ACTION.match(command):
            issue("action-review", path)
        else:
            # Never split shell syntax or claim arbitrary executables are safe.
            issue("opaque-leaf-review", path)

    def invoke(name, path, active):
        if aborted:
            return
        if name in active:
            issue("cycle", path + (name,))
            return
        if name not in scripts:
            issue("missing-script", path + (name,))
            return
        active = active + (name,)
        for event in ("pre" + name, name, "post" + name):
            if event in scripts:
                body(event, path, active)

    invoke(entry, (), ())
    return {"steps": steps, "issues": issues,
            "execution_authorized": False}


if __name__ == "__main__":
    try:
        with open(sys.argv[1], encoding="utf-8") as stream:
            package = json.load(stream)
        result = audit(package, sys.argv[2] if len(sys.argv) > 2 else "test")
    except (OSError, ValueError, IndexError, RecursionError) as exc:
        print(json.dumps({"error": str(exc)}, ensure_ascii=False))
        raise SystemExit(2)
    print(json.dumps(result, ensure_ascii=False, indent=2))

路径中的每一级都能回到 package.json 的一个具体字段。调用边和叶子同时保留,例如 posttest 这一项说明为什么进入 release,posttest → release 则显示实际脚本文本。最多记录 100 个步骤,达到上限会留下 step-limit,后续不再继续展开。这个数只是本教学检查器的工作量限制,不是 npm 的执行限制。

程序退出码 0 只代表成功生成报告;有问题的报告同样可以正常生成。不要把这个退出码当成 CI 的安全放行信号。文件读写或 JSON 输入错误时退出 2。若要接入团队门禁,应先定义人工审核与处理状态的契约,本节没有提供自动授权入口。

用独立预期验证顺序、环和重复调用

保存下面的测试文件后,在同一目录运行 python test_audit.py,再运行 python audit.py package.fixture.json。expected 对象是直接写下的固定预期,没有调用被测函数生成。除了完整报告,还覆盖七组缺项、环、复杂 shell、变量、传参、workspace 与安装入口;重复调用、钩子范围、上限和输入类型另做断言。

三个负控报告分别故意删掉前后钩子、清空待审问题、把授权字段改成 true,均必须与固定预期不相等。除此之外,测试还直接改写本夹具 audit.py 的三个实现位置:跳过前后钩子、不记录未知叶子、错误授予执行权限。每个变异后的函数都实际运行,并被同一份固定预期检出;这三处针对性变异不能代表完整变异覆盖率。检查使用显式异常而非 assert,Python -O 也不会跳过。恢复用例仅在内存副本中删掉 posttest,确认发布分支消失,但两个 Node 叶子仍待审。

完整文件:test_audit.py

"""Fixed contracts remain active under python -O. No package commands run."""
def check(condition, message):
    if not condition:
        raise AssertionError(message)

import json
from copy import deepcopy
from pathlib import Path
from audit import audit
ROOT = Path(__file__).resolve().parent
package = json.loads((ROOT / 'package.fixture.json').read_text())
expected = {'steps': [{'path': ['pretest'], 'command': 'node scripts/check-input.cjs'}, {'path': ['test'], 'command': 'node --test test/summary.test.cjs'}, {'path': ['posttest'], 'command': 'npm run release'}, {'path': ['posttest', 'prerelease'], 'command': 'node scripts/check-release.cjs'}, {'path': ['posttest', 'release'], 'command': 'npm publish'}, {'path': ['posttest', 'postrelease'], 'command': 'node scripts/report.cjs'}], 'issues': [{'code': 'opaque-leaf-review', 'path': ['pretest']}, {'code': 'opaque-leaf-review', 'path': ['test']}, {'code': 'opaque-leaf-review', 'path': ['posttest', 'prerelease']}, {'code': 'action-review', 'path': ['posttest', 'release']}, {'code': 'opaque-leaf-review', 'path': ['posttest', 'postrelease']}], 'execution_authorized': False}
check(audit(package) == expected, 'contract check at original line 27')
cases = [({'test': 'npm run gone'}, [('missing-script', ['test', 'gone'])]), ({'test': 'npm run again', 'again': 'npm test'}, [('cycle', ['test', 'again', 'test'])]), ({'test': 'npm run check && npm publish'}, [('opaque-leaf-review', ['test'])]), ({'test': 'npm run $NEXT'}, [('opaque-leaf-review', ['test'])]), ({'test': 'npm run check -- --fix'}, [('opaque-leaf-review', ['test'])]), ({'test': 'npm --workspace app test'}, [('opaque-leaf-review', ['test'])]), ({'test': 'npm ci'}, [('action-review', ['test'])])]
for scripts, oracle in cases:
    got = audit({'scripts': scripts})
    check([(i['code'], i['path']) for i in got['issues']] == oracle, 'contract check at original line 40')
repeat = audit({'scripts': {'pretest': 'npm run check', 'test': 'npm run check', 'check': 'node check.cjs'}})
check([s['path'] for s in repeat['steps']] == [['pretest'], ['pretest', 'check'], ['test'], ['test', 'check']], 'contract check at original line 43')
hooks = audit({'scripts': {'prepretest': 'npm publish', 'pretest': 'node a.cjs', 'test': 'node b.cjs'}})
check([s['path'] for s in hooks['steps']] == [['pretest'], ['test']], 'contract check at original line 47')
limited = audit(package, max_steps=2)
check(limited['issues'][-1] == {'code': 'step-limit', 'path': ['posttest']}, 'contract check at original line 49')
check(len(limited['steps']) == 2, 'contract check at original line 50')
try:
    audit({'scripts': {'test': 7}})
    raise AssertionError('bad input accepted')
except ValueError as exc:
    check(str(exc) == 'invalid-scripts', 'contract check at original line 55')
mutants = [deepcopy(expected) for _ in range(3)]
mutants[0]['steps'] = [expected['steps'][1]]
mutants[1]['issues'] = []
mutants[2]['execution_authorized'] = True
check(all((m != expected for m in mutants)), 'contract check at original line 61')
recovered = deepcopy(package)
del recovered['scripts']['posttest']
check(audit(recovered) == {'steps': expected['steps'][:2], 'issues': expected['issues'][:2], 'execution_authorized': False}, 'contract check at original line 65')

# Actual implementation mutants, compiled only from this owned audit.py.
source = (ROOT / "audit.py").read_text(encoding="utf-8")
changes = [
    ("hooks", 'for event in ("pre" + name, name, "post" + name):', 'for event in (name,):'),
    ("opaque", 'issue("opaque-leaf-review", path)', 'pass'),
    ("authorization", '"execution_authorized": False', '"execution_authorized": True'),
]
killed = []
for label, old, new in changes:
    check(source.count(old) == 1, "mutation anchor: " + label)
    namespace = {"__name__": "owned_mutant"}
    exec(compile(source.replace(old, new), "<owned-mutant-" + label + ">", "exec"), namespace)
    check(namespace["audit"](package) != expected, "surviving mutant: " + label)
    killed.append(label)
print("PASS fixed report; 7 boundary cases; repeat; hook scope; limit; invalid input; 3 negative reports; recovery; 3 implementation mutants killed: " + ", ".join(killed))

for bad_entry in (None, 7, [], "bad name"):
    try:
        audit(package, entry=bad_entry)
    except ValueError as exc:
        check(str(exc) == "invalid-entry", "invalid entry error")
    else:
        raise AssertionError("bad entry accepted")
print("PASS invalid entry types")

本次本地实际输出如下。测试使用 Python 3.12.14,没有模型调用、联网、依赖安装或真实仓库命令执行。

完整文件:tests-normal.txt

PASS fixed report; 7 boundary cases; repeat; hook scope; limit; invalid input; 3 negative reports; recovery; 3 implementation mutants killed: hooks, opaque, authorization
PASS invalid entry types

同样运行 python -O test_audit.py,输出与上面一致。对于每个变异,替换点必须恰好出现一次,避免实现改动后悄悄没有变异到目标。

读懂报告:发布分支在哪里,未知部分又在哪里

完整文件:actual.json

{
  "steps": [
    {
      "path": [
        "pretest"
      ],
      "command": "node scripts/check-input.cjs"
    },
    {
      "path": [
        "test"
      ],
      "command": "node --test test/summary.test.cjs"
    },
    {
      "path": [
        "posttest"
      ],
      "command": "npm run release"
    },
    {
      "path": [
        "posttest",
        "prerelease"
      ],
      "command": "node scripts/check-release.cjs"
    },
    {
      "path": [
        "posttest",
        "release"
      ],
      "command": "npm publish"
    },
    {
      "path": [
        "posttest",
        "postrelease"
      ],
      "command": "node scripts/report.cjs"
    }
  ],
  "issues": [
    {
      "code": "opaque-leaf-review",
      "path": [
        "pretest"
      ]
    },
    {
      "code": "opaque-leaf-review",
      "path": [
        "test"
      ]
    },
    {
      "code": "opaque-leaf-review",
      "path": [
        "posttest",
        "prerelease"
      ]
    },
    {
      "code": "action-review",
      "path": [
        "posttest",
        "release"
      ]
    },
    {
      "code": "opaque-leaf-review",
      "path": [
        "posttest",
        "postrelease"
      ]
    }
  ],
  "execution_authorized": false
}

第一个需要改变任务决定的位置是 posttest → release。当前任务如果只允许测试,就应停在执行前,报告这条路径和原始脚本文本。不能因为 package.fixture.json 中 private 为 true,就试着运行整串命令验证“是否真的发得出去”。前后还有四个不透明叶子,它们可能读取文件、联网、调用子进程,必须检查真实源码、依赖和运行条件。

修复有不同路线,不能自动替维护者选择。维护者可以批准在单独变更中拆开测试与发布入口,再复核新脚本;也可能提供一个已经验证的纯本地检查入口。直接绕开 npm 去执行 node --test 会丢掉 pretest 中可能必要的准备逻辑,因此不能无说明地把它写成等价替代。

npm 的 npm run 官方文档解释了 ignore-scripts 对显式脚本及其前后阶段的不同影响。这里不把切换该选项当作通用修复:主脚本本身仍可能包含不允许的动作,而且跳过准备阶段可能改变测试含义。真实环境还需要核对 .npmrc、环境变量、包目录、workspace、PATH 与所选 shell。

另跑一个只有打印动作的真实 npm 探针

静态展开报告不能证明实际 npm 行为。下面探针另建临时目录,生成只打印 MARK 标记的 Node 文件:pretest 与 test 分别显式调用 check,check 自己有前后钩子,posttest 打印收尾。另放一个不应自动触发的 prepretest。它不读取、更不运行风险夹具,也不安装依赖、不发布包。

探针仅适用于已有可信 npm、Node 与 /bin/sh 的环境。它清理继承的 npm 配置、Node 启动钩子和 shell 初始化变量,把用户配置、全局配置与 npm 缓存放进同一个临时根目录,关闭更新提示并使用离线选项。命令在临时包目录执行;退出后验证整个目录已删除。这仍不是针对任意不可信运行时的隔离沙箱。

完整文件:probe_npm.py

"""Execute only an owned marker fixture; never package.fixture.json."""
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path


def check(condition, message):
    if not condition:
        raise AssertionError(message)


def main():
    npm = shutil.which('npm')
    node = shutil.which('node')
    check(npm and node and Path('/bin/sh').exists(), 'Requires installed npm, Node, /bin/sh')
    with tempfile.TemporaryDirectory(prefix='npm-marker-probe-') as tmp:
        root = Path(tmp)
        scripts = {
            'prepretest': 'node marker.cjs UNEXPECTED',
            'pretest': 'npm run check',
            'test': 'npm run check',
            'posttest': 'node marker.cjs POST',
            'precheck': 'node marker.cjs CHECK_PRE',
            'check': 'node marker.cjs CHECK',
            'postcheck': 'node marker.cjs CHECK_POST',
        }
        (root / 'package.json').write_text(json.dumps({'private': True, 'scripts': scripts}))
        (root / 'marker.cjs').write_text("console.log('MARK:' + process.argv[2]);\n")
        # Do not inherit npm options or Node startup hooks; distinct config files.
        env = {k: v for k, v in os.environ.items()
               if not k.lower().startswith('npm_config_')
               and k not in {'NODE_OPTIONS', 'NODE_PATH', 'BASH_ENV', 'ENV'}}
        for name in ('user.npmrc', 'global.npmrc'):
            (root / name).write_text('')
        env.update({
            'npm_config_userconfig': str(root / 'user.npmrc'),
            'npm_config_globalconfig': str(root / 'global.npmrc'),
            'npm_config_cache': str(root / 'cache'),
            'npm_config_script_shell': '/bin/sh',
            'npm_config_update_notifier': 'false',
            'npm_config_offline': 'true',
        })
        def run(args):
            return subprocess.run(args, cwd=root, env=env, capture_output=True,
                                  text=True, timeout=30)
        versions = {}
        for label, executable in [('npm', npm), ('node', node)]:
            result = run([executable, '--version'])
            check(result.returncode == 0, label + ' version failed')
            versions[label] = result.stdout.strip()
        cases = [
            ('hooks_enabled', False, ['MARK:CHECK_PRE', 'MARK:CHECK', 'MARK:CHECK_POST',
                                     'MARK:CHECK_PRE', 'MARK:CHECK', 'MARK:CHECK_POST', 'MARK:POST']),
            ('hooks_ignored', True, ['MARK:CHECK']),
        ]
        results = []
        for label, ignored, expected in cases:
            result = run([npm, '--ignore-scripts=' + str(ignored).lower(), 'test'])
            markers = [line for line in result.stdout.splitlines() if line.startswith('MARK:')]
            check(result.returncode == 0, label + ': ' + result.stderr)
            check(markers == expected, label + ': ' + repr(markers))
            results.append({'case': label, 'exit_code': result.returncode, 'markers': markers,
                            'stdout': result.stdout, 'stderr': result.stderr})
    check(not root.exists(), 'temporary fixture cleanup failed')
    return {'versions': versions, 'cases': results, 'temporary_directory_removed': True}


if __name__ == '__main__':
    print(json.dumps(main(), ensure_ascii=False, indent=2))

运行 python probe_npm.py,实际结果如下;python -O probe_npm.py 也得到同样的标记与退出码。stdout 保留了 npm 自己打印的完整执行顺序,便于区分调用行和 MARK 标记。

完整文件:probe-output.json

{
  "versions": {
    "npm": "11.9.0",
    "node": "v24.19.0"
  },
  "cases": [
    {
      "case": "hooks_enabled",
      "exit_code": 0,
      "markers": [
        "MARK:CHECK_PRE",
        "MARK:CHECK",
        "MARK:CHECK_POST",
        "MARK:CHECK_PRE",
        "MARK:CHECK",
        "MARK:CHECK_POST",
        "MARK:POST"
      ],
      "stdout": "\n> pretest\n> npm run check\n\n\n> precheck\n> node marker.cjs CHECK_PRE\n\nMARK:CHECK_PRE\n\n> check\n> node marker.cjs CHECK\n\nMARK:CHECK\n\n> postcheck\n> node marker.cjs CHECK_POST\n\nMARK:CHECK_POST\n\n> test\n> npm run check\n\n\n> precheck\n> node marker.cjs CHECK_PRE\n\nMARK:CHECK_PRE\n\n> check\n> node marker.cjs CHECK\n\nMARK:CHECK\n\n> postcheck\n> node marker.cjs CHECK_POST\n\nMARK:CHECK_POST\n\n> posttest\n> node marker.cjs POST\n\nMARK:POST\n",
      "stderr": ""
    },
    {
      "case": "hooks_ignored",
      "exit_code": 0,
      "markers": [
        "MARK:CHECK"
      ],
      "stdout": "\n> test\n> npm run check\n\n\n> check\n> node marker.cjs CHECK\n\nMARK:CHECK\n",
      "stderr": ""
    }
  ],
  "temporary_directory_removed": true
}

hooks_enabled 中 check 的三个标记出现两轮,最后是 POST;没有 UNEXPECTED。hooks_ignored 中只有 CHECK,说明显式运行的主体仍执行,但外层与嵌套调用的自动前后钩子都跳过。本机版本见结果的 versions:不要把文档页面当前标出的 v11.21.0 当成这次实测的 npm 11.9.0。

探针只验证这些受控生命周期行为。它没有运行生产仓库、npm publish、安装、联网或模型代理,不证明真实项目没有副作用。配置加载失败、超时或标记不符都会使探针报错;不要把错误输出当成有效顺序。先保存报错,再检查本机版本、配置和可执行文件来源,不要为得到绿色结果去运行风险夹具。

把结果写回 AGENTS.md,而不是写一句“命令安全”

在本虚构案例中,命令区可以先留下以下待审文本。它是给维护者看的建议,不是已经修改过的真实仓库规则,也不授予修改 package.json 或执行发布的权限。

## 检查入口的当前状态
- 暂不执行 npm test:posttest 显式调用 release,release 的内容为 npm publish。
- pretest、test、prerelease、postrelease 引用的 Node 实现尚未审阅。
- 请维护者确认是否拆开测试与发布入口;批准后在单独变更中处理。
- 修订后重新展开入口,阅读所有不透明叶子,并记录实际执行的目录、命令和结果。
- 静态报告只帮助定位脚本关系,不代表运行授权或测试通过。

当维护者完成拆分时,要重新读取最终的 package.json,而不是只看旧报告。恢复测试里移除 posttest 的副本,已经说明“发布分支消失”和“全部检查可执行”是两个判断。前者可以从这份静态报告得到,后者仍取决于源码、实际权限与运行环境。

资料核查日期为 2026-10-14。两份引用均为 npm 官方 v11 文档,本次访问页面标注 v11.21.0;本地 npm 版本另行记录。本节的新增价值是命令展开与审阅证据,原生成器表单、按钮、默认值与已有合并案例保持原样。

常见问题

生成器会读取我的仓库吗?

不会。内容由你填写的字段组合而成,在浏览器内生成;仓库路径、命令和完成标准需要你核对。

继续阅读