helloagents 3.0.12 → 3.0.15-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/marketplace.json +6 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +169 -30
- package/README_CN.md +169 -30
- package/bootstrap-lite.md +27 -20
- package/bootstrap.md +30 -23
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +125 -0
- package/install.sh +118 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +3 -3
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +11 -20
- package/scripts/cli-codex.mjs +32 -38
- package/scripts/cli-doctor-render.mjs +4 -0
- package/scripts/cli-doctor.mjs +40 -30
- package/scripts/cli-host-detect.mjs +0 -1
- package/scripts/cli-hosts.mjs +16 -8
- package/scripts/cli-lifecycle-hosts.mjs +92 -27
- package/scripts/cli-lifecycle.mjs +9 -7
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +36 -0
- package/scripts/cli-runtime-root.mjs +72 -0
- package/scripts/cli-toml.mjs +0 -79
- package/scripts/cli-utils.mjs +30 -4
- package/scripts/closeout-state.mjs +35 -62
- package/scripts/delivery-gate-messages.mjs +70 -0
- package/scripts/delivery-gate.mjs +9 -75
- package/scripts/guard-rules.mjs +42 -42
- package/scripts/guard.mjs +44 -24
- package/scripts/notify-context.mjs +19 -28
- package/scripts/notify-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +46 -33
- package/scripts/notify.mjs +60 -32
- package/scripts/project-storage.mjs +35 -66
- package/scripts/ralph-loop.mjs +36 -31
- package/scripts/replay-state.mjs +31 -128
- package/scripts/review-state.mjs +34 -61
- package/scripts/runtime-artifacts.mjs +95 -0
- package/scripts/runtime-context.mjs +35 -29
- package/scripts/runtime-scope.mjs +313 -0
- package/scripts/session-capsule.mjs +202 -0
- package/scripts/turn-state-cli.mjs +17 -0
- package/scripts/turn-state.mjs +185 -66
- package/scripts/turn-stop-gate.mjs +24 -6
- package/scripts/verify-state.mjs +34 -85
- package/scripts/visual-state.mjs +38 -65
- package/scripts/workflow-core.mjs +2 -2
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +17 -13
- package/scripts/workflow-state.mjs +5 -5
- package/skills/commands/build/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +3 -3
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +8 -6
- package/skills/commands/prd/SKILL.md +5 -3
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/hello-debug/SKILL.md +20 -3
- package/skills/hello-review/SKILL.md +2 -2
- package/skills/hello-subagent/SKILL.md +2 -2
- package/skills/hello-test/SKILL.md +6 -2
- package/skills/hello-ui/SKILL.md +4 -4
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +12 -7
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
package/scripts/replay-state.mjs
CHANGED
|
@@ -1,76 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join, normalize, resolve } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
1
|
+
import { dirname } from 'node:path'
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return filePath ? normalize(resolve(filePath)) : ''
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function ensureRuntimeDir() {
|
|
15
|
-
mkdirSync(dirname(REPLAY_CONTEXT_PATH), { recursive: true })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function readReplayContext() {
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(readFileSync(REPLAY_CONTEXT_PATH, 'utf-8'))
|
|
21
|
-
} catch {
|
|
22
|
-
return {}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function writeReplayContext(context) {
|
|
27
|
-
ensureRuntimeDir()
|
|
28
|
-
writeFileSync(REPLAY_CONTEXT_PATH, `${JSON.stringify(context, null, 2)}\n`, 'utf-8')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getReplayKey(cwd, host = '') {
|
|
32
|
-
return `${normalizePath(cwd)}::${host || 'unknown'}`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function findLatestReplaySession(context, cwd) {
|
|
36
|
-
const normalizedCwd = normalizePath(cwd)
|
|
37
|
-
const entries = Object.values(context)
|
|
38
|
-
.filter((entry) => entry?.cwd === normalizedCwd && entry.filePath && existsSync(entry.filePath))
|
|
39
|
-
.sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0))
|
|
40
|
-
return entries[0] || null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getProjectRoot(cwd) {
|
|
44
|
-
const projectRoot = join(cwd, '.helloagents')
|
|
45
|
-
return existsSync(projectRoot) ? projectRoot : ''
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function getReplayDir(cwd) {
|
|
49
|
-
const projectRoot = getProjectRoot(cwd)
|
|
50
|
-
return projectRoot ? join(projectRoot, 'replay') : ''
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function ensureReplayDir(cwd) {
|
|
54
|
-
const replayDir = getReplayDir(cwd)
|
|
55
|
-
if (!replayDir) return ''
|
|
56
|
-
mkdirSync(replayDir, { recursive: true })
|
|
57
|
-
return replayDir
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function listReplaySessionFiles(replayDir) {
|
|
61
|
-
if (!replayDir || !existsSync(replayDir)) return []
|
|
62
|
-
return readdirSync(replayDir)
|
|
63
|
-
.filter((name) => name.endsWith('.jsonl'))
|
|
64
|
-
.sort()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function trimReplaySessions(replayDir) {
|
|
68
|
-
const files = listReplaySessionFiles(replayDir)
|
|
69
|
-
const staleFiles = files.slice(0, Math.max(0, files.length - MAX_REPLAY_SESSIONS))
|
|
70
|
-
for (const fileName of staleFiles) {
|
|
71
|
-
rmSync(join(replayDir, fileName), { force: true })
|
|
72
|
-
}
|
|
73
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
appendSessionEvent,
|
|
5
|
+
getSessionEventsPath,
|
|
6
|
+
resetSessionEvents,
|
|
7
|
+
} from './session-capsule.mjs'
|
|
8
|
+
import { getProjectSessionScope } from './runtime-scope.mjs'
|
|
74
9
|
|
|
75
10
|
function sanitizeReplayValue(value) {
|
|
76
11
|
if (typeof value === 'string') {
|
|
@@ -102,43 +37,6 @@ function sanitizeReplayValue(value) {
|
|
|
102
37
|
return value
|
|
103
38
|
}
|
|
104
39
|
|
|
105
|
-
function getReplaySession(cwd, { host = '', create = false, reset = false } = {}) {
|
|
106
|
-
const replayDir = ensureReplayDir(cwd)
|
|
107
|
-
if (!replayDir) return null
|
|
108
|
-
|
|
109
|
-
const key = getReplayKey(cwd, host)
|
|
110
|
-
const context = readReplayContext()
|
|
111
|
-
const current = context[key] || (!host ? findLatestReplaySession(context, cwd) : null)
|
|
112
|
-
const isExpired = !current?.updatedAt || (Date.now() - current.updatedAt > REPLAY_SESSION_TTL_MS)
|
|
113
|
-
const isMissing = !current?.filePath || !existsSync(current.filePath)
|
|
114
|
-
|
|
115
|
-
if (!reset && !isExpired && !isMissing) {
|
|
116
|
-
context[key] = {
|
|
117
|
-
...current,
|
|
118
|
-
updatedAt: Date.now(),
|
|
119
|
-
}
|
|
120
|
-
writeReplayContext(context)
|
|
121
|
-
return context[key]
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!create) return null
|
|
125
|
-
|
|
126
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
127
|
-
const suffix = Math.random().toString(36).slice(2, 8)
|
|
128
|
-
const sessionId = `${stamp}-${host || 'unknown'}-${suffix}`
|
|
129
|
-
const filePath = join(replayDir, `${sessionId}.jsonl`)
|
|
130
|
-
const next = {
|
|
131
|
-
cwd: normalizePath(cwd),
|
|
132
|
-
host: host || 'unknown',
|
|
133
|
-
sessionId,
|
|
134
|
-
filePath,
|
|
135
|
-
updatedAt: Date.now(),
|
|
136
|
-
}
|
|
137
|
-
context[key] = next
|
|
138
|
-
writeReplayContext(context)
|
|
139
|
-
return next
|
|
140
|
-
}
|
|
141
|
-
|
|
142
40
|
function buildReplayRecommendation(recommendation) {
|
|
143
41
|
if (!recommendation) return {}
|
|
144
42
|
return {
|
|
@@ -151,24 +49,35 @@ function buildReplayRecommendation(recommendation) {
|
|
|
151
49
|
}
|
|
152
50
|
}
|
|
153
51
|
|
|
52
|
+
export function getReplayDir(cwd, options = {}) {
|
|
53
|
+
const eventPath = getSessionEventsPath(cwd, options)
|
|
54
|
+
return eventPath ? dirname(eventPath) : ''
|
|
55
|
+
}
|
|
56
|
+
|
|
154
57
|
export function startReplaySession(cwd, {
|
|
155
58
|
host = '',
|
|
156
59
|
source = 'startup',
|
|
157
60
|
bootstrapFile = '',
|
|
158
61
|
installMode = '',
|
|
62
|
+
payload = {},
|
|
63
|
+
env,
|
|
64
|
+
ppid,
|
|
159
65
|
} = {}) {
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
66
|
+
const scope = getProjectSessionScope(cwd, { payload, env, ppid })
|
|
67
|
+
if (!scope.active) return ''
|
|
162
68
|
|
|
69
|
+
const filePath = resetSessionEvents(cwd, { payload, env, ppid })
|
|
163
70
|
appendReplayEvent(cwd, {
|
|
164
71
|
host,
|
|
165
72
|
event: 'session_started',
|
|
166
73
|
source,
|
|
167
74
|
bootstrapFile,
|
|
168
75
|
installMode,
|
|
169
|
-
|
|
76
|
+
payload,
|
|
77
|
+
env,
|
|
78
|
+
ppid,
|
|
170
79
|
})
|
|
171
|
-
return
|
|
80
|
+
return filePath
|
|
172
81
|
}
|
|
173
82
|
|
|
174
83
|
export function appendReplayEvent(cwd, {
|
|
@@ -182,29 +91,23 @@ export function appendReplayEvent(cwd, {
|
|
|
182
91
|
artifacts = [],
|
|
183
92
|
details = {},
|
|
184
93
|
sessionId = '',
|
|
94
|
+
payload = {},
|
|
95
|
+
env,
|
|
96
|
+
ppid,
|
|
185
97
|
} = {}) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!session?.filePath) return ''
|
|
98
|
+
const scope = getProjectSessionScope(cwd, { payload, env, ppid })
|
|
99
|
+
if (!scope.active || !event) return ''
|
|
189
100
|
|
|
190
|
-
|
|
191
|
-
ts: new Date().toISOString(),
|
|
101
|
+
return appendSessionEvent(cwd, sanitizeReplayValue({
|
|
192
102
|
event,
|
|
193
|
-
host: host ||
|
|
103
|
+
host: host || 'unknown',
|
|
194
104
|
source,
|
|
195
|
-
sessionId: sessionId || session
|
|
105
|
+
sessionId: sessionId || scope.session,
|
|
196
106
|
skillName,
|
|
197
107
|
sourceSkillName,
|
|
198
108
|
recommendation: buildReplayRecommendation(recommendation),
|
|
199
109
|
reason,
|
|
200
110
|
artifacts,
|
|
201
111
|
details,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
writeFileSync(session.filePath, `${JSON.stringify(payload)}\n`, {
|
|
205
|
-
encoding: 'utf-8',
|
|
206
|
-
flag: 'a',
|
|
207
|
-
})
|
|
208
|
-
trimReplaySessions(getReplayDir(cwd))
|
|
209
|
-
return session.filePath
|
|
112
|
+
}), { payload, env, ppid })
|
|
210
113
|
}
|
package/scripts/review-state.mjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { captureWorkspaceFingerprint } from './verify-state.mjs'
|
|
5
3
|
import { appendReplayEvent } from './replay-state.mjs'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
captureWorkspaceFingerprint,
|
|
6
|
+
clearRuntimeEvidence,
|
|
7
|
+
getRuntimeEvidencePath,
|
|
8
|
+
getRuntimeEvidenceRelativePath,
|
|
9
|
+
readRuntimeEvidence,
|
|
10
|
+
validateEvidenceFingerprint,
|
|
11
|
+
validateEvidenceTimestamp,
|
|
12
|
+
writeRuntimeEvidence,
|
|
13
|
+
} from './runtime-artifacts.mjs'
|
|
14
|
+
|
|
15
|
+
export const REVIEW_EVIDENCE_FILE_NAME = 'review.json'
|
|
9
16
|
const VALID_REVIEW_OUTCOMES = new Set(['clean', 'findings'])
|
|
10
17
|
|
|
11
18
|
function normalizeStringArray(values) {
|
|
@@ -20,20 +27,16 @@ function normalizeReviewOutcome(value) {
|
|
|
20
27
|
return VALID_REVIEW_OUTCOMES.has(normalized) ? normalized : ''
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
export function getReviewEvidencePath(cwd) {
|
|
24
|
-
return
|
|
30
|
+
export function getReviewEvidencePath(cwd, options = {}) {
|
|
31
|
+
return getRuntimeEvidencePath(cwd, REVIEW_EVIDENCE_FILE_NAME, options)
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
export function readReviewEvidence(cwd) {
|
|
28
|
-
|
|
29
|
-
return JSON.parse(readFileSync(getReviewEvidencePath(cwd), 'utf-8'))
|
|
30
|
-
} catch {
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
34
|
+
export function readReviewEvidence(cwd, options = {}) {
|
|
35
|
+
return readRuntimeEvidence(cwd, REVIEW_EVIDENCE_FILE_NAME, options)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
export function clearReviewEvidence(cwd) {
|
|
36
|
-
|
|
38
|
+
export function clearReviewEvidence(cwd, options = {}) {
|
|
39
|
+
clearRuntimeEvidence(cwd, REVIEW_EVIDENCE_FILE_NAME, options)
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export function normalizeReviewEvidence(input = {}) {
|
|
@@ -56,8 +59,7 @@ export function writeReviewEvidence(cwd, {
|
|
|
56
59
|
conclusion = '',
|
|
57
60
|
findings = [],
|
|
58
61
|
fileReferences = [],
|
|
59
|
-
} = {}) {
|
|
60
|
-
mkdirSync(join(cwd, '.helloagents'), { recursive: true })
|
|
62
|
+
} = {}, options = {}) {
|
|
61
63
|
const normalized = normalizeReviewEvidence({
|
|
62
64
|
source,
|
|
63
65
|
originCommand,
|
|
@@ -78,11 +80,12 @@ export function writeReviewEvidence(cwd, {
|
|
|
78
80
|
fileReferences: normalized.fileReferences,
|
|
79
81
|
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
80
82
|
}
|
|
81
|
-
|
|
83
|
+
writeRuntimeEvidence(cwd, REVIEW_EVIDENCE_FILE_NAME, payload, options)
|
|
82
84
|
appendReplayEvent(cwd, {
|
|
83
85
|
event: 'review_evidence_written',
|
|
84
86
|
source: normalized.source,
|
|
85
87
|
skillName: normalized.originCommand,
|
|
88
|
+
payload: options.payload || {},
|
|
86
89
|
details: {
|
|
87
90
|
reviewMode: normalized.reviewMode,
|
|
88
91
|
outcome: normalized.outcome,
|
|
@@ -90,59 +93,29 @@ export function writeReviewEvidence(cwd, {
|
|
|
90
93
|
findings: normalized.findings,
|
|
91
94
|
fileReferences: normalized.fileReferences,
|
|
92
95
|
},
|
|
93
|
-
artifacts: [
|
|
96
|
+
artifacts: [getRuntimeEvidenceRelativePath(cwd, REVIEW_EVIDENCE_FILE_NAME, options)],
|
|
94
97
|
})
|
|
95
98
|
return payload
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
function readRequiredReviewEvidence(cwd) {
|
|
99
|
-
const evidence = readReviewEvidence(cwd)
|
|
101
|
+
function readRequiredReviewEvidence(cwd, options = {}) {
|
|
102
|
+
const evidence = readReviewEvidence(cwd, options)
|
|
100
103
|
if (evidence) return { evidence }
|
|
101
104
|
return {
|
|
102
105
|
error: {
|
|
103
106
|
required: true,
|
|
104
107
|
status: 'missing',
|
|
105
|
-
details: ['
|
|
108
|
+
details: ['缺少 review-first 收尾所需的成功审查证据'],
|
|
106
109
|
},
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
function validateReviewTimestamp(evidence, now) {
|
|
111
|
-
|
|
112
|
-
if (!Number.isFinite(updatedAt)) {
|
|
113
|
-
return {
|
|
114
|
-
required: true,
|
|
115
|
-
status: 'invalid',
|
|
116
|
-
evidence,
|
|
117
|
-
details: ['review evidence timestamp is invalid'],
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (now - updatedAt > REVIEW_EVIDENCE_MAX_AGE_MS) {
|
|
121
|
-
return {
|
|
122
|
-
required: true,
|
|
123
|
-
status: 'stale-time',
|
|
124
|
-
evidence,
|
|
125
|
-
details: ['review evidence is older than 30 minutes'],
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return null
|
|
114
|
+
return validateEvidenceTimestamp(evidence, now, '审查证据')
|
|
129
115
|
}
|
|
130
116
|
|
|
131
117
|
function validateReviewFingerprint(cwd, evidence) {
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
currentFingerprint.available
|
|
135
|
-
&& evidence.fingerprint?.available
|
|
136
|
-
&& currentFingerprint.combined !== evidence.fingerprint.combined
|
|
137
|
-
) {
|
|
138
|
-
return {
|
|
139
|
-
required: true,
|
|
140
|
-
status: 'stale-diff',
|
|
141
|
-
evidence,
|
|
142
|
-
details: ['workspace diff changed after the last successful review evidence'],
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return null
|
|
118
|
+
return validateEvidenceFingerprint(cwd, evidence, '成功审查证据')
|
|
146
119
|
}
|
|
147
120
|
|
|
148
121
|
function validateReviewOutcome(evidence) {
|
|
@@ -151,7 +124,7 @@ function validateReviewOutcome(evidence) {
|
|
|
151
124
|
required: true,
|
|
152
125
|
status: 'invalid',
|
|
153
126
|
evidence,
|
|
154
|
-
details: ['
|
|
127
|
+
details: ['审查证据必须记录明确的 outcome 和 conclusion'],
|
|
155
128
|
}
|
|
156
129
|
}
|
|
157
130
|
if (normalizeReviewOutcome(evidence.outcome) !== 'clean') {
|
|
@@ -159,13 +132,13 @@ function validateReviewOutcome(evidence) {
|
|
|
159
132
|
required: true,
|
|
160
133
|
status: 'blocked',
|
|
161
134
|
evidence,
|
|
162
|
-
details: ['
|
|
135
|
+
details: ['最新审查证据仍记录阻塞问题'],
|
|
163
136
|
}
|
|
164
137
|
}
|
|
165
138
|
return null
|
|
166
139
|
}
|
|
167
140
|
|
|
168
|
-
export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
|
|
141
|
+
export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now(), ...options } = {}) {
|
|
169
142
|
if (!required) {
|
|
170
143
|
return {
|
|
171
144
|
required: false,
|
|
@@ -173,7 +146,7 @@ export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now(
|
|
|
173
146
|
}
|
|
174
147
|
}
|
|
175
148
|
|
|
176
|
-
const requiredEvidence = readRequiredReviewEvidence(cwd)
|
|
149
|
+
const requiredEvidence = readRequiredReviewEvidence(cwd, options)
|
|
177
150
|
if (requiredEvidence.error) return requiredEvidence.error
|
|
178
151
|
|
|
179
152
|
const { evidence } = requiredEvidence
|
|
@@ -207,10 +180,10 @@ function main() {
|
|
|
207
180
|
|
|
208
181
|
const input = readStdinJson()
|
|
209
182
|
const cwd = input.cwd || process.cwd()
|
|
210
|
-
const payload = writeReviewEvidence(cwd, input)
|
|
183
|
+
const payload = writeReviewEvidence(cwd, input, { payload: input })
|
|
211
184
|
process.stdout.write(JSON.stringify({
|
|
212
185
|
suppressOutput: true,
|
|
213
|
-
path: getReviewEvidencePath(cwd),
|
|
186
|
+
path: getReviewEvidencePath(cwd, { payload: input }),
|
|
214
187
|
payload,
|
|
215
188
|
}))
|
|
216
189
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clearSessionArtifact,
|
|
5
|
+
getSessionArtifactPath,
|
|
6
|
+
getSessionArtifactRelativePath,
|
|
7
|
+
readSessionArtifact,
|
|
8
|
+
writeSessionArtifact,
|
|
9
|
+
} from './session-capsule.mjs'
|
|
10
|
+
|
|
11
|
+
export const EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
|
|
12
|
+
|
|
13
|
+
function readGitDiffStat(cwd, args) {
|
|
14
|
+
try {
|
|
15
|
+
return execSync(`git diff --stat ${args}`.trim(), {
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
timeout: 10_000,
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
}).trim()
|
|
21
|
+
} catch {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function captureWorkspaceFingerprint(cwd) {
|
|
27
|
+
const unstaged = readGitDiffStat(cwd, 'HEAD')
|
|
28
|
+
const staged = readGitDiffStat(cwd, '--cached')
|
|
29
|
+
const available = unstaged !== null || staged !== null
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
available,
|
|
33
|
+
unstaged: unstaged || '',
|
|
34
|
+
staged: staged || '',
|
|
35
|
+
combined: `${unstaged || ''}\n---\n${staged || ''}`.trim(),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getRuntimeEvidencePath(cwd, fileName, options = {}) {
|
|
40
|
+
return getSessionArtifactPath(cwd, fileName, options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getRuntimeEvidenceRelativePath(cwd, fileName, options = {}) {
|
|
44
|
+
return getSessionArtifactRelativePath(cwd, fileName, options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function readRuntimeEvidence(cwd, fileName, options = {}) {
|
|
48
|
+
return readSessionArtifact(cwd, fileName, options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function clearRuntimeEvidence(cwd, fileName, options = {}) {
|
|
52
|
+
clearSessionArtifact(cwd, fileName, options)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeRuntimeEvidence(cwd, fileName, payload, options = {}) {
|
|
56
|
+
return writeSessionArtifact(cwd, fileName, payload, options)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateEvidenceTimestamp(evidence, now, label) {
|
|
60
|
+
const updatedAt = Date.parse(evidence.updatedAt || '')
|
|
61
|
+
if (!Number.isFinite(updatedAt)) {
|
|
62
|
+
return {
|
|
63
|
+
required: true,
|
|
64
|
+
status: 'invalid',
|
|
65
|
+
evidence,
|
|
66
|
+
details: [`${label}时间戳无效`],
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (now - updatedAt > EVIDENCE_MAX_AGE_MS) {
|
|
70
|
+
return {
|
|
71
|
+
required: true,
|
|
72
|
+
status: 'stale-time',
|
|
73
|
+
evidence,
|
|
74
|
+
details: [`${label}超过 30 分钟`],
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateEvidenceFingerprint(cwd, evidence, label) {
|
|
81
|
+
const currentFingerprint = captureWorkspaceFingerprint(cwd)
|
|
82
|
+
if (
|
|
83
|
+
currentFingerprint.available
|
|
84
|
+
&& evidence.fingerprint?.available
|
|
85
|
+
&& currentFingerprint.combined !== evidence.fingerprint.combined
|
|
86
|
+
) {
|
|
87
|
+
return {
|
|
88
|
+
required: true,
|
|
89
|
+
status: 'stale-diff',
|
|
90
|
+
evidence,
|
|
91
|
+
details: [`工作区变更已不同于最近一次${label}后的状态`],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
@@ -1,56 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { normalize, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clearCapsuleSection,
|
|
5
|
+
getRuntimeScope,
|
|
6
|
+
readCapsuleSection,
|
|
7
|
+
writeCapsuleSection,
|
|
8
|
+
} from './session-capsule.mjs'
|
|
4
9
|
|
|
5
|
-
const RUNTIME_DIR = join(homedir(), '.helloagents', 'runtime')
|
|
6
|
-
const ROUTE_CONTEXT_PATH = join(RUNTIME_DIR, 'route-context.json')
|
|
7
10
|
const ROUTE_CONTEXT_TTL_MS = 30 * 60 * 1000
|
|
8
11
|
|
|
9
12
|
function normalizePath(filePath = '') {
|
|
10
13
|
return filePath ? normalize(resolve(filePath)) : ''
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
function
|
|
14
|
-
|
|
16
|
+
function resolvePayload(options = {}) {
|
|
17
|
+
return options.payload && typeof options.payload === 'object' ? options.payload : options
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export function clearRouteContext() {
|
|
18
|
-
|
|
20
|
+
export function clearRouteContext(options = {}) {
|
|
21
|
+
const payload = resolvePayload(options)
|
|
22
|
+
const cwd = options.cwd || payload.cwd || process.cwd()
|
|
23
|
+
clearCapsuleSection(cwd, 'route', { payload, env: options.env, ppid: options.ppid })
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName }) {
|
|
22
|
-
|
|
26
|
+
export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName, payload = {}, env, ppid }) {
|
|
27
|
+
const scope = getRuntimeScope(cwd, { payload, env, ppid })
|
|
23
28
|
const context = {
|
|
24
29
|
cwd: normalizePath(cwd),
|
|
25
30
|
skillName,
|
|
26
31
|
sourceSkillName,
|
|
27
32
|
zeroSideEffect: skillName === 'idea',
|
|
33
|
+
scope: scope.scope,
|
|
34
|
+
key: scope.key,
|
|
28
35
|
updatedAt: Date.now(),
|
|
29
36
|
}
|
|
30
|
-
|
|
37
|
+
writeCapsuleSection(cwd, 'route', context, { payload, env, ppid })
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
export function readRouteContext() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
...context,
|
|
45
|
-
cwd: normalizePath(context.cwd),
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
40
|
+
export function readRouteContext(options = {}) {
|
|
41
|
+
const payload = resolvePayload(options)
|
|
42
|
+
const cwd = options.cwd || payload.cwd || process.cwd()
|
|
43
|
+
const context = readCapsuleSection(cwd, 'route', { payload, env: options.env, ppid: options.ppid })
|
|
44
|
+
if (!context?.cwd || !context?.skillName || !context?.updatedAt) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
if (Date.now() - context.updatedAt > ROUTE_CONTEXT_TTL_MS) {
|
|
48
|
+
clearRouteContext({ cwd, payload, env: options.env, ppid: options.ppid })
|
|
48
49
|
return null
|
|
49
50
|
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...context,
|
|
54
|
+
cwd: normalizePath(context.cwd),
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
export function getApplicableRouteContext({ cwd = '', filePath = '' } = {}) {
|
|
53
|
-
const context = readRouteContext()
|
|
58
|
+
export function getApplicableRouteContext({ cwd = '', filePath = '', payload = {}, env, ppid } = {}) {
|
|
59
|
+
const context = readRouteContext({ cwd, payload, env, ppid })
|
|
54
60
|
if (!context) return null
|
|
55
61
|
|
|
56
62
|
const normalizedCwd = normalizePath(cwd)
|