@betterdanlins/ai-memory 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 +71 -3
- package/README.zh-CN.md +71 -3
- package/bin/cli.js +105 -3
- package/package.json +1 -1
- package/src/framework.js +57 -17
- package/src/managed-blocks.js +62 -0
- package/src/manifest.js +7 -1
- package/src/model-routing.js +108 -0
- package/src/workflow.js +185 -0
- package/templates/claude/.claude/agents/economy-test-worker.md +8 -0
- package/templates/claude/.claude/agents/premium-implementer.md +8 -0
- package/templates/claude/.claude/agents/premium-planner.md +8 -0
- package/templates/claude/.claude/agents/premium-reviewer.md +8 -0
- package/templates/claude/.claude/agents/standard-implementer.md +8 -0
- package/templates/claude/.claude/agents/standard-test-worker.md +8 -0
- package/templates/claude/.claude/skills/model-routing/SKILL.md +6 -0
- package/templates/claude/CLAUDE.md +6 -0
- package/templates/codex/.agents/skills/model-routing/SKILL.md +6 -0
- package/templates/codex/.codex/agents/economy_test_worker.toml +6 -0
- package/templates/codex/.codex/agents/premium_implementer.toml +6 -0
- package/templates/codex/.codex/agents/premium_planner.toml +6 -0
- package/templates/codex/.codex/agents/premium_reviewer.toml +6 -0
- package/templates/codex/.codex/agents/standard_implementer.toml +6 -0
- package/templates/codex/.codex/agents/standard_test_worker.toml +6 -0
- package/templates/codex/AGENTS.md +6 -0
- package/templates/common/.ai/README.md +4 -1
- package/templates/common/.ai/config/model-routing.json +5 -0
- package/templates/common/.ai/runs/.gitignore.template +2 -0
- package/templates/common/.ai/skills/critic.md +1 -0
- package/templates/common/.ai/skills/delivery-readiness.md +2 -0
- package/templates/common/.ai/skills/feature-design.md +2 -0
- package/templates/common/.ai/skills/model-routing.md +39 -0
- package/templates/common/.ai/skills/requirements-flow.md +2 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const MANAGED_BLOCK_START = '<!-- ai-memory:managed:start -->';
|
|
2
|
+
export const MANAGED_BLOCK_END = '<!-- ai-memory:managed:end -->';
|
|
3
|
+
|
|
4
|
+
const MANAGED_BLOCK_FILES = new Set(['AGENTS.md', 'CLAUDE.md']);
|
|
5
|
+
|
|
6
|
+
export class ManagedBlockError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ManagedBlockError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isManagedBlockFile(dest) {
|
|
14
|
+
return MANAGED_BLOCK_FILES.has(dest);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function replaceManagedBlock(current, desired) {
|
|
18
|
+
const currentBlock = locateManagedBlock(current, '当前文件');
|
|
19
|
+
const desiredBlock = locateManagedBlock(desired, '新版模板');
|
|
20
|
+
const eol = current.includes('\r\n') ? '\r\n' : '\n';
|
|
21
|
+
const replacement = normalizeEol(desired.slice(desiredBlock.start, desiredBlock.end), eol);
|
|
22
|
+
return current.slice(0, currentBlock.start) + replacement + current.slice(currentBlock.end);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hasValidManagedBlock(content) {
|
|
26
|
+
try {
|
|
27
|
+
locateManagedBlock(content, '文件');
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err instanceof ManagedBlockError) return false;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function locateManagedBlock(content, label) {
|
|
36
|
+
if (typeof content !== 'string') throw new ManagedBlockError(`${label}内容必须是字符串`);
|
|
37
|
+
const starts = occurrences(content, MANAGED_BLOCK_START);
|
|
38
|
+
const ends = occurrences(content, MANAGED_BLOCK_END);
|
|
39
|
+
if (starts.length !== 1 || ends.length !== 1) {
|
|
40
|
+
throw new ManagedBlockError(`${label}必须且只能包含一组 ai-memory managed 标记`);
|
|
41
|
+
}
|
|
42
|
+
const start = starts[0];
|
|
43
|
+
const end = ends[0] + MANAGED_BLOCK_END.length;
|
|
44
|
+
if (end <= start) throw new ManagedBlockError(`${label}的 ai-memory managed 标记顺序无效`);
|
|
45
|
+
return { start, end };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function occurrences(content, token) {
|
|
49
|
+
const positions = [];
|
|
50
|
+
let offset = 0;
|
|
51
|
+
while (offset < content.length) {
|
|
52
|
+
const index = content.indexOf(token, offset);
|
|
53
|
+
if (index === -1) break;
|
|
54
|
+
positions.push(index);
|
|
55
|
+
offset = index + token.length;
|
|
56
|
+
}
|
|
57
|
+
return positions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeEol(content, eol) {
|
|
61
|
+
return content.replace(/\r?\n/g, eol);
|
|
62
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function buildManifest(templatesRoot, tools) {
|
|
|
13
13
|
if (!enabled(tools)) continue;
|
|
14
14
|
const groupDir = path.join(templatesRoot, group);
|
|
15
15
|
for (const rel of await walk(groupDir, '')) {
|
|
16
|
-
entries.push({ src: path.join(groupDir, ...rel.split('/')), dest: rel });
|
|
16
|
+
entries.push({ src: path.join(groupDir, ...rel.split('/')), dest: templateDestination(rel) });
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
const sorted = entries.sort((a, b) => a.dest < b.dest ? -1 : a.dest > b.dest ? 1 : 0);
|
|
@@ -21,6 +21,12 @@ export async function buildManifest(templatesRoot, tools) {
|
|
|
21
21
|
return sorted;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function templateDestination(rel) {
|
|
25
|
+
return rel.endsWith('/.gitignore.template') || rel === '.gitignore.template'
|
|
26
|
+
? rel.slice(0, -'.template'.length)
|
|
27
|
+
: rel;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
function assertUniqueDestinations(entries) {
|
|
25
31
|
const sourcesByDest = new Map();
|
|
26
32
|
for (const { src, dest } of entries) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { assertNoSymlinkPath } from './path-safety.js';
|
|
4
|
+
|
|
5
|
+
export const MODEL_ROUTING_DEST = '.ai/config/model-routing.json';
|
|
6
|
+
export const MODEL_ROUTING_SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
export const STAGES = [
|
|
9
|
+
'brainstorm',
|
|
10
|
+
'requirement-finalize',
|
|
11
|
+
'feature-design',
|
|
12
|
+
'write-plan',
|
|
13
|
+
'implementation',
|
|
14
|
+
'routine-tests',
|
|
15
|
+
'test-execution',
|
|
16
|
+
'failure-diagnosis',
|
|
17
|
+
'adversarial-testing',
|
|
18
|
+
'final-review',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const PROFILES = {
|
|
22
|
+
inherit: Object.fromEntries(STAGES.map(stage => [stage, stage === 'test-execution' ? 'none' : 'inherit'])),
|
|
23
|
+
balanced: {
|
|
24
|
+
brainstorm: 'premium',
|
|
25
|
+
'requirement-finalize': 'premium',
|
|
26
|
+
'feature-design': 'premium',
|
|
27
|
+
'write-plan': 'premium',
|
|
28
|
+
implementation: 'standard',
|
|
29
|
+
'routine-tests': 'economy',
|
|
30
|
+
'test-execution': 'none',
|
|
31
|
+
'failure-diagnosis': 'standard',
|
|
32
|
+
'adversarial-testing': 'premium',
|
|
33
|
+
'final-review': 'premium',
|
|
34
|
+
},
|
|
35
|
+
quality: {
|
|
36
|
+
brainstorm: 'premium',
|
|
37
|
+
'requirement-finalize': 'premium',
|
|
38
|
+
'feature-design': 'premium',
|
|
39
|
+
'write-plan': 'premium',
|
|
40
|
+
implementation: 'premium',
|
|
41
|
+
'routine-tests': 'standard',
|
|
42
|
+
'test-execution': 'none',
|
|
43
|
+
'failure-diagnosis': 'premium',
|
|
44
|
+
'adversarial-testing': 'premium',
|
|
45
|
+
'final-review': 'premium',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const MODEL_PROFILES = Object.freeze(Object.keys(PROFILES));
|
|
50
|
+
|
|
51
|
+
export function createModelRoutingConfig(profile = 'inherit') {
|
|
52
|
+
assertProfile(profile);
|
|
53
|
+
return { schemaVersion: MODEL_ROUTING_SCHEMA_VERSION, profile, overrides: {} };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveModelRouting(config) {
|
|
57
|
+
validateModelRoutingConfig(config);
|
|
58
|
+
return { ...PROFILES[config.profile], ...config.overrides };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function readModelRoutingConfig(targetDir) {
|
|
62
|
+
const configPath = path.join(targetDir, ...MODEL_ROUTING_DEST.split('/'));
|
|
63
|
+
await assertNoSymlinkPath(targetDir, configPath);
|
|
64
|
+
let config;
|
|
65
|
+
try {
|
|
66
|
+
config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err.code === 'ENOENT') throw new Error(`缺少模型路由配置: ${MODEL_ROUTING_DEST};请先更新 ai-memory 框架`);
|
|
69
|
+
if (err instanceof SyntaxError) throw new Error(`模型路由配置不是有效 JSON: ${MODEL_ROUTING_DEST}`, { cause: err });
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
validateModelRoutingConfig(config);
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function writeModelRoutingConfig(targetDir, config) {
|
|
77
|
+
validateModelRoutingConfig(config);
|
|
78
|
+
const configPath = path.join(targetDir, ...MODEL_ROUTING_DEST.split('/'));
|
|
79
|
+
await assertNoSymlinkPath(targetDir, configPath);
|
|
80
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
81
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateModelRoutingConfig(config) {
|
|
86
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
87
|
+
throw new Error('模型路由配置必须是对象');
|
|
88
|
+
}
|
|
89
|
+
if (config.schemaVersion !== MODEL_ROUTING_SCHEMA_VERSION) {
|
|
90
|
+
throw new Error(`不支持的模型路由 Schema: ${config.schemaVersion}`);
|
|
91
|
+
}
|
|
92
|
+
assertProfile(config.profile);
|
|
93
|
+
if (!config.overrides || typeof config.overrides !== 'object' || Array.isArray(config.overrides)) {
|
|
94
|
+
throw new Error('模型路由 overrides 必须是对象');
|
|
95
|
+
}
|
|
96
|
+
for (const [stage, tier] of Object.entries(config.overrides)) {
|
|
97
|
+
if (!STAGES.includes(stage)) throw new Error(`未知模型路由阶段: ${stage}`);
|
|
98
|
+
if (!['inherit', 'premium', 'standard', 'economy', 'none'].includes(tier)) {
|
|
99
|
+
throw new Error(`阶段 ${stage} 的模型等级无效: ${tier}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertProfile(profile) {
|
|
105
|
+
if (!MODEL_PROFILES.includes(profile)) {
|
|
106
|
+
throw new Error(`model profile 仅支持 ${MODEL_PROFILES.join('、')},收到: ${profile}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readModelRoutingConfig, resolveModelRouting, STAGES } from './model-routing.js';
|
|
5
|
+
import { assertNoSymlinkPath } from './path-safety.js';
|
|
6
|
+
|
|
7
|
+
const RISKS = ['S', 'M', 'L'];
|
|
8
|
+
|
|
9
|
+
export async function prepareHandoff({
|
|
10
|
+
targetDir, feature, stage, risk, inputs, acceptanceCriteria = [], allowedChangeScope = [], unresolvedDecisions = [],
|
|
11
|
+
}) {
|
|
12
|
+
validateFeature(feature);
|
|
13
|
+
if (!STAGES.includes(stage)) throw new Error(`未知工作流阶段: ${stage}`);
|
|
14
|
+
if (!RISKS.includes(risk)) throw new Error(`risk 仅支持 S、M、L,收到: ${risk}`);
|
|
15
|
+
if (!Array.isArray(inputs) || inputs.length === 0) throw new Error('至少需要一个 --input 正式输入文件');
|
|
16
|
+
if (requiresAcceptance(stage) && acceptanceCriteria.length === 0) {
|
|
17
|
+
throw new Error(`阶段 ${stage} 至少需要一个 --acceptance 验收标准编号`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const routing = resolveModelRouting(await readModelRoutingConfig(targetDir));
|
|
21
|
+
const records = [];
|
|
22
|
+
for (const input of [...new Set(inputs)]) {
|
|
23
|
+
const { relative, absolute } = resolveProjectInput(targetDir, input);
|
|
24
|
+
await assertNoSymlinkPath(targetDir, absolute);
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await readFile(absolute);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === 'ENOENT' || err.code === 'EISDIR') throw new Error(`交接输入不是可读文件: ${relative}`, { cause: err });
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
records.push({ path: relative, sha256: createHash('sha256').update(content).digest('hex') });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handoff = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
featureId: feature,
|
|
38
|
+
riskLevel: risk,
|
|
39
|
+
stage,
|
|
40
|
+
executorTier: routing[stage],
|
|
41
|
+
createdAt: new Date().toISOString(),
|
|
42
|
+
inputs: records,
|
|
43
|
+
acceptanceCriteria: uniqueStrings(acceptanceCriteria),
|
|
44
|
+
unresolvedDecisions: uniqueStrings(unresolvedDecisions),
|
|
45
|
+
allowedChangeScope: uniqueStrings(allowedChangeScope),
|
|
46
|
+
};
|
|
47
|
+
const handoffPath = handoffFile(targetDir, feature);
|
|
48
|
+
await assertNoSymlinkPath(targetDir, handoffPath);
|
|
49
|
+
await mkdir(path.dirname(handoffPath), { recursive: true });
|
|
50
|
+
await writeFile(handoffPath, JSON.stringify(handoff, null, 2) + '\n');
|
|
51
|
+
return { handoff, path: relativeFromRoot(targetDir, handoffPath) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function verifyHandoff({ targetDir, feature, expectedStage }) {
|
|
55
|
+
validateFeature(feature);
|
|
56
|
+
const handoffPath = handoffFile(targetDir, feature);
|
|
57
|
+
await assertNoSymlinkPath(targetDir, handoffPath);
|
|
58
|
+
let handoff;
|
|
59
|
+
try {
|
|
60
|
+
handoff = JSON.parse(await readFile(handoffPath, 'utf8'));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err.code === 'ENOENT') throw new Error(`缺少交接清单: ${relativeFromRoot(targetDir, handoffPath)}`);
|
|
63
|
+
if (err instanceof SyntaxError) throw new Error(`交接清单不是有效 JSON: ${relativeFromRoot(targetDir, handoffPath)}`, { cause: err });
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
validateHandoff(handoff, feature);
|
|
67
|
+
if (expectedStage && handoff.stage !== expectedStage) {
|
|
68
|
+
throw new Error(`交接阶段不匹配: 期望 ${expectedStage},实际 ${handoff.stage}`);
|
|
69
|
+
}
|
|
70
|
+
if (handoff.unresolvedDecisions.length) {
|
|
71
|
+
throw new Error(`仍有 ${handoff.unresolvedDecisions.length} 个未决问题,不能进入 ${handoff.stage}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const routing = resolveModelRouting(await readModelRoutingConfig(targetDir));
|
|
75
|
+
if (handoff.executorTier !== routing[handoff.stage]) {
|
|
76
|
+
throw new Error(`模型路由已变化: ${handoff.stage} 当前应使用 ${routing[handoff.stage]},请重新 prepare`);
|
|
77
|
+
}
|
|
78
|
+
for (const record of handoff.inputs) {
|
|
79
|
+
const { absolute } = resolveProjectInput(targetDir, record.path);
|
|
80
|
+
await assertNoSymlinkPath(targetDir, absolute);
|
|
81
|
+
let current;
|
|
82
|
+
try {
|
|
83
|
+
current = await readFile(absolute);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.code === 'ENOENT') throw new Error(`交接输入已不存在: ${record.path}`);
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
const currentHash = createHash('sha256').update(current).digest('hex');
|
|
89
|
+
if (currentHash !== record.sha256) throw new Error(`交接输入已变化,请重新 prepare: ${record.path}`);
|
|
90
|
+
}
|
|
91
|
+
return handoff;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function recordStageResult({ targetDir, feature, resultPath }) {
|
|
95
|
+
const handoff = await verifyHandoff({ targetDir, feature });
|
|
96
|
+
const source = resolveProjectInput(targetDir, resultPath);
|
|
97
|
+
await assertNoSymlinkPath(targetDir, source.absolute);
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = JSON.parse(await readFile(source.absolute, 'utf8'));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err.code === 'ENOENT') throw new Error(`阶段回执不存在: ${source.relative}`);
|
|
103
|
+
if (err instanceof SyntaxError) throw new Error(`阶段回执不是有效 JSON: ${source.relative}`, { cause: err });
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
validateStageResult(result, handoff);
|
|
107
|
+
const destination = path.join(targetDir, '.ai', 'runs', feature, `${handoff.stage}-result.json`);
|
|
108
|
+
await assertNoSymlinkPath(targetDir, destination);
|
|
109
|
+
await writeFile(destination, JSON.stringify(result, null, 2) + '\n');
|
|
110
|
+
return { result, path: relativeFromRoot(targetDir, destination) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateHandoff(handoff, feature) {
|
|
114
|
+
if (!handoff || handoff.schemaVersion !== 1 || handoff.featureId !== feature) {
|
|
115
|
+
throw new Error(`交接清单格式或 featureId 无效: ${feature}`);
|
|
116
|
+
}
|
|
117
|
+
if (!STAGES.includes(handoff.stage) || !RISKS.includes(handoff.riskLevel)) {
|
|
118
|
+
throw new Error(`交接清单阶段或风险等级无效: ${feature}`);
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(handoff.inputs) || !handoff.inputs.length
|
|
121
|
+
|| !Array.isArray(handoff.acceptanceCriteria)
|
|
122
|
+
|| !Array.isArray(handoff.unresolvedDecisions)
|
|
123
|
+
|| !Array.isArray(handoff.allowedChangeScope)) {
|
|
124
|
+
throw new Error(`交接清单缺少必要字段: ${feature}`);
|
|
125
|
+
}
|
|
126
|
+
if (!['inherit', 'premium', 'standard', 'economy', 'none'].includes(handoff.executorTier)
|
|
127
|
+
|| handoff.inputs.some(record => !record || typeof record.path !== 'string' || !/^[a-f0-9]{64}$/.test(record.sha256))) {
|
|
128
|
+
throw new Error(`交接清单输入或执行等级无效: ${feature}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateStageResult(result, handoff) {
|
|
133
|
+
if (!result || typeof result !== 'object' || result.stage !== handoff.stage) {
|
|
134
|
+
throw new Error(`阶段回执 stage 必须为 ${handoff.stage}`);
|
|
135
|
+
}
|
|
136
|
+
if (!['completed', 'blocked', 'failed'].includes(result.status)) throw new Error(`阶段回执 status 无效: ${result.status}`);
|
|
137
|
+
for (const field of ['changedFiles', 'tests', 'designDeviations', 'unresolvedRisks']) {
|
|
138
|
+
if (!Array.isArray(result[field]) || result[field].some(value => typeof value !== 'string')) {
|
|
139
|
+
throw new Error(`阶段回执 ${field} 必须是字符串数组`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!result.acceptanceCoverage || typeof result.acceptanceCoverage !== 'object' || Array.isArray(result.acceptanceCoverage)) {
|
|
143
|
+
throw new Error('阶段回执 acceptanceCoverage 必须是对象');
|
|
144
|
+
}
|
|
145
|
+
for (const [id, coverage] of Object.entries(result.acceptanceCoverage)) {
|
|
146
|
+
if (!id.trim() || !['covered', 'partial', 'not-covered', 'not-applicable'].includes(coverage)) {
|
|
147
|
+
throw new Error(`阶段回执验收覆盖无效: ${id}=${coverage}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const missing = handoff.acceptanceCriteria.filter(id => !(id in result.acceptanceCoverage));
|
|
151
|
+
if (missing.length) throw new Error(`阶段回执缺少验收覆盖: ${missing.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveProjectInput(targetDir, input) {
|
|
155
|
+
if (typeof input !== 'string' || !input.trim() || path.isAbsolute(input)) throw new Error(`输入路径必须是项目内相对路径: ${input}`);
|
|
156
|
+
const root = path.resolve(targetDir);
|
|
157
|
+
const absolute = path.resolve(root, input);
|
|
158
|
+
const relative = path.relative(root, absolute);
|
|
159
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
160
|
+
throw new Error(`输入路径越出项目目录: ${input}`);
|
|
161
|
+
}
|
|
162
|
+
return { absolute, relative: relative.split(path.sep).join('/') };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handoffFile(targetDir, feature) {
|
|
166
|
+
return path.join(targetDir, '.ai', 'runs', feature, 'handoff.json');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function relativeFromRoot(targetDir, filePath) {
|
|
170
|
+
return path.relative(path.resolve(targetDir), filePath).split(path.sep).join('/');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateFeature(feature) {
|
|
174
|
+
if (typeof feature !== 'string' || !/^[\p{L}\p{N}][\p{L}\p{N}._-]*$/u.test(feature)) {
|
|
175
|
+
throw new Error(`feature 必须是单个安全目录名,收到: ${feature}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function uniqueStrings(values) {
|
|
180
|
+
return [...new Set(values.map(value => value.trim()).filter(Boolean))];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function requiresAcceptance(stage) {
|
|
184
|
+
return ['implementation', 'routine-tests', 'adversarial-testing', 'final-review'].includes(stage);
|
|
185
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: standard-implementer
|
|
3
|
+
description: 按已批准设计和计划实现功能。仅在模型路由要求 standard 实现或修复时使用。
|
|
4
|
+
model: sonnet
|
|
5
|
+
tools: Read, Edit, Write, Bash, Grep, Glob
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
先验证 handoff 并读取全部正式输入。只在 allowedChangeScope 内按计划实现和测试;不得重新定义需求或接口。发现设计缺口时停止并升级给 premium-planner。完成后写结构化阶段回执。
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
+
<!-- ai-memory:managed:start -->
|
|
3
4
|
## AI 协作框架(ai-memory)
|
|
4
5
|
|
|
5
6
|
进场先读 `.ai/memory/MEMORY.md`,随后遵循 `.ai/README.md` 的进场协议。
|
|
7
|
+
本 managed 区块由 ai-memory 更新;项目命令和自定义规则只写到下方 user 区块。
|
|
6
8
|
|
|
7
9
|
- 0→1 项目/首次架构基线/重大重构 → project-inception skill,产物在 `docs/architecture/`;普通功能不要重复触发
|
|
8
10
|
- 需求 what/why 与外部行为定稿 → requirements-flow skill,产物在 `docs/requirements/vX.Y.Z/{draft,final}/`
|
|
@@ -10,6 +12,7 @@
|
|
|
10
12
|
- 交付前 → delivery-readiness skill,按风险验证契约、测试、迁移、回滚、性能与可观测性
|
|
11
13
|
- 记忆更新 → 只在可验收工程节点、关键决策、状态变化或会话切换时写;手动 /update-memory
|
|
12
14
|
- 反驳检查 → S 级精简自检;M/L 级或用户明确要求时用 /critic 独立审查
|
|
15
|
+
- 模型路由 → 读取 `.ai/config/model-routing.json`;非 inherit 时按 model-routing skill 创建/验证 handoff,再调用匹配的 planner/implementer/test-worker/reviewer
|
|
13
16
|
|
|
14
17
|
## 与 superpowers 的编排(未安装则忽略本节)
|
|
15
18
|
|
|
@@ -17,8 +20,11 @@
|
|
|
17
20
|
2. S 级跳过 brainstorming/writing-plans;M 级只对未决高影响 how 选择性使用;L 级可使用完整流程
|
|
18
21
|
3. 所有 Superpowers 结论必须回写 `docs/design/`,临时 spec/plan 不是唯一事实源
|
|
19
22
|
4. code review 使用适合风险等级的流程,叠加 `.ai/skills/code-review.md` 的项目检查项
|
|
23
|
+
<!-- ai-memory:managed:end -->
|
|
20
24
|
|
|
25
|
+
<!-- ai-memory:user:start -->
|
|
21
26
|
## 项目信息
|
|
22
27
|
|
|
23
28
|
- 技术栈:{{techStack}}
|
|
24
29
|
<!-- 在此补充启动/测试/构建命令 -->
|
|
30
|
+
<!-- ai-memory:user:end -->
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
name = "premium_planner"
|
|
2
|
+
description = "高风险需求定稿、功能设计和实施计划;仅在模型路由要求 premium 规划时使用。"
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
developer_instructions = """
|
|
5
|
+
先验证 .ai/runs/<feature>/handoff.json,读取其中全部正式输入和 .ai/skills/model-routing.md。只输出需求、设计或计划,不修改实现。输入过期、矛盾或仍有未决问题时停止。
|
|
6
|
+
"""
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
+
<!-- ai-memory:managed:start -->
|
|
3
4
|
## AI 协作框架(ai-memory)
|
|
4
5
|
|
|
5
6
|
进场先读 `.ai/memory/MEMORY.md`,随后遵循 `.ai/README.md` 的进场协议。
|
|
7
|
+
本 managed 区块由 ai-memory 更新;项目命令和自定义规则只写到下方 user 区块。
|
|
6
8
|
|
|
7
9
|
- 0→1 项目/首次架构基线/重大重构 → project-inception skill,产物在 `docs/architecture/`;普通功能不要重复触发
|
|
8
10
|
- 需求 what/why 与外部行为定稿 → requirements-flow skill,产物在 `docs/requirements/vX.Y.Z/{draft,final}/`
|
|
@@ -10,6 +12,7 @@
|
|
|
10
12
|
- 交付前 → delivery-readiness skill,按风险验证契约、测试、迁移、回滚、性能与可观测性
|
|
11
13
|
- 记忆更新 → 只在可验收工程节点、关键决策、状态变化或会话切换时写
|
|
12
14
|
- 反驳检查 → S 级精简自检;M/L 级或用户明确要求时用 critic skill
|
|
15
|
+
- 模型路由 → 读取 `.ai/config/model-routing.json`;非 inherit 时按 model-routing skill 创建/验证 handoff,再调用 `.codex/agents/` 中匹配的执行者
|
|
13
16
|
|
|
14
17
|
## 与 superpowers 的编排(未安装则忽略本节)
|
|
15
18
|
|
|
@@ -17,8 +20,11 @@
|
|
|
17
20
|
2. S 级跳过 brainstorming/writing-plans;M 级只对未决高影响 how 选择性使用;L 级可使用完整流程
|
|
18
21
|
3. 所有 Superpowers 结论必须回写 `docs/design/`,临时 spec/plan 不是唯一事实源
|
|
19
22
|
4. code review 使用适合风险等级的流程,叠加 `.ai/skills/code-review.md` 的项目检查项
|
|
23
|
+
<!-- ai-memory:managed:end -->
|
|
20
24
|
|
|
25
|
+
<!-- ai-memory:user:start -->
|
|
21
26
|
## 项目信息
|
|
22
27
|
|
|
23
28
|
- 技术栈:{{techStack}}
|
|
24
29
|
<!-- 在此补充启动/测试/构建命令 -->
|
|
30
|
+
<!-- ai-memory:user:end -->
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
1. 读 `memory/MEMORY.md`(记忆索引)
|
|
9
9
|
2. 读 `memory/session-log.md` 末尾条目(接上进度)
|
|
10
10
|
3. 按任务类型按需加载 `skills/` 对应方法论
|
|
11
|
+
4. 跨模型阶段先按 `skills/model-routing.md` 创建并校验 handoff,不用对话摘要替代正式输入
|
|
11
12
|
|
|
12
13
|
## 目录
|
|
13
14
|
|
|
14
15
|
- `ai-memory.json` — 框架版本、Schema、启用工具、文件所有权与生成基线哈希;供安全升级预检使用
|
|
16
|
+
- `config/model-routing.json` — 可选阶段模型策略;默认 `inherit` 不改变旧行为
|
|
15
17
|
- `memory/` — 记忆层:MEMORY.md 索引、project-state 全景、session-log 流水、user-profile/feedback 用户级记忆、features/ 功能档案
|
|
16
|
-
- `
|
|
18
|
+
- `runs/` — 本地交接清单和阶段回执;默认不进入 Git
|
|
19
|
+
- `skills/` — 方法论层:project-inception、requirements-flow、feature-design、delivery-readiness、model-routing、architecture、code-review、critic、memory-update
|
|
17
20
|
|
|
18
21
|
## 与工具专属配置的关系
|
|
19
22
|
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
4. 将结果写入 `docs/design/...` 的“交付验证”章节;S 级无设计文档时记入 session-log。
|
|
17
17
|
5. 输出明确结论:`ready`、`conditional` 或 `not-ready`,并列出阻塞项。
|
|
18
18
|
|
|
19
|
+
启用模型路由时,机械性测试可交给 economy 执行者,但测试策略、安全/迁移/并发场景和最终审查仍由 premium 执行者负责。测试命令直接运行,不调用模型。reviewer 第一轮保持只读,修复后重新审查。
|
|
20
|
+
|
|
19
21
|
## 检查面
|
|
20
22
|
|
|
21
23
|
| 检查面 | 必须回答的问题 |
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
7. M 级做一次精简自检;L 级按 critic 方法做独立或聚焦审查。
|
|
20
20
|
8. 更新 `project-state.md` 为 `designed`,实现计划只引用本设计,不重新生成同义 spec。
|
|
21
21
|
|
|
22
|
+
启用模型路由时,`feature-design` 与需要的 `write-plan` 分别创建 handoff;实现阶段必须重新 prepare,引用最终需求、设计和计划的当前哈希。不要把 brainstorming 对话作为实现输入。
|
|
23
|
+
|
|
22
24
|
## Superpowers 路由
|
|
23
25
|
|
|
24
26
|
- S:跳过 brainstorming 和 writing-plans。
|