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 +15 -7
- package/cli.mjs +32 -32
- package/package.json +1 -1
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +58 -600
- package/templates/.cursor/skills/specline-pipeline/references/error-recovery-details.md +49 -0
- package/templates/.cursor/skills/specline-pipeline/references/event-log-spec.md +59 -0
- package/templates/.cursor/skills/specline-pipeline/references/pipeline-state-schema.md +87 -0
- package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +221 -0
- package/templates/.specline-config.yaml +0 -1
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
|
|
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 |
|
|
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
|
-
*
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
511
|
-
if (!existsSync(
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
723
|
+
success('项目模板已是最新,无需同步 (v' + VERSION + ')');
|
|
724
724
|
} else {
|
|
725
725
|
log('📊 同步摘要:');
|
|
726
726
|
log(' 总模板文件: ' + allPaths.size);
|