helloagents 3.0.32 → 3.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +3 -4
  3. package/README.md +72 -73
  4. package/README_CN.md +72 -73
  5. package/bootstrap-lite.md +10 -12
  6. package/bootstrap.md +22 -24
  7. package/gemini-extension.json +1 -1
  8. package/install.ps1 +21 -3
  9. package/install.sh +19 -2
  10. package/package.json +2 -2
  11. package/scripts/capability-registry.mjs +5 -3
  12. package/scripts/cli-doctor-codex.mjs +150 -1
  13. package/scripts/cli-doctor-render.mjs +2 -1
  14. package/scripts/cli-lifecycle-hosts.mjs +76 -34
  15. package/scripts/cli-lifecycle.mjs +50 -15
  16. package/scripts/cli-messages.mjs +5 -5
  17. package/scripts/delivery-gate-messages.mjs +5 -4
  18. package/scripts/delivery-gate.mjs +11 -22
  19. package/scripts/guard.mjs +1 -1
  20. package/scripts/notify-closeout.mjs +61 -22
  21. package/scripts/notify-context.mjs +6 -6
  22. package/scripts/notify-payload.mjs +8 -0
  23. package/scripts/notify-route.mjs +1 -1
  24. package/scripts/notify-ui.mjs +14 -1
  25. package/scripts/notify.mjs +80 -4
  26. package/scripts/plan-contract.mjs +10 -14
  27. package/scripts/project-session-cleanup.mjs +45 -31
  28. package/scripts/qa-review-state.mjs +313 -0
  29. package/scripts/ralph-loop.mjs +86 -14
  30. package/scripts/runtime-scope.mjs +1 -3
  31. package/scripts/session-capsule.mjs +51 -13
  32. package/scripts/state-document.mjs +77 -0
  33. package/scripts/workflow-core.mjs +13 -19
  34. package/scripts/workflow-plan-files.mjs +1 -1
  35. package/scripts/workflow-recommendation.mjs +55 -67
  36. package/scripts/workflow-state.mjs +8 -8
  37. package/skills/commands/auto/SKILL.md +12 -12
  38. package/skills/commands/build/SKILL.md +9 -10
  39. package/skills/commands/commit/SKILL.md +1 -1
  40. package/skills/commands/help/SKILL.md +11 -13
  41. package/skills/commands/init/SKILL.md +18 -9
  42. package/skills/commands/loop/SKILL.md +70 -96
  43. package/skills/commands/plan/SKILL.md +7 -8
  44. package/skills/commands/prd/SKILL.md +3 -3
  45. package/skills/commands/qa/SKILL.md +49 -0
  46. package/skills/hello-ui/SKILL.md +3 -3
  47. package/skills/helloagents/SKILL.md +12 -15
  48. package/skills/qa-review/SKILL.md +92 -0
  49. package/templates/plans/contract.json +4 -7
  50. package/templates/plans/plan.md +1 -1
  51. package/templates/plans/tasks.md +1 -1
  52. package/templates/verify.yaml +1 -1
  53. package/scripts/review-state.mjs +0 -193
  54. package/scripts/verify-state.mjs +0 -175
  55. package/skills/commands/global/SKILL.md +0 -71
  56. package/skills/commands/verify/SKILL.md +0 -46
  57. package/skills/commands/wiki/SKILL.md +0 -57
  58. package/skills/hello-review/SKILL.md +0 -42
  59. package/skills/hello-verify/SKILL.md +0 -144
@@ -65,18 +65,18 @@ function renderInstallMessage(context, mode, state) {
65
65
 
66
66
  if (install) {
67
67
  return msg(
68
- `\n ✅ HelloAGENTS 已安装(standby 模式)!\n\n Claude Code: 已自动配置(~/.claude/CLAUDE.md + hooks)\n Gemini CLI: 已自动配置(~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n standby 模式下,hello-* 技能不会自动触发。\n 在项目中使用 ~wiki 或 ~init 仅创建/同步知识库;用 ~global 初始化项目级全局模式;也可用 ~command 按需调用。\n\n 切换模式:\n helloagents --global 项目级全局模式(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)`,
69
- `\n ✅ HelloAGENTS installed (standby mode)!\n\n Claude Code: Auto-configured (~/.claude/CLAUDE.md + hooks)\n Gemini CLI: Auto-configured (~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n In standby mode, hello-* skills won't auto-trigger.\n Use ~wiki or ~init to create or sync the KB only; use ~global to initialize project-level global mode; ~command stays available on demand.\n\n Switch modes:\n helloagents --global Project-level global mode (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)`,
68
+ `\n ✅ HelloAGENTS 已安装(standby 模式)!\n\n Claude Code: 已自动配置(~/.claude/CLAUDE.md + hooks)\n Gemini CLI: 已自动配置(~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n standby 模式下,hello-* 技能不会自动触发。\n 在项目中使用 ~init 初始化完整项目工作流;未初始化时也可继续用 ~command 按需调用。\n\n 切换模式:\n helloagents --global 宿主级全局部署(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)`,
69
+ `\n ✅ HelloAGENTS installed (standby mode)!\n\n Claude Code: Auto-configured (~/.claude/CLAUDE.md + hooks)\n Gemini CLI: Auto-configured (~/.gemini/GEMINI.md)\n Codex: ${codexStandbyStatus(context)}\n\n ${restartHint(msg)}\n\n In standby mode, hello-* skills won't auto-trigger.\n Use ~init to initialize the full project workflow; uninitialized repos can still use ~command on demand.\n\n Switch modes:\n helloagents --global Host-wide global deployment (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)`,
70
70
  )
71
71
  }
72
72
 
73
73
  return msg(
74
74
  refresh
75
75
  ? ` standby 模式已刷新,CLI 注入与链接已同步最新文件。\n ${restartHint(msg)}\n ${removeHint(msg)}`
76
- : ` 项目可通过 ~wiki 或 ~init 创建/同步知识库;用 ~global 初始化项目级全局模式;未初始化时仅注入轻量规则。\n ${restartHint(msg)}\n ${removeHint(msg)}`,
76
+ : ` 项目可通过 ~init 初始化完整工作流;未初始化时仅注入轻量规则。\n ${restartHint(msg)}\n ${removeHint(msg)}`,
77
77
  refresh
78
78
  ? ` Standby mode refreshed; injected files and links were synchronized.\n ${restartHint(msg)}\n ${removeHint(msg)}`
79
- : ` Projects can use ~wiki or ~init to create/sync the KB; use ~global to initialize project-level global mode. Projects that are not initialized get lite rules only.\n ${restartHint(msg)}\n ${removeHint(msg)}`,
79
+ : ` Projects can use ~init to initialize the full workflow; projects that are not initialized get lite rules only.\n ${restartHint(msg)}\n ${removeHint(msg)}`,
80
80
  )
81
81
  }
82
82
 
@@ -90,7 +90,7 @@ HelloAGENTS v${pkgVersion} — The orchestration kernel for AI CLIs
90
90
  helloagents-js ${msg('(受管宿主配置的跨平台稳定入口)', '(cross-platform stable entrypoint for managed host configs)')}
91
91
 
92
92
  ${msg('模式切换', 'Mode switching')}:
93
- helloagents --global ${msg('项目级全局模式(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)', 'Project-level global mode (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)')}
93
+ helloagents --global ${msg('宿主级全局部署(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)', 'Host-wide global deployment (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)')}
94
94
  helloagents --standby ${msg('标准模式(非插件安装,hello-* 不自动触发,默认)', "Standby mode (non-plugin install, hello-* won't auto-trigger, default)")}
95
95
 
96
96
  ${msg('单 CLI 管理', 'Scoped CLI management')}:
@@ -24,10 +24,8 @@ function issueHeading(issue) {
24
24
  return '任务缺少可交付元数据'
25
25
  case 'missing-contract':
26
26
  return '方案包缺少可信的结构化契约'
27
- case 'missing-verify-evidence':
28
- return '当前工作流缺少最新验证证据'
29
- case 'missing-review-evidence':
30
- return '当前工作流缺少最新审查证据'
27
+ case 'missing-qa-review-evidence':
28
+ return '当前工作流缺少最新 qa-review 证据'
31
29
  case 'missing-advisor-evidence':
32
30
  return '当前工作流缺少最新 advisor 证据'
33
31
  case 'missing-visual-evidence':
@@ -62,6 +60,9 @@ export function buildDeliveryBlockReason(issues, recommendation, gateHint) {
62
60
  if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
63
61
  lines.push('视觉验收动作:先写入当前会话 `artifacts/visual.json`,记录 `tooling`、`screensChecked`、`statesChecked`、`status` 和 `summary`,再报告完成。')
64
62
  }
63
+ if (issues.some((issue) => issue.type === 'missing-qa-review-evidence')) {
64
+ lines.push('质量闭环动作:先完成 `~qa` 或写入当前会话 `artifacts/qa-review.json`,记录结论、问题定位、验证命令与最新结果,再报告完成。')
65
+ }
65
66
  if (gateHint) {
66
67
  lines.push(gateHint)
67
68
  }
@@ -9,10 +9,9 @@ import { fileURLToPath } from 'node:url'
9
9
  import { getAdvisorEvidenceStatus } from './advisor-state.mjs'
10
10
  import { getCloseoutEvidenceStatus } from './closeout-state.mjs'
11
11
  import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
12
+ import { getQaReviewEvidenceStatus } from './qa-review-state.mjs'
12
13
  import { getVisualEvidenceStatus } from './visual-state.mjs'
13
14
  import { buildDeliveryGateHint, getDeliveryAction, getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
14
- import { getReviewEvidenceStatus } from './review-state.mjs'
15
- import { getVerifyEvidenceStatus } from './verify-state.mjs'
16
15
  import { buildDeliveryBlockReason, buildUnderSpecifiedDetails } from './delivery-gate-messages.mjs'
17
16
 
18
17
  function selectGatePlans(snapshot) {
@@ -84,20 +83,12 @@ function collectPlanIssues(planEntries) {
84
83
  return issues
85
84
  }
86
85
 
87
- function collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus) {
88
- if (verificationStatus?.required && verificationStatus.status !== 'valid') {
86
+ function collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
87
+ if (qaStatus?.required && qaStatus.status !== 'valid') {
89
88
  issues.push({
90
- type: 'missing-verify-evidence',
89
+ type: 'missing-qa-review-evidence',
91
90
  planName: 'delivery',
92
- details: verificationStatus.details,
93
- })
94
- }
95
-
96
- if (reviewStatus?.required && reviewStatus.status !== 'valid') {
97
- issues.push({
98
- type: 'missing-review-evidence',
99
- planName: 'delivery',
100
- details: reviewStatus.details,
91
+ details: qaStatus.details,
101
92
  })
102
93
  }
103
94
  if (advisorStatus?.required && advisorStatus.status !== 'valid') {
@@ -124,9 +115,9 @@ function collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisor
124
115
  }
125
116
  }
126
117
 
127
- function collectGateIssues(planEntries, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus) {
118
+ function collectGateIssues(planEntries, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
128
119
  const issues = collectPlanIssues(planEntries)
129
- collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
120
+ collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus)
130
121
  return issues
131
122
  }
132
123
 
@@ -143,11 +134,10 @@ export function evaluateDeliveryGate(data = {}) {
143
134
  const workflowOptions = { payload: data }
144
135
  const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
145
136
  const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
146
- const verificationStatus = getVerifyEvidenceStatus(cwd, workflowOptions)
147
137
  const deliveryAction = getDeliveryAction(cwd, workflowOptions)
148
138
  const gatePlans = selectGatePlans(snapshot)
149
- const reviewStatus = getReviewEvidenceStatus(cwd, {
150
- required: deliveryAction?.phase === 'verify' && deliveryAction?.mode === 'review-first',
139
+ const qaStatus = getQaReviewEvidenceStatus(cwd, {
140
+ required: deliveryAction?.phase === 'qa' || deliveryAction?.phase === 'consolidate',
151
141
  ...workflowOptions,
152
142
  })
153
143
  if (gatePlans.length === 0) {
@@ -169,8 +159,7 @@ export function evaluateDeliveryGate(data = {}) {
169
159
  })
170
160
  const closeoutRequired = (
171
161
  gatePlans.every((entry) => entry.missingFiles.length === 0 && entry.templateIssues.length === 0 && entry.taskSummary.total > 0 && entry.taskSummary.open === 0 && entry.taskSummary.underSpecifiedCount === 0)
172
- && (!verificationStatus.required || verificationStatus.status === 'valid')
173
- && (!reviewStatus.required || reviewStatus.status === 'valid')
162
+ && (!qaStatus.required || qaStatus.status === 'valid')
174
163
  && (!advisorStatus.required || advisorStatus.status === 'valid')
175
164
  && (!visualStatus.required || visualStatus.status === 'valid')
176
165
  )
@@ -179,7 +168,7 @@ export function evaluateDeliveryGate(data = {}) {
179
168
  ...workflowOptions,
180
169
  })
181
170
 
182
- const issues = collectGateIssues(gatePlans, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
171
+ const issues = collectGateIssues(gatePlans, qaStatus, advisorStatus, visualStatus, closeoutStatus)
183
172
  if (issues.length === 0) {
184
173
  return { suppressOutput: true }
185
174
  }
package/scripts/guard.mjs CHANGED
@@ -76,7 +76,7 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
76
76
  if (!recommendation) return null
77
77
  if (matches.some((match) => match.gate === 'post-verify')) {
78
78
  return {
79
- reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 VERIFY / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n处理路径:${recommendation.nextPath}\n${recommendation.guidance}`,
79
+ reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 QA / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n处理路径:${recommendation.nextPath}\n${recommendation.guidance}`,
80
80
  }
81
81
  }
82
82
  if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {
@@ -1,15 +1,56 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
3
- import { dirname } from 'node:path'
3
+ import { dirname, join } from 'node:path'
4
+ import { homedir } from 'node:os'
4
5
 
5
- import { getRuntimeEvidencePath, readRuntimeEvidence, writeRuntimeEvidence } from './runtime-artifacts.mjs'
6
-
7
- export const CODEX_CLOSEOUT_EVIDENCE_FILE = 'codex-native-stop.json'
8
- export const CODEX_QUICK_NOTIFY_EVIDENCE_FILE = 'codex-quick-notify.json'
9
- const CODEX_CLOSEOUT_LOCK_FILE = 'codex-native-stop.lock'
6
+ export const CODEX_NOTIFY_STATE_FILE = 'notify-state.json'
7
+ export const CODEX_NOTIFY_LOCK_FILE = 'notify.lock'
10
8
  const WEAK_KEY_TTL_MS = 10_000
11
9
  const LOCK_STALE_MS = 120_000
12
10
 
11
+ function getHomeDir(env = process.env) {
12
+ return env.HOME || env.USERPROFILE || homedir()
13
+ }
14
+
15
+ export function getCodexNotifyDir(env = process.env) {
16
+ return join(getHomeDir(env), '.codex', '.helloagents')
17
+ }
18
+
19
+ export function getCodexNotifyStatePath(env = process.env) {
20
+ return join(getCodexNotifyDir(env), CODEX_NOTIFY_STATE_FILE)
21
+ }
22
+
23
+ export function getCodexNotifyLockPath(env = process.env) {
24
+ return join(getCodexNotifyDir(env), CODEX_NOTIFY_LOCK_FILE)
25
+ }
26
+
27
+ function readNotifyState(env = process.env) {
28
+ try {
29
+ const value = JSON.parse(readFileSync(getCodexNotifyStatePath(env), 'utf-8'))
30
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
31
+ return { version: 1, nativeStop: null, quickNotify: null }
32
+ }
33
+ return {
34
+ version: 1,
35
+ nativeStop: value.nativeStop && typeof value.nativeStop === 'object' ? value.nativeStop : null,
36
+ quickNotify: value.quickNotify && typeof value.quickNotify === 'object' ? value.quickNotify : null,
37
+ }
38
+ } catch {
39
+ return { version: 1, nativeStop: null, quickNotify: null }
40
+ }
41
+ }
42
+
43
+ function writeNotifyState(state, env = process.env) {
44
+ const filePath = getCodexNotifyStatePath(env)
45
+ mkdirSync(dirname(filePath), { recursive: true })
46
+ writeFileSync(filePath, `${JSON.stringify({
47
+ version: 1,
48
+ nativeStop: state?.nativeStop || null,
49
+ quickNotify: state?.quickNotify || null,
50
+ }, null, 2)}\n`, 'utf-8')
51
+ return filePath
52
+ }
53
+
13
54
  function getTurnId(payload = {}) {
14
55
  return String(payload.turnId || payload.turn_id || payload['turn-id'] || '').trim()
15
56
  }
@@ -118,13 +159,10 @@ export function matchesCodexCloseoutEvidence(evidence, snapshot, now = Date.now(
118
159
  return intersects(snapshot.weakKeys, weakKeys)
119
160
  }
120
161
 
121
- /**
122
- * Try to claim the current Codex closeout so Stop and native notify handle one turn only once.
123
- */
124
162
  export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, source = '' } = {}) {
125
163
  const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
126
- const lockPath = getRuntimeEvidencePath(cwd, CODEX_CLOSEOUT_LOCK_FILE, { payload })
127
- const evidencePath = getRuntimeEvidencePath(cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, { payload })
164
+ const lockPath = getCodexNotifyLockPath()
165
+ const evidencePath = getCodexNotifyStatePath()
128
166
  const now = Date.now()
129
167
  const lockPayload = {
130
168
  source,
@@ -164,8 +202,8 @@ export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, s
164
202
  }
165
203
  }
166
204
 
167
- const evidence = readRuntimeEvidence(cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, { payload })
168
- if (matchesCodexCloseoutEvidence(evidence, snapshot, now)) {
205
+ const notifyState = readNotifyState()
206
+ if (matchesCodexCloseoutEvidence(notifyState.nativeStop, snapshot, now)) {
169
207
  releaseLockFile(lockPath)
170
208
  return {
171
209
  claimed: false,
@@ -186,15 +224,13 @@ export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, s
186
224
  }
187
225
  }
188
226
 
189
- /**
190
- * Persist the handled closeout fingerprint and release the in-flight lock.
191
- */
192
227
  export function finalizeCodexCloseoutClaim(claim, meta = {}) {
193
228
  if (!claim?.claimed) return
194
229
 
195
230
  try {
196
231
  if (meta.handled !== false) {
197
- writeRuntimeEvidence(claim.cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, {
232
+ const notifyState = readNotifyState()
233
+ notifyState.nativeStop = {
198
234
  version: 2,
199
235
  updatedAt: new Date().toISOString(),
200
236
  source: meta.source || claim.source || '',
@@ -205,7 +241,8 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
205
241
  messageHash: claim.snapshot.messageHash,
206
242
  strongKeys: claim.snapshot.strongKeys,
207
243
  weakKeys: claim.snapshot.weakKeys,
208
- }, { payload: claim.payload })
244
+ }
245
+ writeNotifyState(notifyState)
209
246
  }
210
247
  } finally {
211
248
  releaseLockFile(claim.lockPath)
@@ -214,7 +251,8 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
214
251
 
215
252
  export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null, event = '' } = {}) {
216
253
  const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
217
- return writeRuntimeEvidence(cwd, CODEX_QUICK_NOTIFY_EVIDENCE_FILE, {
254
+ const notifyState = readNotifyState()
255
+ notifyState.quickNotify = {
218
256
  version: 1,
219
257
  updatedAt: new Date().toISOString(),
220
258
  event,
@@ -223,11 +261,12 @@ export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = n
223
261
  messageHash: snapshot.messageHash,
224
262
  strongKeys: snapshot.strongKeys,
225
263
  weakKeys: snapshot.weakKeys,
226
- }, { payload })
264
+ }
265
+ return writeNotifyState(notifyState)
227
266
  }
228
267
 
229
268
  export function hasCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null } = {}) {
230
269
  const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
231
- const evidence = readRuntimeEvidence(cwd, CODEX_QUICK_NOTIFY_EVIDENCE_FILE, { payload })
232
- return matchesCodexCloseoutEvidence(evidence, snapshot)
270
+ const notifyState = readNotifyState()
271
+ return matchesCodexCloseoutEvidence(notifyState.quickNotify, snapshot)
233
272
  }
@@ -11,7 +11,7 @@ import {
11
11
  const COMMAND_ALIASES = {
12
12
  do: 'build',
13
13
  design: 'plan',
14
- review: 'verify',
14
+ review: 'qa',
15
15
  };
16
16
 
17
17
  function buildRuntimeRootBlock(pkgRoot) {
@@ -49,13 +49,13 @@ function buildAliasRouteNote(skillName) {
49
49
  return '兼容别名映射:本次按 ~plan 规则执行;方案文件使用 `plan.md`,项目级 UI 契约仍使用 `DESIGN.md`。';
50
50
  }
51
51
  if (skillName === 'review') {
52
- return '兼容别名映射:本次按 ~verify 的审查优先模式执行。';
52
+ return '兼容别名映射:本次按 ~qa 规则执行;统一走 qa-review 质量闭环。';
53
53
  }
54
54
  return '';
55
55
  }
56
56
 
57
57
  function buildDelegatedTaskHint() {
58
- return '若当前输入明显来自上级代理、控制器或多代理协作上下文,且本次输出会交回上级代理继续汇总、决策或复述,而不是直接交付给最终用户,则按子代理处理:直接完成局部任务并返回结果、证据或阻塞项,不使用 HelloAGENTS 外层输出格式,不写 turn-state,不做面向最终用户的收尾。'
58
+ return '若当前任务由上级代理、控制器或宿主协作/委派机制创建,或本次输出会交回上级代理继续汇总、决策或复述,而不是直接交付给最终用户,则一律按子代理处理:直接完成局部任务并返回结果、证据或阻塞项;禁止输出 HelloAGENTS 外层格式、`🔄 下一步:`、turn-state 或面向最终用户的收尾。'
59
59
  }
60
60
 
61
61
  export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFile, host }) {
@@ -137,7 +137,7 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
137
137
  if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
138
138
  if (stateSyncHint) context += `\n\n## 状态文件提醒\n${stateSyncHint}`;
139
139
  context += settingsBlock;
140
- if (source === 'resume' || source === 'compact') {
140
+ if ((source === 'resume' || source === 'compact') && stateSnapshot.exists) {
141
141
  context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\`;先看当前用户消息,如果仍是同一任务,再参考状态文件。`;
142
142
  }
143
143
  return context;
@@ -165,8 +165,8 @@ export function buildSemanticRouteInstruction(cwd, payload = {}) {
165
165
  '请根据用户请求的真实意图选路,不依赖关键词表。',
166
166
  buildDelegatedTaskHint(),
167
167
  'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
168
- '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
169
- '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
168
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~qa=统一质量审查/验证/修复/收尾;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
169
+ '若判定为 T3,默认先走 ~plan / ~prd;纯质量审查、验真或收尾请求才优先 ~qa。',
170
170
  `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 已读取的 hello-ui 规则;同时所有 UI 任务都必须满足 UI 质量基线。`,
171
171
  projectStorageHint,
172
172
  workflowHint ? `项目状态:${workflowHint}` : '',
@@ -14,6 +14,14 @@ const PAYLOAD_KEY_ALIASES = {
14
14
  stop_hook_active: 'stopHookActive',
15
15
  'goal-id': 'goalId',
16
16
  goal_id: 'goalId',
17
+ is_subagent: 'isSubagent',
18
+ sub_agent: 'subagent',
19
+ parent_agent_id: 'parentAgentId',
20
+ parent_turn_id: 'parentTurnId',
21
+ delegated_by_agent_id: 'delegatedByAgentId',
22
+ agent_id: 'agentId',
23
+ agent_role: 'agentRole',
24
+ agent_kind: 'agentKind',
17
25
  }
18
26
 
19
27
  function assignAlias(target, source, sourceKey, targetKey) {
@@ -22,7 +22,7 @@ function shouldBypassRoute(prompt) {
22
22
 
23
23
  function buildHelpExtraRules(skillName) {
24
24
  if (skillName !== 'help') return ''
25
- return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式或已初始化项目时生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
25
+ return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json;自动激活技能说明仅在宿主全局模式或已初始化项目时生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
26
26
  }
27
27
 
28
28
  function routeExplicitCommand({
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { platform } from 'node:os';
6
6
  import { join } from 'node:path';
7
- import { existsSync } from 'node:fs';
7
+ import { appendFileSync, existsSync } from 'node:fs';
8
8
  import { execFileSync, spawn } from 'node:child_process';
9
9
 
10
10
  const PLAT = platform();
@@ -19,6 +19,17 @@ const NOTIFY_MESSAGES = {
19
19
 
20
20
  const WIN_APPID = 'HelloAgents.Notification';
21
21
  const DISABLE_OS_NOTIFICATIONS = process.env.HELLOAGENTS_DISABLE_OS_NOTIFICATIONS === '1';
22
+ const TEST_NOTIFY_LOG = String(process.env.HELLOAGENTS_NOTIFY_TEST_LOG || '').trim();
23
+
24
+ function recordTestTransport(kind, event) {
25
+ if (!TEST_NOTIFY_LOG) return false;
26
+ try {
27
+ appendFileSync(TEST_NOTIFY_LOG, `${kind} ${String(event || '')}\n`, 'utf-8');
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
22
33
 
23
34
  function escapeToastText(value = '') {
24
35
  return String(value)
@@ -100,6 +111,7 @@ function runSoundHelper(pkgRoot, event, mode = 'background') {
100
111
 
101
112
  export function playSound(pkgRoot, event, options = {}) {
102
113
  if (DISABLE_OS_NOTIFICATIONS) return;
114
+ if (recordTestTransport('sound', event)) return;
103
115
  const wav = resolveWav(pkgRoot, event);
104
116
  if (!wav) { process.stderr.write('\x07'); return; }
105
117
  if (runSoundHelper(pkgRoot, event, options.mode === 'blocking' ? 'blocking' : 'background')) return;
@@ -152,6 +164,7 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
152
164
 
153
165
  export function desktopNotify(pkgRoot, event, extra) {
154
166
  if (DISABLE_OS_NOTIFICATIONS) return;
167
+ if (recordTestTransport('desktop', event)) return;
155
168
  const notification = buildDesktopNotificationContent(event, extra);
156
169
  try {
157
170
  if (PLAT === 'win32') {
@@ -41,7 +41,7 @@ const EVENT_NAME = {
41
41
  UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
42
42
  PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
43
43
  };
44
- const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
44
+ const RALPH_LOOP_ROUTE_COMMANDS = new Set(['qa', 'loop']);
45
45
  const CODEX_HOOKS_FILE = join(homedir(), '.codex', 'hooks.json');
46
46
  const GATE_MODULE_LOADERS = {
47
47
  'turn-stop-gate': () => import('./turn-stop-gate.mjs'),
@@ -88,6 +88,59 @@ function getSettings() {
88
88
  return readSettings(CONFIG_FILE);
89
89
  }
90
90
 
91
+ function hasTruthyAgentFlag(value) {
92
+ if (typeof value === 'boolean') return value;
93
+ const normalized = String(value || '').trim().toLowerCase();
94
+ return ['1', 'true', 'yes', 'subagent'].includes(normalized);
95
+ }
96
+
97
+ function hasNonEmptyValue(value) {
98
+ return String(value || '').trim().length > 0;
99
+ }
100
+
101
+ function looksLikeCodexDelegatedTurn(payload = {}) {
102
+ if (!IS_CODEX || !payload || typeof payload !== 'object') return false;
103
+ const client = String(payload.client || '').trim();
104
+ const inputMessages = Array.isArray(payload.inputMessages) ? payload.inputMessages : [];
105
+ return !client && inputMessages.length > 1;
106
+ }
107
+
108
+ function isSubagentPayload(payload = {}) {
109
+ if (!payload || typeof payload !== 'object') return false;
110
+
111
+ if ([payload.isSubagent, payload.subagent].some(hasTruthyAgentFlag)) {
112
+ return true;
113
+ }
114
+
115
+ const roleLike = [
116
+ payload.role,
117
+ payload.agentRole,
118
+ payload.agent_role,
119
+ payload.agentKind,
120
+ payload.agent_kind,
121
+ payload.kind,
122
+ ]
123
+ .map((value) => String(value || '').trim().toLowerCase())
124
+ .filter(Boolean);
125
+
126
+ if (roleLike.some((value) => ['subagent', 'delegate', 'delegated', 'worker', 'explorer'].includes(value))) {
127
+ return true;
128
+ }
129
+
130
+ if ([
131
+ payload.parentAgentId,
132
+ payload.parent_agent_id,
133
+ payload.parentTurnId,
134
+ payload.parent_turn_id,
135
+ payload.delegatedByAgentId,
136
+ payload.delegated_by_agent_id,
137
+ ].some(hasNonEmptyValue)) {
138
+ return true;
139
+ }
140
+
141
+ return looksLikeCodexDelegatedTurn(payload);
142
+ }
143
+
91
144
  function shouldRunRalphLoop(cwd, turnState, payload = {}) {
92
145
  if (!turnState || turnState.kind !== 'complete') return false;
93
146
  if (turnState.requiresDeliveryGate) return true;
@@ -230,13 +283,28 @@ async function runRalphLoop(payload, { turnState } = {}) {
230
283
  blockEvent: 'verify_gate_blocked',
231
284
  exportName: 'evaluateRalphLoop',
232
285
  evaluateArgs: [payload, {
233
- isSubagent: false,
286
+ isSubagent: isSubagentPayload(payload),
234
287
  isGemini: IS_GEMINI,
235
288
  hookEventName: HOST === 'codex' ? 'Stop' : (IS_GEMINI ? 'SessionEnd' : 'Stop'),
236
289
  }],
237
290
  });
238
291
  }
239
292
 
293
+ async function runCodexSubagentGate(payload) {
294
+ if (!IS_CODEX || !isSubagentPayload(payload)) return false;
295
+ return await runInlineGate({
296
+ payload,
297
+ source: 'ralph-loop',
298
+ blockEvent: 'verify_gate_blocked',
299
+ exportName: 'evaluateRalphLoop',
300
+ evaluateArgs: [payload, {
301
+ isSubagent: true,
302
+ isGemini: false,
303
+ hookEventName: 'Stop',
304
+ }],
305
+ });
306
+ }
307
+
240
308
  async function runDeliveryGate(payload) {
241
309
  return await runInlineGate({
242
310
  payload,
@@ -391,7 +459,7 @@ function cmdInject() {
391
459
  const cwd = payload.cwd || process.cwd();
392
460
  const settings = getSettings();
393
461
  const bootstrapFile = resolveBootstrapFile(cwd, settings, HOST);
394
- const shouldEnsureProjectLocal = bootstrapFile === 'bootstrap.md' || source === 'resume' || source === 'compact';
462
+ const shouldEnsureProjectLocal = isProjectRuntimeActive(cwd);
395
463
 
396
464
  startReplaySession(cwd, {
397
465
  host: HOST,
@@ -447,6 +515,11 @@ async function cmdStop() {
447
515
  const payload = readPayloadFromStdin();
448
516
  const cwd = payload.cwd || process.cwd();
449
517
  const turnPayload = attachTurnSession(payload, cwd);
518
+ if (await runCodexSubagentGate(turnPayload)) return;
519
+ if (IS_CODEX && isSubagentPayload(turnPayload)) {
520
+ emptySuppress();
521
+ return;
522
+ }
450
523
  const turnState = readMainTurnState(cwd, turnPayload);
451
524
  const managedCodexStopHook = IS_CODEX && hasManagedCodexStopHook();
452
525
  const skipCompleteNotify = managedCodexStopHook && hasCodexQuickNotifyEvidence(cwd, {
@@ -504,7 +577,10 @@ async function cmdCodexNotify() {
504
577
  return;
505
578
  }
506
579
  if (type !== 'agent-turn-complete') return;
507
- if (hasManagedCodexStopHook()) {
580
+ const managedCodexStopHook = hasManagedCodexStopHook();
581
+ if (managedCodexStopHook && !String(turnPayload.client || '').trim()) return;
582
+ if (isSubagentPayload(turnPayload)) return;
583
+ if (managedCodexStopHook) {
508
584
  const turnState = readMainTurnState(cwd, turnPayload);
509
585
  if (shouldEmitManagedCodexCompleteNotify(cwd, turnState, turnPayload)) {
510
586
  notifyByLevel('complete', buildNotifyExtra(data), getSettings(), { mode: 'blocking' });
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
  import { resolveProjectPlanDir } from './project-storage.mjs'
5
5
 
6
6
  export const PLAN_CONTRACT_FILE_NAME = 'contract.json'
7
- const VALID_VERIFY_MODES = new Set(['test-first', 'review-first'])
7
+ const VALID_QA_MODES = new Set(['standard', 'deep'])
8
8
  const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'gemini'])
9
9
 
10
10
  function normalizeStringArray(values) {
@@ -14,9 +14,9 @@ function normalizeStringArray(values) {
14
14
  .filter(Boolean))]
15
15
  }
16
16
 
17
- function normalizeVerifyMode(value) {
17
+ function normalizeQaMode(value) {
18
18
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
19
- return VALID_VERIFY_MODES.has(normalized) ? normalized : ''
19
+ return VALID_QA_MODES.has(normalized) ? normalized : ''
20
20
  }
21
21
 
22
22
  function normalizeUiStyleAdvisorContract(input = {}) {
@@ -88,12 +88,11 @@ export function readPlanContract(planDir) {
88
88
 
89
89
  export function normalizePlanContract(input = {}) {
90
90
  return {
91
- version: 1,
91
+ version: 2,
92
92
  source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
93
93
  originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
94
- verifyMode: normalizeVerifyMode(input.verifyMode),
95
- reviewerFocus: normalizeStringArray(input.reviewerFocus),
96
- testerFocus: normalizeStringArray(input.testerFocus),
94
+ qaMode: normalizeQaMode(input.qaMode),
95
+ qaFocus: normalizeStringArray(input.qaFocus),
97
96
  ui: normalizeUiContract(input.ui),
98
97
  advisor: normalizeAdvisorContract(input.advisor),
99
98
  }
@@ -128,14 +127,11 @@ export function getPlanContractIssues(contract = null) {
128
127
  const advisorRequirement = getAdvisorRequirement(normalized)
129
128
  const visualValidation = getVisualValidationRequirement(normalized)
130
129
  const issues = []
131
- if (!normalizeVerifyMode(normalized.verifyMode)) {
132
- issues.push('contract.json missing valid verifyMode')
130
+ if (!normalizeQaMode(normalized.qaMode)) {
131
+ issues.push('contract.json missing valid qaMode')
133
132
  }
134
- if (normalizeStringArray(normalized.testerFocus).length === 0) {
135
- issues.push('contract.json missing testerFocus')
136
- }
137
- if (normalizeVerifyMode(normalized.verifyMode) === 'review-first' && normalizeStringArray(normalized.reviewerFocus).length === 0) {
138
- issues.push('contract.json missing reviewerFocus for review-first flow')
133
+ if (normalizeStringArray(normalized.qaFocus).length === 0) {
134
+ issues.push('contract.json missing qaFocus')
139
135
  }
140
136
  if (normalized.ui?.required && normalizeStringArray(normalized.ui.sourcePriority).length === 0) {
141
137
  issues.push('contract.json missing ui.sourcePriority')