@ecc-hgy/ae 0.3.0 → 0.5.0
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 +64 -17
- package/package.json +1 -1
- package/skills/diagnose/SKILL.md +1 -1
- package/skills/karpathy-guidelines/SKILL.md +64 -0
- package/skills/powerautomate-email-to-sharepoint-excel/powerautomate-email-to-sharepoint-excel-skill.md +496 -0
- package/skills/review/SKILL.md +2 -2
- package/skills/tdd/SKILL.md +2 -2
- package/skills/to-issues/SKILL.md +2 -2
- package/skills/to-issues/todo-template.md +2 -2
- package/skills/to-prd/SKILL.md +3 -3
- package/skills/using-agentic-engineering/SKILL.md +62 -0
- package/skills/verification-before-completion/SKILL.md +2 -2
- package/skills/writing-plans/SKILL.md +2 -2
- package/src/cli.js +1 -1
- package/src/commands/init.js +7 -4
- package/src/commands/setup.js +70 -11
- package/src/platforms.js +132 -0
- package/src/skeleton.js +4 -0
- package/templates/entries/AGENTS.md +0 -21
- package/templates/entries/README.md +24 -10
- package/templates/entries/spec/ADR/AGENTS.md +30 -0
- package/templates/entries/spec/ADR/CLAUDE.md +5 -0
- package/templates/entries/spec/AGENTS.md +35 -0
- package/templates/entries/spec/CLAUDE.md +5 -0
- package/templates/entries/spec/INDEX.md +13 -0
- package/templates/entries/spec/README.md +4 -0
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createDirectories, renderEntryFiles } from '../skeleton.js';
|
|
4
4
|
import { templatesPath } from '../utils/paths.js';
|
|
5
5
|
import { formatInitReport } from '../utils/report.js';
|
|
6
|
+
import { PLATFORMS, getPlatformSkillsDir } from '../platforms.js';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_GOAL = 'TODO: 用一句话说明这个项目要解决什么问题。';
|
|
8
9
|
|
|
@@ -111,10 +112,12 @@ async function ensureTargetDirectory(target, dryRun) {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
async function hasInstalledAssets(target) {
|
|
114
|
-
|
|
115
|
-
(await isDirectory(path.join(target,
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
for (const platform of PLATFORMS) {
|
|
116
|
+
if (await isDirectory(path.join(target, getPlatformSkillsDir(platform)))) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
async function statMaybe(value) {
|
package/src/commands/setup.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { mkdir, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
import { copyTreeIdempotent } from '../utils/copy.js';
|
|
4
5
|
import { skillsPath } from '../utils/paths.js';
|
|
5
6
|
import { formatSetupReport } from '../utils/report.js';
|
|
7
|
+
import {
|
|
8
|
+
PLATFORMS,
|
|
9
|
+
detectPlatforms,
|
|
10
|
+
getPlatformSkillsDir,
|
|
11
|
+
listPlatformIds,
|
|
12
|
+
parsePlatformList,
|
|
13
|
+
resolvePlatforms,
|
|
14
|
+
} from '../platforms.js';
|
|
6
15
|
|
|
7
16
|
const HELP = `Usage: ae setup [options]
|
|
8
17
|
|
|
9
18
|
Options:
|
|
10
19
|
--target <path> Install AE assets into this project directory. Defaults to cwd.
|
|
20
|
+
--platform <ids> Install for comma-separated platforms, e.g. claude,codex,cursor.
|
|
11
21
|
--dry-run Show what would be installed without writing files.
|
|
12
|
-
-h, --help Show help
|
|
22
|
+
-h, --help Show help
|
|
23
|
+
|
|
24
|
+
Platforms:
|
|
25
|
+
${listPlatformIds().join(', ')}`;
|
|
13
26
|
|
|
14
27
|
export async function run(argv = []) {
|
|
15
28
|
const options = parseArgs(argv);
|
|
@@ -22,20 +35,20 @@ export async function run(argv = []) {
|
|
|
22
35
|
const target = path.resolve(options.target ?? process.cwd());
|
|
23
36
|
|
|
24
37
|
try {
|
|
38
|
+
const explicitPlatforms =
|
|
39
|
+
options.platformIds.length > 0 ? resolvePlatforms(options.platformIds) : undefined;
|
|
25
40
|
await ensureTargetDirectory(target, options.dryRun);
|
|
41
|
+
const selectedPlatforms = explicitPlatforms ?? (await selectPlatforms(target));
|
|
26
42
|
|
|
27
43
|
const created = [];
|
|
28
44
|
const skipped = [];
|
|
29
|
-
const skillGroups =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
prefix: path.join('.codex', 'skills'),
|
|
37
|
-
},
|
|
38
|
-
];
|
|
45
|
+
const skillGroups = selectedPlatforms.map((platform) => {
|
|
46
|
+
const skillsDir = getPlatformSkillsDir(platform);
|
|
47
|
+
return {
|
|
48
|
+
dest: path.join(target, skillsDir),
|
|
49
|
+
prefix: skillsDir,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
39
52
|
|
|
40
53
|
for (const group of skillGroups) {
|
|
41
54
|
const result = await copyTreeIdempotent(skillsPath(), group.dest, {
|
|
@@ -57,6 +70,7 @@ export async function run(argv = []) {
|
|
|
57
70
|
function parseArgs(argv) {
|
|
58
71
|
const options = {
|
|
59
72
|
target: undefined,
|
|
73
|
+
platformIds: [],
|
|
60
74
|
dryRun: false,
|
|
61
75
|
help: false,
|
|
62
76
|
};
|
|
@@ -67,6 +81,13 @@ function parseArgs(argv) {
|
|
|
67
81
|
options.help = true;
|
|
68
82
|
} else if (arg === '--dry-run') {
|
|
69
83
|
options.dryRun = true;
|
|
84
|
+
} else if (arg === '--platform') {
|
|
85
|
+
const value = argv[index + 1];
|
|
86
|
+
if (!value) {
|
|
87
|
+
throw new Error('--platform requires a comma-separated platform list');
|
|
88
|
+
}
|
|
89
|
+
options.platformIds.push(...parsePlatformList(value));
|
|
90
|
+
index += 1;
|
|
70
91
|
} else if (arg === '--target') {
|
|
71
92
|
const value = argv[index + 1];
|
|
72
93
|
if (!value) {
|
|
@@ -82,6 +103,44 @@ function parseArgs(argv) {
|
|
|
82
103
|
return options;
|
|
83
104
|
}
|
|
84
105
|
|
|
106
|
+
async function selectPlatforms(target) {
|
|
107
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`No platforms selected. In non-interactive mode, pass --platform <ids>. Available: ${listPlatformIds().join(', ')}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const detected = await detectPlatforms(target);
|
|
114
|
+
const detectedIds = detected.map((platform) => platform.id);
|
|
115
|
+
const defaultIds = detectedIds;
|
|
116
|
+
|
|
117
|
+
console.log('Select target platforms for AE skills:');
|
|
118
|
+
for (const [index, platform] of PLATFORMS.entries()) {
|
|
119
|
+
const marker = defaultIds.includes(platform.id) ? '*' : ' ';
|
|
120
|
+
console.log(` ${index + 1}. [${marker}] ${platform.name} (${platform.id}) -> ${getPlatformSkillsDir(platform)}/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const defaultPrompt = defaultIds.length > 0 ? ` [${defaultIds.join(',')}]` : '';
|
|
124
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
+
try {
|
|
126
|
+
const answer = await rl.question(`Platform ids or numbers, comma-separated${defaultPrompt}: `);
|
|
127
|
+
const selected = answer.trim() ? parseInteractivePlatformAnswer(answer) : defaultIds;
|
|
128
|
+
return resolvePlatforms(selected);
|
|
129
|
+
} finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseInteractivePlatformAnswer(answer) {
|
|
135
|
+
return parsePlatformList(answer).map((item) => {
|
|
136
|
+
const number = Number(item);
|
|
137
|
+
if (Number.isInteger(number) && number >= 1 && number <= PLATFORMS.length) {
|
|
138
|
+
return PLATFORMS[number - 1].id;
|
|
139
|
+
}
|
|
140
|
+
return item;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
85
144
|
async function ensureTargetDirectory(target, dryRun) {
|
|
86
145
|
try {
|
|
87
146
|
const current = await stat(target);
|
package/src/platforms.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const PLATFORMS = [
|
|
5
|
+
{ id: 'claude', name: 'Claude Code', skillsDir: '.claude', aliases: ['claude-code'] },
|
|
6
|
+
{ id: 'codex', name: 'Codex', skillsDir: '.codex' },
|
|
7
|
+
{ id: 'windsurf', name: 'Windsurf', skillsDir: '.windsurf' },
|
|
8
|
+
{
|
|
9
|
+
id: 'github-copilot',
|
|
10
|
+
name: 'GitHub Copilot',
|
|
11
|
+
skillsDir: '.github',
|
|
12
|
+
aliases: ['copilot', 'github'],
|
|
13
|
+
detectionPaths: [
|
|
14
|
+
'.github/copilot-instructions.md',
|
|
15
|
+
'.github/instructions',
|
|
16
|
+
'.github/prompts',
|
|
17
|
+
'.github/skills',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{ id: 'kilocode', name: 'Kilo Code', skillsDir: '.kilocode', aliases: ['kilo-code'] },
|
|
21
|
+
{ id: 'kimicode', name: 'Kimi Code', skillsDir: '.kimi-code', aliases: ['kimi', 'kimi-code'] },
|
|
22
|
+
{ id: 'codebuddy', name: 'CodeBuddy', skillsDir: '.codebuddy', aliases: ['codebuddy-code'] },
|
|
23
|
+
{ id: 'qoder', name: 'Qoder', skillsDir: '.qoder' },
|
|
24
|
+
{ id: 'trae', name: 'Trae', skillsDir: '.trae' },
|
|
25
|
+
{ id: 'cursor', name: 'Cursor', skillsDir: '.cursor' },
|
|
26
|
+
{ id: 'opencode', name: 'OpenCode', skillsDir: '.opencode', aliases: ['open-code'] },
|
|
27
|
+
{ id: 'antigravity', name: 'Antigravity', skillsDir: '.agents' },
|
|
28
|
+
{ id: 'gemini', name: 'Gemini CLI', skillsDir: '.gemini', aliases: ['gemini-cli'] },
|
|
29
|
+
{ id: 'qwen', name: 'Qwen Code', skillsDir: '.qwen', aliases: ['qwen-code'] },
|
|
30
|
+
{ id: 'kiro', name: 'Kiro', skillsDir: '.kiro' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const PLATFORM_BY_ID_OR_ALIAS = new Map(
|
|
34
|
+
PLATFORMS.flatMap((platform) => [
|
|
35
|
+
[platform.id, platform],
|
|
36
|
+
...(platform.aliases ?? []).map((alias) => [alias, platform]),
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export function listPlatformIds() {
|
|
41
|
+
return PLATFORMS.map((platform) => platform.id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolvePlatforms(ids) {
|
|
45
|
+
const resolved = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const unknown = [];
|
|
48
|
+
|
|
49
|
+
for (const rawId of ids) {
|
|
50
|
+
const id = rawId.trim().toLowerCase();
|
|
51
|
+
if (!id) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const platform = PLATFORM_BY_ID_OR_ALIAS.get(id);
|
|
56
|
+
if (!platform) {
|
|
57
|
+
unknown.push(rawId);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!seen.has(platform.id)) {
|
|
62
|
+
resolved.push(platform);
|
|
63
|
+
seen.add(platform.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (unknown.length > 0) {
|
|
68
|
+
throw new Error(`Unknown platform(s): ${unknown.join(', ')}. Available: ${listPlatformIds().join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (resolved.length === 0) {
|
|
72
|
+
throw new Error(`No platforms selected. Available: ${listPlatformIds().join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parsePlatformList(value) {
|
|
79
|
+
return value
|
|
80
|
+
.split(',')
|
|
81
|
+
.map((item) => item.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getPlatformSkillsDir(platform) {
|
|
86
|
+
return path.join(platform.skillsDir, 'skills');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function detectPlatforms(target) {
|
|
90
|
+
const detected = [];
|
|
91
|
+
|
|
92
|
+
for (const platform of PLATFORMS) {
|
|
93
|
+
if (platform.detectionPaths?.length > 0) {
|
|
94
|
+
for (const detectionPath of platform.detectionPaths) {
|
|
95
|
+
if (await pathExists(path.join(target, detectionPath))) {
|
|
96
|
+
detected.push(platform);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (await isDirectory(path.join(target, platform.skillsDir))) {
|
|
104
|
+
detected.push(platform);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return detected;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function pathExists(value) {
|
|
112
|
+
try {
|
|
113
|
+
await stat(value);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code === 'ENOENT') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function isDirectory(value) {
|
|
124
|
+
try {
|
|
125
|
+
return (await stat(value)).isDirectory();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code === 'ENOENT') {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/skeleton.js
CHANGED
|
@@ -27,6 +27,10 @@ const ENTRY_FILES = [
|
|
|
27
27
|
'handoff.md',
|
|
28
28
|
path.join('spec', 'README.md'),
|
|
29
29
|
path.join('spec', 'INDEX.md'),
|
|
30
|
+
path.join('spec', 'AGENTS.md'),
|
|
31
|
+
path.join('spec', 'CLAUDE.md'),
|
|
32
|
+
path.join('spec', 'ADR', 'AGENTS.md'),
|
|
33
|
+
path.join('spec', 'ADR', 'CLAUDE.md'),
|
|
30
34
|
];
|
|
31
35
|
|
|
32
36
|
export async function createDirectories(target, options = {}) {
|
|
@@ -1,24 +1,3 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
3
|
> {{PROJECT_GOAL}}
|
|
4
|
-
|
|
5
|
-
## Project-Specific Rules
|
|
6
|
-
|
|
7
|
-
<!-- 在这里写本项目的稳定规则,例如:
|
|
8
|
-
- 代码风格 / 注释语言
|
|
9
|
-
- 提交规范
|
|
10
|
-
- 协作偏好
|
|
11
|
-
这些规则会被 Claude Code / Codex 在每次 session 自动加载。
|
|
12
|
-
未来新规则直接追加到本段,不需要其他工具命令。 -->
|
|
13
|
-
|
|
14
|
-
## Stack
|
|
15
|
-
|
|
16
|
-
TODO: 简述项目技术栈与运行环境。
|
|
17
|
-
|
|
18
|
-
## Structure
|
|
19
|
-
|
|
20
|
-
TODO: 简述目录结构,标注关键入口。
|
|
21
|
-
|
|
22
|
-
## Operating Guidelines
|
|
23
|
-
|
|
24
|
-
TODO: 简述代码、测试、提交、PR 的操作规范。
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
{{PROJECT_GOAL}}
|
|
3
|
+
> {{PROJECT_GOAL}}
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<!--
|
|
6
|
+
根 README 的核心:让别人 30 秒搞清「为什么有用、能做什么、怎么用」。
|
|
7
|
+
下面是建议骨架,按需增删、不必都填,也不必拘泥小节名:
|
|
8
|
+
- 它是什么 + 一句话价值 → 上面的标题 + 一行(已占位)
|
|
9
|
+
- 为什么 / 给谁 → 解决什么问题、面向谁
|
|
10
|
+
- Quick Start → 最快见效的一段命令 / 步骤(装好 + 第一个能见效的操作)
|
|
11
|
+
- 使用示例 → 典型场景(必要时再贴一段真实用法)
|
|
12
|
+
- 目录地图 / 文档入口 → 关键目录 + 深入文档的链接
|
|
13
|
+
-->
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
## 这是什么
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
TODO: 一段话说清它解决什么问题、给谁用、用它能做什么。
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
## Quick Start
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
TODO: 最快跑起来的命令或步骤。
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
<!-- ## 使用示例 —— 有典型场景时再加,贴一段真实用法 -->
|
|
24
|
+
|
|
25
|
+
## 目录地图
|
|
26
|
+
|
|
27
|
+
- `spec/`:SDD/TDD 规格产物——需求索引看 `spec/INDEX.md`,项目地图看 `spec/README.md`。
|
|
28
|
+
- `reference/`:原始素材,只读心态。
|
|
29
|
+
- `data/`:运行产物、导出、缓存。
|
|
30
|
+
- `scripts/`:辅助脚本。
|
|
31
|
+
|
|
32
|
+
<!-- 文档入口:AGENTS.md(项目指令)· handoff.md(当前态快照)· spec/INDEX.md(需求索引) -->
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# spec/ADR/ 规则
|
|
2
|
+
|
|
3
|
+
> 架构决策记录(ADR)规则。在 `spec/ADR/` 下创建或修改 ADR 时遵守本文件。
|
|
4
|
+
|
|
5
|
+
## 命名
|
|
6
|
+
|
|
7
|
+
`NNNN-<标题>.md`,**4 位序号**,kebab-case 标题。例:`0001-postgres-as-primary-store.md`。
|
|
8
|
+
|
|
9
|
+
## frontmatter
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
---
|
|
13
|
+
status: accepted # proposed | accepted | superseded
|
|
14
|
+
date: 2026-06-14
|
|
15
|
+
supersedes: 0000-xxx.md # 仅当取代旧 ADR 时填
|
|
16
|
+
---
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
正文按具体决策自然组织(背景 / 决策 / 理由 / 影响),不强加重模板。
|
|
20
|
+
|
|
21
|
+
## 状态机
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
proposed ─(评审接受)─► accepted ─(新 ADR 取代)─► superseded
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `proposed`:新提案,未拍板(由 writing-plans / review / improve-codebase-architecture 创建时默认)。
|
|
28
|
+
- `accepted`:已评审接受;视为**约束**,其他需求不得违背。
|
|
29
|
+
- `superseded`:被新 ADR 取代;新 ADR 的 `supersedes` 指回旧编号,**不删旧文件**。
|
|
30
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# spec/ 规则
|
|
2
|
+
|
|
3
|
+
> 本目录(`spec/`)的工作规则。在 `spec/` 下读写文件时遵守本文件。
|
|
4
|
+
|
|
5
|
+
## 总领
|
|
6
|
+
|
|
7
|
+
S1 节点 → 产物 → skill 的路由总览,见随包的 `using-agentic-engineering` skill——它判定"进不进 AE 流程"并分发到各节点。
|
|
8
|
+
|
|
9
|
+
两道硬门禁:**A1 未对齐不写 prd**;**B2 期望态未对齐不修复**。
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## R1:落盘后同步 INDEX
|
|
13
|
+
落盘 `spec/needs/<需求名>/` 下任何文件后,**必须同步更新 `spec/INDEX.md`** 对应行。若不确定如何更新,按上面「INDEX 推导规则」手动重算(未来可用 `ae index-rebuild` 幂等重扫)。
|
|
14
|
+
|
|
15
|
+
### INDEX 推导规则
|
|
16
|
+
|
|
17
|
+
`spec/INDEX.md` 给模型看:一次扫描看到所有需求 + 状态 + 进度。`当前节点` 列**不存储**在任何 frontmatter,由该需求下文件状态**实时推导**:
|
|
18
|
+
|
|
19
|
+
| 条件 | 当前节点 |
|
|
20
|
+
|------|---------|
|
|
21
|
+
| prd 不存在 或 status=draft | A1 / A2 |
|
|
22
|
+
| prd active + design 不存在/draft | A3 设计 |
|
|
23
|
+
| prd+design active + todo 不存在 | A4 拆解 |
|
|
24
|
+
| prd+design active + todo 有未完成项 | A5 执行 |
|
|
25
|
+
| todo 全勾 + 未端到端验证 | A6 verify |
|
|
26
|
+
| 所有 issues 已验收 + 已交付 | 已完成 |
|
|
27
|
+
| 存在 active 的 issues | 进 B 子路径,按 issues 状态推 B2…B7 |
|
|
28
|
+
| prd / design 任一 archived | 已归档 |
|
|
29
|
+
|
|
30
|
+
- INDEX 是**推导态**:文件状态变就重新推导,**不要手编 `当前节点` 列**。
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## 何时升 ADR
|
|
34
|
+
|
|
35
|
+
决策影响**多个需求**或属项目级不变量时,从 `design.md` 升到`spec/ADR`(判断:推翻它会改动 >1 个需求)。单需求的技术决策留在该需求的 `design.md`。
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## 需求
|
|
4
4
|
|
|
5
|
+
<!--
|
|
6
|
+
各列怎么填:
|
|
7
|
+
- 需求名:`spec/needs/<需求名>/` 的 kebab 目录名
|
|
8
|
+
- prd / design:对应文件 frontmatter 的 status(draft/active/archived);文件不存在填 -
|
|
9
|
+
- todo:todo.md 的 已勾数/总数(如 5/8,即拆解任务的完成进度);无 todo.md 填 -。这是 A5 内部子进度,不是节点位次(节点看「当前节点」列)
|
|
10
|
+
- issues:该需求 issues/ 下文件数 + 是否有未结(如 0 / 1 active);无则 -
|
|
11
|
+
- 当前节点:由文件状态推导得出,不手编(推导规则见 spec/AGENTS.md「INDEX 推导规则」)
|
|
12
|
+
-->
|
|
13
|
+
|
|
5
14
|
| 需求名 | prd | design | todo | issues | 当前节点 |
|
|
6
15
|
|--------|-----|--------|------|--------|----------|
|
|
7
16
|
|
|
8
17
|
## ADR
|
|
9
18
|
|
|
19
|
+
<!-- 编号 4 位 / 状态 proposed|accepted|superseded / 日期,均取自各 ADR frontmatter(详见 spec/ADR/AGENTS.md) -->
|
|
20
|
+
|
|
10
21
|
| 编号 | 标题 | 状态 | 日期 |
|
|
11
22
|
|------|------|------|------|
|
|
12
23
|
|
|
13
24
|
## 跨需求引用
|
|
14
25
|
|
|
26
|
+
<!-- 从各 prd/design frontmatter 的 related-needs 聚合 -->
|
|
27
|
+
|
|
15
28
|
- 暂无
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Spec Map
|
|
2
2
|
|
|
3
|
+
> 给人看:回忆"项目长什么样"。以 ASCII 图为主,可省细节、突出全貌。**README ≠ INDEX 镜像**——INDEX 给模型看进度,这里给人看全貌。
|
|
4
|
+
|
|
3
5
|
## Needs
|
|
4
6
|
|
|
5
7
|
```text
|
|
@@ -12,6 +14,8 @@ TODO: 用 ASCII 图画出需求地图。
|
|
|
12
14
|
TODO: 用 ASCII 图画出核心业务流程。
|
|
13
15
|
```
|
|
14
16
|
|
|
17
|
+
> 全局架构总览画在这里(而非写进某个需求的 design.md)。
|
|
18
|
+
|
|
15
19
|
## Relationships
|
|
16
20
|
|
|
17
21
|
```text
|