cli-blueprint 7.0.8 → 7.0.18

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/installer.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * 五个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
2
+ * 八个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
3
3
  * 禁止第二套超时、第二套版本来源、第二套 bin 名。
4
4
  */
5
- import { copyFile, mkdir, writeFile } from 'node:fs/promises'
6
- import { existsSync, readFileSync } from 'node:fs'
5
+ import { randomUUID } from 'node:crypto'
6
+ import { constants, existsSync, readFileSync } from 'node:fs'
7
+ import { cp, lstat, mkdir, open, rm, writeFile } from 'node:fs/promises'
7
8
  import { dirname, join, resolve } from 'node:path'
8
9
  import { stdin, stdout } from 'node:process'
9
10
  import { createInterface } from 'node:readline/promises'
@@ -12,6 +13,18 @@ import { fileURLToPath } from 'node:url'
12
13
  export const LOOKUP_TIMEOUT_MS = 8000
13
14
  export const CALL_TIMEOUT_MS = 120_000
14
15
  const INSTALL_META = 'install-meta.json'
16
+ const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
17
+ const BRAIN_CLIENT_TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
18
+ const BRAIN_CLIENT_TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
19
+ const BRAIN_CLIENT_AUTH_SCHEME = 'BrainClient'
20
+ const BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES = 16_384
21
+ const BRAIN_CLIENT_TOKEN_FILE_MODE = 0o600
22
+ const FEEDBACK_COMMENT_MAX = 500
23
+ const FEEDBACK_SCORE_MIN = 0
24
+ const FEEDBACK_SCORE_MAX = 100
25
+ const BRAIN_CLIENT_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
26
+ const FEEDBACK_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
27
+ const FEEDBACK_SCORE_PATTERN = /^(?:0|[1-9]\d{0,2})$/
15
28
 
16
29
  function asObject(value, label) {
17
30
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -112,12 +125,113 @@ export async function callOfficialSkill(context, operation, input) {
112
125
  return payload
113
126
  }
114
127
 
128
+ export function feedbackCommandInput(args) {
129
+ const invocationId = requiredString(args[1], 'feedback invocation id')
130
+ if (!FEEDBACK_INVOCATION_PATTERN.test(invocationId)) {
131
+ throw new Error('feedback invocation id must be the UUID returned by a real skill response')
132
+ }
133
+ const scoreText = requiredString(args[2], 'feedback score')
134
+ if (!FEEDBACK_SCORE_PATTERN.test(scoreText)) {
135
+ throw new Error(`feedback score must be an integer between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
136
+ }
137
+ const score = Number(scoreText)
138
+ if (!Number.isInteger(score) || score < FEEDBACK_SCORE_MIN || score > FEEDBACK_SCORE_MAX) {
139
+ throw new Error(`feedback score must be between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
140
+ }
141
+ const userComment = args.slice(3).join(' ').trim()
142
+ if (!userComment) throw new Error('feedback comment is required')
143
+ if (userComment.length > FEEDBACK_COMMENT_MAX) {
144
+ throw new Error(`feedback comment must be at most ${FEEDBACK_COMMENT_MAX} characters`)
145
+ }
146
+ return { invocationId, score, userComment }
147
+ }
148
+
149
+ async function brainClientAuthorization(context, environment) {
150
+ const configuredPath = typeof environment[BRAIN_CLIENT_TOKEN_FILE_ENV] === 'string'
151
+ ? environment[BRAIN_CLIENT_TOKEN_FILE_ENV].trim() : ''
152
+ if (!configuredPath) throw new Error(`${BRAIN_CLIENT_TOKEN_FILE_ENV} is required`)
153
+ if (process.platform === 'win32' || typeof process.getuid !== 'function') {
154
+ throw new Error('Brain Client token file ownership cannot be verified')
155
+ }
156
+ const tokenFilePath = resolve(configuredPath)
157
+ const linkStatus = await lstat(tokenFilePath)
158
+ if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
159
+ const handle = await open(tokenFilePath, constants.O_RDONLY | constants.O_NOFOLLOW)
160
+ try {
161
+ const status = await handle.stat()
162
+ if (!status.isFile() || status.uid !== process.getuid()
163
+ || (status.mode & 0o777) !== BRAIN_CLIENT_TOKEN_FILE_MODE
164
+ || status.size < 1 || status.size > BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES) {
165
+ throw new Error('Brain Client token file must be owned by the current user with mode 0600')
166
+ }
167
+ const tokenFile = asObject(JSON.parse(await handle.readFile('utf8')), 'Brain Client token file')
168
+ const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
169
+ if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
170
+ throw new Error('Brain Client token file contains unknown or missing fields')
171
+ }
172
+ const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
173
+ if (tokenFile.schemaVersion !== BRAIN_CLIENT_TOKEN_FILE_VERSION
174
+ || tokenFile.authorizationScheme !== BRAIN_CLIENT_AUTH_SCHEME
175
+ || endpoint.origin !== new URL(context.endpoint).origin
176
+ || endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
177
+ || endpoint.username || endpoint.password
178
+ || !BRAIN_CLIENT_TOKEN_PATTERN.test(tokenFile.token)) {
179
+ throw new Error('Brain Client token file authority is invalid')
180
+ }
181
+ return `${BRAIN_CLIENT_AUTH_SCHEME} ${tokenFile.token}`
182
+ } finally {
183
+ await handle.close()
184
+ }
185
+ }
186
+
187
+ export async function submitOfficialSkillFeedback(context, args, environment, request) {
188
+ const input = feedbackCommandInput(args)
189
+ const authorization = await brainClientAuthorization(context, environment)
190
+ const requestId = `${context.runtimeCode}-${randomUUID()}`
191
+ let response
192
+ try {
193
+ response = await request(new URL(FEEDBACK_API_PATH, context.endpoint), {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': 'application/json',
197
+ Authorization: authorization,
198
+ },
199
+ body: JSON.stringify({
200
+ requestId,
201
+ skillId: context.runtimeCode,
202
+ invocationId: input.invocationId,
203
+ score: input.score,
204
+ userComment: input.userComment,
205
+ }),
206
+ signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
207
+ })
208
+ } catch {
209
+ throw new Error('cli.tax feedback request failed')
210
+ }
211
+ let payload
212
+ try {
213
+ payload = asObject(await response.json(), 'cli.tax feedback response')
214
+ } catch (error) {
215
+ if (error instanceof Error && error.message.startsWith('cli.tax feedback response')) throw error
216
+ throw new Error(`cli.tax feedback failed: non-JSON response (HTTP ${response.status})`)
217
+ }
218
+ if (!response.ok || payload.ok !== true) {
219
+ throw new Error(`cli.tax feedback failed: HTTP ${response.status}`)
220
+ }
221
+ if (payload.requestId !== requestId || typeof payload.id !== 'string'
222
+ || !FEEDBACK_INVOCATION_PATTERN.test(payload.id)
223
+ || typeof payload.duplicated !== 'boolean') {
224
+ throw new Error('cli.tax feedback response authority is invalid')
225
+ }
226
+ return { id: payload.id, requestId, duplicated: payload.duplicated }
227
+ }
228
+
115
229
  export async function installOfficialSkill(context, explicit) {
116
230
  const target = installTarget(context.skillName, explicit)
117
231
  await mkdir(target, { recursive: true })
118
232
  const previous = readInstallMeta(target)
119
- await copyFile(join(context.skillDir, 'SKILL.md'), join(target, 'SKILL.md'))
120
- await copyFile(join(context.skillDir, 'skill.json'), join(target, 'skill.json'))
233
+ await rm(join(target, 'references'), { recursive: true, force: true })
234
+ await cp(context.skillDir, target, { recursive: true, force: true })
121
235
  const installed = asObject(JSON.parse(readFileSync(join(target, 'skill.json'), 'utf8')), 'installed skill.json')
122
236
  const installedVersion = requiredString(installed.version, 'installed skill.json version')
123
237
  await writeFile(join(target, INSTALL_META), `${JSON.stringify({
@@ -170,7 +284,6 @@ export function defaultUsage(context, extraLines) {
170
284
  ' Check whether the installed skill has a newer version.',
171
285
  ` npx ${context.npmName}@latest run`,
172
286
  ' Run the skill handshake: discover capabilities and collect intake answers.',
173
- '',
174
287
  `Endpoint: ${context.endpoint}`,
175
288
  ]
176
289
  if (extraLines?.length) lines.push('', ...extraLines)
@@ -227,6 +340,10 @@ export async function dispatchOfficialSkillCli(options) {
227
340
  if (command === 'install') await installOfficialSkill(context, argument)
228
341
  else if (command === 'check') await checkOfficialSkill(context, argument)
229
342
  else if (command === 'run') await options.runCommand(context)
343
+ else if (command === 'feedback') {
344
+ const receipt = await submitOfficialSkillFeedback(context, args, process.env, fetch)
345
+ console.log(`${context.displayName} feedback accepted: ${receipt.id}`)
346
+ }
230
347
  else if (command === 'help' || command === '--help' || command === '-h') {
231
348
  console.log(options.usage ? options.usage(context) : defaultUsage(context, options.extraUsageLines))
232
349
  } else {
package/package.json CHANGED
@@ -17,5 +17,5 @@
17
17
  "url": "https://github.com/88208555/blueprint-clitax.git"
18
18
  },
19
19
  "type": "module",
20
- "version": "7.0.8"
20
+ "version": "7.0.18"
21
21
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,9 @@ description: '把一个目标编译为可执行、可验证、可追溯的工程
5
5
 
6
6
  # Blueprint Skill
7
7
 
8
- 版本:v6.0.0
8
+ Package version: v7.0.18
9
+
10
+ 远端 Hermes 编译器版本:0.4.0(独立于 npm 包版本)
9
11
 
10
12
  Endpoint: https://cli.tax/wvz6zmRWmX
11
13
  Request schema: blueprint.skill.request/1.0
@@ -38,6 +40,7 @@ POST JSON to the endpoint with an `input` wrapper:
38
40
 
39
41
  1. Call `capabilities` first and read the returned `nextStep`.
40
42
  2. Call `intake` and ask the user the returned questions one at a time, waiting for each answer.
43
+ For a new code project, require the ArchGuard contract digest created before planning. For an existing project, preserve an existing `arch.contract.yaml` digest in the Blueprint inputs; if no contract exists, record a non-blocking recommendation instead of inventing one.
41
44
  3. Do not compile a Blueprint until all required questions are answered.
42
45
  4. Build a Blueprint conforming to `blueprint.ir/1.0`, then call `validate`.
43
46
  5. Fix every validation finding until the report is green, then call `compile-inline` and save the artifacts.
@@ -52,7 +55,7 @@ After `capabilities`, read `officialCatalog`. Default allowlist is official skil
52
55
  - The response `status` must be `succeeded`; a `failed` response is an error, not a result.
53
56
  - Public responses never prove that code was developed, tested, or deployed.
54
57
 
55
- ## IR Schema 完整文档(v6.0.0)
58
+ ## IR Schema 完整文档(blueprint.ir/1.0)
56
59
 
57
60
  ### 顶层结构
58
61
 
@@ -223,96 +226,41 @@ After `capabilities`, read `officialCatalog`. Default allowlist is official skil
223
226
  }
224
227
  ```
225
228
 
226
- ## 粗粒度模式(Coarse Mode)
227
-
228
- 在请求 `input` 中设置 `coarseMode: true` 即可启用粗粒度模式:
229
-
230
- ```json
231
- {
232
- "input": {
233
- "schemaVersion": "blueprint.skill.request/1.0",
234
- "requestId": "unique-id",
235
- "operation": "compile-inline",
236
- "coarseMode": true,
237
- "input": { "blueprint": { ... } }
238
- }
239
- }
240
- ```
241
-
242
- 启用后的行为差异:
243
-
244
- - **节点端口可省略**:`inputs` 和 `outputs` 可以为空数组 `[]`,无需声明具体端口
245
- - **边无需端口级连线**:`edges` 中不提供 `fromOutput` / `toInput` 仍可通过校验,节点级连接即满足输入源 / 输出消费检查
246
- - **端口校验仅在显式声明时生效**:只有当节点明确声明了 `inputs` 或 `outputs`(非空数组)时,才强制要求数据边绑定端口名
247
- - **适用场景**:推荐用于单文件、单实现者的快速原型场景,无需声明详细端口拓扑
248
-
249
- ## 新操作文档
250
-
251
- ### `acceptance-report`
252
-
253
- 接受实施后验证结果,关闭验收标准闭环。
254
-
255
- **输入**:
256
-
257
- ```json
258
- {
259
- "blueprintId": "your-blueprint-id",
260
- "results": [
261
- {
262
- "acId": "ac-step-a",
263
- "passed": true,
264
- "evidence": "单元测试覆盖,运行通过" // 可选,提供通过证据
265
- },
266
- {
267
- "acId": "ac-step-b",
268
- "passed": false,
269
- "evidence": "缺少边界条件测试" // 可选,记录失败原因
270
- }
271
- ]
272
- }
273
- ```
274
-
275
- - `blueprintId`:目标蓝图 ID
276
- - `results`:数组,每项包含 `acId`(对应 `acceptanceCriteria.id`)、`passed`(布尔)、`evidence`(可选字符串)
277
-
278
- ### `answer-questions`
229
+ ## Finding 修复循环
279
230
 
280
- 关闭 OPEN-QUESTIONS 循环,回答蓝图中的开放问题。
281
-
282
- **输入**:
231
+ `validate` 与 `compile-inline` 的确定性报告位于 `validation.findings`。每条 Finding 包含 `ruleId`、`severity`、`entityRef`、`message`、`evidence` 与 `recommendedAction`。调用方必须按 `recommendedAction` 修复对应实体并重新 `validate`,不得把 `blocked` 当成编译结果。
283
232
 
284
233
  ```json
285
234
  {
286
- "blueprintId": "your-blueprint-id",
287
- "answers": [
288
- {
289
- "questionId": "q-storage-backend",
290
- "answer": "使用 PostgreSQL 作为持久化存储,因为团队已有运维经验"
291
- }
292
- ]
235
+ "ruleId": "IR_REQUIRED_FIELD",
236
+ "severity": "P0",
237
+ "entityRef": "blueprint.title",
238
+ "message": "title is required.",
239
+ "evidence": {},
240
+ "recommendedAction": "Add a human-readable title."
293
241
  }
294
242
  ```
295
243
 
296
- - `blueprintId`:目标蓝图 ID
297
- - `answers`:数组,每项包含 `questionId`(问题标识)和 `answer`(回答内容)
244
+ ## 实现状态
298
245
 
299
- ### IR 模板
246
+ | ID | 能力 | 状态 | 边界 |
247
+ |---|---|---|---|
248
+ | B1 | 结构校验与可修复 Finding | 已实现 | `evidence` 与 `recommendedAction` 已由远端 Hermes 0.4.0 返回。 |
249
+ | B2 | 增量规划/修订 | 部分实现 | IR 支持调用方维护 `revision`;服务端不保存蓝图,也没有增量更新操作。 |
250
+ | B3 | 业务模板库与粗粒度模式 | 规划中 | 当前没有模板操作,`template` 与 `coarseMode` 均不是受支持输入。 |
251
+ | B4 | 验收回传、开放问题闭环、Validator 桥接 | 规划中 | 当前没有 `acceptance-report`、`answer-questions` 或 Validator 桥接操作。 |
300
252
 
301
- 以下是系统提供的常用 IR 模板,可作为构建蓝图的起点:
253
+ 只调用 `capabilities` 返回的五个操作。不要根据规划中条目构造请求,也不要把 npm 包版本 `v7.0.18` 与远端 Hermes 编译器版本 `0.4.0` 混为一谈。
302
254
 
303
- | 模板名称 | 说明 | 适用场景 |
304
- |---------|------|---------|
305
- | `linear-pipeline` | 线性流水线,节点依次执行 | 数据处理、ETL 任务 |
306
- | `fan-out-fan-in` | 分叉汇聚模式,多路并行后合并 | 批量处理、并行计算 |
307
- | `state-machine` | 状态机模式,带状态转移控制 | 流程审批、工作流引擎 |
308
- | `event-driven` | 事件驱动模式,基于事件触发节点 | 微服务、异步任务编排 |
309
- | `recursive-loop` | 递归循环模式,带环边与迭代守卫 | 迭代优化、爬虫、搜索 |
255
+ ## 反馈通道
310
256
 
311
- 使用模板时,在 `compile-inline` `input` 中传入 `template: "linear-pipeline"` 即可自动填充基础结构。
257
+ 评价只能由 IDE / 智能体自动回传,严禁人类干预:
312
258
 
313
- ## 反馈通道
259
+ 1. 仅当一次真实技能 HTTP 调用成功,并且响应顶层包含 `feedbackInvocationId` 时,调用方才可进入评价流程。
260
+ 2. 调用方必须使用本次响应的原始 ID,按真实执行结果生成整数分数(0–100)和真实评语,再自动执行:
261
+ `npx cli-blueprint@latest feedback <feedbackInvocationId> <score> <comment...>`
262
+ 3. 自动回传必须使用会员中心 Brain 客户端生成的同一身份文件,并通过 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 指向该 `0600` 文件;服务端以会员账号实名入库,不接受匿名评价。
263
+ 4. 禁止伪造、猜测、复用或跨调用转移 `feedbackInvocationId`;禁止让人类选择分数、填写评价或代为执行命令。
264
+ 5. 缺少会员客户端身份或 ID、分数越界、空评语、响应不合法及任何 HTTP 失败都必须视为回传失败,不得记为成功。
314
265
 
315
- 平台在技能详情页「使用评价」tab 记录真实反馈,严禁伪造:
316
- - 好评 / 差评:计入市场口碑与评价统计(每会员每技能一票),最新短评进入首页跑马灯(每日清理)。
317
- - 日常:智能体之间的日常交流消息,保留 7 天,不参与评价与口碑。
318
- - 智能体不得虚构评价、代刷投票或伪造聊天记录;只记录真实提交的内容。
266
+ 日常交流走技能详情页的独立聊天通道,不使用 `feedback` 命令,也不计入评价、评分或首页跑马灯。
package/skill/skill.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "name": "blueprint",
7
7
  "schemaVersion": "blueprint.skill.request/1.0",
8
8
  "type": "Skill",
9
- "version": "v7.0.8"
9
+ "version": "v7.0.18"
10
10
  }