helloagents 3.0.19 → 3.0.21-beta.1
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +26 -20
- package/README_CN.md +24 -18
- package/bootstrap-lite.md +4 -2
- package/bootstrap.md +4 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/cli-doctor-codex.mjs +5 -5
- package/scripts/cli-doctor-render.mjs +1 -1
- package/scripts/cli-doctor.mjs +2 -2
- package/scripts/cli-messages.mjs +5 -5
- package/scripts/delivery-gate-messages.mjs +4 -4
- package/scripts/guard-rules.mjs +2 -2
- package/scripts/guard.mjs +3 -3
- package/scripts/notify-context.mjs +1 -1
- package/scripts/notify.mjs +3 -1
- package/scripts/project-session-cleanup.mjs +106 -0
- package/scripts/project-storage.mjs +5 -5
- package/scripts/ralph-loop.mjs +1 -1
- package/scripts/runtime-scope.mjs +132 -62
- package/scripts/runtime-user-cleanup.mjs +63 -0
- package/scripts/session-capsule.mjs +29 -2
- package/scripts/session-token.mjs +0 -2
- package/scripts/workflow-core.mjs +1 -1
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +1 -1
- package/skills/commands/auto/SKILL.md +3 -3
- package/skills/commands/build/SKILL.md +6 -6
- package/skills/commands/clean/SKILL.md +5 -5
- package/skills/commands/commit/SKILL.md +3 -3
- package/skills/commands/init/SKILL.md +3 -3
- package/skills/commands/loop/SKILL.md +2 -2
- package/skills/commands/plan/SKILL.md +7 -7
- package/skills/commands/prd/SKILL.md +7 -6
- package/skills/commands/verify/SKILL.md +2 -2
- package/skills/commands/wiki/SKILL.md +1 -1
- package/skills/hello-arch/SKILL.md +2 -2
- package/skills/hello-errors/SKILL.md +1 -1
- package/skills/hello-subagent/SKILL.md +1 -1
- package/skills/hello-ui/SKILL.md +4 -4
- package/skills/hello-verify/SKILL.md +1 -1
- package/skills/hello-write/SKILL.md +1 -1
- package/skills/helloagents/SKILL.md +9 -10
|
@@ -117,7 +117,7 @@ export function getProjectSessionStateScope(cwd, options = {}) {
|
|
|
117
117
|
stateScope: 'session',
|
|
118
118
|
stateSessionToken: scope.session,
|
|
119
119
|
stateSessionMode: scope.sessionMode,
|
|
120
|
-
|
|
120
|
+
stateWorkspace: scope.workspace || scope.branch,
|
|
121
121
|
sessionDir: scope.sessionDir,
|
|
122
122
|
statePath: scope.statePath,
|
|
123
123
|
}
|
|
@@ -168,7 +168,7 @@ export function getProjectStoreSummary(cwd, options = {}) {
|
|
|
168
168
|
stateScope: stateScope.stateScope,
|
|
169
169
|
stateSessionToken: stateScope.stateSessionToken,
|
|
170
170
|
stateSessionMode: stateScope.stateSessionMode,
|
|
171
|
-
|
|
171
|
+
stateWorkspace: stateScope.stateWorkspace,
|
|
172
172
|
sessionStateDir: stateScope.sessionDir,
|
|
173
173
|
artifactsDir,
|
|
174
174
|
usesSharedStore: projectStoreMode === 'repo-shared',
|
|
@@ -247,7 +247,7 @@ export function buildProjectStorageHint(cwd, options = {}) {
|
|
|
247
247
|
const hints = []
|
|
248
248
|
hints.push(`当前状态文件写入 \`${summary.promptStatePath}\``)
|
|
249
249
|
if (summary.stateSessionMode === 'default') {
|
|
250
|
-
hints.push(
|
|
250
|
+
hints.push(`当前宿主未提供稳定会话标识,因此使用工作区默认位置 \`${summary.stateSessionToken}\``)
|
|
251
251
|
}
|
|
252
252
|
if (summary.usesSharedStore) {
|
|
253
253
|
hints.push(`项目存储:\`project_store_mode=repo-shared\`;本地激活/会话运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
|
|
@@ -266,7 +266,7 @@ export function buildProjectStorageBlock(cwd, options = {}) {
|
|
|
266
266
|
activation_dir: summary.promptActivationDir,
|
|
267
267
|
state_scope: summary.stateScope,
|
|
268
268
|
state_path: summary.promptStatePath,
|
|
269
|
-
|
|
269
|
+
state_workspace: summary.stateWorkspace,
|
|
270
270
|
state_session_token: summary.stateSessionToken,
|
|
271
271
|
state_session_mode: summary.stateSessionMode,
|
|
272
272
|
session_state_dir: summary.promptSessionStateDir,
|
|
@@ -278,7 +278,7 @@ export function buildProjectStorageBlock(cwd, options = {}) {
|
|
|
278
278
|
const explanations = []
|
|
279
279
|
explanations.push('说明:状态文件只使用 `state_path`。')
|
|
280
280
|
if (summary.stateSessionMode === 'default') {
|
|
281
|
-
explanations.push('
|
|
281
|
+
explanations.push('说明:当前宿主未提供稳定会话标识,因此使用工作区默认位置。')
|
|
282
282
|
}
|
|
283
283
|
if (summary.usesSharedStore) {
|
|
284
284
|
explanations.push('说明:状态文件与会话产物写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
|
package/scripts/ralph-loop.mjs
CHANGED
|
@@ -135,7 +135,7 @@ function handleFailure(failures, cwd, options = {}) {
|
|
|
135
135
|
writeBreaker(cwd, breaker, options);
|
|
136
136
|
|
|
137
137
|
const breakerWarning = breaker.consecutive_failures >= 3
|
|
138
|
-
? `\n\n⚠️ [断路器] 已连续 ${breaker.consecutive_failures}
|
|
138
|
+
? `\n\n⚠️ [断路器] 已连续 ${breaker.consecutive_failures} 次验证失败。当前修复思路可能有误,先处理:\n 1. 重新分析根因,不要继续在同一方向上硬修\n 2. 检查是否存在架构层面的问题\n 3. 考虑回退到上一个正常状态重新开始`
|
|
139
139
|
: '';
|
|
140
140
|
|
|
141
141
|
const details = failures.map(f => `\u2717 ${f.cmd}\n${f.output}`).join('\n\n');
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto'
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { dirname, join, normalize, resolve } from 'node:path'
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
6
|
|
|
7
7
|
import { resolveSessionToken } from './session-token.mjs'
|
|
8
8
|
import { USER_RUNTIME_MAX_AGE_MS } from './runtime-ttl.mjs'
|
|
9
|
+
import { cleanupUserRuntimeRoot, getUserRuntimeRoot } from './runtime-user-cleanup.mjs'
|
|
9
10
|
|
|
10
11
|
export const PROJECT_DIR_NAME = '.helloagents'
|
|
11
12
|
export const PROJECT_SESSIONS_DIR_NAME = 'sessions'
|
|
12
13
|
export const PROJECT_ARTIFACTS_DIR_NAME = 'artifacts'
|
|
13
14
|
export const CAPSULE_FILE_NAME = 'capsule.json'
|
|
14
15
|
export const EVENTS_FILE_NAME = 'events.jsonl'
|
|
16
|
+
export const ACTIVE_SESSION_FILE_NAME = 'active.json'
|
|
15
17
|
export const DEFAULT_STATE_SESSION_TOKEN = 'default'
|
|
16
18
|
export const USER_RUNTIME_DIR_NAME = 'runtime'
|
|
17
|
-
export { USER_RUNTIME_MAX_AGE_MS }
|
|
19
|
+
export { cleanupUserRuntimeRoot, getUserRuntimeRoot, USER_RUNTIME_MAX_AGE_MS }
|
|
18
20
|
|
|
19
21
|
function normalizePath(filePath = '') {
|
|
20
22
|
return filePath ? normalize(resolve(filePath)) : ''
|
|
@@ -68,6 +70,22 @@ function resolveGitBranchName(cwd) {
|
|
|
68
70
|
return symbolicName && symbolicName !== 'HEAD' ? symbolicName : ''
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function resolveGitShortHead(cwd) {
|
|
74
|
+
return runGit(cwd, ['rev-parse', '--short', 'HEAD'])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveWorkspaceName(cwd) {
|
|
78
|
+
const branchName = resolveGitBranchName(cwd)
|
|
79
|
+
if (branchName) return sanitizeRuntimeSegment(branchName, 'workspace')
|
|
80
|
+
|
|
81
|
+
if (resolveGitTopLevel(cwd)) {
|
|
82
|
+
const shortHead = sanitizeRuntimeSegment(resolveGitShortHead(cwd), '')
|
|
83
|
+
return shortHead ? `detached-${shortHead}` : 'detached'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return 'workspace'
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
export function sanitizeRuntimeSegment(value = '', fallback = '') {
|
|
72
90
|
const normalized = String(value)
|
|
73
91
|
.trim()
|
|
@@ -101,10 +119,6 @@ export function getProjectRoot(cwd) {
|
|
|
101
119
|
return activeDir ? dirname(activeDir) : normalizePath(cwd || process.cwd())
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
export function getUserRuntimeRoot(home = getHomeDir()) {
|
|
105
|
-
return join(home, PROJECT_DIR_NAME, USER_RUNTIME_DIR_NAME)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
122
|
function isUserHomeHelloagentsDir(dirPath) {
|
|
109
123
|
const homeCandidates = [
|
|
110
124
|
getHomeDir(),
|
|
@@ -151,57 +165,19 @@ function findProjectActivationDir(cwd) {
|
|
|
151
165
|
return ''
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
function
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function cleanupUserRuntimeRoot({
|
|
165
|
-
home = getHomeDir(),
|
|
166
|
-
now = Date.now(),
|
|
167
|
-
maxAgeMs = USER_RUNTIME_MAX_AGE_MS,
|
|
168
|
-
} = {}) {
|
|
169
|
-
const root = getUserRuntimeRoot(home)
|
|
170
|
-
const result = {
|
|
171
|
-
root,
|
|
172
|
-
removedExpiredDirs: [],
|
|
173
|
-
errors: [],
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (!existsSync(root)) return result
|
|
177
|
-
|
|
178
|
-
let entries = []
|
|
179
|
-
try {
|
|
180
|
-
entries = readdirSync(root, { withFileTypes: true })
|
|
181
|
-
} catch (error) {
|
|
182
|
-
result.errors.push(`${root}: ${error.message}`)
|
|
183
|
-
return result
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
for (const entry of entries) {
|
|
187
|
-
if (!entry.isDirectory()) continue
|
|
188
|
-
const dirPath = join(root, entry.name)
|
|
189
|
-
try {
|
|
190
|
-
if (now - statSync(dirPath).mtimeMs > maxAgeMs) {
|
|
191
|
-
removePathIfExists(dirPath, result, 'removedExpiredDirs')
|
|
192
|
-
}
|
|
193
|
-
} catch (error) {
|
|
194
|
-
result.errors.push(`${dirPath}: ${error.message}`)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return result
|
|
168
|
+
function resolvePayloadSessionToken(payload = {}) {
|
|
169
|
+
if (payload?._helloagentsSessionAlias) return ''
|
|
170
|
+
return resolveSessionToken({
|
|
171
|
+
payload,
|
|
172
|
+
env: {},
|
|
173
|
+
ppid: 0,
|
|
174
|
+
allowPpidFallback: false,
|
|
175
|
+
})
|
|
199
176
|
}
|
|
200
177
|
|
|
201
|
-
function
|
|
202
|
-
void ppid
|
|
178
|
+
function resolveEnvSessionToken(env = process.env) {
|
|
203
179
|
return resolveSessionToken({
|
|
204
|
-
payload,
|
|
180
|
+
payload: {},
|
|
205
181
|
env,
|
|
206
182
|
ppid: 0,
|
|
207
183
|
allowPpidFallback: false,
|
|
@@ -217,29 +193,122 @@ function resolveTransientSessionToken({ payload = {}, env = process.env, ppid =
|
|
|
217
193
|
})
|
|
218
194
|
}
|
|
219
195
|
|
|
196
|
+
function getActiveSessionPath(activationDir) {
|
|
197
|
+
return join(activationDir, PROJECT_SESSIONS_DIR_NAME, ACTIVE_SESSION_FILE_NAME)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveActiveSessionToken({ activationDir, projectRoot, workspace, now = Date.now() } = {}) {
|
|
201
|
+
const active = readJsonFile(getActiveSessionPath(activationDir), null)
|
|
202
|
+
if (!active || typeof active !== 'object') return ''
|
|
203
|
+
if (active.cwd && !samePath(active.cwd, projectRoot)) return ''
|
|
204
|
+
|
|
205
|
+
const activeWorkspace = sanitizeRuntimeSegment(active.workspace || active.branch || '', '')
|
|
206
|
+
if (activeWorkspace && activeWorkspace !== workspace) return ''
|
|
207
|
+
|
|
208
|
+
const updatedAt = Date.parse(active.updatedAt || '')
|
|
209
|
+
if (!Number.isFinite(updatedAt) || now - updatedAt > USER_RUNTIME_MAX_AGE_MS) return ''
|
|
210
|
+
|
|
211
|
+
return sanitizeRuntimeSegment(active.session, '')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveActiveAliasSession({ activationDir, projectRoot, workspace, alias, now = Date.now() } = {}) {
|
|
215
|
+
if (!alias) return ''
|
|
216
|
+
const active = readJsonFile(getActiveSessionPath(activationDir), null)
|
|
217
|
+
if (!active || typeof active !== 'object') return ''
|
|
218
|
+
if (active.cwd && !samePath(active.cwd, projectRoot)) return ''
|
|
219
|
+
|
|
220
|
+
const activeWorkspace = sanitizeRuntimeSegment(active.workspace || active.branch || '', '')
|
|
221
|
+
if (activeWorkspace && activeWorkspace !== workspace) return ''
|
|
222
|
+
|
|
223
|
+
const updatedAt = Date.parse(active.updatedAt || '')
|
|
224
|
+
if (!Number.isFinite(updatedAt) || now - updatedAt > USER_RUNTIME_MAX_AGE_MS) return ''
|
|
225
|
+
|
|
226
|
+
const aliases = active.aliases && typeof active.aliases === 'object' ? active.aliases : {}
|
|
227
|
+
return sanitizeRuntimeSegment(aliases[alias], '')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function writeActiveProjectSession(scope, { host = '', source = '', env = process.env } = {}) {
|
|
231
|
+
if (!scope?.active || !scope.activationDir || !scope.session) return ''
|
|
232
|
+
|
|
233
|
+
const activePath = getActiveSessionPath(scope.activationDir)
|
|
234
|
+
const current = readJsonFile(activePath, null) || {}
|
|
235
|
+
const aliases = current.aliases && typeof current.aliases === 'object' ? current.aliases : {}
|
|
236
|
+
const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
|
|
237
|
+
if (envToken && envToken !== scope.session) aliases[envToken] = scope.session
|
|
238
|
+
|
|
239
|
+
writeJsonFileAtomic(activePath, {
|
|
240
|
+
version: 1,
|
|
241
|
+
cwd: scope.cwd,
|
|
242
|
+
workspace: scope.workspace || scope.branch,
|
|
243
|
+
session: scope.session,
|
|
244
|
+
sessionMode: scope.sessionMode,
|
|
245
|
+
host,
|
|
246
|
+
source,
|
|
247
|
+
aliases,
|
|
248
|
+
updatedAt: new Date().toISOString(),
|
|
249
|
+
})
|
|
250
|
+
return activePath
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function chooseProjectSession({ payload, env, activationDir, projectRoot, workspace }) {
|
|
254
|
+
const payloadToken = sanitizeRuntimeSegment(resolvePayloadSessionToken(payload), '')
|
|
255
|
+
if (payloadToken) return { session: payloadToken, sessionMode: 'host-session' }
|
|
256
|
+
|
|
257
|
+
const payloadAlias = sanitizeRuntimeSegment(payload?._helloagentsSessionAlias, '')
|
|
258
|
+
const payloadAliasToken = resolveActiveAliasSession({
|
|
259
|
+
activationDir,
|
|
260
|
+
projectRoot,
|
|
261
|
+
workspace,
|
|
262
|
+
alias: payloadAlias,
|
|
263
|
+
})
|
|
264
|
+
if (payloadAliasToken) return { session: payloadAliasToken, sessionMode: 'active-session' }
|
|
265
|
+
|
|
266
|
+
const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
|
|
267
|
+
const aliasToken = resolveActiveAliasSession({
|
|
268
|
+
activationDir,
|
|
269
|
+
projectRoot,
|
|
270
|
+
workspace,
|
|
271
|
+
alias: envToken,
|
|
272
|
+
})
|
|
273
|
+
if (aliasToken) return { session: aliasToken, sessionMode: 'active-session' }
|
|
274
|
+
|
|
275
|
+
if (envToken) return { session: envToken, sessionMode: 'host-session' }
|
|
276
|
+
|
|
277
|
+
const activeToken = resolveActiveSessionToken({ activationDir, projectRoot, workspace })
|
|
278
|
+
if (activeToken) return { session: activeToken, sessionMode: 'active-session' }
|
|
279
|
+
|
|
280
|
+
return { session: DEFAULT_STATE_SESSION_TOKEN, sessionMode: 'default' }
|
|
281
|
+
}
|
|
282
|
+
|
|
220
283
|
export function getProjectSessionScope(cwd, options = {}) {
|
|
221
284
|
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
222
285
|
const projectRoot = getProjectRoot(normalizedCwd)
|
|
223
|
-
const { payload = {}, env = process.env
|
|
224
|
-
const stableToken = resolveStableSessionToken({ payload, env, ppid })
|
|
225
|
-
const branch = sanitizeRuntimeSegment(resolveGitBranchName(projectRoot), 'detached')
|
|
226
|
-
const session = sanitizeRuntimeSegment(stableToken, DEFAULT_STATE_SESSION_TOKEN)
|
|
286
|
+
const { payload = {}, env = process.env } = normalizeRuntimeOptions(options)
|
|
227
287
|
const activationDir = getProjectActivationDir(projectRoot)
|
|
228
|
-
const
|
|
288
|
+
const workspace = resolveWorkspaceName(projectRoot)
|
|
289
|
+
const { session, sessionMode } = chooseProjectSession({
|
|
290
|
+
payload,
|
|
291
|
+
env,
|
|
292
|
+
activationDir,
|
|
293
|
+
projectRoot,
|
|
294
|
+
workspace,
|
|
295
|
+
})
|
|
296
|
+
const sessionDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME, workspace, session)
|
|
229
297
|
|
|
230
298
|
return {
|
|
231
299
|
cwd: projectRoot,
|
|
232
300
|
active: isProjectRuntimeActive(projectRoot),
|
|
233
|
-
branch,
|
|
301
|
+
branch: workspace,
|
|
302
|
+
workspace,
|
|
234
303
|
session,
|
|
235
|
-
sessionMode
|
|
304
|
+
sessionMode,
|
|
236
305
|
activationDir,
|
|
237
306
|
sessionDir,
|
|
238
307
|
statePath: join(sessionDir, 'STATE.md'),
|
|
239
308
|
capsulePath: join(sessionDir, CAPSULE_FILE_NAME),
|
|
240
309
|
eventsPath: join(sessionDir, EVENTS_FILE_NAME),
|
|
241
310
|
artifactsDir: join(sessionDir, PROJECT_ARTIFACTS_DIR_NAME),
|
|
242
|
-
key: `${
|
|
311
|
+
key: `${projectRoot}::${workspace}::${session}`,
|
|
243
312
|
}
|
|
244
313
|
}
|
|
245
314
|
|
|
@@ -259,6 +328,7 @@ function buildTransientRuntimeDir(cwd, options = {}) {
|
|
|
259
328
|
return {
|
|
260
329
|
cwd: normalizedCwd,
|
|
261
330
|
branch: 'transient',
|
|
331
|
+
workspace: 'transient',
|
|
262
332
|
session: token,
|
|
263
333
|
sessionMode: token === DEFAULT_STATE_SESSION_TOKEN ? 'default' : 'transient-session',
|
|
264
334
|
sessionDir: join(getUserRuntimeRoot(), hash),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
import { USER_RUNTIME_MAX_AGE_MS } from './runtime-ttl.mjs'
|
|
6
|
+
|
|
7
|
+
const PROJECT_DIR_NAME = '.helloagents'
|
|
8
|
+
const USER_RUNTIME_DIR_NAME = 'runtime'
|
|
9
|
+
|
|
10
|
+
function getHomeDir(env = process.env) {
|
|
11
|
+
return env.HOME || env.USERPROFILE || homedir()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getUserRuntimeRoot(home = getHomeDir()) {
|
|
15
|
+
return join(home, PROJECT_DIR_NAME, USER_RUNTIME_DIR_NAME)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function removePathIfExists(filePath, result, bucket) {
|
|
19
|
+
if (!existsSync(filePath)) return
|
|
20
|
+
try {
|
|
21
|
+
rmSync(filePath, { recursive: true, force: true })
|
|
22
|
+
result[bucket].push(filePath)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
result.errors.push(`${filePath}: ${error.message}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cleanupUserRuntimeRoot({
|
|
29
|
+
home = getHomeDir(),
|
|
30
|
+
now = Date.now(),
|
|
31
|
+
maxAgeMs = USER_RUNTIME_MAX_AGE_MS,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const root = getUserRuntimeRoot(home)
|
|
34
|
+
const result = {
|
|
35
|
+
root,
|
|
36
|
+
removedExpiredDirs: [],
|
|
37
|
+
errors: [],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!existsSync(root)) return result
|
|
41
|
+
|
|
42
|
+
let entries = []
|
|
43
|
+
try {
|
|
44
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
45
|
+
} catch (error) {
|
|
46
|
+
result.errors.push(`${root}: ${error.message}`)
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory()) continue
|
|
52
|
+
const dirPath = join(root, entry.name)
|
|
53
|
+
try {
|
|
54
|
+
if (now - statSync(dirPath).mtimeMs > maxAgeMs) {
|
|
55
|
+
removePathIfExists(dirPath, result, 'removedExpiredDirs')
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
result.errors.push(`${dirPath}: ${error.message}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getRuntimeScope,
|
|
7
7
|
readJsonFile,
|
|
8
8
|
removeRuntimeFile,
|
|
9
|
+
writeActiveProjectSession,
|
|
9
10
|
writeJsonFileAtomic,
|
|
10
11
|
} from './runtime-scope.mjs'
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ function buildEmptyCapsule(scope) {
|
|
|
18
19
|
key: scope.key,
|
|
19
20
|
cwd: scope.cwd,
|
|
20
21
|
branch: scope.branch,
|
|
22
|
+
workspace: scope.workspace || scope.branch,
|
|
21
23
|
session: scope.session,
|
|
22
24
|
sessionMode: scope.sessionMode,
|
|
23
25
|
updatedAt: new Date().toISOString(),
|
|
@@ -36,6 +38,10 @@ function normalizeOptions(options = {}) {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function getEventSessionAlias(eventPayload = {}) {
|
|
42
|
+
return eventPayload.sessionId || eventPayload.session_id || eventPayload['session-id'] || ''
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
function getScope(cwd, options = {}) {
|
|
40
46
|
const normalizedOptions = normalizeOptions(options)
|
|
41
47
|
if (normalizedOptions.project === true) {
|
|
@@ -66,7 +72,7 @@ export function getSessionArtifactPath(cwd, fileName, options = {}) {
|
|
|
66
72
|
export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
|
|
67
73
|
const scope = getScope(cwd, options)
|
|
68
74
|
if (scope.scope === 'project-session') {
|
|
69
|
-
return `.helloagents/sessions/${scope.branch}/${scope.session}/artifacts/${fileName}`
|
|
75
|
+
return `.helloagents/sessions/${scope.workspace || scope.branch}/${scope.session}/artifacts/${fileName}`
|
|
70
76
|
}
|
|
71
77
|
return `~/.helloagents/runtime/${basename(scope.sessionDir)}/artifacts/${fileName}`
|
|
72
78
|
}
|
|
@@ -82,6 +88,7 @@ export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
|
82
88
|
key: scope.key,
|
|
83
89
|
cwd: scope.cwd,
|
|
84
90
|
branch: scope.branch,
|
|
91
|
+
workspace: scope.workspace || scope.branch,
|
|
85
92
|
session: scope.session,
|
|
86
93
|
sessionMode: scope.sessionMode,
|
|
87
94
|
}
|
|
@@ -96,11 +103,15 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
|
96
103
|
key: scope.key,
|
|
97
104
|
cwd: scope.cwd,
|
|
98
105
|
branch: scope.branch,
|
|
106
|
+
workspace: scope.workspace || scope.branch,
|
|
99
107
|
session: scope.session,
|
|
100
108
|
sessionMode: scope.sessionMode,
|
|
101
109
|
updatedAt: new Date().toISOString(),
|
|
102
110
|
}
|
|
103
111
|
writeJsonFileAtomic(scope.capsulePath, nextCapsule)
|
|
112
|
+
writeActiveProjectSession(scope, {
|
|
113
|
+
env: normalizeOptions(options).env,
|
|
114
|
+
})
|
|
104
115
|
return nextCapsule
|
|
105
116
|
}
|
|
106
117
|
|
|
@@ -138,7 +149,18 @@ export function clearCapsuleSection(cwd, section, options = {}) {
|
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
141
|
-
const
|
|
152
|
+
const normalizedOptions = normalizeOptions(options)
|
|
153
|
+
const sessionAlias = getEventSessionAlias(eventPayload)
|
|
154
|
+
const scopedOptions = sessionAlias
|
|
155
|
+
? {
|
|
156
|
+
...normalizedOptions,
|
|
157
|
+
payload: {
|
|
158
|
+
...(normalizedOptions.payload || {}),
|
|
159
|
+
_helloagentsSessionAlias: sessionAlias,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
: normalizedOptions
|
|
163
|
+
const scope = getScope(cwd, scopedOptions)
|
|
142
164
|
if (scope.scope === 'project-session' && !scope.active) return ''
|
|
143
165
|
const eventName = eventPayload?.event || ''
|
|
144
166
|
if (!eventName) return ''
|
|
@@ -155,6 +177,11 @@ export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
|
155
177
|
encoding: 'utf-8',
|
|
156
178
|
flag: 'a',
|
|
157
179
|
})
|
|
180
|
+
writeActiveProjectSession(scope, {
|
|
181
|
+
host: eventPayload.host || '',
|
|
182
|
+
source: eventPayload.source || eventName,
|
|
183
|
+
env: scopedOptions.env,
|
|
184
|
+
})
|
|
158
185
|
return scope.eventsPath
|
|
159
186
|
}
|
|
160
187
|
|
|
@@ -187,7 +187,7 @@ export function readStateSnapshot(cwd, options = {}) {
|
|
|
187
187
|
stateScope: stateScope.stateScope,
|
|
188
188
|
stateSessionToken: stateScope.stateSessionToken,
|
|
189
189
|
stateSessionMode: stateScope.stateSessionMode,
|
|
190
|
-
|
|
190
|
+
stateWorkspace: stateScope.stateWorkspace,
|
|
191
191
|
sessionScoped: stateScope.stateScope === 'session',
|
|
192
192
|
exists,
|
|
193
193
|
content,
|
|
@@ -313,7 +313,7 @@ function buildBuildOrchestrationHint(plan) {
|
|
|
313
313
|
if (openTasks.every((item) => item.files.length === 0)) {
|
|
314
314
|
return '编排提示:当前有多个开放任务,但 tasks.md 尚未写清契约元数据;考虑子代理并行前先补足文件路径、完成标准与验证方式。'
|
|
315
315
|
}
|
|
316
|
-
return '
|
|
316
|
+
return '编排提示:当前仍有多个开放任务,但文件范围存在重叠;暂不并行子代理,优先串行推进。'
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
export function buildOrchestrationHintFromSnapshot(snapshot, cwd, recommendation = buildRecommendation(snapshot, cwd)) {
|
|
@@ -7,14 +7,14 @@ policy:
|
|
|
7
7
|
Trigger: ~auto <任务描述>
|
|
8
8
|
|
|
9
9
|
`~auto` 是自动执行命令。它根据任务类型、复杂度、风险等级与项目状态,在 `~idea`、`~plan`、`~build`、`~verify`、`~prd` 之间选择合适主路径,并连续推进。
|
|
10
|
-
`~auto` 不止做一次选路;主路径一旦确定,就按需要继续执行后续阶段,默认持续推进直到完成交付,只有命中
|
|
10
|
+
`~auto` 不止做一次选路;主路径一旦确定,就按需要继续执行后续阶段,默认持续推进直到完成交付,只有命中 HelloAGENTS 阻塞判定时才停下。
|
|
11
11
|
|
|
12
12
|
## 铁律
|
|
13
13
|
- 不为了“自动化”而强行走重流程
|
|
14
14
|
- 复杂度与风险不足以支撑更重路径时,优先选更轻但能保证质量的路线
|
|
15
15
|
- `T3` 高风险或不可逆操作默认不直接进入 `~build`;优先先走 `~plan` 或 `~prd`,纯审查/纯验证请求才可先进入 `~verify`
|
|
16
16
|
- 主路径一旦确定,立即读取对应 command skill,并在阶段完成后继续执行后续阶段,避免同一任务重复探索或重复等待
|
|
17
|
-
- 选路不替代授权;涉及外部副作用或高风险不可逆操作时,仍遵守
|
|
17
|
+
- 选路不替代授权;涉及外部副作用或高风险不可逆操作时,仍遵守 HelloAGENTS 阻塞判定与确认规则
|
|
18
18
|
- 用户显式使用 `~auto`,表示已授权在当前任务边界内沿选定主路径持续执行;若本轮运行在 Codex `/goal` 下,`/goal` 只提供长程续跑与预算,`~auto` 仍按方案包、`state_path` 与验证契约推进;`~plan` / `~prd` 作为中间阶段时,不再额外询问“是否开始执行”,除非仍有真实阻塞;不得把 `🔄 下一步` 当作阶段交接或继续执行占位
|
|
19
19
|
- 优先消费当前上下文中已注入的 ROUTE / TIER、当前工作流约束与项目状态;不要在 `~auto` 内另建一套关键词路由表
|
|
20
20
|
|
|
@@ -69,6 +69,6 @@ Trigger: ~auto <任务描述>
|
|
|
69
69
|
|
|
70
70
|
### 5. 何时允许停下
|
|
71
71
|
|
|
72
|
-
- 仅在
|
|
72
|
+
- 仅在 HelloAGENTS 阻塞判定成立时停下:真实歧义、缺必需信息/文件/凭据、未授权外部副作用、高风险或不可逆操作
|
|
73
73
|
- 不得把 `~plan` / `~prd` 中“是否进入执行”的默认询问原样带入 `~auto` 流程
|
|
74
74
|
- 不得把“给出方案”“给出任务列表”“未执行修改”“等待下一步确认”“给出建议下一步”当作 `~auto` 的默认完成态;用户显式要求 `~auto` 时,默认目标是把当前任务推进到可交付完成
|
|
@@ -7,7 +7,7 @@ policy:
|
|
|
7
7
|
Trigger: ~build [description]
|
|
8
8
|
|
|
9
9
|
`~build` 是执行实现命令。它负责读取现有需求、方案包与项目上下文,完成实现、局部验证、修复循环,并把结果交给后续验证与收尾。
|
|
10
|
-
执行 `~build`
|
|
10
|
+
执行 `~build` 时,通用阶段边界按当前已加载的 HelloAGENTS 规则执行;本 skill 负责补充实现前定位、实现约束,以及进入 `~verify` / 收尾前的实现边界。
|
|
11
11
|
`.helloagents/` 在本 skill 中统一按项目级存储路径理解:状态文件只使用 `state_path`;会话证据使用当前 `state_path` 所在目录下的 `artifacts/*.json`;若 `project_store_mode=repo-shared`,知识库、`DESIGN.md`、`verify.yaml` 与方案包按当前上下文中已注入的项目知识/方案目录解析。
|
|
12
12
|
|
|
13
13
|
## 铁律
|
|
@@ -20,7 +20,7 @@ Trigger: ~build [description]
|
|
|
20
20
|
|
|
21
21
|
### 1. 恢复与定位
|
|
22
22
|
|
|
23
|
-
-
|
|
23
|
+
- 优先按当前已加载的 HelloAGENTS 规则恢复当前任务,并遵循“.helloagents/ 文件读取优先级”;若当前消息明确要继续上次任务、会话刚经历恢复 / 压缩,或本轮运行在 Codex active goal 下,先读取 `state_path`,再用当前用户消息、活跃方案包 / PRD 与代码事实确认当前任务
|
|
24
24
|
- 若存在最近的活跃方案包,读取对应的:
|
|
25
25
|
- `requirements.md`
|
|
26
26
|
- `plan.md`
|
|
@@ -29,13 +29,13 @@ Trigger: ~build [description]
|
|
|
29
29
|
- 实现时优先把 `tasks.md` 中每个任务的“完成标准”当作本轮实现约束,不要只按任务标题猜测范围
|
|
30
30
|
- `contract.json` 存在时,优先按其中的 `verifyMode`、`reviewerFocus`、`testerFocus` 理解后续验证边界
|
|
31
31
|
- 若本轮运行在 Codex active goal 下,按 `tasks.md` 未完成项、`contract.json` 与 `state_path` 恢复实现位置;不要自动创建新 goal,也不要把 goal 目标原文替代方案包
|
|
32
|
-
-
|
|
33
|
-
- 其余项目知识库与相关代码文件,按
|
|
32
|
+
- 若当前上下文中已注入“当前工作流约束”或“当前推荐下一命令”,先服从它;只有推荐仍为 `~build`,或用户明确提出新增实现范围时,才继续 `~build`
|
|
33
|
+
- 其余项目知识库与相关代码文件,按 HelloAGENTS 项目上下文要求读取
|
|
34
34
|
- 若任务涉及 UI,按以下优先级读取并遵循:当前活跃 `plan.md` / PRD 中的 UI 决策 > 逻辑 `.helloagents/DESIGN.md`(实际路径按当前项目存储模式解析) > `hello-ui` 通用规则
|
|
35
35
|
- 若已激活项目且当前任务属于整页新建、设计系统改造、或跨多个组件的视觉重做,但逻辑 `.helloagents/DESIGN.md` 不存在,先按模板创建最小设计契约,再继续大规模实现
|
|
36
36
|
|
|
37
37
|
如果 `.helloagents/` 不存在:
|
|
38
|
-
-
|
|
38
|
+
- 按当前已加载的 HelloAGENTS 规则创建 `.helloagents/` 与最小流程状态
|
|
39
39
|
- 仅补足执行当前任务所需的最小状态,不自动展开完整知识库
|
|
40
40
|
|
|
41
41
|
### 2. 需求与范围确认
|
|
@@ -65,5 +65,5 @@ Trigger: ~build [description]
|
|
|
65
65
|
- 有方案包时,只同步本次实现直接影响的任务状态;未完成项保持打开
|
|
66
66
|
- 当前实现已闭合、且需要进入交付或收尾时,转入 `~verify`
|
|
67
67
|
- 若 Codex active goal 仍有未完成 AFK 任务,继续下一项可执行任务;若目标已满足,先转入 `~verify` 与 HelloAGENTS 收尾,再标记 goal complete
|
|
68
|
-
- 状态文件、知识库、`CHANGELOG.md`、modules
|
|
68
|
+
- 状态文件、知识库、`CHANGELOG.md`、modules 文档与归档边界,按当前已加载的 HelloAGENTS 规则进入 VERIFY / CONSOLIDATE
|
|
69
69
|
- 不在 `~build` 内把仍未闭合的方案包整体报告为已完成
|
|
@@ -6,17 +6,17 @@ policy:
|
|
|
6
6
|
---
|
|
7
7
|
Trigger: ~clean
|
|
8
8
|
|
|
9
|
-
执行 `~clean`
|
|
9
|
+
执行 `~clean` 时,方案包归档、临时文件清理和状态文件更新范围按当前已加载的 HelloAGENTS 规则执行;本命令只负责判定哪些方案包可以清理,以及输出清理摘要。
|
|
10
10
|
`.helloagents/` 在本 skill 中统一按项目级存储路径理解:状态文件只使用 `state_path`,临时运行态文件保持项目本地;若 `project_store_mode=repo-shared`,`plans/` 与 `archive/` 按当前上下文中已注入的项目知识/方案目录解析。
|
|
11
11
|
|
|
12
12
|
## 流程
|
|
13
13
|
|
|
14
14
|
1. 扫描 `.helloagents/plans/` 下的方案包(按当前项目存储模式解析;`project_store_mode=repo-shared` 时使用共享知识/方案目录)
|
|
15
15
|
2. 判定完成状态:优先以 tasks.md 中所有任务已标记 [√] 为准;只有任务清单无法判断时,才读取 `state_path` 中与当前方案一致的“主线目标”+“正在做什么”作为辅助信息
|
|
16
|
-
3. 已完成的方案包 → 按
|
|
17
|
-
4. 清理
|
|
18
|
-
5. 按
|
|
19
|
-
|
|
16
|
+
3. 已完成的方案包 → 按 HelloAGENTS 归档规则移入 `.helloagents/archive/YYYY-MM/`(按当前项目存储模式解析),并同步更新 `.helloagents/archive/_index.md`
|
|
17
|
+
4. 清理 HelloAGENTS 临时文件
|
|
18
|
+
5. 按 HelloAGENTS 流程状态规则更新 `state_path`;若当前状态指向已归档方案包,则清空对应方案路径
|
|
19
|
+
6. 输出清理摘要(归档了几个方案包、清理了哪些文件)
|
|
20
20
|
|
|
21
21
|
## 不删除
|
|
22
22
|
- 除按流程状态规则必须重写的 `state_path` 外,不删除流程状态文件
|
|
@@ -6,7 +6,7 @@ policy:
|
|
|
6
6
|
---
|
|
7
7
|
Trigger: ~commit [message]
|
|
8
8
|
|
|
9
|
-
执行 `~commit`
|
|
9
|
+
执行 `~commit` 时,知识库同步与状态文件更新范围按当前已加载的 HelloAGENTS CONSOLIDATE / 流程状态要求执行;本命令只负责生成提交信息、读取提交归属配置并完成提交动作。
|
|
10
10
|
|
|
11
11
|
## 流程
|
|
12
12
|
|
|
@@ -20,11 +20,11 @@ Trigger: ~commit [message]
|
|
|
20
20
|
- ""(空,默认)→ 不添加归属
|
|
21
21
|
- 有内容(如 "Co-Authored-By: HelloAGENTS")→ 添加该内容到 commit message
|
|
22
22
|
6. 执行 git commit
|
|
23
|
-
7. 若 `state_path` 已存在,按
|
|
23
|
+
7. 若 `state_path` 已存在,按 HelloAGENTS“已有则更新”要求同步当前已提交状态
|
|
24
24
|
|
|
25
25
|
## 知识库同步
|
|
26
26
|
提交后,继续复用上方已解析的同一份设置获取 `kb_create_mode`,不要再次读取 `~/.helloagents/helloagents.json`:
|
|
27
27
|
- 0 = 跳过
|
|
28
28
|
- 1 = 编码任务自动同步(默认)
|
|
29
29
|
- 2 = 始终同步
|
|
30
|
-
|
|
30
|
+
同步范围与更新格式按当前已加载的 HelloAGENTS CONSOLIDATE 阶段执行。
|
|
@@ -7,7 +7,7 @@ policy:
|
|
|
7
7
|
Trigger: ~init
|
|
8
8
|
|
|
9
9
|
~init 是用户显式命令,创建完整知识库,不受 kb_create_mode 限制。
|
|
10
|
-
执行 `~init` 时,`.helloagents/`
|
|
10
|
+
执行 `~init` 时,`.helloagents/` 目录结构、模板格式和状态文件规则按当前已加载的 HelloAGENTS 规则执行;本命令额外负责项目级规则文件和各宿主项目级 HelloAGENTS 包根链接。
|
|
11
11
|
`.helloagents/` 在本 skill 中统一按项目级存储路径理解:项目本地 `.helloagents/` 继续承担激活目录;状态文件只使用 `state_path`;若 `project_store_mode=repo-shared`,知识库、`DESIGN.md` 与方案包按当前上下文中已注入的项目知识/方案目录写入。
|
|
12
12
|
|
|
13
13
|
## 流程
|
|
@@ -15,13 +15,13 @@ Trigger: ~init
|
|
|
15
15
|
### 阶段 1:环境搭建(必做)
|
|
16
16
|
|
|
17
17
|
1. 创建 `.helloagents/` 目录 + `state_path`(按 templates/STATE.md 格式,初始“主线目标”写当前初始化任务,初始状态为空闲)
|
|
18
|
-
2. 定位插件根目录:优先读取当前上下文中已注入的“当前 HelloAGENTS
|
|
18
|
+
2. 定位插件根目录:优先读取当前上下文中已注入的“当前 HelloAGENTS 包根目录”;若上下文未提供,再根据当前已加载的 HelloAGENTS 规则来源反推,禁止猜测其他目录
|
|
19
19
|
3. 刷新各宿主项目级 HelloAGENTS 包根链接(删除旧的重建):
|
|
20
20
|
- `.claude/skills/helloagents` symlink → `{插件根目录}/`
|
|
21
21
|
- `.gemini/skills/helloagents` symlink → `{插件根目录}/`
|
|
22
22
|
- `.codex/skills/helloagents` symlink → `{插件根目录}/`
|
|
23
23
|
这些链接用于项目级规则定位 HelloAGENTS 的 `skills/`、`templates/` 和 `scripts/`;宿主若支持递归发现 `SKILL.md`,也可直接识别包内 skills。
|
|
24
|
-
4. 读取 `{插件根目录}
|
|
24
|
+
4. 读取 `{插件根目录}` 中的全量规则模板,用 `<!-- HELLOAGENTS_START -->` / `<!-- HELLOAGENTS_END -->` 标记包裹后写入:
|
|
25
25
|
- `AGENTS.md`(项目根目录,Codex 读取)
|
|
26
26
|
- `CLAUDE.md`(项目根目录,Claude Code 读取)
|
|
27
27
|
- `.gemini/GEMINI.md`(Gemini CLI 读取,需先创建 .gemini/ 目录)
|