specline 1.3.0 → 1.3.2

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 CHANGED
@@ -43,7 +43,7 @@
43
43
  - **确定性门禁**:每个阶段用 Shell 脚本的退出码判定是否通过,不做模糊判断
44
44
  - **黑盒测试**:测试 Agent 只看 Spec 文档,不能读取任何实现源码
45
45
  - **断点续跑**:随时中断,下次从最后一个可信门禁自动恢复(tasks.md 的 `[x]`/`[ ]` 标记进度)
46
- - **人机协作**:3 个人工检查点——Spec 确认、Review 可选复核、归档确认
46
+ - **人机协作**:3 个人工检查点——Spec 确认、Review 可选复核、归档确认,支持 `full`/`minimal`/`none` 三级自动化策略配置(`specline/config.yaml` 中 `pipeline.human_gate_policy`)
47
47
  - **Hook 约束体系**:sessionStart 注入 pipeline 上下文 → preToolUse 违规拦截 → postToolUse 操作后提醒,确保长对话中 Agent 不偏离流水线逻辑
48
48
  - **安全 Hook**:自动拦截危险 Shell 命令(如 `rm -rf`、`curl|bash`)+ 代码变更后自动格式化
49
49
  - **零外部依赖**:不依赖 OpenSpec CLI,全部功能自包含
@@ -77,10 +77,14 @@ my-project/
77
77
  │ ├── agents/ ← 9 个 Specline Agent 定义
78
78
  │ ├── commands/ ← 3 个 Slash 命令入口
79
79
  │ ├── skills/ ← 6 个 Skill 指令
80
+ │ │ └── specline-pipeline/
81
+ │ │ ├── SKILL.md ← 核心编排指令(~500 行)
82
+ │ │ ├── templates/ ← 子 Agent prompt 模板
83
+ │ │ └── references/ ← Schema / 事件日志 / 约束参考文档
80
84
  │ ├── hooks/ ← 7 个 Gate/Hook 脚本
81
85
  │ └── hooks.json ← Cursor Hook 配置
82
86
  ├── specline/ ← 运行时目录
83
- │ ├── config.yaml
87
+ │ ├── config.yaml ← 项目配置(含 pipeline 人机门禁策略)
84
88
  │ ├── changes/ ← 变更目录
85
89
  │ │ └── archive/ ← 归档目录
86
90
  │ └── specs/ ← 主规格目录
@@ -132,8 +136,8 @@ PHASE 1: SPEC(规格)
132
136
  ├── design.md — 技术设计(架构/数据流/决策)
133
137
  └── tasks.md — 任务清单(Type/Depends/Covers/Testable/Files + [ ] 进度标记)
134
138
  → specline-spec-reviewer 审核
135
- → Gate: grep + jq 格式校验
136
- → 🟡 人工确认 Spec 和任务规划
139
+ → Gate: grep + jq 格式校验 + semantic 语义检查(Covers 引用悬空 / 依赖环路 / 异常场景缺失 / 模糊需求检测)
140
+ → 🟡 人工确认 Spec 和任务规划(策略可配:`full` 需确认 / `minimal` `none` 自动通过)
137
141
 
138
142
  PHASE 2: CODING(编码)
139
143
  解析 tasks.md → 按依赖 DAG 分层 → 同批次前后端/config Agent 并发
@@ -152,7 +156,7 @@ PHASE 4: TEST(测试)
152
156
  → 自动重试最多 2 次
153
157
 
154
158
  PHASE 5: ARCHIVE(归档)
155
- → 🟡 人工确认归档
159
+ → 🟡 人工确认归档(策略可配:`full` `minimal` 需确认 / `none` 自动归档)
156
160
  → delta specs 合并到主规格目录
157
161
  → 按日期归档到 specline/changes/archive/
158
162
  ✅ 完成
@@ -205,6 +209,10 @@ PHASE 5: ARCHIVE(归档)
205
209
  ▼ ▼
206
210
  specline-pipeline SKILL ← 编排层 编排者直接执行(无子 Agent)
207
211
  │ Read → Write → ReadLints → Shell → 归档
212
+ ├── SKILL.md 核心编排指令(~500 行)
213
+ ├── templates/ subagent-prompts.md(3 套 prompt 模板)
214
+ └── references/ Schema / 事件日志 / 约束参考文档
215
+
208
216
  ┌───┼──────────────────┬──────────────────────┐
209
217
  ▼ ▼ ▼ ▼
210
218
  9 个子 Agent specline-pipeline- Cursor Hooks
@@ -218,7 +226,7 @@ specline-pipeline SKILL ← 编排层 编排者直接执行(
218
226
  |------|------|
219
227
  | `specline init [path]` | 在指定路径(默认当前目录)初始化 Specline 项目,复制模板文件并生成锁文件 |
220
228
  | `specline update` | 检查 CLI 是否有新版本可用(npm registry),输出更新提示 |
221
- | `specline sync [--dry-run] [path]` | 将上游最新模板文件同步到项目,基于 Lock File 智能识别安全更新/冲突/仅本地修改。`--dry-run` 预览变更不实际写入 |
229
+ | `specline sync [--dry-run] [path]` | 将上游最新模板文件同步到项目,基于 Lock File 智能识别安全更新/冲突/仅本地修改。hooks.json 语义合并(保留用户自定义 hook)、config.yaml 注释级更新(保留用户配置值)、CONFLICT 覆盖前自动创建 `.orig` 备份。`--dry-run` 预览变更不实际写入 |
222
230
  | `specline --version` | 显示当前 CLI 版本号 |
223
231
  | `specline --help` | 显示帮助信息 |
224
232
 
@@ -242,7 +250,7 @@ specline-pipeline SKILL ← 编排层 编排者直接执行(
242
250
 
243
251
  | 门禁 | 检查内容 |
244
252
  |------|---------|
245
- | Spec | `grep` 检查 Purpose/Requirements/Scenarios 章节完整性、WHEN/THEN 配对、Testable 字段格式与一致性 |
253
+ | Spec | 结构性检查(`grep` 检查章节完整性、WHEN/THEN 配对、字段格式)+ 语义检查(`semantic` 子命令:Covers 引用悬空、依赖环路、异常场景缺失、模糊需求、反向覆盖、Type-文件一致性,分 ERROR/WARNING/INFO 三级严重度) |
246
254
  | Build | `tsc --noEmit` / `python -m compileall` 编译检查 + Testable 任务单元测试文件存在性与语法检查 |
247
255
  | Lint | `ruff` / `eslint` 退出码 + code-review.json 中 error 数量 |
248
256
  | Test | 测试框架退出码 + 覆盖率阈值 |
package/cli.mjs CHANGED
@@ -88,11 +88,13 @@ function writeLockFile(projectDir, lockData) {
88
88
  }
89
89
 
90
90
  /**
91
- * 遍历 TEMPLATES_DIR 所有文件,构建锁数据结构
91
+ * 遍历指定目录所有文件,构建锁数据结构
92
+ * rootDir: 要遍历的根目录(必须是目标项目目录,这样 init 后锁哈希与实际文件一致)
92
93
  * 返回 { version, synced_at, files: Map<string, string> }
93
94
  */
94
- function buildLockData(projectDir) {
95
+ function buildLockData(projectDir, rootDir) {
95
96
  const files = new Map();
97
+ const walkRoot = rootDir || TEMPLATES_DIR;
96
98
 
97
99
  function walk(dir, base) {
98
100
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -107,7 +109,7 @@ function buildLockData(projectDir) {
107
109
  }
108
110
  }
109
111
 
110
- walk(TEMPLATES_DIR, '');
112
+ walk(walkRoot, '');
111
113
 
112
114
  return {
113
115
  version: VERSION,
@@ -345,10 +347,10 @@ function cmd_init(targetPath) {
345
347
  process.exit(1);
346
348
  }
347
349
 
348
- const configFile = join(target, '.specline-config.yaml');
350
+ const lockFile = join(target, 'specline', '.specline-lock.yaml');
349
351
  const forceMode = process.argv.includes('--force') || process.argv.includes('-f');
350
352
 
351
- if (existsSync(configFile) && !forceMode) {
353
+ if (existsSync(lockFile) && !forceMode) {
352
354
  warn('Specline 已在此项目中初始化。使用 --force 强制覆盖。');
353
355
  process.exit(0);
354
356
  }
@@ -404,13 +406,6 @@ function cmd_init(targetPath) {
404
406
  const skillsCount = countFiles(join(target, '.cursor', 'skills'));
405
407
  const hooksCount = countFiles(join(target, '.cursor', 'hooks'));
406
408
 
407
- // 写入初始化配置
408
- const initConfig = `# Specline 项目配置
409
- version: "${VERSION}"
410
- initialized_at: "${new Date().toISOString()}"
411
- `;
412
- writeFileSync(configFile, initConfig, 'utf-8');
413
-
414
409
  success('Specline 初始化完成');
415
410
  log(`📁 文件: ${commandsCount} commands, ${skillsCount} skills, ${agentsCount} agents, ${hooksCount} hooks`);
416
411
  log('');
@@ -423,7 +418,7 @@ initialized_at: "${new Date().toISOString()}"
423
418
  if (existsSync(lockPath) && !forceMode) {
424
419
  warn('锁文件已存在,跳过');
425
420
  } else {
426
- const lockData = buildLockData(target);
421
+ const lockData = buildLockData(target, target);
427
422
  writeLockFile(target, lockData);
428
423
  success('已生成锁文件');
429
424
  }
@@ -507,10 +502,19 @@ function cmd_sync({ dryRun, targetPath }) {
507
502
  const target = resolve(cwd, targetPath || '.');
508
503
 
509
504
  // 1. 检查项目是否已初始化
510
- const configFile = join(target, '.specline-config.yaml');
511
- if (!existsSync(configFile)) {
512
- error('未检测到 Specline 项目,请先运行 specline init');
513
- process.exit(1);
505
+ const lockFile = join(target, 'specline', '.specline-lock.yaml');
506
+ if (!existsSync(lockFile)) {
507
+ // 向后兼容:检查旧版 .specline-config.yaml
508
+ const oldMarker = join(target, '.specline-config.yaml');
509
+ if (existsSync(oldMarker)) {
510
+ warn('检测到旧版项目,正在自动迁移...');
511
+ const lockData = buildLockData(target, target);
512
+ writeLockFile(target, lockData);
513
+ success('已从旧版项目迁移,生成了锁文件');
514
+ } else {
515
+ error('未检测到 Specline 项目,请先运行 specline init');
516
+ process.exit(1);
517
+ }
514
518
  }
515
519
 
516
520
  // 2. 构建上游模板哈希映射
@@ -521,20 +525,14 @@ function cmd_sync({ dryRun, targetPath }) {
521
525
  const lockData = readLockFile(target);
522
526
 
523
527
  // 4. 版本校验
524
- if (lockData) {
525
- if (lockData.version === VERSION) {
526
- success('项目模板已与 CLI 版本同步 (v' + VERSION + ')');
527
- process.exit(0);
528
- }
529
- if (compareVersions(lockData.version, VERSION) > 0) {
530
- warn('锁文件版本 (v' + lockData.version + ') 高于 CLI 版本 (v' + VERSION + '),继续同步可能导致问题');
531
- if (!process.stdin.isTTY) {
532
- error('非交互式环境,已跳过同步');
533
- process.exit(1);
534
- }
535
- error('锁文件版本高于 CLI,请先更新 CLI');
528
+ if (lockData && compareVersions(lockData.version, VERSION) > 0) {
529
+ warn('锁文件版本 (v' + lockData.version + ') 高于 CLI 版本 (v' + VERSION + '),继续同步可能导致问题');
530
+ if (!process.stdin.isTTY) {
531
+ error('非交互式环境,已跳过同步');
536
532
  process.exit(1);
537
533
  }
534
+ error('锁文件版本高于 CLI,请先更新 CLI');
535
+ process.exit(1);
538
536
  }
539
537
 
540
538
  // 5. 收集所有需要分类的路径
@@ -621,7 +619,8 @@ function cmd_sync({ dryRun, targetPath }) {
621
619
  const labels = { NEW: '➕ 新增', WILL_UPDATE: '🔄 更新', CONFLICT: '⚠️ 冲突(将备份后覆盖)', NO_LOCK_CONFLICT: '⚠️ 无锁记录', UPSTREAM_REMOVED: '🗑️ 上游移除' };
622
620
  log(labels[r.type] + ' ' + r.path);
623
621
  }
624
- if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0 && stats.upstreamRemoved === 0) {
622
+ if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0
623
+ && stats.skippedModified === 0 && stats.upstreamRemoved === 0) {
625
624
  log('所有模板文件已是最新,无需同步');
626
625
  } else {
627
626
  log('\n以上为预览,未实际执行。去掉 --dry-run 以执行同步。');
@@ -718,9 +717,10 @@ function cmd_sync({ dryRun, targetPath }) {
718
717
  });
719
718
 
720
719
  // 11. 输出摘要
721
- if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0 && stats.upstreamRemoved === 0
720
+ if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0
721
+ && stats.skippedModified === 0 && stats.upstreamRemoved === 0
722
722
  && !mergeStats.hooksMerged && !mergeStats.configUpdated) {
723
- log('所有模板文件已是最新,无需同步');
723
+ success('项目模板已是最新,无需同步 (v' + VERSION + ')');
724
724
  } else {
725
725
  log('📊 同步摘要:');
726
726
  log(' 总模板文件: ' + allPaths.size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specline",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Spec-driven AI coding pipeline with deterministic quality gates for Cursor IDE",
5
5
  "bin": {
6
6
  "specline": "./cli.mjs"