helloagents 3.0.19 → 3.0.20-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 +24 -20
- package/README_CN.md +22 -18
- package/bootstrap-lite.md +3 -2
- package/bootstrap.md +3 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/cli-doctor-codex.mjs +4 -4
- package/scripts/cli-doctor.mjs +2 -2
- package/scripts/cli-messages.mjs +2 -2
- 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/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/skills/commands/auto/SKILL.md +3 -3
- package/skills/commands/build/SKILL.md +5 -5
- 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 +1 -1
- 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 +5 -5
|
@@ -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,
|
|
@@ -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`
|
|
@@ -30,12 +30,12 @@ Trigger: ~build [description]
|
|
|
30
30
|
- `contract.json` 存在时,优先按其中的 `verifyMode`、`reviewerFocus`、`testerFocus` 理解后续验证边界
|
|
31
31
|
- 若本轮运行在 Codex active goal 下,按 `tasks.md` 未完成项、`contract.json` 与 `state_path` 恢复实现位置;不要自动创建新 goal,也不要把 goal 目标原文替代方案包
|
|
32
32
|
- 若当前上下文中已注入“当前工作流约束”或“当前建议下一命令”,先服从它;只有推荐仍为 `~build`,或用户明确提出新增实现范围时,才继续 `~build`
|
|
33
|
-
- 其余项目知识库与相关代码文件,按
|
|
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/ 目录)
|
|
@@ -22,7 +22,7 @@ Trigger: ~loop <目标描述> [--iterations N] [--metric "命令"] [--direction
|
|
|
22
22
|
2. 确保 `.helloagents/` 目录和 `state_path` 存在;文件不存在时按 `templates/STATE.md` 创建。`~loop` 必须维护这个状态文件,不受 `kb_create_mode` 控制;“主线目标”固定写本次优化目标,避免混入其他任务
|
|
23
23
|
3. 运行指标命令获取基线值,记录到 results log
|
|
24
24
|
4. 如有守卫命令,运行确认基线通过
|
|
25
|
-
5. 创建当前会话的 `.helloagents/sessions/{
|
|
25
|
+
5. 创建当前会话的 `.helloagents/sessions/{workspace}/{session}/artifacts/loop-results.tsv`
|
|
26
26
|
6. 根据优化目标标记可能需要的 hello-* 质量技能(如性能优化标记 hello-perf,UI 优化标记 hello-ui)
|
|
27
27
|
7. 重写 `state_path`:记录主线目标=当前优化目标、基线指标、守卫命令、下一步设为第一轮迭代的具体动作
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ iteration commit metric delta guard status description
|
|
|
36
36
|
## 八阶段循环
|
|
37
37
|
|
|
38
38
|
`~loop` 的八阶段循环是统一执行流程(ROUTE/TIER→SPEC→PLAN→BUILD→VERIFY→CONSOLIDATE)在迭代优化场景下的特化形式。每轮迭代的“修改”阶段遵循已标记的 hello-* 质量技能规范,“验证”阶段遵循 hello-verify 的验证规范。
|
|
39
|
-
执行 `~loop`
|
|
39
|
+
执行 `~loop` 时,涉及公共阶段边界、阻塞判定与收尾要求的部分,仍按当前已加载的 HelloAGENTS 规则执行;本 skill 负责补充 loop 场景的迭代顺序与回滚规则。
|
|
40
40
|
若本轮运行在 Codex `/goal` 下,`/goal` 只作为外层长程续跑与预算控制;`~loop` 仍负责指标、守卫、实验提交、keep/revert、results log、`state_path` 与收尾验证,不把 `/goal` 当成循环逻辑本身。
|
|
41
41
|
|
|
42
42
|
除非达到迭代上限或命中阻塞判定,否则继续执行,不额外询问是否继续,也不把 `🔄 下一步` 当作单轮结果或继续执行占位。
|