cli-aimlock 7.0.19 → 7.0.25
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/README.md +50 -4
- package/aimlock-context-map.mjs +42 -0
- package/aimlock-coordination.mjs +97 -0
- package/aimlock-local-fs.mjs +172 -0
- package/aimlock-local-gate.mjs +275 -0
- package/aimlock-local-runner.mjs +407 -0
- package/aimlock-runtime.mjs +1322 -0
- package/broker.mjs +301 -0
- package/cli.mjs +191 -14
- package/installer.mjs +57 -154
- package/package.json +12 -2
- package/skill/SKILL.md +112 -105
- package/skill/skill.json +3 -3
package/broker.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { constants } from 'node:fs'
|
|
3
|
+
import { lstat, open } from 'node:fs/promises'
|
|
4
|
+
import { resolve, win32 } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const LOOKUP_TIMEOUT_MS = 8000
|
|
7
|
+
export const CALL_TIMEOUT_MS = 120_000
|
|
8
|
+
const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
|
|
9
|
+
const TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
|
|
10
|
+
const TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
|
|
11
|
+
const AUTH_SCHEME = 'BrainClient'
|
|
12
|
+
const TOKEN_FILE_MAX_BYTES = 16_384
|
|
13
|
+
const POSIX_TOKEN_FILE_MODE = 0o600
|
|
14
|
+
const WINDOWS_BROKER_DIRECTORY = ['CLI.Tax', 'broker']
|
|
15
|
+
const FEEDBACK_COMMENT_MAX = 500
|
|
16
|
+
const EVALUATION_DURATION_MAX = 86_400_000
|
|
17
|
+
const SCORE_MIN = 0
|
|
18
|
+
const SCORE_MAX = 100
|
|
19
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
|
|
20
|
+
const INVOCATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
21
|
+
const DIGEST_PATTERN = /^[0-9a-f]{64}$/
|
|
22
|
+
const PROTOCOL_STATUSES = new Set(['succeeded', 'blocked', 'failed'])
|
|
23
|
+
const VALIDATION_STATES = new Set(['passed', 'failed', 'incomplete'])
|
|
24
|
+
const EVALUATION_SCHEMA = 'skill-automatic-evaluation/1.0'
|
|
25
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
26
|
+
const REQUEST_SCHEMA_PATTERN = /^([A-Za-z0-9.-]+\.skill)\.request\/([0-9]+\.[0-9]+)$/
|
|
27
|
+
|
|
28
|
+
function asObject(value, label) {
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
30
|
+
throw new Error(`${label} must be an object`)
|
|
31
|
+
}
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requiredString(value, label) {
|
|
36
|
+
const text = typeof value === 'string' ? value.trim() : ''
|
|
37
|
+
if (!text) throw new Error(`${label} is required`)
|
|
38
|
+
return text
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function boundedInteger(value, label) {
|
|
42
|
+
if (!Number.isFinite(value) || value < 0 || !Number.isSafeInteger(value)) {
|
|
43
|
+
throw new Error(`${label} must be a non-negative safe integer`)
|
|
44
|
+
}
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function insideWindowsDirectory(candidate, directory) {
|
|
49
|
+
const relative = win32.relative(directory, candidate)
|
|
50
|
+
return relative === '' || (!relative.startsWith('..\\') && relative !== '..' && !win32.isAbsolute(relative))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function brainClientTokenPath(environment, platform = process.platform) {
|
|
54
|
+
const configured = requiredString(environment[TOKEN_FILE_ENV], TOKEN_FILE_ENV)
|
|
55
|
+
if (platform !== 'win32') return resolve(configured)
|
|
56
|
+
if (!win32.isAbsolute(configured)) {
|
|
57
|
+
throw new Error('Windows Brain Client token file path must be absolute')
|
|
58
|
+
}
|
|
59
|
+
const localAppData = requiredString(environment.LOCALAPPDATA, 'LOCALAPPDATA')
|
|
60
|
+
const brokerDirectory = win32.resolve(localAppData, ...WINDOWS_BROKER_DIRECTORY)
|
|
61
|
+
const candidate = win32.resolve(configured)
|
|
62
|
+
if (!insideWindowsDirectory(candidate, brokerDirectory)) {
|
|
63
|
+
throw new Error(`Windows Brain Client token file must be inside ${brokerDirectory}`)
|
|
64
|
+
}
|
|
65
|
+
return candidate
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertTokenFileStatus(status, platform, currentUserId) {
|
|
69
|
+
if (!status.isFile() || status.size < 1 || status.size > TOKEN_FILE_MAX_BYTES) {
|
|
70
|
+
throw new Error('Brain Client token file must be a non-empty restricted file')
|
|
71
|
+
}
|
|
72
|
+
if (platform === 'win32') return
|
|
73
|
+
if (!Number.isInteger(currentUserId)) {
|
|
74
|
+
throw new Error('Brain Client token file ownership cannot be verified')
|
|
75
|
+
}
|
|
76
|
+
if (status.uid !== currentUserId || (status.mode & 0o777) !== POSIX_TOKEN_FILE_MODE) {
|
|
77
|
+
throw new Error('Brain Client token file must be owned by the current user with mode 0600')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseTokenFile(source) {
|
|
82
|
+
let tokenFile
|
|
83
|
+
try {
|
|
84
|
+
tokenFile = asObject(JSON.parse(source), 'Brain Client token file')
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error('Brain Client token file must contain valid JSON')
|
|
87
|
+
}
|
|
88
|
+
const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
|
|
89
|
+
if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
|
|
90
|
+
throw new Error('Brain Client token file contains unknown or missing fields')
|
|
91
|
+
}
|
|
92
|
+
return tokenFile
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function brainClientAuthorization(context, environment, dependencies = {}) {
|
|
96
|
+
const platform = dependencies.platform ?? process.platform
|
|
97
|
+
const tokenFilePath = brainClientTokenPath(environment, platform)
|
|
98
|
+
const inspectPath = dependencies.lstat ?? lstat
|
|
99
|
+
const openPath = dependencies.open ?? open
|
|
100
|
+
const currentUserId = platform === 'win32'
|
|
101
|
+
? null
|
|
102
|
+
: (dependencies.getuid ?? process.getuid)?.()
|
|
103
|
+
const linkStatus = await inspectPath(tokenFilePath)
|
|
104
|
+
if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
|
|
105
|
+
const noFollow = platform === 'win32' ? 0 : (constants.O_NOFOLLOW ?? 0)
|
|
106
|
+
const handle = await openPath(tokenFilePath, constants.O_RDONLY | noFollow)
|
|
107
|
+
try {
|
|
108
|
+
const status = await handle.stat()
|
|
109
|
+
assertTokenFileStatus(status, platform, currentUserId)
|
|
110
|
+
const tokenFile = parseTokenFile(await handle.readFile('utf8'))
|
|
111
|
+
const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
|
|
112
|
+
if (tokenFile.schemaVersion !== TOKEN_FILE_VERSION
|
|
113
|
+
|| tokenFile.authorizationScheme !== AUTH_SCHEME
|
|
114
|
+
|| endpoint.origin !== new URL(context.endpoint).origin
|
|
115
|
+
|| endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
|
|
116
|
+
|| endpoint.username || endpoint.password
|
|
117
|
+
|| !TOKEN_PATTERN.test(tokenFile.token)) {
|
|
118
|
+
throw new Error('Brain Client token file authority is invalid')
|
|
119
|
+
}
|
|
120
|
+
return `${AUTH_SCHEME} ${tokenFile.token}`
|
|
121
|
+
} finally {
|
|
122
|
+
await handle.close()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function canonicalJson(value) {
|
|
127
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
128
|
+
return JSON.stringify(value)
|
|
129
|
+
}
|
|
130
|
+
if (typeof value === 'number') {
|
|
131
|
+
if (!Number.isFinite(value)) throw new Error('automatic evaluation contains a non-finite number')
|
|
132
|
+
return JSON.stringify(value)
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
|
|
135
|
+
const record = asObject(value, 'automatic evaluation')
|
|
136
|
+
return `{${Object.keys(record).sort().map((key) => (
|
|
137
|
+
`${JSON.stringify(key)}:${canonicalJson(record[key])}`
|
|
138
|
+
)).join(',')}}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function expectedResponseSchema(requestSchema) {
|
|
142
|
+
const matched = requiredString(requestSchema, 'skill request schemaVersion')
|
|
143
|
+
.match(REQUEST_SCHEMA_PATTERN)
|
|
144
|
+
if (!matched) throw new Error('skill request schemaVersion is invalid')
|
|
145
|
+
return `${matched[1]}.response/${matched[2]}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function protocolResponse(protocolValue, requestEnvelope) {
|
|
149
|
+
const protocol = asObject(protocolValue, 'skill protocol response')
|
|
150
|
+
const responseSchema = expectedResponseSchema(requestEnvelope.schemaVersion)
|
|
151
|
+
if (protocol.schemaVersion !== responseSchema) {
|
|
152
|
+
throw new Error('skill protocol response schemaVersion does not match the request')
|
|
153
|
+
}
|
|
154
|
+
if (protocol.requestId !== requestEnvelope.requestId) {
|
|
155
|
+
throw new Error('skill protocol response requestId does not match the request')
|
|
156
|
+
}
|
|
157
|
+
const status = requiredString(protocol.status, 'skill protocol status')
|
|
158
|
+
if (!PROTOCOL_STATUSES.has(status)) {
|
|
159
|
+
throw new Error('skill protocol status must be succeeded, blocked, or failed')
|
|
160
|
+
}
|
|
161
|
+
return { protocol, responseSchema, status }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function authoritativeEvaluation(value, expected) {
|
|
165
|
+
const evaluation = asObject(value, 'server automatic evaluation')
|
|
166
|
+
const expectedKeys = [
|
|
167
|
+
'digest', 'durationMs', 'findingCount', 'operation', 'p0Count', 'p1Count', 'p2Count',
|
|
168
|
+
'requestId', 'responseSchemaVersion', 'schemaVersion', 'score', 'status', 'userComment',
|
|
169
|
+
'validation',
|
|
170
|
+
]
|
|
171
|
+
if (Object.keys(evaluation).sort().join('\n') !== expectedKeys.join('\n')) {
|
|
172
|
+
throw new Error('server automatic evaluation contains unknown or missing fields')
|
|
173
|
+
}
|
|
174
|
+
if (evaluation.schemaVersion !== EVALUATION_SCHEMA
|
|
175
|
+
|| evaluation.operation !== expected.operation
|
|
176
|
+
|| evaluation.requestId !== expected.requestId
|
|
177
|
+
|| evaluation.responseSchemaVersion !== expected.responseSchema
|
|
178
|
+
|| evaluation.status !== expected.status
|
|
179
|
+
|| !VALIDATION_STATES.has(evaluation.validation)
|
|
180
|
+
|| typeof evaluation.userComment !== 'string' || !evaluation.userComment.trim()
|
|
181
|
+
|| Buffer.byteLength(evaluation.userComment, 'utf8') > FEEDBACK_COMMENT_MAX
|
|
182
|
+
|| typeof evaluation.digest !== 'string' || !DIGEST_PATTERN.test(evaluation.digest)) {
|
|
183
|
+
throw new Error('server automatic evaluation authority is invalid')
|
|
184
|
+
}
|
|
185
|
+
for (const field of ['durationMs', 'findingCount', 'p0Count', 'p1Count', 'p2Count', 'score']) {
|
|
186
|
+
boundedInteger(evaluation[field], `server automatic evaluation ${field}`)
|
|
187
|
+
}
|
|
188
|
+
if (evaluation.score < SCORE_MIN || evaluation.score > SCORE_MAX
|
|
189
|
+
|| evaluation.durationMs > EVALUATION_DURATION_MAX
|
|
190
|
+
|| evaluation.findingCount < evaluation.p0Count + evaluation.p1Count + evaluation.p2Count) {
|
|
191
|
+
throw new Error('server automatic evaluation bounds are invalid')
|
|
192
|
+
}
|
|
193
|
+
if ((evaluation.status !== 'succeeded' || evaluation.validation !== 'passed'
|
|
194
|
+
|| evaluation.p0Count > 0 || evaluation.p1Count > 0) && evaluation.score >= 60) {
|
|
195
|
+
throw new Error('server automatic evaluation cannot report a positive score')
|
|
196
|
+
}
|
|
197
|
+
const { digest, ...core } = evaluation
|
|
198
|
+
const actualDigest = createHash('sha256').update(canonicalJson(core)).digest('hex')
|
|
199
|
+
if (digest !== actualDigest) throw new Error('server automatic evaluation digest is invalid')
|
|
200
|
+
return evaluation
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function responsePayload(response, label) {
|
|
204
|
+
try {
|
|
205
|
+
return asObject(await response.json(), label)
|
|
206
|
+
} catch {
|
|
207
|
+
throw new Error(`${label} is not valid JSON (HTTP ${response.status})`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function invocationRequest(context, operation, input) {
|
|
212
|
+
const normalizedOperation = requiredString(operation, 'skill operation')
|
|
213
|
+
if (!IDENTIFIER_PATTERN.test(normalizedOperation)) {
|
|
214
|
+
throw new Error('skill operation is invalid')
|
|
215
|
+
}
|
|
216
|
+
expectedResponseSchema(context.schemaVersion)
|
|
217
|
+
return {
|
|
218
|
+
schemaVersion: context.schemaVersion,
|
|
219
|
+
requestId: `${context.runtimeCode}-${randomUUID()}`,
|
|
220
|
+
operation: normalizedOperation,
|
|
221
|
+
input: asObject(input, 'skill operation input'),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function invokeOfficialSkill(context, operation, input, dependencies) {
|
|
226
|
+
const environment = asObject(dependencies.environment, 'broker environment')
|
|
227
|
+
if (typeof dependencies.request !== 'function') {
|
|
228
|
+
throw new Error('broker request dependency is required')
|
|
229
|
+
}
|
|
230
|
+
const authorization = await brainClientAuthorization(context, environment, dependencies.credentialAccess)
|
|
231
|
+
const requestEnvelope = invocationRequest(context, operation, input)
|
|
232
|
+
let response
|
|
233
|
+
try {
|
|
234
|
+
response = await dependencies.request(context.endpoint, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json', Authorization: authorization },
|
|
237
|
+
body: JSON.stringify({ input: requestEnvelope }),
|
|
238
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
239
|
+
})
|
|
240
|
+
} catch {
|
|
241
|
+
throw new Error(`${context.displayName} ${operation} invocation failed`)
|
|
242
|
+
}
|
|
243
|
+
const payload = await responsePayload(response, `${context.displayName} ${operation} response`)
|
|
244
|
+
if (!response.ok || payload.ok !== true) {
|
|
245
|
+
throw new Error(`${context.displayName} ${operation} failed: HTTP ${response.status}`)
|
|
246
|
+
}
|
|
247
|
+
const invocationId = payload.feedbackInvocationId
|
|
248
|
+
if (typeof invocationId !== 'string' || !INVOCATION_PATTERN.test(invocationId)) {
|
|
249
|
+
throw new Error(`${context.displayName} ${operation} response is missing a valid feedbackInvocationId`)
|
|
250
|
+
}
|
|
251
|
+
const feedbackReceiptId = payload.feedbackReceiptId
|
|
252
|
+
const feedbackRequestId = payload.feedbackRequestId
|
|
253
|
+
if (typeof feedbackReceiptId !== 'string' || !INVOCATION_PATTERN.test(feedbackReceiptId)
|
|
254
|
+
|| feedbackRequestId !== `automatic-${invocationId}`) {
|
|
255
|
+
throw new Error(`${context.displayName} ${operation} response is missing a committed feedback receipt`)
|
|
256
|
+
}
|
|
257
|
+
const protocolAuthority = protocolResponse(payload.output, requestEnvelope)
|
|
258
|
+
const evaluation = authoritativeEvaluation(payload.feedbackEvaluation, {
|
|
259
|
+
operation: requestEnvelope.operation,
|
|
260
|
+
requestId: requestEnvelope.requestId,
|
|
261
|
+
responseSchema: protocolAuthority.responseSchema,
|
|
262
|
+
status: protocolAuthority.status,
|
|
263
|
+
})
|
|
264
|
+
const feedback = {
|
|
265
|
+
id: feedbackReceiptId,
|
|
266
|
+
requestId: feedbackRequestId,
|
|
267
|
+
duplicated: false,
|
|
268
|
+
}
|
|
269
|
+
return { response: payload, invocationId, evaluation, feedback }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function callOfficialSkill(context, operation, input, dependencies) {
|
|
273
|
+
return (await invokeOfficialSkill(context, operation, input, dependencies)).response
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function invokeCommandInput(args) {
|
|
277
|
+
const operation = requiredString(args[1], 'skill operation')
|
|
278
|
+
const source = args.slice(2).join(' ').trim()
|
|
279
|
+
if (!source) return { operation, input: {} }
|
|
280
|
+
try {
|
|
281
|
+
return { operation, input: asObject(JSON.parse(source), 'skill operation input') }
|
|
282
|
+
} catch {
|
|
283
|
+
throw new Error('skill operation input must be a JSON object')
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function brokerCommandInput(source) {
|
|
288
|
+
let parsed
|
|
289
|
+
try {
|
|
290
|
+
parsed = asObject(JSON.parse(source), 'broker request')
|
|
291
|
+
} catch {
|
|
292
|
+
throw new Error('broker request must be a JSON object')
|
|
293
|
+
}
|
|
294
|
+
if (Object.keys(parsed).some((key) => !['operation', 'input'].includes(key))) {
|
|
295
|
+
throw new Error('broker request contains unknown fields')
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
operation: requiredString(parsed.operation, 'skill operation'),
|
|
299
|
+
input: asObject(parsed.input, 'skill operation input'),
|
|
300
|
+
}
|
|
301
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -1,26 +1,203 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { dirname } from 'node:path'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { cwd, stdin, stdout } from 'node:process'
|
|
4
|
+
import { createInterface } from 'node:readline/promises'
|
|
3
5
|
import { fileURLToPath } from 'node:url'
|
|
4
|
-
import { dispatchOfficialSkillCli, runIntakeHandshake } from './installer.mjs'
|
|
6
|
+
import { defaultUsage, dispatchOfficialSkillCli, runIntakeHandshake } from './installer.mjs'
|
|
7
|
+
import {
|
|
8
|
+
LOCAL_CAPABILITIES,
|
|
9
|
+
extendReadBudget,
|
|
10
|
+
guardedWriteFile,
|
|
11
|
+
initializeReadBudget,
|
|
12
|
+
issueMutationPass,
|
|
13
|
+
probeRepositoryDemand,
|
|
14
|
+
readBudgetStatus,
|
|
15
|
+
readFileWithinBudget,
|
|
16
|
+
reassessMode,
|
|
17
|
+
verifyMutationPassFile,
|
|
18
|
+
} from './aimlock-local-runner.mjs'
|
|
19
|
+
|
|
20
|
+
const BYPASS_LINE_BUDGET = 500
|
|
21
|
+
const DIFFICULTIES = new Set(['low', 'medium', 'high'])
|
|
22
|
+
const RISKS = new Set(['low', 'medium', 'high'])
|
|
23
|
+
const TRUE_ANSWERS = new Set(['yes', 'y', 'true', '是', '需要', 'да'])
|
|
24
|
+
const FALSE_ANSWERS = new Set(['no', 'n', 'false', '否', '不需要', 'нет'])
|
|
25
|
+
const BYPASS_NOTICE = [
|
|
26
|
+
'需求较小且低风险,不建议使用 Aimlock;请直接处理,或只调用一个匹配的专项技能。',
|
|
27
|
+
'This request is small and low risk; Aimlock is not recommended. Handle it directly or use one matched specialist skill.',
|
|
28
|
+
'Запрос небольшой и низкорисковый; Aimlock не рекомендуется. Выполните его напрямую или используйте один профильный навык.',
|
|
29
|
+
].join('\n')
|
|
30
|
+
const COMMON_RUN_USAGE = " Run this skill's applicability or onboarding flow; only a real HTTP invocation can trigger automatic evaluation."
|
|
31
|
+
const AIMLOCK_RUN_USAGE = ' Collect six applicability facts locally; bypass makes no skill HTTP call, automatic evaluation, or requirements file, while active work runs authenticated intake.'
|
|
32
|
+
|
|
33
|
+
const APPLICABILITY_QUESTIONS = [
|
|
34
|
+
{ id: 'goal', prompt: '目标 / Goal / Цель', parse: parseText },
|
|
35
|
+
{ id: 'targetHints', prompt: '目标路径(逗号分隔) / Target paths / Целевые пути', parse: parseTargetHints },
|
|
36
|
+
{ id: 'explicitAimlockRequested', prompt: '是否明确要求启用 Aimlock / Explicitly require Aimlock / Явно включить Aimlock (yes|no)', parse: parseBoolean },
|
|
37
|
+
]
|
|
5
38
|
|
|
6
39
|
const INTAKE_QUESTIONS = [
|
|
7
|
-
{ id: 'goal', required: true, prompt: '
|
|
8
|
-
{ id: '
|
|
9
|
-
{ id: '
|
|
10
|
-
{ id: '
|
|
11
|
-
{ id: '
|
|
12
|
-
{ id: '
|
|
13
|
-
{ id: '
|
|
40
|
+
{ id: 'goal', required: true, prompt: '完成标准与禁止改动 / Goal and forbidden changes / Цель и запрещённые изменения', example: '只改税率常量一行,不改其它计税逻辑' },
|
|
41
|
+
{ id: 'difficulty', required: true, prompt: '需求难度 / Difficulty / Сложность: low, medium, or high?', example: 'medium' },
|
|
42
|
+
{ id: 'risk', required: true, prompt: '风险等级 / Risk / Риск: low, medium, or high?', example: 'low' },
|
|
43
|
+
{ id: 'targetFiles', required: true, prompt: '目标文件 / Target files / Целевые файлы (unknown if not located)', example: 'apps/web/src/tax.ts' },
|
|
44
|
+
{ id: 'estimatedChangedLines', required: true, prompt: '预计改动行数 / Estimated changed lines / Оценка строк', example: '1' },
|
|
45
|
+
{ id: 'crossModule', required: true, prompt: '是否跨模块 / Cross-module / Межмодульно: yes or no?', example: 'no' },
|
|
46
|
+
{ id: 'needParallel', required: true, prompt: '是否必须并行 / Parallel required / Нужна параллельность: yes or no?', example: 'no' },
|
|
47
|
+
{ id: 'explicitAimlockRequested', required: true, prompt: '是否明确要求启用 Aimlock / Explicitly require Aimlock / Явно включить Aimlock: yes or no?', example: 'no' },
|
|
48
|
+
{ id: 'goalKind', required: true, prompt: '需求类型 / Goal kind / Тип цели: code, calculator, mixed, or docs?', example: 'code' },
|
|
49
|
+
{ id: 'deliveryDoc', required: true, prompt: '是否生成交付文档 / Delivery document / Нужен отчёт: yes or no?', example: 'no' },
|
|
14
50
|
]
|
|
15
51
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
52
|
+
function parseBoolean(source) {
|
|
53
|
+
const value = source.trim().toLowerCase()
|
|
54
|
+
if (TRUE_ANSWERS.has(value)) return true
|
|
55
|
+
if (FALSE_ANSWERS.has(value)) return false
|
|
56
|
+
throw new Error('expected yes or no')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseText(source) {
|
|
60
|
+
const value = source.trim()
|
|
61
|
+
if (!value) throw new Error('a non-empty value is required')
|
|
62
|
+
return value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseTargetHints(source) {
|
|
66
|
+
const values = source.split(',').map((value) => value.trim()).filter(Boolean)
|
|
67
|
+
if (!values.length) throw new Error('at least one target path is required')
|
|
68
|
+
return values
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function localAimlockApplicability(facts) {
|
|
72
|
+
const valid = DIFFICULTIES.has(facts.difficulty)
|
|
73
|
+
&& RISKS.has(facts.risk)
|
|
74
|
+
&& Number.isSafeInteger(facts.estimatedChangedLines)
|
|
75
|
+
&& facts.estimatedChangedLines >= 0
|
|
76
|
+
&& typeof facts.crossModule === 'boolean'
|
|
77
|
+
&& typeof facts.needParallel === 'boolean'
|
|
78
|
+
&& typeof facts.explicitAimlockRequested === 'boolean'
|
|
79
|
+
if (!valid) throw new Error('Aimlock applicability facts are incomplete or invalid')
|
|
80
|
+
const bypass = facts.difficulty === 'low'
|
|
81
|
+
&& facts.estimatedChangedLines <= BYPASS_LINE_BUDGET
|
|
82
|
+
&& facts.crossModule === false
|
|
83
|
+
&& facts.risk !== 'high'
|
|
84
|
+
&& facts.needParallel === false
|
|
85
|
+
&& facts.explicitAimlockRequested === false
|
|
86
|
+
return { mode: bypass ? 'bypass' : 'active', useAimlock: !bypass }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function aimlockUsage(context) {
|
|
90
|
+
const usage = defaultUsage(context)
|
|
91
|
+
if (!usage.includes(COMMON_RUN_USAGE)) throw new Error('Shared CLI run usage contract changed')
|
|
92
|
+
return usage.replace(COMMON_RUN_USAGE, AIMLOCK_RUN_USAGE)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function collectApplicability(input, output) {
|
|
96
|
+
const readline = createInterface({ input, output })
|
|
97
|
+
const answers = {}
|
|
98
|
+
try {
|
|
99
|
+
for (const question of APPLICABILITY_QUESTIONS) {
|
|
100
|
+
for (;;) {
|
|
101
|
+
const source = await readline.question(`${question.prompt}\n> `)
|
|
102
|
+
try {
|
|
103
|
+
answers[question.id] = question.parse(source)
|
|
104
|
+
break
|
|
105
|
+
} catch (error) {
|
|
106
|
+
output.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
readline.close()
|
|
112
|
+
}
|
|
113
|
+
return answers
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runAimlockWith(context, dependencies) {
|
|
117
|
+
const facts = await dependencies.collectApplicability()
|
|
118
|
+
const decision = localAimlockApplicability(facts)
|
|
119
|
+
if (!decision.useAimlock) {
|
|
120
|
+
dependencies.writeNotice(BYPASS_NOTICE)
|
|
121
|
+
return decision
|
|
122
|
+
}
|
|
123
|
+
await dependencies.runHandshake(context, {
|
|
19
124
|
questions: INTAKE_QUESTIONS,
|
|
20
125
|
outputFile: 'AIMLOCK-REQUIREMENTS.json',
|
|
21
126
|
afterCapabilities(output) {
|
|
22
127
|
const notice = output.firstUseNotice?.zh
|
|
23
128
|
if (typeof notice === 'string' && notice.trim()) console.log(notice)
|
|
24
129
|
},
|
|
25
|
-
})
|
|
26
|
-
|
|
130
|
+
})
|
|
131
|
+
return decision
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function runAimlock(context) {
|
|
135
|
+
return runAimlockWith(context, {
|
|
136
|
+
collectApplicability: async () => {
|
|
137
|
+
const answers = await collectApplicability(stdin, stdout)
|
|
138
|
+
const probe = await probeRepositoryDemand({
|
|
139
|
+
repositoryRoot: cwd(), goal: answers.goal, targetHints: answers.targetHints,
|
|
140
|
+
})
|
|
141
|
+
console.log(JSON.stringify({ mode: probe.mode, facts: probe.facts }))
|
|
142
|
+
return { ...probe.facts, explicitAimlockRequested: answers.explicitAimlockRequested }
|
|
143
|
+
},
|
|
144
|
+
writeNotice: (message) => console.log(message),
|
|
145
|
+
runHandshake: runIntakeHandshake,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function readJsonInput(input) {
|
|
150
|
+
let source = ''
|
|
151
|
+
for await (const chunk of input) source += chunk
|
|
152
|
+
if (!source.trim()) return {}
|
|
153
|
+
const value = JSON.parse(source)
|
|
154
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
155
|
+
throw new Error('local operation input must be a JSON object')
|
|
156
|
+
}
|
|
157
|
+
return value
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runLocalOperation(operation, repositoryRoot, input) {
|
|
161
|
+
const scoped = { ...input, repositoryRoot }
|
|
162
|
+
if (operation === 'capabilities') return LOCAL_CAPABILITIES
|
|
163
|
+
if (operation === 'probe') return probeRepositoryDemand(scoped)
|
|
164
|
+
if (operation === 'reassess') return reassessMode(input)
|
|
165
|
+
if (operation === 'budget-init') return initializeReadBudget(scoped)
|
|
166
|
+
if (operation === 'budget-read') return readFileWithinBudget(scoped)
|
|
167
|
+
if (operation === 'budget-status') return readBudgetStatus(scoped)
|
|
168
|
+
if (operation === 'budget-extend') return extendReadBudget(scoped)
|
|
169
|
+
if (operation === 'gate-issue') return issueMutationPass(scoped)
|
|
170
|
+
if (operation === 'gate-verify') return verifyMutationPassFile(scoped)
|
|
171
|
+
if (operation === 'guarded-write') return guardedWriteFile(scoped)
|
|
172
|
+
throw new Error(`unsupported local operation: ${operation}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function dispatchLocal(args) {
|
|
176
|
+
const operation = args[0]?.trim()
|
|
177
|
+
const repositoryRoot = args[1]?.trim()
|
|
178
|
+
if (!operation || !repositoryRoot) {
|
|
179
|
+
throw new Error('usage: cli-aimlock local <operation> <repositoryRoot>')
|
|
180
|
+
}
|
|
181
|
+
return runLocalOperation(operation, repositoryRoot, await readJsonInput(stdin))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const cliPath = fileURLToPath(import.meta.url)
|
|
185
|
+
if (process.argv[1] && resolve(process.argv[1]) === cliPath) {
|
|
186
|
+
if (process.argv[2] === 'local') {
|
|
187
|
+
try {
|
|
188
|
+
console.log(JSON.stringify(await dispatchLocal(process.argv.slice(3))))
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(JSON.stringify({ status: 'failed', code: error?.code ?? 'AIMLOCK_LOCAL_FAILED',
|
|
191
|
+
message: error instanceof Error ? error.message : String(error) }))
|
|
192
|
+
process.exitCode = error?.code === 'AIMLOCK_DECISION_REQUIRED' ? 2 : 1
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
await dispatchOfficialSkillCli({
|
|
196
|
+
packageRoot: dirname(cliPath),
|
|
197
|
+
runCommand: runAimlock,
|
|
198
|
+
usage: aimlockUsage,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { runLocalOperation }
|