Compare commits
No commits in common. "1abfcbf59dff3cee7d964bdbf7615e85f5b49ec1" and "7075d863c26c441d5eeb48c9868a39fc2b612355" have entirely different histories.
1abfcbf59d
...
7075d863c2
28
README.md
28
README.md
@ -15,8 +15,7 @@
|
|||||||
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。
|
||||||
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
|
||||||
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
|
||||||
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、实体 OUI 风格 MAC、QQNT `machine-info` 写入、持久化 runtime dir 和 `DB_TIMEZONE` 默认值。
|
- 静态检查 NapCat 设备身份护栏:确认 API 仍保留 QQNT 可见 hostname、稳定 `02:42:*` 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 入口。
|
||||||
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
|
||||||
@ -41,11 +40,6 @@ 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
|
||||||
@ -55,15 +49,6 @@ 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 的客户端配置里:
|
||||||
@ -110,11 +95,6 @@ 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` | 生成或执行远程只读健康检查命令 |
|
||||||
@ -133,11 +113,6 @@ 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`。 |
|
||||||
@ -179,7 +154,6 @@ 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`,按仓库分组确认范围。
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
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.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
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,11 +10,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
# 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."]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
# 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."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
# 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."]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
# 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,10 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
DeployObservationInput,
|
DeployObservationInput,
|
||||||
GlobalCodeReviewInput,
|
GlobalCodeReviewInput,
|
||||||
NapcatRemoteDevHandoffInput,
|
|
||||||
NapcatRuntimeReleaseReadinessInput,
|
|
||||||
NapcatSyncCandidateReviewInput,
|
|
||||||
NasCodexBootstrapPlanInput,
|
|
||||||
ObsidianContextInput,
|
ObsidianContextInput,
|
||||||
ObsidianSyncInput,
|
ObsidianSyncInput,
|
||||||
ObsidianValidateInput,
|
ObsidianValidateInput,
|
||||||
@ -12,7 +8,6 @@ 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')) {
|
||||||
@ -67,16 +62,6 @@ 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'])
|
||||||
@ -138,135 +123,6 @@ 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,11 +69,6 @@ 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,14 +22,6 @@ 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 {
|
||||||
@ -43,11 +35,6 @@ 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',
|
||||||
@ -383,137 +370,11 @@ 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、实体 OUI MAC、machine-info、runtime dir 和 DB_TIMEZONE。',
|
'静态检查 API NapCat 设备身份护栏:QQNT 可见 hostname、02:42 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,14 +1,9 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdirSync, 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";
|
||||||
@ -40,27 +35,6 @@ 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,
|
||||||
@ -80,788 +54,7 @@ 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(
|
||||||
@ -1833,18 +1026,6 @@ 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,13 +3,8 @@ 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,
|
||||||
@ -23,13 +18,6 @@ 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));
|
||||||
@ -55,38 +43,6 @@ 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')) {
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,130 +0,0 @@
|
|||||||
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, physical-style MAC, machine-info, runtime dir, and DB timezone.
|
* @returns Static guardrail report covering hostname, MAC, machine-info, runtime dir, and DB timezone.
|
||||||
*/
|
*/
|
||||||
export function buildNapcatDeviceProfileCheck(
|
export function buildNapcatDeviceProfileCheck(
|
||||||
input: NapcatDeviceProfileCheckInput = {},
|
input: NapcatDeviceProfileCheckInput = {},
|
||||||
@ -76,12 +76,10 @@ 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(
|
||||||
'physical-oui-mac-strategy',
|
'docker-bridge-mac-strategy',
|
||||||
deviceIdentity.includes('physical-oui-mac-v1') &&
|
deviceIdentity.includes('docker-bridge-mac-v1') &&
|
||||||
deviceIdentity.includes('hasPhysicalOuiMacPrefix') &&
|
deviceIdentity.includes('02:42:'),
|
||||||
deviceIdentity.includes('isRejectedVirtualMacPrefix') &&
|
'device identity uses docker-bridge-mac-v1 and stable 02:42:* MACs',
|
||||||
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',
|
||||||
@ -232,26 +230,13 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
|
|||||||
},
|
},
|
||||||
'qqbot-account-scan': {
|
'qqbot-account-scan': {
|
||||||
assertions: [
|
assertions: [
|
||||||
'扫码创建接口先返回 pending sessionId,再通过 SSE/status 展示容器创建、二维码生成、等待扫码和终态。',
|
'扫码创建返回 qrcode/containerName/webuiPort。',
|
||||||
'新增账号首次容器创建必须在第一轮 Docker run 注入设备身份和中文桌面 runtime profile,而不是登录后补 env。',
|
|
||||||
'取消/过期/失败会清理未绑定 NapCat 容器。',
|
'取消/过期/失败会清理未绑定 NapCat 容器。',
|
||||||
'若真实接口 smoke 会创建线上 QQBot 账号或 NapCat 容器,必须先确认清理方案;不能造脏数据时用 RED/GREEN、只读线上计数复核和未执行真实造号原因替代。',
|
|
||||||
'删除账号会二次确认并删除专属容器。',
|
'删除账号会二次确认并删除专属容器。',
|
||||||
],
|
],
|
||||||
preflight: [
|
preflight: ['NAS SSH 可达', 'NapCat WebUI 可达', 'API QQBot env 配置完整'],
|
||||||
'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,40 +189,6 @@ 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