Compare commits
20 Commits
7075d863c2
...
1abfcbf59d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1abfcbf59d | |||
| f9ce9b6015 | |||
| 36da962bb0 | |||
| 4fc7c4a776 | |||
| d53b17f6f1 | |||
| 30222fd85e | |||
| fc750ddcf1 | |||
| 6fc5e65986 | |||
| b767732fd3 | |||
| c16a867c60 | |||
| 3f7ed99aec | |||
| 99408d3cec | |||
| 7619dd5928 | |||
| 1f60506c89 | |||
| 4b18c1bec8 | |||
| d5e9bf5b01 | |||
| e29ce3be86 | |||
| 9052ecbc2a | |||
| efc6f50199 | |||
| 32d02b5a8f |
28
README.md
28
README.md
@ -15,7 +15,8 @@
|
|||||||
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
||||||
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
||||||
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
||||||
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、稳定 `02:42:*` MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
|
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、实体 OUI 风格 MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
|
||||||
|
- 生成 NapCatQQ 上游同步与 runtime 发布自动化入口:MCP/CLI 默认 dry-run,覆盖 upstream audit、sync candidate review、runtime readiness、remote dev handoff 和 NAS Codex bootstrap plan;upstream audit 在显式 `execute=true` 且 `useCodex=true` 时可运行 Codex,NAS Codex bootstrap 在显式 `execute=true` 时只返回目录准备和 CLI 包检查/安装计划,不复制 secrets、不写 systemd、不触发发布。
|
||||||
- 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。
|
- 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。
|
||||||
- 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。
|
- 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。
|
||||||
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
||||||
@ -40,6 +41,11 @@ pnpm run obsidian-context -- --module ktWorkflow
|
|||||||
pnpm run obsidian-validate
|
pnpm run obsidian-validate
|
||||||
pnpm run obsidian-sync
|
pnpm run obsidian-sync
|
||||||
pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --doc-sync "无需文档更新" --cleanup "cleanup-history dry-run deleted=0" --cleanup-final-deleted 0 --review "global-review findings=0" --superpowers-review "Superpowers reviewer completed; no Critical/Important findings" --problem "无新卡点" --solution "无新增稳定解法"
|
pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --doc-sync "无需文档更新" --cleanup "cleanup-history dry-run deleted=0" --cleanup-final-deleted 0 --review "global-review findings=0" --superpowers-review "Superpowers reviewer completed; no Critical/Important findings" --problem "无新卡点" --solution "无新增稳定解法"
|
||||||
|
pnpm run napcat-upstream-audit -- --artifact-root .kt-workspace/test-artifacts/napcat-upstream-sync
|
||||||
|
pnpm run napcat-sync-candidate-review
|
||||||
|
pnpm run napcat-runtime-release-readiness
|
||||||
|
pnpm run napcat-remote-dev-handoff
|
||||||
|
pnpm run nas-codex-bootstrap
|
||||||
pnpm run napcat-device-profile-check
|
pnpm run napcat-device-profile-check
|
||||||
pnpm run cleanup-history -- --dry-run
|
pnpm run cleanup-history -- --dry-run
|
||||||
pnpm run cleanup-history -- --execute
|
pnpm run cleanup-history -- --execute
|
||||||
@ -49,6 +55,15 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
|
|
||||||
如果本机 Node/npm/pnpm 版本不对,先执行 `nvm ls` 查看已安装版本,再用 `nvm use <version>` 切换到目标版本;不要先扫盘找 `node.exe` 路径。
|
如果本机 Node/npm/pnpm 版本不对,先执行 `nvm ls` 查看已安装版本,再用 `nvm use <version>` 切换到目标版本;不要先扫盘找 `node.exe` 路径。
|
||||||
|
|
||||||
|
## Jenkins 入口模板
|
||||||
|
|
||||||
|
- `ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile` 是 NapCatQQ 上游审计入口,默认只调用 ktWorkflow 的 `napcat-upstream-audit` dry-run,保留人工审查边界。
|
||||||
|
- Codex 上游审计必须同时设置 `RUN_CODEX_AUDIT=true` 并填写 `CONFIRM_CODEX_WORKSPACE_WRITE=RUN_CODEX_WORKSPACE_WRITE_AUDIT`;这是因为 Codex runner 使用 workspace-write sandbox,不能作为定时任务默认行为。
|
||||||
|
- `ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile` 是运行时发布入口,先调用 `napcat-runtime-release-readiness`,再通过显式参数触发运行时镜像构建和 API promotion;下游 job 使用白名单 choice,避免任意 Jenkins job 名被传入。
|
||||||
|
- 生产 API promotion 除了 `PROMOTE_API_RUNTIME=true` 和明确 image/profile 参数外,还必须填写 `CONFIRM_API_PROMOTION_TARGET=PROMOTE_KT_TEMPLATE_API_MAIN`。
|
||||||
|
- 两个模板默认使用 NAS 工作区 `/vol1/docker/kt-codex/workspace/KT`,运行证据落在 `/vol1/docker/kt-codex/artifacts` 下。
|
||||||
|
- 模板只保存入口和参数形状,不记录 Jenkins credential、Token、SSH key 或其他真实凭据。
|
||||||
|
|
||||||
## MCP 客户端配置
|
## MCP 客户端配置
|
||||||
|
|
||||||
把下面配置加入支持 stdio MCP 的客户端配置里:
|
把下面配置加入支持 stdio MCP 的客户端配置里:
|
||||||
@ -95,6 +110,11 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
|
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
|
||||||
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
|
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
|
||||||
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
|
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
|
||||||
|
| `kt_napcat_upstream_audit` | 生成或执行 NapCatQQ upstream release 审计,默认 dry-run,显式 `execute/useCodex` 后才运行 Codex |
|
||||||
|
| `kt_napcat_sync_candidate_review` | 生成 NapCat sync candidate review 的 prompt/context/只读命令骨架,Task 5 不执行 Codex 或 Git 写操作 |
|
||||||
|
| `kt_napcat_runtime_release_readiness` | 生成 NapCat runtime release readiness 的 prompt/context/只读检查骨架,不自动构建或发布 |
|
||||||
|
| `kt_napcat_remote_dev_handoff` | 生成 NapCat 自动化远程开发 handoff 的 prompt/context 骨架,不创建线程或同步仓库 |
|
||||||
|
| `kt_nas_codex_bootstrap_plan` | 生成可信 NAS Codex bootstrap dry-run/execute command plan;execute 只规划目录准备和 CLI 包检查/安装,不复制 secrets、不写 systemd |
|
||||||
| `kt_napcat_device_profile_check` | 静态检查 API NapCat 设备身份护栏,覆盖 hostname、MAC、`machine-info`、runtime dir 和 DB timezone |
|
| `kt_napcat_device_profile_check` | 静态检查 API NapCat 设备身份护栏,覆盖 hostname、MAC、`machine-info`、runtime dir 和 DB timezone |
|
||||||
| `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 |
|
| `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 |
|
||||||
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
|
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
|
||||||
@ -113,6 +133,11 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
|
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
|
||||||
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移、QQBot 重型服务单测导入风险、QQBot 插件 registry/controller smoke 未 mock 具体插件实现风险、OneBot/QQ 登录态边界、NapCat 密码登录验证码 wait 阶段、日志窗口锚点、验证码状态早于日志 URL 的短轮询、验证码 pending 被状态轮询误清理、API Pod SSH 日志读取超时及清理前 pending 回归,以及普通处理中状态误吃旧验证码日志回归;明显测试占位 token 不报凭据风险,确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
|
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移、QQBot 重型服务单测导入风险、QQBot 插件 registry/controller smoke 未 mock 具体插件实现风险、OneBot/QQ 登录态边界、NapCat 密码登录验证码 wait 阶段、日志窗口锚点、验证码状态早于日志 URL 的短轮询、验证码 pending 被状态轮询误清理、API Pod SSH 日志读取超时及清理前 pending 回归,以及普通处理中状态误吃旧验证码日志回归;明显测试占位 token 不报凭据风险,确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
|
||||||
|
| `pnpm run napcat-upstream-audit` | 默认 dry-run 输出 NapCatQQ 上游 release 审计分类、只读 Git 命令和 artifact 路径;显式 `--execute --use-codex` 才运行 Codex。 |
|
||||||
|
| `pnpm run napcat-sync-candidate-review` | 输出 sync candidate review 的 prompt/context/只读命令骨架,不执行 Codex 或 Git 写操作。 |
|
||||||
|
| `pnpm run napcat-runtime-release-readiness` | 输出 runtime release readiness 的 prompt/context/只读检查骨架,不构建镜像、不调用 kubectl、不发布。 |
|
||||||
|
| `pnpm run napcat-remote-dev-handoff` | 输出远程开发 handoff 的 prompt/context 骨架,不创建线程、不同步仓库、不执行 Codex。 |
|
||||||
|
| `pnpm run nas-codex-bootstrap` | 输出可信 NAS Codex bootstrap dry-run plan;传 `--execute` 只生成目录准备和 CLI 包检查/安装命令计划,不真实执行 SSH、不复制 secrets、不写 systemd。 |
|
||||||
| `pnpm run napcat-device-profile-check` | 静态检查 API NapCat 设备身份护栏,防止回退到未知设备风险配置。 |
|
| `pnpm run napcat-device-profile-check` | 静态检查 API NapCat 设备身份护栏,防止回退到未知设备风险配置。 |
|
||||||
| `pnpm run deploy-observation` | 默认 dry-run 输出只读 NAS 观测命令;传 `--execute` 时执行 Jenkins/K8s/health/smoke 观测并写入 `.kt-workspace/test-artifacts/deploy-observation`。 |
|
| `pnpm run deploy-observation` | 默认 dry-run 输出只读 NAS 观测命令;传 `--execute` 时执行 Jenkins/K8s/health/smoke 观测并写入 `.kt-workspace/test-artifacts/deploy-observation`。 |
|
||||||
| `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 |
|
| `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 |
|
||||||
@ -154,6 +179,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
|||||||
- 大方向结束前调用 `kt_workstream_closeout` 或 `pnpm run workstream-closeout`;必须分别传入 `--review` 的 KT 全局审查证据和 `--superpowers-review` 的 Superpowers code review 证据;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。
|
- 大方向结束前调用 `kt_workstream_closeout` 或 `pnpm run workstream-closeout`;必须分别传入 `--review` 的 KT 全局审查证据和 `--superpowers-review` 的 Superpowers code review 证据;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。
|
||||||
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。`findings` 先判定真实风险或工具误报,真实风险修业务代码,误报修 `mcp/ktWorkflow/src/tools/review.ts` 并复跑。
|
- 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。`findings` 先判定真实风险或工具误报,真实风险修业务代码,误报修 `mcp/ktWorkflow/src/tools/review.ts` 并复跑。
|
||||||
- QQBot NapCat 登录 review 会检查验证码 pending、Docker 日志窗口、API Pod 读取超时,以及 `needNewDevice/jumpUrl` 是否进入 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` 或兼容的 `deviceVerifyUrl` pending 状态。
|
- QQBot NapCat 登录 review 会检查验证码 pending、Docker 日志窗口、API Pod 读取超时,以及 `needNewDevice/jumpUrl` 是否进入 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` 或兼容的 `deviceVerifyUrl` pending 状态。
|
||||||
|
- QQBot NapCat 二维码刷新卡在生成阶段时,优先计数当前容器近 5-20 分钟日志里的“重置已失效登录服务后重新生成二维码”、`Login Error,ErrType: 7 ErrCode: 4` 和“二维码已保存/二维码解码URL”;如果 reset/error 数远高于二维码产出,按 `docs/qqbot-nas-runtime.md` 的 v5 稳定解法排查 NapCat native login service reset 风暴,先修 fork/镜像,再看 API SSE。
|
||||||
- 代码或配置改动后调用 `kt_change_doc_sync`,把需要同步的 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 入口补齐;无需同步时把原因写进收尾证据。
|
- 代码或配置改动后调用 `kt_change_doc_sync`,把需要同步的 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 入口补齐;无需同步时把原因写进收尾证据。
|
||||||
- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review 都过门;清理证据必须显式传入,不能依赖缺省值。
|
- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级、KT 全局 review 和 Superpowers code review 都过门;清理证据必须显式传入,不能依赖缺省值。
|
||||||
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
|
- 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。
|
||||||
|
|||||||
104
ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile
Normal file
104
ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
options {
|
||||||
|
skipDefaultCheckout(true)
|
||||||
|
timestamps()
|
||||||
|
timeout(time: 60, unit: 'MINUTES')
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
string(name: 'NAPCAT_FORK_REF', defaultValue: 'codex/qr-refresh-login-state', description: 'Reviewed NapCatQQ fork branch, tag, or commit.')
|
||||||
|
string(name: 'UPSTREAM_RELEASE_TAG', defaultValue: '', description: 'Reviewed upstream release tag, when applicable.')
|
||||||
|
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', defaultValue: '', description: 'Explicit runtime image tag to build or promote, for example kt-napcat-desktop-cn:desktop-cn-v9.')
|
||||||
|
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', defaultValue: '', description: 'Explicit API desktop profile version override.')
|
||||||
|
booleanParam(name: 'BUILD_RUNTIME_IMAGE', defaultValue: false, description: 'When true, trigger the reviewed Jenkins runtime image build/publish job.')
|
||||||
|
choice(name: 'RUNTIME_BUILD_JOB', choices: ['KT-NapCatQQ/Runtime-Image-Build'], description: 'Whitelisted Jenkins job that performs the reviewed runtime image build and publish.')
|
||||||
|
booleanParam(name: 'PROMOTE_API_RUNTIME', defaultValue: false, description: 'When true, trigger API promotion with explicit image/profile overrides.')
|
||||||
|
choice(name: 'API_PROMOTION_JOB', choices: ['KT-Template/KT-Template-API/main'], description: 'Whitelisted API Jenkins job that accepts NapCat runtime override parameters.')
|
||||||
|
string(name: 'CONFIRM_API_PROMOTION_TARGET', defaultValue: '', description: 'Type PROMOTE_KT_TEMPLATE_API_MAIN before triggering the production API promotion job.')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
KT_WORKSPACE_ROOT = '/vol1/docker/kt-codex/workspace/KT'
|
||||||
|
KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow'
|
||||||
|
KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('ktWorkflow readiness') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-runtime-release-readiness -- \
|
||||||
|
--artifact-root "$KT_ARTIFACT_ROOT/napcat-runtime-release" \
|
||||||
|
--fork-branch "$NAPCAT_FORK_REF" \
|
||||||
|
--upstream-release-tag "$UPSTREAM_RELEASE_TAG" \
|
||||||
|
--napcat-image-tag "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \
|
||||||
|
--profile "$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Runtime image build and publish') {
|
||||||
|
when {
|
||||||
|
expression { return params.BUILD_RUNTIME_IMAGE }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
requireRuntimeOverrides(
|
||||||
|
params.QQBOT_NAPCAT_IMAGE_OVERRIDE,
|
||||||
|
params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE
|
||||||
|
)
|
||||||
|
build job: params.RUNTIME_BUILD_JOB,
|
||||||
|
wait: true,
|
||||||
|
parameters: [
|
||||||
|
string(name: 'NAPCAT_FORK_REF', value: params.NAPCAT_FORK_REF),
|
||||||
|
string(name: 'UPSTREAM_RELEASE_TAG', value: params.UPSTREAM_RELEASE_TAG),
|
||||||
|
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE),
|
||||||
|
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE),
|
||||||
|
string(name: 'KT_ARTIFACT_ROOT', value: "${env.KT_ARTIFACT_ROOT}/napcat-runtime-release")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('API promotion') {
|
||||||
|
when {
|
||||||
|
expression { return params.PROMOTE_API_RUNTIME }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
if (params.CONFIRM_API_PROMOTION_TARGET != 'PROMOTE_KT_TEMPLATE_API_MAIN') {
|
||||||
|
error('CONFIRM_API_PROMOTION_TARGET must equal PROMOTE_KT_TEMPLATE_API_MAIN before API promotion runs.')
|
||||||
|
}
|
||||||
|
requireRuntimeOverrides(
|
||||||
|
params.QQBOT_NAPCAT_IMAGE_OVERRIDE,
|
||||||
|
params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE
|
||||||
|
)
|
||||||
|
build job: params.API_PROMOTION_JOB,
|
||||||
|
wait: true,
|
||||||
|
parameters: [
|
||||||
|
string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE),
|
||||||
|
string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops runtime build or promotion unless both release override values are explicit.
|
||||||
|
* @param imageTag Reviewed NapCat runtime image tag supplied by the operator.
|
||||||
|
* @param profile Reviewed desktop profile version supplied by the operator.
|
||||||
|
* @return Nothing; aborts this pipeline through `error` when a required value is missing.
|
||||||
|
*/
|
||||||
|
void requireRuntimeOverrides(String imageTag, String profile) {
|
||||||
|
if (!imageTag?.trim()) {
|
||||||
|
error('QQBOT_NAPCAT_IMAGE_OVERRIDE is required before build or promotion.')
|
||||||
|
}
|
||||||
|
if (!profile?.trim()) {
|
||||||
|
error('QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE is required before build or promotion.')
|
||||||
|
}
|
||||||
|
}
|
||||||
51
ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile
Normal file
51
ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
options {
|
||||||
|
skipDefaultCheckout(true)
|
||||||
|
timestamps()
|
||||||
|
timeout(time: 30, unit: 'MINUTES')
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
booleanParam(name: 'RUN_CODEX_AUDIT', defaultValue: false, description: 'When true, allow the optional Codex workspace-write audit after the default dry-run audit.')
|
||||||
|
string(name: 'CONFIRM_CODEX_WORKSPACE_WRITE', defaultValue: '', description: 'Type RUN_CODEX_WORKSPACE_WRITE_AUDIT to confirm Codex may run with a workspace-write sandbox.')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow'
|
||||||
|
KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('ktWorkflow upstream audit dry-run') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \
|
||||||
|
--artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/dry-run"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('ktWorkflow upstream audit with Codex') {
|
||||||
|
when {
|
||||||
|
expression { return params.RUN_CODEX_AUDIT }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
if (params.CONFIRM_CODEX_WORKSPACE_WRITE != 'RUN_CODEX_WORKSPACE_WRITE_AUDIT') {
|
||||||
|
error('CONFIRM_CODEX_WORKSPACE_WRITE must equal RUN_CODEX_WORKSPACE_WRITE_AUDIT before Codex workspace-write audit runs.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \
|
||||||
|
--execute \
|
||||||
|
--use-codex \
|
||||||
|
--artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/codex"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,11 @@
|
|||||||
"deploy-observation": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --deploy-observation",
|
"deploy-observation": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --deploy-observation",
|
||||||
"global-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --global-review",
|
"global-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --global-review",
|
||||||
"napcat-device-profile-check": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-device-profile-check",
|
"napcat-device-profile-check": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-device-profile-check",
|
||||||
|
"napcat-remote-dev-handoff": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-remote-dev-handoff",
|
||||||
|
"napcat-runtime-release-readiness": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-runtime-release-readiness",
|
||||||
|
"napcat-sync-candidate-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-sync-candidate-review",
|
||||||
|
"napcat-upstream-audit": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --napcat-upstream-audit",
|
||||||
|
"nas-codex-bootstrap": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --nas-codex-bootstrap",
|
||||||
"obsidian-context": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-context",
|
"obsidian-context": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-context",
|
||||||
"obsidian-sync": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-sync",
|
"obsidian-sync": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-sync",
|
||||||
"obsidian-validate": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-validate",
|
"obsidian-validate": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-validate",
|
||||||
|
|||||||
41
prompts/napcat/remote-dev-handoff.md
Normal file
41
prompts/napcat/remote-dev-handoff.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# NapCat Remote Dev Handoff
|
||||||
|
|
||||||
|
You are summarizing a supplied remote development context packet for the next human-controlled step.
|
||||||
|
|
||||||
|
Do not edit files.
|
||||||
|
Do not run merge, commit, push, deploy, or Docker mutation commands.
|
||||||
|
Use only the supplied context packet.
|
||||||
|
Return JSON that matches the requested schema.
|
||||||
|
|
||||||
|
## Handoff Scope
|
||||||
|
|
||||||
|
Summarize only packet evidence for:
|
||||||
|
|
||||||
|
- repository branches and dirty status
|
||||||
|
- artifact directories and notable generated outputs
|
||||||
|
- Jenkins status, build numbers, commit/image pointers, and unknown deployment state
|
||||||
|
- blockers, missing evidence, or unsafe assumptions
|
||||||
|
- safe next commands that are read-only or dry-run oriented
|
||||||
|
|
||||||
|
Do not ask Codex to modify files. Do not include merge, commit, push, deploy, Docker mutation, or destructive cleanup commands as next commands.
|
||||||
|
|
||||||
|
## Requested Schema
|
||||||
|
|
||||||
|
Return only this JSON object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"repoStates": [
|
||||||
|
{
|
||||||
|
"repo": "Repository path or label.",
|
||||||
|
"branch": "Current branch or unknown.",
|
||||||
|
"dirtyStatus": "clean | dirty | unknown",
|
||||||
|
"head": "Commit hash or unknown."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactDirectories": ["Artifact directory or missing evidence note."],
|
||||||
|
"jenkinsStatus": "Jenkins status summary or missing evidence note.",
|
||||||
|
"blockers": ["Blocker or missing evidence note."],
|
||||||
|
"safeNextCommands": ["Read-only or dry-run command."]
|
||||||
|
}
|
||||||
|
```
|
||||||
34
prompts/napcat/runtime-release-readiness.md
Normal file
34
prompts/napcat/runtime-release-readiness.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# NapCat Runtime Release Readiness
|
||||||
|
|
||||||
|
You are reviewing a supplied NapCat runtime release packet before a human promotes an image/profile into the API runtime.
|
||||||
|
|
||||||
|
Do not edit files.
|
||||||
|
Do not run merge, commit, push, deploy, or Docker mutation commands.
|
||||||
|
Use only the supplied context packet.
|
||||||
|
Return JSON that matches the requested schema.
|
||||||
|
|
||||||
|
## Readiness Checks
|
||||||
|
|
||||||
|
Base every section on packet evidence:
|
||||||
|
|
||||||
|
- source validation: branch, commit, upstream base, fork patch marker, and dirty status
|
||||||
|
- image validation: image tag, image inspect/build evidence, marker evidence, and runtime asset presence
|
||||||
|
- API promotion readiness: explicit image/profile values, configuration alignment, and Jenkins/API promotion prerequisites
|
||||||
|
- online smoke readiness: QQBot/NapCat login, WebUI, command smoke, and rollback smoke expectations
|
||||||
|
- rollback pointer: last known good branch, commit, image tag, profile, and API runtime values
|
||||||
|
|
||||||
|
If any section lacks evidence, state the missing evidence plainly.
|
||||||
|
|
||||||
|
## Requested Schema
|
||||||
|
|
||||||
|
Return only this JSON object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sourceValidation": "Evidence-backed source validation status.",
|
||||||
|
"imageValidation": "Evidence-backed image validation status.",
|
||||||
|
"apiPromotionReadiness": "Evidence-backed API promotion readiness status.",
|
||||||
|
"onlineSmokeReadiness": "Evidence-backed online smoke readiness status.",
|
||||||
|
"rollbackPointer": "Specific rollback pointer or the missing evidence needed to name one."
|
||||||
|
}
|
||||||
|
```
|
||||||
34
prompts/napcat/schemas/runtime-release-readiness.schema.json
Normal file
34
prompts/napcat/schemas/runtime-release-readiness.schema.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"sourceValidation",
|
||||||
|
"imageValidation",
|
||||||
|
"apiPromotionReadiness",
|
||||||
|
"onlineSmokeReadiness",
|
||||||
|
"rollbackPointer"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"sourceValidation": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"imageValidation": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"apiPromotionReadiness": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"onlineSmokeReadiness": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"rollbackPointer": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
prompts/napcat/schemas/upstream-audit.schema.json
Normal file
33
prompts/napcat/schemas/upstream-audit.schema.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["classification", "reasonCodes", "recommendedAction", "summary"],
|
||||||
|
"properties": {
|
||||||
|
"classification": {
|
||||||
|
"enum": ["safe-candidate", "manual-review", "blocked"]
|
||||||
|
},
|
||||||
|
"reasonCodes": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"enum": [
|
||||||
|
"NO_UPSTREAM_CHANGE",
|
||||||
|
"HOT_ZONE_CHANGED",
|
||||||
|
"FORK_PATCH_OVERLAP",
|
||||||
|
"DRY_MERGE_CONFLICT",
|
||||||
|
"BUILD_GRAPH_CHANGED",
|
||||||
|
"METADATA_UNAVAILABLE",
|
||||||
|
"CODEX_SCHEMA_INVALID"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recommendedAction": {
|
||||||
|
"enum": ["no-op", "create-candidate-branch", "request-human-review", "block"]
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
prompts/napcat/sync-candidate-review.md
Normal file
43
prompts/napcat/sync-candidate-review.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# NapCat Sync Candidate Review
|
||||||
|
|
||||||
|
You are reviewing a supplied NapCat candidate branch context packet before a human decides whether to continue the sync.
|
||||||
|
|
||||||
|
Do not edit files.
|
||||||
|
Do not run merge, commit, push, deploy, or Docker mutation commands.
|
||||||
|
Use only the supplied context packet.
|
||||||
|
Return JSON that matches the requested schema.
|
||||||
|
|
||||||
|
## Review Scope
|
||||||
|
|
||||||
|
Review only evidence present in the packet. Cover these areas:
|
||||||
|
|
||||||
|
- login service reset behavior and stale native login service state
|
||||||
|
- QR refresh, stale QR prevention, QR expiry, and scan confirmation state
|
||||||
|
- WebUI login runtime state, reconciled QQ online state, and false online signals
|
||||||
|
- captcha and new-device boundaries, including pending state preservation
|
||||||
|
- package, shell, framework, webui build output, and runtime asset layout
|
||||||
|
- test gaps, missing smoke evidence, and unverified online behavior
|
||||||
|
|
||||||
|
## Finding Rules
|
||||||
|
|
||||||
|
Flag only actionable risks grounded in packet evidence. If evidence is missing, report it as a test gap or blocked review item instead of guessing.
|
||||||
|
|
||||||
|
## Requested Schema
|
||||||
|
|
||||||
|
Return only this JSON object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"decision": "approve | manual-review | block",
|
||||||
|
"summary": "Short review summary.",
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"severity": "critical | important | minor",
|
||||||
|
"area": "login-reset | qr-refresh | webui-runtime | captcha-new-device | package-build | test-gap",
|
||||||
|
"evidence": "Packet evidence for the finding.",
|
||||||
|
"requiredAction": "Human-readable action before promotion."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"testGaps": ["Missing validation evidence."]
|
||||||
|
}
|
||||||
|
```
|
||||||
53
prompts/napcat/upstream-audit.md
Normal file
53
prompts/napcat/upstream-audit.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# NapCat Upstream Audit
|
||||||
|
|
||||||
|
You are reviewing a supplied NapCat upstream release context packet for KT automation. Decide whether the upstream release can become a candidate branch, needs manual review, or is blocked.
|
||||||
|
|
||||||
|
Do not edit files.
|
||||||
|
Do not run merge, commit, push, deploy, or Docker mutation commands.
|
||||||
|
Use only the supplied context packet.
|
||||||
|
Return JSON that matches the requested schema.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
Use the packet sections for upstream release metadata, fork patch range, changed files, dry-merge result, build graph diff, and Codex parser status. Treat missing packet sections as unavailable metadata.
|
||||||
|
|
||||||
|
## Classification Rules
|
||||||
|
|
||||||
|
Choose exactly one `classification`:
|
||||||
|
|
||||||
|
- `safe-candidate`: no upstream change exists, or upstream changes are outside hot zones, do not overlap fork patches, dry merge is clean, build graph is stable, and metadata is available.
|
||||||
|
- `manual-review`: upstream changed login/runtime/build-adjacent areas or fork-adjacent code, but the packet does not prove a hard blocker.
|
||||||
|
- `blocked`: dry merge conflicts, build graph changes that require maintainer action, missing required metadata, or invalid Codex output schema.
|
||||||
|
|
||||||
|
Use one or more `reasonCodes` from this exact list:
|
||||||
|
|
||||||
|
- `NO_UPSTREAM_CHANGE`
|
||||||
|
- `HOT_ZONE_CHANGED`
|
||||||
|
- `FORK_PATCH_OVERLAP`
|
||||||
|
- `DRY_MERGE_CONFLICT`
|
||||||
|
- `BUILD_GRAPH_CHANGED`
|
||||||
|
- `METADATA_UNAVAILABLE`
|
||||||
|
- `CODEX_SCHEMA_INVALID`
|
||||||
|
|
||||||
|
## Hot Zones
|
||||||
|
|
||||||
|
Treat these as NapCat login/runtime hot zones:
|
||||||
|
|
||||||
|
- login service reset and native login service state
|
||||||
|
- QR refresh, stale QR, QR expiry, and scan confirmation state
|
||||||
|
- WebUI login runtime state and reconciled QQ online state
|
||||||
|
- captcha and new-device verification boundaries
|
||||||
|
- package, shell, framework, webui build graph or output layout
|
||||||
|
|
||||||
|
## Requested Schema
|
||||||
|
|
||||||
|
Return only this JSON object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"classification": "safe-candidate | manual-review | blocked",
|
||||||
|
"reasonCodes": ["NO_UPSTREAM_CHANGE"],
|
||||||
|
"recommendedAction": "no-op | create-candidate-branch | request-human-review | block",
|
||||||
|
"summary": "Short evidence-based decision."
|
||||||
|
}
|
||||||
|
```
|
||||||
144
src/core/cli.ts
144
src/core/cli.ts
@ -1,6 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
DeployObservationInput,
|
DeployObservationInput,
|
||||||
GlobalCodeReviewInput,
|
GlobalCodeReviewInput,
|
||||||
|
NapcatRemoteDevHandoffInput,
|
||||||
|
NapcatRuntimeReleaseReadinessInput,
|
||||||
|
NapcatSyncCandidateReviewInput,
|
||||||
|
NasCodexBootstrapPlanInput,
|
||||||
ObsidianContextInput,
|
ObsidianContextInput,
|
||||||
ObsidianSyncInput,
|
ObsidianSyncInput,
|
||||||
ObsidianValidateInput,
|
ObsidianValidateInput,
|
||||||
@ -8,6 +12,7 @@ import type {
|
|||||||
WorkstreamReusablePattern,
|
WorkstreamReusablePattern,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { workstreamReusablePatternValues } from './constants.js';
|
import { workstreamReusablePatternValues } from './constants.js';
|
||||||
|
import type { NapcatUpstreamAuditInput } from '../tools/napcatAutomation.types.js';
|
||||||
|
|
||||||
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
|
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
|
||||||
if (argv.includes('--content-scan-all') || argv.includes('--contentScanMode=all')) {
|
if (argv.includes('--content-scan-all') || argv.includes('--contentScanMode=all')) {
|
||||||
@ -62,6 +67,16 @@ function readOptionList(argv: string[], names: string[]): string[] {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether any supported boolean CLI flag is present in the raw argv list.
|
||||||
|
* @param argv - Full process argv including node, script, mode flag, and user options.
|
||||||
|
* @param names - Flag names to accept in dashed or camelCase form.
|
||||||
|
* @returns True when the caller provided one of the named flags.
|
||||||
|
*/
|
||||||
|
function hasFlag(argv: string[], names: string[]): boolean {
|
||||||
|
return names.some((name) => argv.includes(name));
|
||||||
|
}
|
||||||
|
|
||||||
export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseoutInput {
|
export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseoutInput {
|
||||||
const patternSet = new Set<string>(workstreamReusablePatternValues);
|
const patternSet = new Set<string>(workstreamReusablePatternValues);
|
||||||
const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern'])
|
const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern'])
|
||||||
@ -123,6 +138,135 @@ export function parseDeployObservationCliArgs(argv: string[]): DeployObservation
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the NapCat upstream audit CLI mode into the deterministic audit builder input.
|
||||||
|
* @param argv - Full process argv from `src/server.ts`; `--execute` and `--use-codex` must both be explicit before Codex can run.
|
||||||
|
* @returns Upstream audit input with dry-run defaults for all optional metadata lists.
|
||||||
|
*/
|
||||||
|
export function parseNapcatUpstreamAuditCliArgs(
|
||||||
|
argv: string[],
|
||||||
|
): NapcatUpstreamAuditInput {
|
||||||
|
return {
|
||||||
|
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
|
||||||
|
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
|
||||||
|
createCandidateBranch: hasFlag(argv, ['--create-candidate-branch', '--createCandidateBranch']),
|
||||||
|
dryMergeConflict: hasFlag(argv, ['--dry-merge-conflict', '--dryMergeConflict']),
|
||||||
|
execute: hasFlag(argv, ['--execute']),
|
||||||
|
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
|
||||||
|
forkPatchFiles: readOptionList(argv, [
|
||||||
|
'--fork-patch-file',
|
||||||
|
'--fork-patch-files',
|
||||||
|
'--forkPatchFile',
|
||||||
|
'--forkPatchFiles',
|
||||||
|
]),
|
||||||
|
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
|
||||||
|
lastAcceptedUpstreamBase: readOption(argv, [
|
||||||
|
'--last-accepted-upstream-base',
|
||||||
|
'--lastAcceptedUpstreamBase',
|
||||||
|
]),
|
||||||
|
outputSchemaPath: readOption(argv, ['--output-schema', '--outputSchemaPath']),
|
||||||
|
upstreamChangedFiles: readOptionList(argv, [
|
||||||
|
'--upstream-changed-file',
|
||||||
|
'--upstream-changed-files',
|
||||||
|
'--upstreamChangedFile',
|
||||||
|
'--upstreamChangedFiles',
|
||||||
|
]),
|
||||||
|
upstreamReleaseTag: readOption(argv, [
|
||||||
|
'--upstream-release-tag',
|
||||||
|
'--upstream-release-ref',
|
||||||
|
'--upstreamReleaseTag',
|
||||||
|
]),
|
||||||
|
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
|
||||||
|
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
|
||||||
|
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses common dry-run NapCat prompt tool options shared by candidate review, readiness, and handoff modes.
|
||||||
|
* @param argv - Full process argv from a NapCat automation CLI script.
|
||||||
|
* @returns Prompt skeleton input with `execute` false unless explicitly requested by the caller.
|
||||||
|
*/
|
||||||
|
function parseNapcatPromptToolCliArgs(argv: string[]): NapcatSyncCandidateReviewInput {
|
||||||
|
return {
|
||||||
|
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
|
||||||
|
candidateBranch: readOption(argv, ['--candidate-branch', '--candidateBranch']),
|
||||||
|
execute: hasFlag(argv, ['--execute']),
|
||||||
|
forkBranch: readOption(argv, ['--fork-branch', '--forkBranch']),
|
||||||
|
forkRepo: readOption(argv, ['--fork-repo', '--forkRepo']),
|
||||||
|
lastAcceptedUpstreamBase: readOption(argv, [
|
||||||
|
'--last-accepted-upstream-base',
|
||||||
|
'--lastAcceptedUpstreamBase',
|
||||||
|
]),
|
||||||
|
upstreamReleaseTag: readOption(argv, [
|
||||||
|
'--upstream-release-tag',
|
||||||
|
'--upstream-release-ref',
|
||||||
|
'--upstreamReleaseTag',
|
||||||
|
]),
|
||||||
|
upstreamRepo: readOption(argv, ['--upstream-repo', '--upstreamRepo']),
|
||||||
|
useCodex: hasFlag(argv, ['--use-codex', '--useCodex']),
|
||||||
|
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the NapCat sync-candidate review CLI mode.
|
||||||
|
* @param argv - Full process argv from `pnpm run napcat-sync-candidate-review`.
|
||||||
|
* @returns Dry-run prompt skeleton input for human/Codex candidate review handoff.
|
||||||
|
*/
|
||||||
|
export function parseNapcatSyncCandidateReviewCliArgs(
|
||||||
|
argv: string[],
|
||||||
|
): NapcatSyncCandidateReviewInput {
|
||||||
|
return parseNapcatPromptToolCliArgs(argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the NapCat runtime release readiness CLI mode.
|
||||||
|
* @param argv - Full process argv from `pnpm run napcat-runtime-release-readiness`.
|
||||||
|
* @returns Dry-run prompt skeleton input with image/profile hints and no default mutation.
|
||||||
|
*/
|
||||||
|
export function parseNapcatRuntimeReleaseReadinessCliArgs(
|
||||||
|
argv: string[],
|
||||||
|
): NapcatRuntimeReleaseReadinessInput {
|
||||||
|
return {
|
||||||
|
...parseNapcatPromptToolCliArgs(argv),
|
||||||
|
apiImageTag: readOption(argv, ['--api-image-tag', '--apiImageTag']),
|
||||||
|
napcatImageTag: readOption(argv, ['--napcat-image-tag', '--napcatImageTag']),
|
||||||
|
profile: readOption(argv, ['--profile']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the NapCat remote development handoff CLI mode.
|
||||||
|
* @param argv - Full process argv from `src/server.ts --napcat-remote-dev-handoff`.
|
||||||
|
* @returns Dry-run prompt skeleton input for a remote workspace handoff packet.
|
||||||
|
*/
|
||||||
|
export function parseNapcatRemoteDevHandoffCliArgs(
|
||||||
|
argv: string[],
|
||||||
|
): NapcatRemoteDevHandoffInput {
|
||||||
|
return {
|
||||||
|
...parseNapcatPromptToolCliArgs(argv),
|
||||||
|
targetHost: readOption(argv, ['--target-host', '--targetHost']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the NAS Codex bootstrap planning CLI mode.
|
||||||
|
* @param argv - Full process argv from `pnpm run nas-codex-bootstrap`.
|
||||||
|
* @returns Dry-run bootstrap plan input; `execute` is false unless the caller explicitly opts in.
|
||||||
|
*/
|
||||||
|
export function parseNasCodexBootstrapCliArgs(
|
||||||
|
argv: string[],
|
||||||
|
): NasCodexBootstrapPlanInput {
|
||||||
|
return {
|
||||||
|
artifactRoot: readOption(argv, ['--artifact-root', '--artifactRoot']),
|
||||||
|
codexHome: readOption(argv, ['--codex-home', '--codexHome']),
|
||||||
|
execute: hasFlag(argv, ['--execute']),
|
||||||
|
serviceName: readOption(argv, ['--service-name', '--serviceName']),
|
||||||
|
workspaceRoot: readOption(argv, ['--workspace-root', '--workspaceRoot']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function parseObsidianCliArgs(argv: string[]): ObsidianValidateInput {
|
export function parseObsidianCliArgs(argv: string[]): ObsidianValidateInput {
|
||||||
return {
|
return {
|
||||||
checkLayout: !argv.includes('--no-layout'),
|
checkLayout: !argv.includes('--no-layout'),
|
||||||
|
|||||||
@ -69,6 +69,11 @@ export const registeredToolNames = [
|
|||||||
'kt_commit_plan',
|
'kt_commit_plan',
|
||||||
'kt_push_plan',
|
'kt_push_plan',
|
||||||
'kt_business_test_plan',
|
'kt_business_test_plan',
|
||||||
|
'kt_napcat_upstream_audit',
|
||||||
|
'kt_napcat_sync_candidate_review',
|
||||||
|
'kt_napcat_runtime_release_readiness',
|
||||||
|
'kt_napcat_remote_dev_handoff',
|
||||||
|
'kt_nas_codex_bootstrap_plan',
|
||||||
'kt_napcat_device_profile_check',
|
'kt_napcat_device_profile_check',
|
||||||
'kt_blocker_resolution',
|
'kt_blocker_resolution',
|
||||||
'kt_remote_health_check',
|
'kt_remote_health_check',
|
||||||
|
|||||||
@ -22,6 +22,14 @@ import { buildVerificationPlan } from './tools/verification.js';
|
|||||||
import { buildWorkstreamCloseout } from './tools/closeout.js';
|
import { buildWorkstreamCloseout } from './tools/closeout.js';
|
||||||
import { buildChangeDocSync } from './tools/docSync.js';
|
import { buildChangeDocSync } from './tools/docSync.js';
|
||||||
import { buildDeployObservation } from './tools/deployObservation.js';
|
import { buildDeployObservation } from './tools/deployObservation.js';
|
||||||
|
import {
|
||||||
|
buildNapcatRemoteDevHandoff,
|
||||||
|
buildNapcatRuntimeReleaseReadiness,
|
||||||
|
buildNapcatSyncCandidateReview,
|
||||||
|
buildNapcatUpstreamAudit,
|
||||||
|
buildNasCodexBootstrapPlan,
|
||||||
|
napcatAutomationDefaults,
|
||||||
|
} from './tools/napcatAutomation.js';
|
||||||
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
|
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
|
||||||
import { workstreamReusablePatternValues } from './core/constants.js';
|
import { workstreamReusablePatternValues } from './core/constants.js';
|
||||||
export function response(data: unknown): McpTextResponse {
|
export function response(data: unknown): McpTextResponse {
|
||||||
@ -35,6 +43,11 @@ export function response(data: unknown): McpTextResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers every ktWorkflow MCP tool against the provided MCP server instance.
|
||||||
|
* @param server - MCP SDK server that owns tool names, schemas, descriptions, and async handlers.
|
||||||
|
* @returns Nothing; handlers are attached to `server` for later stdio transport use.
|
||||||
|
*/
|
||||||
export function registerTools(server: McpServer): void {
|
export function registerTools(server: McpServer): void {
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kt_read_context',
|
'kt_read_context',
|
||||||
@ -370,11 +383,137 @@ export function registerTools(server: McpServer): void {
|
|||||||
async (input) => response(buildBusinessTestPlan(input)),
|
async (input) => response(buildBusinessTestPlan(input)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
'kt_napcat_upstream_audit',
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'生成或执行 NapCatQQ 上游 release 审计:默认 dry-run,只读输出 hot zone、fork patch overlap、dry merge 和 Codex 审计入口;execute/useCodex 均需显式开启才会运行 Codex。',
|
||||||
|
inputSchema: {
|
||||||
|
artifactRoot: z
|
||||||
|
.string()
|
||||||
|
.default('.kt-workspace/test-artifacts/napcat-upstream-audit'),
|
||||||
|
codexHome: z.string().optional(),
|
||||||
|
createCandidateBranch: z.boolean().default(false),
|
||||||
|
dryMergeConflict: z.boolean().default(false),
|
||||||
|
execute: z.boolean().default(false),
|
||||||
|
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||||
|
forkPatchFiles: z.array(z.string()).default([]),
|
||||||
|
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||||
|
lastAcceptedUpstreamBase: z
|
||||||
|
.string()
|
||||||
|
.default(napcatAutomationDefaults.lastAcceptedUpstreamBase),
|
||||||
|
outputSchemaPath: z.string().optional(),
|
||||||
|
upstreamChangedFiles: z.array(z.string()).default([]),
|
||||||
|
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||||
|
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||||
|
useCodex: z.boolean().default(false),
|
||||||
|
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||||
|
},
|
||||||
|
title: 'KT NapCat Upstream Audit',
|
||||||
|
},
|
||||||
|
async (input) => response(buildNapcatUpstreamAudit(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
'kt_napcat_sync_candidate_review',
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'生成 NapCatQQ sync candidate 人工/Codex 复审的 dry-run prompt、上下文和只读命令骨架;Task 5 不执行 Codex 或 Git 写操作。',
|
||||||
|
inputSchema: {
|
||||||
|
artifactRoot: z
|
||||||
|
.string()
|
||||||
|
.default('.kt-workspace/test-artifacts/napcat-automation/sync-candidate-review'),
|
||||||
|
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
|
||||||
|
execute: z.boolean().default(false),
|
||||||
|
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||||
|
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||||
|
lastAcceptedUpstreamBase: z
|
||||||
|
.string()
|
||||||
|
.default(napcatAutomationDefaults.lastAcceptedUpstreamBase),
|
||||||
|
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||||
|
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||||
|
useCodex: z.boolean().default(false),
|
||||||
|
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||||
|
},
|
||||||
|
title: 'KT NapCat Sync Candidate Review',
|
||||||
|
},
|
||||||
|
async (input) => response(buildNapcatSyncCandidateReview(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
'kt_napcat_runtime_release_readiness',
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'生成 NapCat runtime release readiness dry-run prompt、上下文和只读检查骨架;不自动构建镜像、不调用 kubectl、不发布。',
|
||||||
|
inputSchema: {
|
||||||
|
apiImageTag: z.string().default(napcatAutomationDefaults.apiImageTag),
|
||||||
|
artifactRoot: z
|
||||||
|
.string()
|
||||||
|
.default('.kt-workspace/test-artifacts/napcat-automation/runtime-release-readiness'),
|
||||||
|
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
|
||||||
|
execute: z.boolean().default(false),
|
||||||
|
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||||
|
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||||
|
napcatImageTag: z.string().default(napcatAutomationDefaults.runtimeImageTag),
|
||||||
|
profile: z.string().default(napcatAutomationDefaults.runtimeProfile),
|
||||||
|
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||||
|
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||||
|
useCodex: z.boolean().default(false),
|
||||||
|
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||||
|
},
|
||||||
|
title: 'KT NapCat Runtime Release Readiness',
|
||||||
|
},
|
||||||
|
async (input) => response(buildNapcatRuntimeReleaseReadiness(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
'kt_napcat_remote_dev_handoff',
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'生成 NapCat 自动化远程开发 handoff 的 dry-run prompt 和上下文骨架;不创建线程、不执行 Codex、不同步仓库。',
|
||||||
|
inputSchema: {
|
||||||
|
artifactRoot: z
|
||||||
|
.string()
|
||||||
|
.default('.kt-workspace/test-artifacts/napcat-automation/remote-dev-handoff'),
|
||||||
|
candidateBranch: z.string().default(napcatAutomationDefaults.candidateBranch),
|
||||||
|
execute: z.boolean().default(false),
|
||||||
|
forkBranch: z.string().default(napcatAutomationDefaults.forkBranch),
|
||||||
|
forkRepo: z.string().default(napcatAutomationDefaults.forkRepo),
|
||||||
|
targetHost: z.string().default(napcatAutomationDefaults.targetHost),
|
||||||
|
upstreamReleaseTag: z.string().default(napcatAutomationDefaults.upstreamReleaseRef),
|
||||||
|
upstreamRepo: z.string().default(napcatAutomationDefaults.upstreamRepo),
|
||||||
|
useCodex: z.boolean().default(false),
|
||||||
|
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||||
|
},
|
||||||
|
title: 'KT NapCat Remote Dev Handoff',
|
||||||
|
},
|
||||||
|
async (input) => response(buildNapcatRemoteDevHandoff(input)),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
'kt_nas_codex_bootstrap_plan',
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'生成可信 NAS Codex 自动化环境 bootstrap 的 dry-run plan;Task 5 仅输出只读检查和占位计划,不安装、不同步、不写 systemd。',
|
||||||
|
inputSchema: {
|
||||||
|
artifactRoot: z
|
||||||
|
.string()
|
||||||
|
.default('.kt-workspace/test-artifacts/napcat-automation/nas-codex-bootstrap'),
|
||||||
|
codexHome: z.string().default(napcatAutomationDefaults.codexHome),
|
||||||
|
execute: z.boolean().default(false),
|
||||||
|
serviceName: z.string().default(napcatAutomationDefaults.bootstrapServiceName),
|
||||||
|
workspaceRoot: z.string().default(napcatAutomationDefaults.workspaceRoot),
|
||||||
|
},
|
||||||
|
title: 'KT NAS Codex Bootstrap Plan',
|
||||||
|
},
|
||||||
|
async (input) => response(buildNasCodexBootstrapPlan(input)),
|
||||||
|
);
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kt_napcat_device_profile_check',
|
'kt_napcat_device_profile_check',
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
'静态检查 API NapCat 设备身份护栏:QQNT 可见 hostname、02:42 MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
|
'静态检查 API NapCat 设备身份护栏:QQNT 可见 hostname、实体 OUI MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
project: z.string().default('api'),
|
project: z.string().default('api'),
|
||||||
},
|
},
|
||||||
|
|||||||
821
src/selfTest.ts
821
src/selfTest.ts
@ -1,9 +1,14 @@
|
|||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
parseNasCodexBootstrapCliArgs,
|
||||||
parseDeployObservationCliArgs,
|
parseDeployObservationCliArgs,
|
||||||
parseGlobalReviewCliArgs,
|
parseGlobalReviewCliArgs,
|
||||||
|
parseNapcatRemoteDevHandoffCliArgs,
|
||||||
|
parseNapcatRuntimeReleaseReadinessCliArgs,
|
||||||
|
parseNapcatSyncCandidateReviewCliArgs,
|
||||||
|
parseNapcatUpstreamAuditCliArgs,
|
||||||
parseWorkstreamCloseoutCliArgs,
|
parseWorkstreamCloseoutCliArgs,
|
||||||
} from "./core/cli.js";
|
} from "./core/cli.js";
|
||||||
import { registeredToolNames } from "./core/constants.js";
|
import { registeredToolNames } from "./core/constants.js";
|
||||||
@ -35,6 +40,27 @@ import {
|
|||||||
findTaskRecordGovernanceFindings,
|
findTaskRecordGovernanceFindings,
|
||||||
isBenignCredentialReviewValue,
|
isBenignCredentialReviewValue,
|
||||||
} from "./tools/review.js";
|
} from "./tools/review.js";
|
||||||
|
import {
|
||||||
|
napcatAutomationReasonCodes,
|
||||||
|
parseNapcatAutomationClassification,
|
||||||
|
upstreamAuditOutputSchema,
|
||||||
|
} from "./tools/napcatAutomation.types.js";
|
||||||
|
import {
|
||||||
|
buildNapcatCodexExecCommand,
|
||||||
|
buildNapcatRemoteDevHandoff,
|
||||||
|
buildNapcatRuntimeReleaseReadiness,
|
||||||
|
buildNapcatSyncCandidateReview,
|
||||||
|
buildNapcatUpstreamAudit,
|
||||||
|
buildNasCodexBootstrapPlan,
|
||||||
|
classifyNapcatUpstreamAudit,
|
||||||
|
napcatAutomationDefaults,
|
||||||
|
} from "./tools/napcatAutomation.js";
|
||||||
|
import { registerTools } from "./registerTools.js";
|
||||||
|
import {
|
||||||
|
loadNapcatAutomationPrompt,
|
||||||
|
type NapcatAutomationPromptName,
|
||||||
|
napcatAutomationPromptNames,
|
||||||
|
} from "./tools/napcatAutomation.prompts.js";
|
||||||
import {
|
import {
|
||||||
buildBusinessTestPlan,
|
buildBusinessTestPlan,
|
||||||
buildNapcatDeviceProfileCheck,
|
buildNapcatDeviceProfileCheck,
|
||||||
@ -54,7 +80,788 @@ import {
|
|||||||
buildPushPlan,
|
buildPushPlan,
|
||||||
buildRemoteHealthCheck,
|
buildRemoteHealthCheck,
|
||||||
} from "./tools/workflow.js";
|
} from "./tools/workflow.js";
|
||||||
|
/**
|
||||||
|
* Runs ktWorkflow guardrail smoke checks from the CLI self-test entry; writes local evidence under the KT workspace and throws on any broken workflow contract.
|
||||||
|
* @returns A promise that resolves after all contract checks and evidence writing complete.
|
||||||
|
*/
|
||||||
export async function runSelfTest(): Promise<void> {
|
export async function runSelfTest(): Promise<void> {
|
||||||
|
const napcatAuditSchemaSmoke = upstreamAuditOutputSchema.safeParse({
|
||||||
|
classification: "manual-review",
|
||||||
|
reasonCodes: ["HOT_ZONE_CHANGED"],
|
||||||
|
recommendedAction: "request-human-review",
|
||||||
|
summary: "Login hot-zone changed.",
|
||||||
|
});
|
||||||
|
if (!napcatAuditSchemaSmoke.success) {
|
||||||
|
throw new Error("NapCat upstream audit schema self-check failed");
|
||||||
|
}
|
||||||
|
const napcatAuditExtraPropertySmoke = upstreamAuditOutputSchema.safeParse({
|
||||||
|
classification: "manual-review",
|
||||||
|
reasonCodes: ["HOT_ZONE_CHANGED"],
|
||||||
|
recommendedAction: "request-human-review",
|
||||||
|
summary: "Login hot-zone changed.",
|
||||||
|
unexpectedField: "Codex output schema rejects this.",
|
||||||
|
});
|
||||||
|
if (napcatAuditExtraPropertySmoke.success) {
|
||||||
|
throw new Error("NapCat upstream audit schema strict self-check failed");
|
||||||
|
}
|
||||||
|
if (!napcatAutomationReasonCodes.includes("HOT_ZONE_CHANGED")) {
|
||||||
|
throw new Error("NapCat reason-code list self-check failed");
|
||||||
|
}
|
||||||
|
const napcatCli = parseNapcatUpstreamAuditCliArgs([
|
||||||
|
"node",
|
||||||
|
"server",
|
||||||
|
"--napcat-upstream-audit",
|
||||||
|
"--execute",
|
||||||
|
"--use-codex",
|
||||||
|
"--artifact-root",
|
||||||
|
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync",
|
||||||
|
]);
|
||||||
|
if (!napcatCli.execute || !napcatCli.useCodex) {
|
||||||
|
throw new Error("NapCat upstream audit CLI parser self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
napcatCli.artifactRoot !==
|
||||||
|
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync"
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat upstream audit artifactRoot CLI parser self-check failed");
|
||||||
|
}
|
||||||
|
const napcatCandidateCli = parseNapcatSyncCandidateReviewCliArgs([
|
||||||
|
"node",
|
||||||
|
"server",
|
||||||
|
"--napcat-sync-candidate-review",
|
||||||
|
]);
|
||||||
|
const napcatRuntimeCli = parseNapcatRuntimeReleaseReadinessCliArgs([
|
||||||
|
"node",
|
||||||
|
"server",
|
||||||
|
"--napcat-runtime-release-readiness",
|
||||||
|
]);
|
||||||
|
const napcatRemoteHandoffCli = parseNapcatRemoteDevHandoffCliArgs([
|
||||||
|
"node",
|
||||||
|
"server",
|
||||||
|
"--napcat-remote-dev-handoff",
|
||||||
|
]);
|
||||||
|
const nasBootstrapCli = parseNasCodexBootstrapCliArgs([
|
||||||
|
"node",
|
||||||
|
"server",
|
||||||
|
"--nas-codex-bootstrap",
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
napcatCandidateCli.execute ||
|
||||||
|
napcatRuntimeCli.execute ||
|
||||||
|
napcatRemoteHandoffCli.execute ||
|
||||||
|
nasBootstrapCli.execute
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat auxiliary CLI dry-run default self-check failed");
|
||||||
|
}
|
||||||
|
const napcatCandidateDefaults = buildNapcatSyncCandidateReview({}).context;
|
||||||
|
const napcatRuntimeDefaults = buildNapcatRuntimeReleaseReadiness({}).context;
|
||||||
|
const napcatRemoteDefaults = buildNapcatRemoteDevHandoff({}).context;
|
||||||
|
const nasBootstrapDefaults = buildNasCodexBootstrapPlan({}).context;
|
||||||
|
if (
|
||||||
|
napcatCandidateDefaults.candidateBranch !==
|
||||||
|
napcatAutomationDefaults.candidateBranch ||
|
||||||
|
napcatCandidateDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||||||
|
napcatCandidateDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||||||
|
napcatCandidateDefaults.lastAcceptedUpstreamBase !==
|
||||||
|
napcatAutomationDefaults.lastAcceptedUpstreamBase ||
|
||||||
|
napcatCandidateDefaults.upstreamReleaseTag !==
|
||||||
|
napcatAutomationDefaults.upstreamReleaseRef ||
|
||||||
|
napcatCandidateDefaults.upstreamRepo !== napcatAutomationDefaults.upstreamRepo ||
|
||||||
|
napcatCandidateDefaults.workspaceRoot !==
|
||||||
|
napcatAutomationDefaults.workspaceRoot
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat candidate review default alignment self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
napcatRuntimeDefaults.apiImageTag !==
|
||||||
|
napcatAutomationDefaults.apiImageTag ||
|
||||||
|
napcatRuntimeDefaults.candidateBranch !==
|
||||||
|
napcatAutomationDefaults.candidateBranch ||
|
||||||
|
napcatRuntimeDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||||||
|
napcatRuntimeDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||||||
|
napcatRuntimeDefaults.napcatImageTag !==
|
||||||
|
napcatAutomationDefaults.runtimeImageTag ||
|
||||||
|
napcatRuntimeDefaults.profile !== napcatAutomationDefaults.runtimeProfile ||
|
||||||
|
napcatRuntimeDefaults.upstreamReleaseTag !==
|
||||||
|
napcatAutomationDefaults.upstreamReleaseRef ||
|
||||||
|
napcatRuntimeDefaults.workspaceRoot !==
|
||||||
|
napcatAutomationDefaults.workspaceRoot
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat runtime readiness default alignment self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
napcatRemoteDefaults.candidateBranch !==
|
||||||
|
napcatAutomationDefaults.candidateBranch ||
|
||||||
|
napcatRemoteDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||||||
|
napcatRemoteDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||||||
|
napcatRemoteDefaults.targetHost !== napcatAutomationDefaults.targetHost ||
|
||||||
|
napcatRemoteDefaults.upstreamReleaseTag !==
|
||||||
|
napcatAutomationDefaults.upstreamReleaseRef ||
|
||||||
|
napcatRemoteDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat remote handoff default alignment self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
nasBootstrapDefaults.codexHome !== napcatAutomationDefaults.codexHome ||
|
||||||
|
nasBootstrapDefaults.serviceName !==
|
||||||
|
napcatAutomationDefaults.bootstrapServiceName ||
|
||||||
|
nasBootstrapDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
|
||||||
|
) {
|
||||||
|
throw new Error("NAS Codex bootstrap default alignment self-check failed");
|
||||||
|
}
|
||||||
|
const bootstrap = buildNasCodexBootstrapPlan({ execute: false });
|
||||||
|
const bootstrapCommandText = bootstrap.commands
|
||||||
|
.map((item) => item.command)
|
||||||
|
.join("\n");
|
||||||
|
if (!bootstrapCommandText.includes("/vol1/docker/kt-codex/home/.codex")) {
|
||||||
|
throw new Error("NAS Codex bootstrap CODEX_HOME self-check failed");
|
||||||
|
}
|
||||||
|
if (bootstrapCommandText.includes("C:\\Users")) {
|
||||||
|
throw new Error("NAS Codex bootstrap Windows path guard self-check failed");
|
||||||
|
}
|
||||||
|
for (const expected of [
|
||||||
|
"node --version",
|
||||||
|
"corepack --version",
|
||||||
|
"pnpm --version",
|
||||||
|
"git --version",
|
||||||
|
"docker --version",
|
||||||
|
"codex --version",
|
||||||
|
"test -d /vol1/docker/kt-codex/workspace/KT",
|
||||||
|
"test -d /vol1/docker/kt-codex/home/.codex",
|
||||||
|
]) {
|
||||||
|
if (!bootstrapCommandText.includes(expected)) {
|
||||||
|
throw new Error(`NAS Codex bootstrap dry-run command missing: ${expected}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const command of bootstrap.commands) {
|
||||||
|
if (!command.readOnly) {
|
||||||
|
throw new Error("NAS Codex bootstrap dry-run command must be read-only");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const executeBootstrap = buildNasCodexBootstrapPlan({ execute: true });
|
||||||
|
const executeBootstrapCommandText = executeBootstrap.commands
|
||||||
|
.map((item) => item.command)
|
||||||
|
.join("\n");
|
||||||
|
if (
|
||||||
|
executeBootstrap.execute !== true ||
|
||||||
|
executeBootstrap.commands.every((command) => command.readOnly)
|
||||||
|
) {
|
||||||
|
throw new Error("NAS Codex bootstrap execute plan self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!executeBootstrapCommandText.includes("install -d -m 700 /vol1/docker/kt-codex/home/.codex") ||
|
||||||
|
!executeBootstrapCommandText.includes("npm install -g @openai/codex")
|
||||||
|
) {
|
||||||
|
throw new Error("NAS Codex bootstrap execute package plan self-check failed");
|
||||||
|
}
|
||||||
|
for (const forbidden of [
|
||||||
|
"git push",
|
||||||
|
"git merge",
|
||||||
|
"docker build",
|
||||||
|
"docker pull",
|
||||||
|
"kubectl",
|
||||||
|
"systemctl enable",
|
||||||
|
"systemctl start",
|
||||||
|
"cp ~/.codex",
|
||||||
|
"scp ~/.codex",
|
||||||
|
"C:\\Users",
|
||||||
|
]) {
|
||||||
|
if (executeBootstrapCommandText.includes(forbidden)) {
|
||||||
|
throw new Error(`NAS Codex bootstrap forbidden command leaked: ${forbidden}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let windowsBootstrapPathRejected = false;
|
||||||
|
try {
|
||||||
|
buildNasCodexBootstrapPlan({
|
||||||
|
codexHome: "C:\\Users\\example\\.codex",
|
||||||
|
execute: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
windowsBootstrapPathRejected = String(error).includes(
|
||||||
|
"Unsafe NAS Codex bootstrap path",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!windowsBootstrapPathRejected) {
|
||||||
|
throw new Error("NAS Codex bootstrap unsafe Windows path self-check failed");
|
||||||
|
}
|
||||||
|
const napcatCandidateOverride = buildNapcatSyncCandidateReview({
|
||||||
|
candidateBranch: "custom-candidate",
|
||||||
|
lastAcceptedUpstreamBase: "custom-base",
|
||||||
|
});
|
||||||
|
const napcatCandidateOverrideCommandText =
|
||||||
|
napcatCandidateOverride.commands.map((item) => item.command).join("\n");
|
||||||
|
if (
|
||||||
|
!napcatCandidateOverrideCommandText.includes("custom-candidate") ||
|
||||||
|
!napcatCandidateOverrideCommandText.includes("custom-base") ||
|
||||||
|
napcatCandidateOverrideCommandText.includes(
|
||||||
|
napcatAutomationDefaults.candidateBranch,
|
||||||
|
) ||
|
||||||
|
napcatCandidateOverrideCommandText.includes(
|
||||||
|
napcatAutomationDefaults.lastAcceptedUpstreamBase,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat candidate command override self-check failed");
|
||||||
|
}
|
||||||
|
const napcatRuntimeOverride = buildNapcatRuntimeReleaseReadiness({
|
||||||
|
napcatImageTag: "custom-image",
|
||||||
|
});
|
||||||
|
const napcatRuntimeOverrideCommandText =
|
||||||
|
napcatRuntimeOverride.commands.map((item) => item.command).join("\n");
|
||||||
|
if (
|
||||||
|
!napcatRuntimeOverrideCommandText.includes("custom-image") ||
|
||||||
|
napcatRuntimeOverrideCommandText.includes(
|
||||||
|
napcatAutomationDefaults.runtimeImageTag,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat runtime command override self-check failed");
|
||||||
|
}
|
||||||
|
const napcatAuditOverride = buildNapcatUpstreamAudit({
|
||||||
|
dryMergeConflict: false,
|
||||||
|
forkBranch: "custom-fork",
|
||||||
|
forkPatchFiles: [],
|
||||||
|
lastAcceptedUpstreamBase: "custom-base",
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
upstreamReleaseTag: "custom-upstream",
|
||||||
|
});
|
||||||
|
const napcatAuditOverrideCommandText =
|
||||||
|
napcatAuditOverride.commands.map((item) => item.command).join("\n");
|
||||||
|
if (
|
||||||
|
!napcatAuditOverrideCommandText.includes("custom-fork") ||
|
||||||
|
!napcatAuditOverrideCommandText.includes("custom-base") ||
|
||||||
|
!napcatAuditOverrideCommandText.includes("custom-upstream") ||
|
||||||
|
napcatAuditOverrideCommandText.includes(napcatAutomationDefaults.forkBranch) ||
|
||||||
|
napcatAuditOverrideCommandText.includes(
|
||||||
|
napcatAutomationDefaults.lastAcceptedUpstreamBase,
|
||||||
|
) ||
|
||||||
|
napcatAuditOverrideCommandText.includes(
|
||||||
|
napcatAutomationDefaults.upstreamReleaseRef,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat upstream audit command override self-check failed");
|
||||||
|
}
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
readFileSync(resolveInsideRoot("mcp/ktWorkflow/package.json"), "utf8"),
|
||||||
|
) as { scripts?: Record<string, string> };
|
||||||
|
if (!packageJson.scripts?.["napcat-remote-dev-handoff"]) {
|
||||||
|
throw new Error("NapCat remote handoff npm script self-check failed");
|
||||||
|
}
|
||||||
|
const registeredByFakeServer: string[] = [];
|
||||||
|
const fakeMcpServer = {
|
||||||
|
/**
|
||||||
|
* Records registered MCP tool names so self-test proves the actual registerTools surface, not only the constants list.
|
||||||
|
* @param name - MCP tool name passed by `registerTools`.
|
||||||
|
* @returns Nothing; the name is appended to `registeredByFakeServer`.
|
||||||
|
*/
|
||||||
|
registerTool(name: string): void {
|
||||||
|
registeredByFakeServer.push(name);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registerTools(fakeMcpServer as never);
|
||||||
|
for (const toolName of [
|
||||||
|
"kt_napcat_upstream_audit",
|
||||||
|
"kt_napcat_sync_candidate_review",
|
||||||
|
"kt_napcat_runtime_release_readiness",
|
||||||
|
"kt_napcat_remote_dev_handoff",
|
||||||
|
"kt_nas_codex_bootstrap_plan",
|
||||||
|
] as const) {
|
||||||
|
if (!registeredToolNames.includes(toolName)) {
|
||||||
|
throw new Error(`NapCat MCP tool registry self-check failed: ${toolName}`);
|
||||||
|
}
|
||||||
|
if (!registeredByFakeServer.includes(toolName)) {
|
||||||
|
throw new Error(`NapCat MCP registerTools self-check failed: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parseNapcatAutomationClassification("manual-review") !== "manual-review") {
|
||||||
|
throw new Error("NapCat classification parser self-check failed");
|
||||||
|
}
|
||||||
|
const hotZoneAudit = classifyNapcatUpstreamAudit({
|
||||||
|
dryMergeConflict: false,
|
||||||
|
forkPatchFiles: ["packages/napcat-core/login/runtime.ts"],
|
||||||
|
upstreamChangedFiles: ["packages/napcat-core/login/runtime.ts"],
|
||||||
|
});
|
||||||
|
if (hotZoneAudit.classification !== "manual-review") {
|
||||||
|
throw new Error("NapCat hot-zone audit classification self-check failed");
|
||||||
|
}
|
||||||
|
if (!hotZoneAudit.reasonCodes.includes("HOT_ZONE_CHANGED")) {
|
||||||
|
throw new Error("NapCat hot-zone reason-code self-check failed");
|
||||||
|
}
|
||||||
|
const dryRunAudit = buildNapcatUpstreamAudit({
|
||||||
|
dryMergeConflict: false,
|
||||||
|
forkPatchFiles: ["packages/napcat-core/runtime/base.ts"],
|
||||||
|
upstreamChangedFiles: ["packages/napcat-core/runtime/upstream.ts"],
|
||||||
|
});
|
||||||
|
if (dryRunAudit.execute !== false || dryRunAudit.artifacts.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat upstream audit dry-run artifact plan self-check failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const dryRunAuditDefaultCommands = dryRunAudit.commands.map(
|
||||||
|
(item) => item.command,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
dryRunAuditDefaultCommands.some((command) =>
|
||||||
|
command.includes("FORK_BRANCH:-HEAD"),
|
||||||
|
) ||
|
||||||
|
!dryRunAuditDefaultCommands.some((command) =>
|
||||||
|
command.includes(`FORK_BRANCH:-${napcatAutomationDefaults.forkBranch}`),
|
||||||
|
) ||
|
||||||
|
!dryRunAuditDefaultCommands.some((command) =>
|
||||||
|
command.includes(
|
||||||
|
`UPSTREAM_RELEASE_REF:-${napcatAutomationDefaults.upstreamReleaseRef}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat upstream audit command default alignment self-check failed");
|
||||||
|
}
|
||||||
|
const codexCommand = buildNapcatCodexExecCommand({
|
||||||
|
artifactRoot: "/vol1/docker/kt-codex/artifacts/smoke",
|
||||||
|
contextPacketPath:
|
||||||
|
"/vol1/docker/kt-codex/artifacts/smoke/context-packet.md",
|
||||||
|
outputSchemaPath:
|
||||||
|
"/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json",
|
||||||
|
promptName: "upstream-audit",
|
||||||
|
workspaceRoot: "/vol1/docker/kt-codex/workspace/KT",
|
||||||
|
});
|
||||||
|
if (!codexCommand.includes("--ask-for-approval never")) {
|
||||||
|
throw new Error("NapCat Codex runner approval policy self-check failed");
|
||||||
|
}
|
||||||
|
if (!codexCommand.includes("codex --ask-for-approval never exec")) {
|
||||||
|
throw new Error("NapCat Codex runner top-level approval order self-check failed");
|
||||||
|
}
|
||||||
|
if (codexCommand.includes("codex exec") || codexCommand.includes("$(cat")) {
|
||||||
|
throw new Error("NapCat Codex runner stdin safety self-check failed");
|
||||||
|
}
|
||||||
|
for (const expected of [
|
||||||
|
"CODEX_HOME='/vol1/docker/kt-codex/home/.codex'",
|
||||||
|
"--cd '/vol1/docker/kt-codex/workspace/KT'",
|
||||||
|
"--profile-v2 automation",
|
||||||
|
"--sandbox workspace-write",
|
||||||
|
"--json",
|
||||||
|
"--ephemeral",
|
||||||
|
"--output-schema '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json'",
|
||||||
|
"--output-last-message '/vol1/docker/kt-codex/artifacts/smoke/codex-upstream-audit.md'",
|
||||||
|
"- < '/vol1/docker/kt-codex/artifacts/smoke/context-packet.md'",
|
||||||
|
]) {
|
||||||
|
if (!codexCommand.includes(expected)) {
|
||||||
|
throw new Error(`NapCat Codex runner command missing: ${expected}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (codexCommand.includes("git push") || codexCommand.includes("kubectl")) {
|
||||||
|
throw new Error("NapCat Codex runner mutation guard self-check failed");
|
||||||
|
}
|
||||||
|
const upstreamSyncJenkinsTemplate = readFileSync(
|
||||||
|
resolveInsideRoot(
|
||||||
|
"mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const runtimeReleaseJenkinsTemplate = readFileSync(
|
||||||
|
resolveInsideRoot(
|
||||||
|
"mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
for (const expected of [
|
||||||
|
"RUN_CODEX_AUDIT",
|
||||||
|
"CONFIRM_CODEX_WORKSPACE_WRITE",
|
||||||
|
"RUN_CODEX_WORKSPACE_WRITE_AUDIT",
|
||||||
|
]) {
|
||||||
|
if (!upstreamSyncJenkinsTemplate.includes(expected)) {
|
||||||
|
throw new Error(`NapCat upstream Jenkins template missing: ${expected}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Verifies that the default upstream audit Jenkins call stays dry-run before the optional Codex execution stage.
|
||||||
|
* @param template - Full Jenkinsfile text to inspect.
|
||||||
|
* @returns Nothing; throws when the default audit call is unsafe.
|
||||||
|
*/
|
||||||
|
const assertUpstreamJenkinsDefaultAuditDryRun = (template: string): void => {
|
||||||
|
const defaultStageStart = template.indexOf(
|
||||||
|
"stage('ktWorkflow upstream audit dry-run')",
|
||||||
|
);
|
||||||
|
const codexStageStart = template.indexOf(
|
||||||
|
"stage('ktWorkflow upstream audit with Codex')",
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
defaultStageStart === -1 ||
|
||||||
|
codexStageStart === -1 ||
|
||||||
|
defaultStageStart >= codexStageStart
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat upstream Jenkins template default audit stage order self-check failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const defaultUpstreamAuditStage = template.slice(
|
||||||
|
defaultStageStart,
|
||||||
|
codexStageStart,
|
||||||
|
);
|
||||||
|
if (!defaultUpstreamAuditStage.includes("run napcat-upstream-audit --")) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat upstream Jenkins template default audit stage missing audit command",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const forbiddenFlag of ["--execute", "--use-codex"]) {
|
||||||
|
if (defaultUpstreamAuditStage.includes(forbiddenFlag)) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat upstream Jenkins template default audit must not include ${forbiddenFlag}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assertUpstreamJenkinsDefaultAuditDryRun(upstreamSyncJenkinsTemplate);
|
||||||
|
const unsafeDefaultUpstreamAuditTemplate = [
|
||||||
|
"stage('ktWorkflow upstream audit dry-run') {",
|
||||||
|
" pnpm --dir \"$KT_WORKFLOW_DIR\" run napcat-upstream-audit -- \\",
|
||||||
|
" --execute \\",
|
||||||
|
" --use-codex",
|
||||||
|
"}",
|
||||||
|
"stage('ktWorkflow upstream audit with Codex') {",
|
||||||
|
" pnpm --dir \"$KT_WORKFLOW_DIR\" run napcat-upstream-audit -- \\",
|
||||||
|
" --execute \\",
|
||||||
|
" --use-codex",
|
||||||
|
"}",
|
||||||
|
].join("\n");
|
||||||
|
let unsafeDefaultUpstreamAuditRejected = false;
|
||||||
|
try {
|
||||||
|
assertUpstreamJenkinsDefaultAuditDryRun(
|
||||||
|
unsafeDefaultUpstreamAuditTemplate,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
unsafeDefaultUpstreamAuditRejected = String(error).includes(
|
||||||
|
"NapCat upstream Jenkins template default audit must not include",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!unsafeDefaultUpstreamAuditRejected) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat upstream Jenkins template unsafe default audit self-check failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const forbidden of ["git merge", "git push", "docker build", "kubectl"]) {
|
||||||
|
if (upstreamSyncJenkinsTemplate.includes(forbidden)) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat upstream Jenkins template forbidden command leaked: ${forbidden}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const expected of [
|
||||||
|
"choice(name: 'RUNTIME_BUILD_JOB'",
|
||||||
|
"choice(name: 'API_PROMOTION_JOB'",
|
||||||
|
"CONFIRM_API_PROMOTION_TARGET",
|
||||||
|
"PROMOTE_KT_TEMPLATE_API_MAIN",
|
||||||
|
]) {
|
||||||
|
if (!runtimeReleaseJenkinsTemplate.includes(expected)) {
|
||||||
|
throw new Error(`NapCat runtime Jenkins template missing: ${expected}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const forbidden of [
|
||||||
|
"string(name: 'RUNTIME_BUILD_JOB'",
|
||||||
|
"string(name: 'API_PROMOTION_JOB'",
|
||||||
|
"git merge",
|
||||||
|
"git push",
|
||||||
|
"docker build",
|
||||||
|
"kubectl",
|
||||||
|
]) {
|
||||||
|
if (runtimeReleaseJenkinsTemplate.includes(forbidden)) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat runtime Jenkins template forbidden text leaked: ${forbidden}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const runtimeImageBuildStageStart = runtimeReleaseJenkinsTemplate.indexOf(
|
||||||
|
"stage('Runtime image build and publish')",
|
||||||
|
);
|
||||||
|
const apiPromotionStageStart = runtimeReleaseJenkinsTemplate.indexOf(
|
||||||
|
"stage('API promotion')",
|
||||||
|
);
|
||||||
|
const pipelineEndBeforeRuntimeHelpers = runtimeReleaseJenkinsTemplate.indexOf(
|
||||||
|
"\n}\n\n/**",
|
||||||
|
apiPromotionStageStart,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
runtimeImageBuildStageStart === -1 ||
|
||||||
|
apiPromotionStageStart === -1 ||
|
||||||
|
pipelineEndBeforeRuntimeHelpers === -1 ||
|
||||||
|
runtimeImageBuildStageStart > apiPromotionStageStart
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat runtime Jenkins template stage slice self-check failed");
|
||||||
|
}
|
||||||
|
const runtimeImageBuildStage = runtimeReleaseJenkinsTemplate.slice(
|
||||||
|
runtimeImageBuildStageStart,
|
||||||
|
apiPromotionStageStart,
|
||||||
|
);
|
||||||
|
const apiPromotionStage = runtimeReleaseJenkinsTemplate.slice(
|
||||||
|
apiPromotionStageStart,
|
||||||
|
pipelineEndBeforeRuntimeHelpers,
|
||||||
|
);
|
||||||
|
if (runtimeImageBuildStage.includes("CONFIRM_API_PROMOTION_TARGET")) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat runtime Jenkins runtime image build stage must not check API promotion confirmation",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!apiPromotionStage.includes("CONFIRM_API_PROMOTION_TARGET")) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat runtime Jenkins API promotion stage missing confirmation target check",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!apiPromotionStage.includes("PROMOTE_KT_TEMPLATE_API_MAIN")) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat runtime Jenkins API promotion stage missing production confirmation value",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const apiPromotionConfirmationIndex = apiPromotionStage.indexOf(
|
||||||
|
"CONFIRM_API_PROMOTION_TARGET",
|
||||||
|
);
|
||||||
|
const apiPromotionBuildIndex = apiPromotionStage.indexOf(
|
||||||
|
"build job: params.API_PROMOTION_JOB",
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
apiPromotionConfirmationIndex === -1 ||
|
||||||
|
apiPromotionBuildIndex === -1 ||
|
||||||
|
apiPromotionConfirmationIndex > apiPromotionBuildIndex
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat runtime Jenkins API promotion confirmation must run before API build job",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const codexExecutionArtifactRoot = resolveInsideRoot(
|
||||||
|
".kt-workspace/test-artifacts/napcat-codex-runner-self-test",
|
||||||
|
);
|
||||||
|
const codexInvalidSchemaAudit = buildNapcatUpstreamAudit({
|
||||||
|
artifactRoot: codexExecutionArtifactRoot,
|
||||||
|
codexExecutor: () => ({
|
||||||
|
ok: true,
|
||||||
|
stderr: "schema warning",
|
||||||
|
stdout: "{\"classification\":\"surprising\"}\n",
|
||||||
|
}),
|
||||||
|
dryMergeConflict: false,
|
||||||
|
execute: true,
|
||||||
|
forkPatchFiles: [],
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
useCodex: true,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
codexInvalidSchemaAudit.classification !== "blocked" ||
|
||||||
|
!codexInvalidSchemaAudit.reasonCodes.includes("CODEX_SCHEMA_INVALID")
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat Codex invalid schema fallback self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!existsSync(
|
||||||
|
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.jsonl"),
|
||||||
|
) ||
|
||||||
|
!existsSync(path.join(codexExecutionArtifactRoot, "context-packet.md")) ||
|
||||||
|
readFileSync(
|
||||||
|
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.stderr.log"),
|
||||||
|
"utf8",
|
||||||
|
) !== "schema warning"
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat Codex artifact capture self-check failed");
|
||||||
|
}
|
||||||
|
const codexItemTextArtifactRoot = resolveInsideRoot(
|
||||||
|
".kt-workspace/test-artifacts/napcat-codex-runner-success-self-test",
|
||||||
|
);
|
||||||
|
const codexItemTextAudit = buildNapcatUpstreamAudit({
|
||||||
|
artifactRoot: codexItemTextArtifactRoot,
|
||||||
|
codexExecutor: () => ({
|
||||||
|
ok: true,
|
||||||
|
stderr: "",
|
||||||
|
stdout: `${JSON.stringify({
|
||||||
|
item: {
|
||||||
|
text: JSON.stringify({
|
||||||
|
classification: "safe-candidate",
|
||||||
|
reasonCodes: ["NO_UPSTREAM_CHANGE"],
|
||||||
|
recommendedAction: "create-candidate-branch",
|
||||||
|
summary: "ok",
|
||||||
|
}),
|
||||||
|
type: "agent_message",
|
||||||
|
},
|
||||||
|
type: "item.completed",
|
||||||
|
})}\n`,
|
||||||
|
}),
|
||||||
|
dryMergeConflict: false,
|
||||||
|
execute: true,
|
||||||
|
forkPatchFiles: [],
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
useCodex: true,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
codexItemTextAudit.classification !== "safe-candidate" ||
|
||||||
|
!codexItemTextAudit.reasonCodes.includes("NO_UPSTREAM_CHANGE") ||
|
||||||
|
codexItemTextAudit.recommendedAction !== "create-candidate-branch"
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat Codex item.text JSONL success self-check failed");
|
||||||
|
}
|
||||||
|
const codexThrowArtifactRoot = resolveInsideRoot(
|
||||||
|
".kt-workspace/test-artifacts/napcat-codex-runner-throw-self-test",
|
||||||
|
);
|
||||||
|
const codexThrowAudit = buildNapcatUpstreamAudit({
|
||||||
|
artifactRoot: codexThrowArtifactRoot,
|
||||||
|
codexExecutor: () => {
|
||||||
|
throw new Error("codex timed out");
|
||||||
|
},
|
||||||
|
dryMergeConflict: false,
|
||||||
|
execute: true,
|
||||||
|
forkPatchFiles: [],
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
useCodex: true,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
codexThrowAudit.classification !== "blocked" ||
|
||||||
|
!readFileSync(
|
||||||
|
path.join(codexThrowArtifactRoot, "codex-upstream-audit.stderr.log"),
|
||||||
|
"utf8",
|
||||||
|
).includes("codex timed out")
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat Codex thrown executor stderr self-check failed");
|
||||||
|
}
|
||||||
|
const unsafeArtifactRootPath = path.resolve("/tmp/not-kt");
|
||||||
|
const unsafeArtifactRootExisted = existsSync(unsafeArtifactRootPath);
|
||||||
|
let unsafeArtifactRootRejected = false;
|
||||||
|
let unsafeArtifactExecutorCalled = false;
|
||||||
|
try {
|
||||||
|
buildNapcatUpstreamAudit({
|
||||||
|
artifactRoot: "/tmp/not-kt",
|
||||||
|
codexExecutor: () => {
|
||||||
|
unsafeArtifactExecutorCalled = true;
|
||||||
|
return { ok: true, stderr: "", stdout: "{}\n" };
|
||||||
|
},
|
||||||
|
dryMergeConflict: false,
|
||||||
|
execute: true,
|
||||||
|
forkPatchFiles: [],
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
useCodex: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
unsafeArtifactRootRejected = String(error).includes(
|
||||||
|
"Unsafe NapCat Codex artifactRoot",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (!unsafeArtifactRootExisted && existsSync(unsafeArtifactRootPath)) {
|
||||||
|
rmSync(unsafeArtifactRootPath, { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!unsafeArtifactRootRejected || unsafeArtifactExecutorCalled) {
|
||||||
|
throw new Error("NapCat Codex unsafe artifact root self-check failed");
|
||||||
|
}
|
||||||
|
const dryRunAuditCommands: string[] = [];
|
||||||
|
for (const command of dryRunAudit.commands) {
|
||||||
|
if (!command.readOnly) {
|
||||||
|
throw new Error("NapCat upstream audit dry-run read-only flag failed");
|
||||||
|
}
|
||||||
|
dryRunAuditCommands.push(command.command);
|
||||||
|
}
|
||||||
|
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
|
||||||
|
for (const expected of [
|
||||||
|
"git status --short --branch",
|
||||||
|
"git fetch --dry-run upstream --tags --prune",
|
||||||
|
"git fetch --dry-run origin --prune",
|
||||||
|
"git diff --name-only",
|
||||||
|
"git merge-tree",
|
||||||
|
]) {
|
||||||
|
if (!dryRunAuditCommandText.includes(expected)) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat upstream audit dry-run command missing: ${expected}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const command of dryRunAudit.commands) {
|
||||||
|
if (
|
||||||
|
command.readOnly &&
|
||||||
|
/^git\s+fetch(?:\s|$)/.test(command.command) &&
|
||||||
|
!/\s--dry-run(?:\s|$)/.test(command.command)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat upstream audit read-only fetch must use --dry-run",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const forbidden of [
|
||||||
|
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
|
||||||
|
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
|
||||||
|
{ label: "docker build", regex: /(^|\n)\s*docker\s+build(?:\s|$)/ },
|
||||||
|
{ label: "kubectl", regex: /(^|\n)\s*kubectl(?:\s|$)/ },
|
||||||
|
{ label: "deploy", regex: /(^|\n)\s*deploy(?:\s|$)/ },
|
||||||
|
]) {
|
||||||
|
if (forbidden.regex.test(dryRunAuditCommandText)) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat upstream audit dry-run command is not read-only: ${forbidden.label}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const invalidPathCase of [
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "Windows absolute path",
|
||||||
|
upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "Unix absolute path",
|
||||||
|
upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "UNC path",
|
||||||
|
upstreamChangedFiles: [
|
||||||
|
"\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "Windows rooted path",
|
||||||
|
upstreamChangedFiles: [
|
||||||
|
"\\packages\\napcat-core\\login\\runtime.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "Windows drive-relative path",
|
||||||
|
upstreamChangedFiles: ["C:packages/napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "parent segment path",
|
||||||
|
upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||||||
|
name: "fork patch parent segment path",
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const invalidPathAudit = classifyNapcatUpstreamAudit({
|
||||||
|
dryMergeConflict: false,
|
||||||
|
forkPatchFiles: invalidPathCase.forkPatchFiles,
|
||||||
|
upstreamChangedFiles: invalidPathCase.upstreamChangedFiles,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
invalidPathAudit.classification !== "blocked" ||
|
||||||
|
invalidPathAudit.recommendedAction !== "block" ||
|
||||||
|
!invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE")
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat invalid audit path self-check failed: ${invalidPathCase.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const promptName of napcatAutomationPromptNames) {
|
||||||
|
const promptText = loadNapcatAutomationPrompt(promptName);
|
||||||
|
if (!promptText.includes("Do not edit files")) {
|
||||||
|
throw new Error(`NapCat prompt safety contract missing: ${promptName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let traversalPromptRejected = false;
|
||||||
|
try {
|
||||||
|
loadNapcatAutomationPrompt("../../README" as NapcatAutomationPromptName);
|
||||||
|
} catch (error) {
|
||||||
|
traversalPromptRejected = String(error).includes(
|
||||||
|
"Unsupported NapCat automation prompt",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!traversalPromptRejected) {
|
||||||
|
throw new Error("NapCat prompt traversal self-check failed");
|
||||||
|
}
|
||||||
|
|
||||||
const reviewClassifier = {
|
const reviewClassifier = {
|
||||||
localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"),
|
localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"),
|
||||||
realTokenRejected: !isBenignCredentialReviewValue(
|
realTokenRejected: !isBenignCredentialReviewValue(
|
||||||
@ -1026,6 +1833,18 @@ export async function runSelfTest(): Promise<void> {
|
|||||||
throw new Error("database migration verification self-check failed");
|
throw new Error("database migration verification self-check failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const qqbotAccountScanPlan = buildBusinessTestPlan({
|
||||||
|
flow: "qqbot-account-scan",
|
||||||
|
});
|
||||||
|
const qqbotAccountScanText = JSON.stringify(qqbotAccountScanPlan);
|
||||||
|
if (
|
||||||
|
!qqbotAccountScanText.includes("pending sessionId") ||
|
||||||
|
!qqbotAccountScanText.includes("第一轮 Docker run") ||
|
||||||
|
!qqbotAccountScanText.includes("不能造脏数据")
|
||||||
|
) {
|
||||||
|
throw new Error("QQBot account scan non-dirty smoke self-check failed");
|
||||||
|
}
|
||||||
|
|
||||||
const closeoutNeedsUpgrade = buildWorkstreamCloseout({
|
const closeoutNeedsUpgrade = buildWorkstreamCloseout({
|
||||||
cleanupEvidence: ["cleanup-history dry-run deleted=0。"],
|
cleanupEvidence: ["cleanup-history dry-run deleted=0。"],
|
||||||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||||||
|
|||||||
@ -3,8 +3,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
parseNasCodexBootstrapCliArgs,
|
||||||
parseDeployObservationCliArgs,
|
parseDeployObservationCliArgs,
|
||||||
parseGlobalReviewCliArgs,
|
parseGlobalReviewCliArgs,
|
||||||
|
parseNapcatRemoteDevHandoffCliArgs,
|
||||||
|
parseNapcatRuntimeReleaseReadinessCliArgs,
|
||||||
|
parseNapcatSyncCandidateReviewCliArgs,
|
||||||
|
parseNapcatUpstreamAuditCliArgs,
|
||||||
parseObsidianContextCliArgs,
|
parseObsidianContextCliArgs,
|
||||||
parseObsidianCliArgs,
|
parseObsidianCliArgs,
|
||||||
parseObsidianSyncCliArgs,
|
parseObsidianSyncCliArgs,
|
||||||
@ -18,6 +23,13 @@ import { buildGlobalCodeReview } from './tools/review.js';
|
|||||||
import { registerTools } from './registerTools.js';
|
import { registerTools } from './registerTools.js';
|
||||||
import { runSelfTest } from './selfTest.js';
|
import { runSelfTest } from './selfTest.js';
|
||||||
import { buildNapcatDeviceProfileCheck } from './tools/testing.js';
|
import { buildNapcatDeviceProfileCheck } from './tools/testing.js';
|
||||||
|
import {
|
||||||
|
buildNapcatRemoteDevHandoff,
|
||||||
|
buildNapcatRuntimeReleaseReadiness,
|
||||||
|
buildNapcatSyncCandidateReview,
|
||||||
|
buildNapcatUpstreamAudit,
|
||||||
|
buildNasCodexBootstrapPlan,
|
||||||
|
} from './tools/napcatAutomation.js';
|
||||||
|
|
||||||
if (process.argv.includes('--cleanup-history')) {
|
if (process.argv.includes('--cleanup-history')) {
|
||||||
console.log(JSON.stringify(cleanupHistoryArtifacts(parseCliCleanupArgs(process.argv)), null, 2));
|
console.log(JSON.stringify(cleanupHistoryArtifacts(parseCliCleanupArgs(process.argv)), null, 2));
|
||||||
@ -43,6 +55,38 @@ if (process.argv.includes('--cleanup-history')) {
|
|||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (process.argv.includes('--napcat-upstream-audit')) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(buildNapcatUpstreamAudit(parseNapcatUpstreamAuditCliArgs(process.argv)), null, 2),
|
||||||
|
);
|
||||||
|
} else if (process.argv.includes('--napcat-sync-candidate-review')) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
buildNapcatSyncCandidateReview(parseNapcatSyncCandidateReviewCliArgs(process.argv)),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (process.argv.includes('--napcat-runtime-release-readiness')) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
buildNapcatRuntimeReleaseReadiness(parseNapcatRuntimeReleaseReadinessCliArgs(process.argv)),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (process.argv.includes('--napcat-remote-dev-handoff')) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
buildNapcatRemoteDevHandoff(parseNapcatRemoteDevHandoffCliArgs(process.argv)),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (process.argv.includes('--nas-codex-bootstrap')) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(buildNasCodexBootstrapPlan(parseNasCodexBootstrapCliArgs(process.argv)), null, 2),
|
||||||
|
);
|
||||||
} else if (process.argv.includes('--self-test')) {
|
} else if (process.argv.includes('--self-test')) {
|
||||||
await runSelfTest();
|
await runSelfTest();
|
||||||
} else if (process.argv.includes('--workstream-closeout')) {
|
} else if (process.argv.includes('--workstream-closeout')) {
|
||||||
|
|||||||
64
src/tools/napcatAutomation.prompts.ts
Normal file
64
src/tools/napcatAutomation.prompts.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
export const napcatAutomationPromptNames = [
|
||||||
|
'upstream-audit',
|
||||||
|
'sync-candidate-review',
|
||||||
|
'runtime-release-readiness',
|
||||||
|
'remote-dev-handoff',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type NapcatAutomationPromptName =
|
||||||
|
(typeof napcatAutomationPromptNames)[number];
|
||||||
|
|
||||||
|
const promptsRoot = path.resolve(
|
||||||
|
fileURLToPath(new URL('../..', import.meta.url)),
|
||||||
|
'prompts',
|
||||||
|
'napcat',
|
||||||
|
);
|
||||||
|
const promptsRootBoundary = promptsRoot.endsWith(path.sep)
|
||||||
|
? promptsRoot
|
||||||
|
: `${promptsRoot}${path.sep}`;
|
||||||
|
const napcatAutomationPromptNameSet = new Set<string>(
|
||||||
|
napcatAutomationPromptNames,
|
||||||
|
);
|
||||||
|
const napcatAutomationPromptFileNames = {
|
||||||
|
'upstream-audit': 'upstream-audit.md',
|
||||||
|
'sync-candidate-review': 'sync-candidate-review.md',
|
||||||
|
'runtime-release-readiness': 'runtime-release-readiness.md',
|
||||||
|
'remote-dev-handoff': 'remote-dev-handoff.md',
|
||||||
|
} satisfies Record<NapcatAutomationPromptName, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an external prompt name into the source-controlled NapCat prompt enum.
|
||||||
|
* @param value - Raw prompt name from CLI, JSON, MCP input, or a typed caller; only exact entries in `napcatAutomationPromptNames` are accepted.
|
||||||
|
* @returns A stable prompt name that can be mapped to a fixed file under `prompts/napcat`.
|
||||||
|
*/
|
||||||
|
export function parseNapcatAutomationPromptName(
|
||||||
|
value: string,
|
||||||
|
): NapcatAutomationPromptName {
|
||||||
|
if (napcatAutomationPromptNameSet.has(value)) {
|
||||||
|
return value as NapcatAutomationPromptName;
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported NapCat automation prompt: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a source-controlled NapCat automation prompt by stable prompt name.
|
||||||
|
* @param name - Prompt identifier from `napcatAutomationPromptNames`; runtime callers may still pass untyped external strings, so the loader re-validates and resolves only fixed filenames.
|
||||||
|
* @returns UTF-8 prompt text used by Codex CLI automation.
|
||||||
|
*/
|
||||||
|
export function loadNapcatAutomationPrompt(
|
||||||
|
name: NapcatAutomationPromptName,
|
||||||
|
): string {
|
||||||
|
const promptName = parseNapcatAutomationPromptName(String(name));
|
||||||
|
const promptPath = path.resolve(
|
||||||
|
promptsRoot,
|
||||||
|
napcatAutomationPromptFileNames[promptName],
|
||||||
|
);
|
||||||
|
if (!promptPath.startsWith(promptsRootBoundary)) {
|
||||||
|
throw new Error(`NapCat automation prompt escaped prompt root: ${name}`);
|
||||||
|
}
|
||||||
|
return readFileSync(promptPath, 'utf8');
|
||||||
|
}
|
||||||
1335
src/tools/napcatAutomation.ts
Normal file
1335
src/tools/napcatAutomation.ts
Normal file
File diff suppressed because it is too large
Load Diff
130
src/tools/napcatAutomation.types.ts
Normal file
130
src/tools/napcatAutomation.types.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const napcatAutomationClassifications = [
|
||||||
|
'safe-candidate',
|
||||||
|
'manual-review',
|
||||||
|
'blocked',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const napcatAutomationReasonCodes = [
|
||||||
|
'NO_UPSTREAM_CHANGE',
|
||||||
|
'HOT_ZONE_CHANGED',
|
||||||
|
'FORK_PATCH_OVERLAP',
|
||||||
|
'DRY_MERGE_CONFLICT',
|
||||||
|
'BUILD_GRAPH_CHANGED',
|
||||||
|
'METADATA_UNAVAILABLE',
|
||||||
|
'CODEX_SCHEMA_INVALID',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const napcatAutomationRecommendedActions = [
|
||||||
|
'no-op',
|
||||||
|
'create-candidate-branch',
|
||||||
|
'request-human-review',
|
||||||
|
'block',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const upstreamAuditOutputSchema = z.object({
|
||||||
|
classification: z.enum(napcatAutomationClassifications),
|
||||||
|
reasonCodes: z.array(z.enum(napcatAutomationReasonCodes)).min(1),
|
||||||
|
recommendedAction: z.enum(napcatAutomationRecommendedActions),
|
||||||
|
summary: z.string().min(1),
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
export type NapcatAutomationClassification =
|
||||||
|
(typeof napcatAutomationClassifications)[number];
|
||||||
|
export type NapcatAutomationReasonCode =
|
||||||
|
(typeof napcatAutomationReasonCodes)[number];
|
||||||
|
export type NapcatAutomationRecommendedAction =
|
||||||
|
(typeof napcatAutomationRecommendedActions)[number];
|
||||||
|
|
||||||
|
export interface NapcatAuditClassificationInput {
|
||||||
|
dryMergeConflict: boolean;
|
||||||
|
forkPatchFiles: string[];
|
||||||
|
upstreamChangedFiles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatAuditClassificationResult {
|
||||||
|
classification: NapcatAutomationClassification;
|
||||||
|
reasonCodes: NapcatAutomationReasonCode[];
|
||||||
|
recommendedAction: NapcatAutomationRecommendedAction;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatAuditCommand {
|
||||||
|
command: string;
|
||||||
|
description: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatCodexExecCommandInput {
|
||||||
|
artifactRoot: string;
|
||||||
|
codexHome?: string;
|
||||||
|
contextPacketPath: string;
|
||||||
|
outputSchemaPath: string;
|
||||||
|
promptName: 'upstream-audit';
|
||||||
|
workspaceRoot: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatCodexExecResult {
|
||||||
|
ok: boolean;
|
||||||
|
stderr: string;
|
||||||
|
stdout: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NapcatCodexExecutor = (
|
||||||
|
command: string,
|
||||||
|
) => NapcatCodexExecResult;
|
||||||
|
|
||||||
|
export interface NapcatUpstreamAuditInput
|
||||||
|
extends NapcatAuditClassificationInput {
|
||||||
|
artifactRoot?: string;
|
||||||
|
codexExecutor?: NapcatCodexExecutor;
|
||||||
|
codexHome?: string;
|
||||||
|
createCandidateBranch?: boolean;
|
||||||
|
execute?: boolean;
|
||||||
|
forkBranch?: string;
|
||||||
|
forkRepo?: string;
|
||||||
|
lastAcceptedUpstreamBase?: string;
|
||||||
|
outputSchemaPath?: string;
|
||||||
|
promptName?: 'upstream-audit';
|
||||||
|
upstreamReleaseTag?: string;
|
||||||
|
upstreamRepo?: string;
|
||||||
|
useCodex?: boolean;
|
||||||
|
workspaceRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatAutomationArtifact {
|
||||||
|
path: string;
|
||||||
|
type: 'context' | 'json' | 'jsonl' | 'log' | 'markdown' | 'script';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatAutomationResult {
|
||||||
|
artifacts: NapcatAutomationArtifact[];
|
||||||
|
classification: NapcatAutomationClassification;
|
||||||
|
execute: boolean;
|
||||||
|
recommendedAction: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatUpstreamAuditResult
|
||||||
|
extends NapcatAuditClassificationResult {
|
||||||
|
artifacts: NapcatAutomationArtifact[];
|
||||||
|
commands: NapcatAuditCommand[];
|
||||||
|
execute: boolean;
|
||||||
|
hotZonePatterns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the upstream audit classification emitted by deterministic collectors or Codex.
|
||||||
|
* @param value - Raw classification string from deterministic audit metadata or Codex JSON output; must match the shared NapCat automation taxonomy.
|
||||||
|
* @returns A supported NapCat automation classification for downstream workflow decisions.
|
||||||
|
*/
|
||||||
|
export function parseNapcatAutomationClassification(
|
||||||
|
value: string,
|
||||||
|
): NapcatAutomationClassification {
|
||||||
|
const parsed = z.enum(napcatAutomationClassifications).safeParse(value);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Unsupported NapCat audit classification: ${value}`);
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
|
}
|
||||||
@ -44,7 +44,7 @@ function createNapcatCheck(
|
|||||||
/**
|
/**
|
||||||
* Checks that the API project still contains the NapCat device-profile guardrails needed to avoid unknown QQ devices.
|
* Checks that the API project still contains the NapCat device-profile guardrails needed to avoid unknown QQ devices.
|
||||||
* @param input - Project alias or path to scan; defaults to the API backend.
|
* @param input - Project alias or path to scan; defaults to the API backend.
|
||||||
* @returns Static guardrail report covering hostname, MAC, machine-info, runtime dir, and DB timezone.
|
* @returns Static guardrail report covering hostname, physical-style MAC, machine-info, runtime dir, and DB timezone.
|
||||||
*/
|
*/
|
||||||
export function buildNapcatDeviceProfileCheck(
|
export function buildNapcatDeviceProfileCheck(
|
||||||
input: NapcatDeviceProfileCheckInput = {},
|
input: NapcatDeviceProfileCheckInput = {},
|
||||||
@ -76,10 +76,12 @@ export function buildNapcatDeviceProfileCheck(
|
|||||||
'device identity uses qqnt-visible-hostname-v1 and pc-<8hex>',
|
'device identity uses qqnt-visible-hostname-v1 and pc-<8hex>',
|
||||||
),
|
),
|
||||||
createNapcatCheck(
|
createNapcatCheck(
|
||||||
'docker-bridge-mac-strategy',
|
'physical-oui-mac-strategy',
|
||||||
deviceIdentity.includes('docker-bridge-mac-v1') &&
|
deviceIdentity.includes('physical-oui-mac-v1') &&
|
||||||
deviceIdentity.includes('02:42:'),
|
deviceIdentity.includes('hasPhysicalOuiMacPrefix') &&
|
||||||
'device identity uses docker-bridge-mac-v1 and stable 02:42:* MACs',
|
deviceIdentity.includes('isRejectedVirtualMacPrefix') &&
|
||||||
|
deviceIdentity.includes('NAPCAT_PHYSICAL_OUI_PREFIXES'),
|
||||||
|
'device identity uses physical-oui-mac-v1 and rejects Docker/QEMU virtual MAC prefixes',
|
||||||
),
|
),
|
||||||
createNapcatCheck(
|
createNapcatCheck(
|
||||||
'machine-info-options',
|
'machine-info-options',
|
||||||
@ -230,13 +232,26 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
|
|||||||
},
|
},
|
||||||
'qqbot-account-scan': {
|
'qqbot-account-scan': {
|
||||||
assertions: [
|
assertions: [
|
||||||
'扫码创建返回 qrcode/containerName/webuiPort。',
|
'扫码创建接口先返回 pending sessionId,再通过 SSE/status 展示容器创建、二维码生成、等待扫码和终态。',
|
||||||
|
'新增账号首次容器创建必须在第一轮 Docker run 注入设备身份和中文桌面 runtime profile,而不是登录后补 env。',
|
||||||
'取消/过期/失败会清理未绑定 NapCat 容器。',
|
'取消/过期/失败会清理未绑定 NapCat 容器。',
|
||||||
|
'若真实接口 smoke 会创建线上 QQBot 账号或 NapCat 容器,必须先确认清理方案;不能造脏数据时用 RED/GREEN、只读线上计数复核和未执行真实造号原因替代。',
|
||||||
'删除账号会二次确认并删除专属容器。',
|
'删除账号会二次确认并删除专属容器。',
|
||||||
],
|
],
|
||||||
preflight: ['NAS SSH 可达', 'NapCat WebUI 可达', 'API QQBot env 配置完整'],
|
preflight: [
|
||||||
|
'NAS SSH 可达',
|
||||||
|
'NapCat WebUI 可达',
|
||||||
|
'API QQBot env 配置完整',
|
||||||
|
'确认本轮是否允许创建真实 QQBot 账号和 NapCat 容器',
|
||||||
|
],
|
||||||
projects: ['api', 'admin'],
|
projects: ['api', 'admin'],
|
||||||
steps: ['打开账号连接页', '点击扫码新增账号', '轮询二维码状态', '确认账号回填', '测试删除联动容器'],
|
steps: [
|
||||||
|
'打开账号连接页',
|
||||||
|
'点击扫码新增账号并确认接口不等待 Docker/WebUI 长耗时',
|
||||||
|
'订阅 scan/events 或轮询 status 确认中文进度',
|
||||||
|
'确认账号回填和 runtime/profile 归属',
|
||||||
|
'测试删除联动容器或记录未造号的替代验证证据',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
'qqbot-login-sse': {
|
'qqbot-login-sse': {
|
||||||
assertions: [
|
assertions: [
|
||||||
|
|||||||
34
src/types.ts
34
src/types.ts
@ -189,6 +189,40 @@ export interface NapcatDeviceProfileCheckInput {
|
|||||||
project?: string;
|
project?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NapcatPromptToolInput {
|
||||||
|
artifactRoot?: string;
|
||||||
|
candidateBranch?: string;
|
||||||
|
execute?: boolean;
|
||||||
|
forkBranch?: string;
|
||||||
|
forkRepo?: string;
|
||||||
|
upstreamReleaseTag?: string;
|
||||||
|
upstreamRepo?: string;
|
||||||
|
useCodex?: boolean;
|
||||||
|
workspaceRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatSyncCandidateReviewInput extends NapcatPromptToolInput {
|
||||||
|
lastAcceptedUpstreamBase?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatRuntimeReleaseReadinessInput extends NapcatPromptToolInput {
|
||||||
|
apiImageTag?: string;
|
||||||
|
napcatImageTag?: string;
|
||||||
|
profile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatRemoteDevHandoffInput extends NapcatPromptToolInput {
|
||||||
|
targetHost?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NasCodexBootstrapPlanInput {
|
||||||
|
artifactRoot?: string;
|
||||||
|
codexHome?: string;
|
||||||
|
execute?: boolean;
|
||||||
|
serviceName?: string;
|
||||||
|
workspaceRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BlockerResolutionInput {
|
export interface BlockerResolutionInput {
|
||||||
attempts?: number;
|
attempts?: number;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user