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.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -4
- package/README.md +72 -73
- package/README_CN.md +72 -73
- package/bootstrap-lite.md +10 -12
- package/bootstrap.md +22 -24
- package/gemini-extension.json +1 -1
- package/install.ps1 +21 -3
- package/install.sh +19 -2
- package/package.json +2 -2
- package/scripts/capability-registry.mjs +5 -3
- package/scripts/cli-doctor-codex.mjs +150 -1
- package/scripts/cli-doctor-render.mjs +2 -1
- package/scripts/cli-lifecycle-hosts.mjs +76 -34
- package/scripts/cli-lifecycle.mjs +50 -15
- package/scripts/cli-messages.mjs +5 -5
- package/scripts/delivery-gate-messages.mjs +5 -4
- package/scripts/delivery-gate.mjs +11 -22
- package/scripts/guard.mjs +1 -1
- package/scripts/notify-closeout.mjs +61 -22
- package/scripts/notify-context.mjs +6 -6
- package/scripts/notify-payload.mjs +8 -0
- package/scripts/notify-route.mjs +1 -1
- package/scripts/notify-ui.mjs +14 -1
- package/scripts/notify.mjs +80 -4
- package/scripts/plan-contract.mjs +10 -14
- package/scripts/project-session-cleanup.mjs +45 -31
- package/scripts/qa-review-state.mjs +313 -0
- package/scripts/ralph-loop.mjs +86 -14
- package/scripts/runtime-scope.mjs +1 -3
- package/scripts/session-capsule.mjs +51 -13
- package/scripts/state-document.mjs +77 -0
- package/scripts/workflow-core.mjs +13 -19
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +55 -67
- package/scripts/workflow-state.mjs +8 -8
- package/skills/commands/auto/SKILL.md +12 -12
- package/skills/commands/build/SKILL.md +9 -10
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +11 -13
- package/skills/commands/init/SKILL.md +18 -9
- package/skills/commands/loop/SKILL.md +70 -96
- package/skills/commands/plan/SKILL.md +7 -8
- package/skills/commands/prd/SKILL.md +3 -3
- package/skills/commands/qa/SKILL.md +49 -0
- package/skills/hello-ui/SKILL.md +3 -3
- package/skills/helloagents/SKILL.md +12 -15
- package/skills/qa-review/SKILL.md +92 -0
- package/templates/plans/contract.json +4 -7
- package/templates/plans/plan.md +1 -1
- package/templates/plans/tasks.md +1 -1
- package/templates/verify.yaml +1 -1
- package/scripts/review-state.mjs +0 -193
- package/scripts/verify-state.mjs +0 -175
- package/skills/commands/global/SKILL.md +0 -71
- package/skills/commands/verify/SKILL.md +0 -46
- package/skills/commands/wiki/SKILL.md +0 -57
- package/skills/hello-review/SKILL.md +0 -42
- package/skills/hello-verify/SKILL.md +0 -144
package/scripts/cli-messages.mjs
CHANGED
|
@@ -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 在项目中使用 ~
|
|
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 ~
|
|
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
|
-
: ` 项目可通过 ~
|
|
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 ~
|
|
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('
|
|
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-
|
|
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,
|
|
88
|
-
if (
|
|
86
|
+
function collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
|
|
87
|
+
if (qaStatus?.required && qaStatus.status !== 'valid') {
|
|
89
88
|
issues.push({
|
|
90
|
-
type: 'missing-
|
|
89
|
+
type: 'missing-qa-review-evidence',
|
|
91
90
|
planName: 'delivery',
|
|
92
|
-
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,
|
|
118
|
+
function collectGateIssues(planEntries, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
|
|
128
119
|
const issues = collectPlanIssues(planEntries)
|
|
129
|
-
collectEvidenceIssues(issues,
|
|
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
|
|
150
|
-
required: deliveryAction?.phase === '
|
|
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
|
-
&& (!
|
|
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,
|
|
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 命令:当前工作流尚未进入
|
|
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
|
-
|
|
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 =
|
|
127
|
-
const evidencePath =
|
|
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
|
|
168
|
-
if (matchesCodexCloseoutEvidence(
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
232
|
-
return matchesCodexCloseoutEvidence(
|
|
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: '
|
|
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 '兼容别名映射:本次按 ~
|
|
52
|
+
return '兼容别名映射:本次按 ~qa 规则执行;统一走 qa-review 质量闭环。';
|
|
53
53
|
}
|
|
54
54
|
return '';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function buildDelegatedTaskHint() {
|
|
58
|
-
return '
|
|
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=明确实现;~
|
|
169
|
-
'若判定为 T3,默认先走 ~plan / ~prd
|
|
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) {
|
package/scripts/notify-route.mjs
CHANGED
|
@@ -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
|
|
25
|
+
return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json;自动激活技能说明仅在宿主全局模式或已初始化项目时生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function routeExplicitCommand({
|
package/scripts/notify-ui.mjs
CHANGED
|
@@ -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') {
|
package/scripts/notify.mjs
CHANGED
|
@@ -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(['
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
17
|
+
function normalizeQaMode(value) {
|
|
18
18
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
19
|
-
return
|
|
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:
|
|
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
|
-
|
|
95
|
-
|
|
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 (!
|
|
132
|
-
issues.push('contract.json missing valid
|
|
130
|
+
if (!normalizeQaMode(normalized.qaMode)) {
|
|
131
|
+
issues.push('contract.json missing valid qaMode')
|
|
133
132
|
}
|
|
134
|
-
if (normalizeStringArray(normalized.
|
|
135
|
-
issues.push('contract.json missing
|
|
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')
|