@dommaker/harness 1.2.0 → 1.2.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/CHANGELOG.md +12 -0
- package/dist/cli/commands/check.js +1 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/constraints-report.js +1 -1
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +6 -3
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +34 -25
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/init.d.ts +21 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +137 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/constraints/checkers/governance-presence.d.ts +20 -0
- package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -0
- package/dist/core/constraints/checkers/governance-presence.js +76 -0
- package/dist/core/constraints/checkers/governance-presence.js.map +1 -0
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +2 -0
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +21 -0
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/constraints/injection-drift.d.ts +29 -4
- package/dist/core/constraints/injection-drift.d.ts.map +1 -1
- package/dist/core/constraints/injection-drift.js +52 -20
- package/dist/core/constraints/injection-drift.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/governance-presence.test.ts +129 -0
- package/src/__tests__/iron-laws.test.ts +7 -7
- package/src/cli/commands/CONTEXT.md +4 -1
- package/src/cli/commands/__tests__/constraints-report.test.ts +2 -2
- package/src/cli/commands/__tests__/constraints-retire.test.ts +45 -4
- package/src/cli/commands/__tests__/init-injection.test.ts +202 -0
- package/src/cli/commands/__tests__/init.test.ts +43 -0
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +28 -0
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/constraints-report.ts +1 -1
- package/src/cli/commands/constraints-retire.ts +38 -27
- package/src/cli/commands/init.ts +150 -3
- package/src/core/CONTEXT.md +1 -1
- package/src/core/constraints/__tests__/injection-drift.test.ts +90 -0
- package/src/core/constraints/checkers/governance-presence.ts +74 -0
- package/src/core/constraints/checkers/index.ts +2 -0
- package/src/core/constraints/definitions/guidelines.ts +22 -0
- package/src/core/constraints/injection-drift.ts +60 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 约束系统测试(ADR-0001:kind 二元模型,
|
|
2
|
+
* 约束系统测试(ADR-0001:kind 二元模型,26 条清单构成 + 注册表闭环)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect } from '@jest/globals';
|
|
@@ -16,17 +16,17 @@ import { getConstraintCheck, registeredCheckCount } from '../core/constraints/ch
|
|
|
16
16
|
import type { ConstraintContext } from '../types/constraint';
|
|
17
17
|
|
|
18
18
|
describe('Constraint System', () => {
|
|
19
|
-
describe('清单构成(ADR-0001:42 →
|
|
20
|
-
it('getAllConstraints 返回
|
|
19
|
+
describe('清单构成(ADR-0001:42 → 26)', () => {
|
|
20
|
+
it('getAllConstraints 返回 26 条:10 check + 16 prompt', () => {
|
|
21
21
|
const all = getAllConstraints();
|
|
22
|
-
expect(all).toHaveLength(
|
|
23
|
-
expect(all.filter(c => c.kind === 'check')).toHaveLength(
|
|
22
|
+
expect(all).toHaveLength(26);
|
|
23
|
+
expect(all.filter(c => c.kind === 'check')).toHaveLength(10);
|
|
24
24
|
expect(all.filter(c => c.kind === 'prompt')).toHaveLength(16);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('check 层 = 5 iron +
|
|
27
|
+
it('check 层 = 5 iron + 5 guideline', () => {
|
|
28
28
|
expect(Object.keys(IRON_LAWS)).toHaveLength(5);
|
|
29
|
-
expect(Object.keys(GUIDELINES)).toHaveLength(
|
|
29
|
+
expect(Object.keys(GUIDELINES)).toHaveLength(5);
|
|
30
30
|
Object.values(IRON_LAWS).forEach(c => {
|
|
31
31
|
expect(c.kind).toBe('check');
|
|
32
32
|
expect(c.level).toBe('iron_law');
|
|
@@ -12,7 +12,7 @@ H5(#44)起:
|
|
|
12
12
|
- 各命令文件:check / validate / passes-gate / init / report / status / spec / sync-docs / knowledge / sdd / failure / posteval-plan / release / analyze-sessions / update-user-model / constraints / doc-freshness-check / spec-baseline-check
|
|
13
13
|
- 6 门禁命令实现在 `acceptance` / `command` / `contract` / `performance` / `review` / `security`(其 CLI 元数据在 `src/gates/definitions.ts`,形状同为 `CommandDefinition`,ADR-0007)
|
|
14
14
|
|
|
15
|
-
`constraints` 下挂治理子命令:`constraints report`(使用统计 + 退役候选诊断 + 配置健康 + 注入漂移,`--export` 脱敏)、`constraints retire`(交互选择 + 人确认退役;带 id 直达需显式 `--yes`(#24 人确认闸门),无 `--yes` 报错 + 非零退出码且不落盘;内置落 config.yml retired 元数据,custom 落 custom-constraints.yml 条目 retired 段(#82 D6 一处真相)+ KnowledgeStore 沉淀 +
|
|
15
|
+
`constraints` 下挂治理子命令:`constraints report`(使用统计 + 退役候选诊断 + 配置健康 + 注入漂移,`--export` 脱敏)、`constraints retire`(交互选择 + 人确认退役;带 id 直达需显式 `--yes`(#24 人确认闸门),无 `--yes` 报错 + 非零退出码且不落盘;内置落 config.yml retired 元数据,custom 落 custom-constraints.yml 条目 retired 段(#82 D6 一处真相)+ KnowledgeStore 沉淀 + 治理注入段同步)。
|
|
16
16
|
|
|
17
17
|
## 依赖关系
|
|
18
18
|
- 依赖 `src/core/constraints/` 约束引擎
|
|
@@ -30,5 +30,8 @@ H5(#44)起:
|
|
|
30
30
|
|
|
31
31
|
## 注意事项
|
|
32
32
|
- 新增命令需同步更新 CLAUDE.md / CAPABILITIES.md / src/CONTEXT.md
|
|
33
|
+
- init 治理约束段写入落点(studio #302,ADR 2026-08-21 落点模型):新仓 → AGENTS.md `PRESERVE:governance` 段(sync-docs 重新生成时保留);旧模型仓(CLAUDE.md 已有 HARNESS_CONSTRAINTS 标记或 `## Governance Rules` 块)→ 续写 CLAUDE.md,幂等重跑不制造双份正本。治理段「在场守护」由 core 侧 governance_presence checker 承担(段缺失/为空时 check 报警)
|
|
34
|
+
- PRESERVE:governance 段内写入纪律:机器管理的只有 HARNESS_CONSTRAINTS 标记区间——有标记只换标记区间,无标记(纯手写段)在段尾追加注入段;段内其余手写内容(治理契约引言/流程/纪律等)必须原样保留(曾整段替换清空手写内容的回归,init-injection.test 有防回归用例)
|
|
35
|
+
- 注入段读取侧同一路由(studio #307):`resolveInjectionTarget`(core/constraints/injection-drift)按「CLAUDE.md 有标记优先,否则 AGENTS.md」定位注入段,`detectInjectionDrift` 与 `constraints retire` 的注入段同步共用;未注入 = 两处均无完整标记段
|
|
33
36
|
- knowledge 命令包含 13 个子操作(list/search/import/decay/stats/upsert/sync-status/sync-rag/audit/snapshot/migrate/index/health)
|
|
34
37
|
- 特殊路由(选项条件、子命令兜底、退出码处理、裸跑语义)表达在定义表的 optionRoutes / subcommands / subcommandStrict / bareRunsAction / afterRun 字段,bin 是纯通用引擎、不含单命令知识
|
|
@@ -57,14 +57,14 @@ describe('buildConstraintsUsageReport', () => {
|
|
|
57
57
|
const report = buildConstraintsUsageReport(root);
|
|
58
58
|
|
|
59
59
|
expect(report.traceFileExists).toBe(false);
|
|
60
|
-
expect(report.stats.length).toBe(
|
|
60
|
+
expect(report.stats.length).toBe(10); // 5 iron + 5 guideline
|
|
61
61
|
for (const s of report.stats) {
|
|
62
62
|
expect(s.total).toBe(0);
|
|
63
63
|
expect(s.evaluated).toBe(0);
|
|
64
64
|
expect(s.failRate).toBe(0);
|
|
65
65
|
expect(s.firstAt).toBeUndefined();
|
|
66
66
|
}
|
|
67
|
-
expect(report.candidates.length).toBe(
|
|
67
|
+
expect(report.candidates.length).toBe(10);
|
|
68
68
|
expect(report.candidates.every(c => c.kind === 'zero_trigger')).toBe(true);
|
|
69
69
|
// prompt 注入清单(standard preset、无 scenes → 14 条通用 prompt)
|
|
70
70
|
expect(report.activePromptIds.length).toBe(14);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* harness constraints retire 测试(ADR-0001 P5)
|
|
3
3
|
*
|
|
4
4
|
* 执行逻辑(retireConstraint,纯函数化):config.yml 写入形态、
|
|
5
|
-
* KnowledgeStore
|
|
5
|
+
* KnowledgeStore 记录、治理注入段同步(CLAUDE.md / AGENTS.md 落点路由)、重复 retire / 未知 id 保护。
|
|
6
6
|
* 交互流程(runRetireInteractive):注入 stdin 流测核心分支
|
|
7
7
|
* (候选选择 + iron 二次确认拒绝;无候选手动输入 + 确认执行)。
|
|
8
8
|
*
|
|
@@ -145,7 +145,7 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
145
145
|
|
|
146
146
|
const result = retireConstraint(root, 'no_bypass_checkpoint', { now: FIXED_NOW });
|
|
147
147
|
expect(result.status).toBe('retired');
|
|
148
|
-
expect(result.
|
|
148
|
+
expect(result.injectionSynced).toBe(true);
|
|
149
149
|
|
|
150
150
|
const after = fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8');
|
|
151
151
|
expect(after).toContain('HARNESS_CONSTRAINTS_START');
|
|
@@ -164,10 +164,51 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
164
164
|
fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# 用户自写\n', 'utf-8');
|
|
165
165
|
const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
166
166
|
expect(result.status).toBe('retired');
|
|
167
|
-
expect(result.
|
|
167
|
+
expect(result.injectionSynced).toBe(false);
|
|
168
168
|
expect(fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8')).toBe('# 用户自写\n');
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it('新模型仓:AGENTS.md PRESERVE:governance 内含标记段时同步重渲染(无 CLAUDE.md)', () => {
|
|
172
|
+
const root = makeTmpProject();
|
|
173
|
+
const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
|
|
174
|
+
expect(before).toContain('no_bypass_checkpoint');
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
path.join(root, 'AGENTS.md'),
|
|
177
|
+
`# AGENTS.md\n\n<!-- PRESERVE:governance -->\n## Governance Rules\n${before}<!-- /PRESERVE:governance -->\n\n其他内容\n`,
|
|
178
|
+
'utf-8'
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const result = retireConstraint(root, 'no_bypass_checkpoint', { now: FIXED_NOW });
|
|
182
|
+
expect(result.status).toBe('retired');
|
|
183
|
+
expect(result.injectionSynced).toBe(true);
|
|
184
|
+
expect(result.injectionFile).toBe('AGENTS.md');
|
|
185
|
+
|
|
186
|
+
const after = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf-8');
|
|
187
|
+
expect(after).toContain('HARNESS_CONSTRAINTS_START');
|
|
188
|
+
expect(after).toContain('HARNESS_CONSTRAINTS_END');
|
|
189
|
+
expect(after).not.toContain('**no_bypass_checkpoint**');
|
|
190
|
+
expect(after).toContain('PRESERVE:governance');
|
|
191
|
+
expect(after).toContain('其他内容');
|
|
192
|
+
// 不制造 CLAUDE.md
|
|
193
|
+
expect(fs.existsSync(path.join(root, 'CLAUDE.md'))).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('两文件均有标记段时旧模型仓豁免:只同步 CLAUDE.md,AGENTS.md 不动', () => {
|
|
197
|
+
const root = makeTmpProject();
|
|
198
|
+
const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
|
|
199
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), `# 项目\n\n## Governance Rules\n${before}`, 'utf-8');
|
|
200
|
+
const agentsMd = `# AGENTS.md\n\n<!-- PRESERVE:governance -->\n## Governance Rules\n${before}<!-- /PRESERVE:governance -->\n`;
|
|
201
|
+
fs.writeFileSync(path.join(root, 'AGENTS.md'), agentsMd, 'utf-8');
|
|
202
|
+
|
|
203
|
+
const result = retireConstraint(root, 'no_bypass_checkpoint', { now: FIXED_NOW });
|
|
204
|
+
expect(result.status).toBe('retired');
|
|
205
|
+
expect(result.injectionSynced).toBe(true);
|
|
206
|
+
expect(result.injectionFile).toBe('CLAUDE.md');
|
|
207
|
+
|
|
208
|
+
expect(fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8')).not.toContain('**no_bypass_checkpoint**');
|
|
209
|
+
expect(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf-8')).toBe(agentsMd);
|
|
210
|
+
});
|
|
211
|
+
|
|
171
212
|
it('重复 retire:already_retired,不覆盖原 retired 元数据', () => {
|
|
172
213
|
const root = makeTmpProject();
|
|
173
214
|
const first = retireConstraint(root, 'capability_sync', { reason: '第一次', now: FIXED_NOW });
|
|
@@ -214,7 +255,7 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
214
255
|
|
|
215
256
|
expect(result.status).toBe('retired');
|
|
216
257
|
expect(result.landing).toBe('custom-constraints.yml');
|
|
217
|
-
expect(result.
|
|
258
|
+
expect(result.injectionSynced).toBe(true);
|
|
218
259
|
// config.yml 不产生该 id 段
|
|
219
260
|
expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
|
|
220
261
|
// yml 保留规则原文 + retired 元数据
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
setupClaudeMdConstraints,
|
|
19
19
|
setupClaudeMdOutputStyle,
|
|
20
|
+
setupAgentsMdConstraints,
|
|
21
|
+
setupGovernanceConstraints,
|
|
20
22
|
} from '../init';
|
|
21
23
|
import { getEffectiveConstraints } from '../../../core/effective-constraints';
|
|
22
24
|
import { getAllConstraints } from '../../../core/constraints/definitions';
|
|
@@ -232,3 +234,203 @@ describe('setupClaudeMdOutputStyle(标记化)', () => {
|
|
|
232
234
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('跳过'));
|
|
233
235
|
});
|
|
234
236
|
});
|
|
237
|
+
|
|
238
|
+
const GOVERNANCE_PRESERVE_BEGIN = '<!-- PRESERVE:governance -->';
|
|
239
|
+
const GOVERNANCE_PRESERVE_END = '<!-- /PRESERVE:governance -->';
|
|
240
|
+
|
|
241
|
+
describe('setupAgentsMdConstraints(新落点模型:治理契约 → AGENTS.md PRESERVE:governance 段)', () => {
|
|
242
|
+
let tempDir: string;
|
|
243
|
+
let agentsMdPath: string;
|
|
244
|
+
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'temp-test-init-agents-'));
|
|
247
|
+
agentsMdPath = path.join(tempDir, 'AGENTS.md');
|
|
248
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
jest.restoreAllMocks();
|
|
253
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('AGENTS.md 不存在时创建骨架并写入 PRESERVE:governance 段', async () => {
|
|
257
|
+
await setupAgentsMdConstraints(tempDir);
|
|
258
|
+
|
|
259
|
+
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
|
260
|
+
expect(content).toContain('# AGENTS.md');
|
|
261
|
+
expect(content).toContain(GOVERNANCE_PRESERVE_BEGIN);
|
|
262
|
+
expect(content).toContain(GOVERNANCE_PRESERVE_END);
|
|
263
|
+
expect(content).toContain('## Governance Rules');
|
|
264
|
+
expect(content).toContain(CONSTRAINTS_START_MARKER);
|
|
265
|
+
expect(content).toContain(CONSTRAINTS_END_MARKER);
|
|
266
|
+
expect(content).toContain('### Iron Laws');
|
|
267
|
+
// PRESERVE 标记在约束段之外(sync-docs 重新生成时整段保留)
|
|
268
|
+
expect(content.indexOf(GOVERNANCE_PRESERVE_BEGIN)).toBeLessThan(content.indexOf(CONSTRAINTS_START_MARKER));
|
|
269
|
+
expect(content.indexOf(CONSTRAINTS_END_MARKER)).toBeLessThan(content.indexOf(GOVERNANCE_PRESERVE_END));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('连跑两次输出一致(幂等)', async () => {
|
|
273
|
+
await setupAgentsMdConstraints(tempDir);
|
|
274
|
+
const first = fs.readFileSync(agentsMdPath, 'utf-8');
|
|
275
|
+
await setupAgentsMdConstraints(tempDir);
|
|
276
|
+
expect(fs.readFileSync(agentsMdPath, 'utf-8')).toBe(first);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('已有 PRESERVE:governance 段(含标记+段内手写内容):只换标记区间,手写内容原样保留', async () => {
|
|
280
|
+
// 回归:曾把 PRESERVE 段内 HARNESS 标记之外的手写内容(治理契约引言/流程等)清空
|
|
281
|
+
fs.writeFileSync(agentsMdPath, [
|
|
282
|
+
'# AGENTS.md',
|
|
283
|
+
'',
|
|
284
|
+
'机器生成导读(保持不变)',
|
|
285
|
+
'',
|
|
286
|
+
GOVERNANCE_PRESERVE_BEGIN,
|
|
287
|
+
'## Governance Rules',
|
|
288
|
+
CONSTRAINTS_START_MARKER,
|
|
289
|
+
'<!-- version: 0.0.0 -->',
|
|
290
|
+
'过期的旧约束内容',
|
|
291
|
+
CONSTRAINTS_END_MARKER,
|
|
292
|
+
'',
|
|
293
|
+
'治理契约引言(手写,保留)',
|
|
294
|
+
'',
|
|
295
|
+
'### 治理变更流程(手写,保留)',
|
|
296
|
+
GOVERNANCE_PRESERVE_END,
|
|
297
|
+
'',
|
|
298
|
+
'<!-- PRESERVE:other -->',
|
|
299
|
+
'另一个手写段(保持不动)',
|
|
300
|
+
'<!-- /PRESERVE:other -->',
|
|
301
|
+
'',
|
|
302
|
+
].join('\n'));
|
|
303
|
+
|
|
304
|
+
await setupAgentsMdConstraints(tempDir);
|
|
305
|
+
|
|
306
|
+
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
|
307
|
+
expect(content).toContain('机器生成导读(保持不变)');
|
|
308
|
+
expect(content).toContain('另一个手写段(保持不动)');
|
|
309
|
+
expect(content).not.toContain('过期的旧约束内容');
|
|
310
|
+
expect(content).toContain(CONSTRAINTS_START_MARKER);
|
|
311
|
+
// 段内标记之外的手写内容原样保留
|
|
312
|
+
expect(content).toContain('治理契约引言(手写,保留)');
|
|
313
|
+
expect(content).toContain('### 治理变更流程(手写,保留)');
|
|
314
|
+
expect(content.indexOf(CONSTRAINTS_END_MARKER)).toBeLessThan(content.indexOf('治理契约引言(手写,保留)'));
|
|
315
|
+
expect(content.indexOf('### 治理变更流程(手写,保留)')).toBeLessThan(content.indexOf(GOVERNANCE_PRESERVE_END));
|
|
316
|
+
// 幂等
|
|
317
|
+
await setupAgentsMdConstraints(tempDir);
|
|
318
|
+
expect(fs.readFileSync(agentsMdPath, 'utf-8')).toBe(content);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('已有 PRESERVE:governance 段但无 HARNESS 标记(纯手写段):段尾追加注入段,手写内容不动', async () => {
|
|
322
|
+
fs.writeFileSync(agentsMdPath, [
|
|
323
|
+
'# AGENTS.md',
|
|
324
|
+
'',
|
|
325
|
+
GOVERNANCE_PRESERVE_BEGIN,
|
|
326
|
+
'治理契约引言(手写,保留)',
|
|
327
|
+
'',
|
|
328
|
+
'### 发布纪律(手写,保留)',
|
|
329
|
+
GOVERNANCE_PRESERVE_END,
|
|
330
|
+
'',
|
|
331
|
+
].join('\n'));
|
|
332
|
+
|
|
333
|
+
await setupAgentsMdConstraints(tempDir);
|
|
334
|
+
|
|
335
|
+
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
|
336
|
+
expect(content).toContain('治理契约引言(手写,保留)');
|
|
337
|
+
expect(content).toContain('### 发布纪律(手写,保留)');
|
|
338
|
+
expect(content).toContain(CONSTRAINTS_START_MARKER);
|
|
339
|
+
// 注入段追加在手写内容之后、PRESERVE 段内
|
|
340
|
+
expect(content.indexOf('### 发布纪律(手写,保留)')).toBeLessThan(content.indexOf(CONSTRAINTS_START_MARKER));
|
|
341
|
+
expect(content.indexOf(CONSTRAINTS_END_MARKER)).toBeLessThan(content.indexOf(GOVERNANCE_PRESERVE_END));
|
|
342
|
+
// 幂等
|
|
343
|
+
await setupAgentsMdConstraints(tempDir);
|
|
344
|
+
expect(fs.readFileSync(agentsMdPath, 'utf-8')).toBe(content);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('AGENTS.md 存在但无 PRESERVE:governance 段:末尾追加,既有内容不动', async () => {
|
|
348
|
+
fs.writeFileSync(agentsMdPath, '# AGENTS.md\n\n既有导读内容\n');
|
|
349
|
+
|
|
350
|
+
await setupAgentsMdConstraints(tempDir);
|
|
351
|
+
|
|
352
|
+
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
|
353
|
+
expect(content).toContain('既有导读内容');
|
|
354
|
+
expect(content).toContain(GOVERNANCE_PRESERVE_BEGIN);
|
|
355
|
+
expect(content.indexOf('既有导读内容')).toBeLessThan(content.indexOf(GOVERNANCE_PRESERVE_BEGIN));
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('PRESERVE:governance 标记残缺(只有单边):不写入并告警', async () => {
|
|
359
|
+
const original = `# AGENTS.md\n\n${GOVERNANCE_PRESERVE_BEGIN}\n残缺的段\n`;
|
|
360
|
+
fs.writeFileSync(agentsMdPath, original);
|
|
361
|
+
const logSpy = jest.spyOn(console, 'log');
|
|
362
|
+
|
|
363
|
+
await setupAgentsMdConstraints(tempDir);
|
|
364
|
+
|
|
365
|
+
expect(fs.readFileSync(agentsMdPath, 'utf-8')).toBe(original);
|
|
366
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('残缺'));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('段内注入文本与 renderConstraintsSection(getEffectiveConstraints()) 一致', async () => {
|
|
370
|
+
await setupAgentsMdConstraints(tempDir);
|
|
371
|
+
|
|
372
|
+
const content = fs.readFileSync(agentsMdPath, 'utf-8');
|
|
373
|
+
const start = content.indexOf(CONSTRAINTS_START_MARKER);
|
|
374
|
+
const end = content.indexOf(CONSTRAINTS_END_MARKER) + CONSTRAINTS_END_MARKER.length + 1;
|
|
375
|
+
const actual = content.slice(start, end);
|
|
376
|
+
const version = /<!-- version: ([^ ]+) -->/.exec(content)![1];
|
|
377
|
+
expect(actual).toBe(renderConstraintsSection(getEffectiveConstraints(tempDir), version));
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('setupGovernanceConstraints(落点路由)', () => {
|
|
382
|
+
let tempDir: string;
|
|
383
|
+
|
|
384
|
+
beforeEach(() => {
|
|
385
|
+
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'temp-test-init-routing-'));
|
|
386
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
afterEach(() => {
|
|
390
|
+
jest.restoreAllMocks();
|
|
391
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('新仓(无 CLAUDE.md):治理契约写向 AGENTS.md,不创建 CLAUDE.md', async () => {
|
|
395
|
+
await setupGovernanceConstraints(tempDir);
|
|
396
|
+
|
|
397
|
+
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
398
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(false);
|
|
399
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
400
|
+
expect(content).toContain(GOVERNANCE_PRESERVE_BEGIN);
|
|
401
|
+
expect(content).toContain(CONSTRAINTS_START_MARKER);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('旧模型仓(CLAUDE.md 已有 HARNESS_CONSTRAINTS 标记):继续写 CLAUDE.md,不动 AGENTS.md', async () => {
|
|
405
|
+
fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), [
|
|
406
|
+
'# CLAUDE.md',
|
|
407
|
+
'',
|
|
408
|
+
'## Governance Rules',
|
|
409
|
+
CONSTRAINTS_START_MARKER,
|
|
410
|
+
'<!-- version: 0.0.0 -->',
|
|
411
|
+
'旧约束',
|
|
412
|
+
CONSTRAINTS_END_MARKER,
|
|
413
|
+
'',
|
|
414
|
+
].join('\n'));
|
|
415
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), '# AGENTS.md\n\n既有导读\n');
|
|
416
|
+
|
|
417
|
+
await setupGovernanceConstraints(tempDir);
|
|
418
|
+
|
|
419
|
+
const claude = fs.readFileSync(path.join(tempDir, 'CLAUDE.md'), 'utf-8');
|
|
420
|
+
expect(claude).not.toContain('旧约束');
|
|
421
|
+
expect(claude).toContain('### Iron Laws');
|
|
422
|
+
// AGENTS.md 不被写入治理段
|
|
423
|
+
expect(fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8')).toBe('# AGENTS.md\n\n既有导读\n');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('旧模型仓(CLAUDE.md 有无标记 Governance Rules 块):路由到 CLAUDE.md', async () => {
|
|
427
|
+
fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), '# CLAUDE.md\n\n## Governance Rules\n\n手写条款\n');
|
|
428
|
+
|
|
429
|
+
await setupGovernanceConstraints(tempDir);
|
|
430
|
+
|
|
431
|
+
const claude = fs.readFileSync(path.join(tempDir, 'CLAUDE.md'), 'utf-8');
|
|
432
|
+
expect(claude).toContain('手写条款');
|
|
433
|
+
expect(claude).toContain(CONSTRAINTS_START_MARKER);
|
|
434
|
+
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(false);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
@@ -308,6 +308,49 @@ describe('init command', () => {
|
|
|
308
308
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('harness-governance.yml 已存在'));
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
+
it('应该跳过治理 CI workflow 当已有 workflow 覆盖治理命令', async () => {
|
|
312
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
313
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
314
|
+
// setupGitHubActions 与治理能力检测都会 readdir workflows 目录
|
|
315
|
+
existingFiles.add(`${PROJECT}/.github/workflows`);
|
|
316
|
+
mockReaddir.mockResolvedValue(['ci.yml']);
|
|
317
|
+
mockFs.readFile.mockImplementation((p: any) =>
|
|
318
|
+
String(p).includes('ci.yml')
|
|
319
|
+
? Promise.resolve('run: npx harness sync-docs --agents && npx harness check && npx harness sync-docs --check --agents')
|
|
320
|
+
: Promise.reject(new Error('ENOENT'))
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await init({ preset: 'standard', governance: 'standard', projectPath: PROJECT });
|
|
324
|
+
const writeCalls = mockFs.writeFile.mock.calls;
|
|
325
|
+
const workflowCall = writeCalls.find((c: any[]) => String(c[0]).includes('harness-governance.yml'));
|
|
326
|
+
expect(workflowCall).toBeUndefined();
|
|
327
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('治理检查已由 ci.yml 覆盖'));
|
|
328
|
+
|
|
329
|
+
mockReaddir.mockResolvedValue([]);
|
|
330
|
+
mockFs.readFile.mockRejectedValue(new Error('ENOENT'));
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('应该正常创建治理 CI workflow 当已有 workflow 不含治理命令', async () => {
|
|
334
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
335
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
336
|
+
existingFiles.add(`${PROJECT}/.github/workflows`);
|
|
337
|
+
mockReaddir.mockResolvedValue(['ci.yml']);
|
|
338
|
+
mockFs.readFile.mockImplementation((p: any) =>
|
|
339
|
+
String(p).includes('ci.yml')
|
|
340
|
+
? Promise.resolve('run: npm test')
|
|
341
|
+
: Promise.reject(new Error('ENOENT'))
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
await init({ preset: 'standard', governance: 'standard', projectPath: PROJECT });
|
|
345
|
+
const writeCalls = mockFs.writeFile.mock.calls;
|
|
346
|
+
const workflowCall = writeCalls.find((c: any[]) => String(c[0]).includes('harness-governance.yml'));
|
|
347
|
+
expect(workflowCall).toBeDefined();
|
|
348
|
+
expect(workflowCall![1]).toContain('Harness Governance');
|
|
349
|
+
|
|
350
|
+
mockReaddir.mockResolvedValue([]);
|
|
351
|
+
mockFs.readFile.mockRejectedValue(new Error('ENOENT'));
|
|
352
|
+
});
|
|
353
|
+
|
|
311
354
|
it('strict 治理应在 CI 中包含 docs check', async () => {
|
|
312
355
|
mockFs.mkdir.mockResolvedValue(undefined);
|
|
313
356
|
mockFs.writeFile.mockResolvedValue(undefined);
|
|
@@ -592,5 +592,33 @@ describe('sync-docs --agents', () => {
|
|
|
592
592
|
|
|
593
593
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
594
594
|
});
|
|
595
|
+
|
|
596
|
+
it('PRESERVE:governance 治理契约段开箱即用:重新生成保留、--check 通过(studio #302)', async () => {
|
|
597
|
+
const testDir = path.join(tempDir, 'preserve-governance');
|
|
598
|
+
createFixture(testDir, { pnpm: true, withCapabilities: true });
|
|
599
|
+
|
|
600
|
+
const GOVERNANCE_BLOCK = [
|
|
601
|
+
'<!-- PRESERVE:governance -->',
|
|
602
|
+
'## Governance Rules',
|
|
603
|
+
'',
|
|
604
|
+
'<!-- HARNESS_CONSTRAINTS_START -->',
|
|
605
|
+
'- **law_a**: 治理契约条款',
|
|
606
|
+
'<!-- HARNESS_CONSTRAINTS_END -->',
|
|
607
|
+
'<!-- /PRESERVE:governance -->',
|
|
608
|
+
].join('\n');
|
|
609
|
+
|
|
610
|
+
await syncDocs({ projectPath: testDir, agents: true });
|
|
611
|
+
appendBlock(testDir, GOVERNANCE_BLOCK);
|
|
612
|
+
await syncDocs({ projectPath: testDir, agents: true });
|
|
613
|
+
|
|
614
|
+
const content = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
|
|
615
|
+
expect(content).toContain(GOVERNANCE_BLOCK);
|
|
616
|
+
|
|
617
|
+
// 幂等 + --check 通过(块内含 HARNESS_CONSTRAINTS 标记不影响漂移比对)
|
|
618
|
+
expect(await syncDocs({ projectPath: testDir, agents: true })).toBe(true);
|
|
619
|
+
expect(await syncDocs({ projectPath: testDir, agents: true, check: true })).toBe(true);
|
|
620
|
+
|
|
621
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
622
|
+
});
|
|
595
623
|
});
|
|
596
624
|
});
|
|
@@ -123,7 +123,7 @@ export async function check(options: CheckOptions): Promise<void> {
|
|
|
123
123
|
const drift = detectInjectionDrift(projectPath);
|
|
124
124
|
if (drift.hasDrift) {
|
|
125
125
|
console.log();
|
|
126
|
-
console.log(chalk.yellow(
|
|
126
|
+
console.log(chalk.yellow(`⚠️ 检测到 ${drift.injectionFile ?? '治理文档'} 约束注入漂移(仅警告,不阻断):`));
|
|
127
127
|
if (drift.versionDrift) {
|
|
128
128
|
console.log(chalk.yellow(` ⚠️⚠️ 注入段版本 (${drift.versionDrift.actual}) ≠ 已安装 harness 版本 (${drift.versionDrift.expected}):agent 上下文中的规则与已安装 harness 版本不一致`));
|
|
129
129
|
}
|
|
@@ -118,7 +118,7 @@ export function renderExportMarkdown(
|
|
|
118
118
|
export function renderInjectionDriftLines(drift: InjectionDrift): string[] {
|
|
119
119
|
const lines: string[] = [];
|
|
120
120
|
if (drift.notInjected) {
|
|
121
|
-
lines.push(' CLAUDE.md
|
|
121
|
+
lines.push(' CLAUDE.md / AGENTS.md 均无约束注入标记段(未注入,不算漂移);可运行 npx @dommaker/harness init 注入');
|
|
122
122
|
if (drift.duplicateHeading) {
|
|
123
123
|
lines.push(' 另检测到多个 "## Governance Rules" 标题(历史遗留重复章节),建议手工清理后重跑 npx @dommaker/harness init');
|
|
124
124
|
}
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* retired: { at, reason, stats: { total, fail, failRate } }
|
|
21
21
|
*
|
|
22
22
|
* 每条同时写一条 KnowledgeStore 记录(consumptionMode: 'signal'),
|
|
23
|
-
*
|
|
23
|
+
* 并同步治理注入段(CLAUDE.md 或 AGENTS.md,存在 HARNESS_CONSTRAINTS 标记时;
|
|
24
|
+
* 落点路由与 init 一致,见 core/constraints/injection-drift resolveInjectionTarget)。
|
|
24
25
|
*
|
|
25
26
|
* retire 不是删除——恢复方法:内置删 config.yml 中 constraints.<id> 段;
|
|
26
27
|
* custom 删 custom-constraints.yml 中 custom_constraints.<id>.retired 段。
|
|
@@ -35,7 +36,8 @@ import * as readline from 'readline';
|
|
|
35
36
|
import * as yaml from 'js-yaml';
|
|
36
37
|
import chalk from 'chalk';
|
|
37
38
|
import { getConstraint } from '../../core/constraints/definitions';
|
|
38
|
-
import { renderConstraintsSection,
|
|
39
|
+
import { renderConstraintsSection, CONSTRAINTS_END_MARKER } from '../../core/constraints/injection-renderer';
|
|
40
|
+
import { resolveInjectionTarget } from '../../core/constraints/injection-drift';
|
|
39
41
|
import { getEffectiveConstraints } from '../../core/effective-constraints';
|
|
40
42
|
import { loadRawProjectConfig, ProjectConfigLoader } from '../../core/project-config-loader';
|
|
41
43
|
import { FileKnowledgeStore } from '../../knowledge/store';
|
|
@@ -65,8 +67,10 @@ export interface RetireResult {
|
|
|
65
67
|
/** 退役落点:custom 约束落 custom-constraints.yml(#82 D6),内置落 config.yml */
|
|
66
68
|
landing: 'config.yml' | 'custom-constraints.yml';
|
|
67
69
|
stats: { total: number; fail: number; failRate: number };
|
|
68
|
-
/** CLAUDE.md
|
|
69
|
-
|
|
70
|
+
/** 治理注入段(CLAUDE.md / AGENTS.md)是否已同步 */
|
|
71
|
+
injectionSynced: boolean;
|
|
72
|
+
/** 实际同步的落点文件(injectionSynced 时存在) */
|
|
73
|
+
injectionFile?: string;
|
|
70
74
|
/** KnowledgeStore 条目 id(status='retired' 时存在) */
|
|
71
75
|
knowledgeEntryId?: string;
|
|
72
76
|
}
|
|
@@ -265,27 +269,25 @@ function saveRetireKnowledge(
|
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
/**
|
|
268
|
-
*
|
|
272
|
+
* 同步治理注入段(CLAUDE.md 或 AGENTS.md,存在 HARNESS_CONSTRAINTS 标记时)
|
|
269
273
|
*
|
|
274
|
+
* 落点路由复用 resolveInjectionTarget(studio #307:新模型仓注入段在
|
|
275
|
+
* AGENTS.md PRESERVE:governance 内;旧模型仓 CLAUDE.md 优先)。
|
|
270
276
|
* 复用 renderConstraintsSection(P3 导出纯函数)按退役后生效集重渲染。
|
|
271
277
|
* 无标记段不动(不追加——retire 不承担 init 职责)。
|
|
272
278
|
*/
|
|
273
|
-
function
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
276
|
-
|
|
277
|
-
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
278
|
-
const startIdx = content.indexOf(CONSTRAINTS_START_MARKER);
|
|
279
|
-
const endIdx = content.indexOf(CONSTRAINTS_END_MARKER);
|
|
280
|
-
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return false;
|
|
279
|
+
function syncGovernanceInjection(projectRoot: string): { synced: boolean; file?: string } {
|
|
280
|
+
const target = resolveInjectionTarget(projectRoot);
|
|
281
|
+
if (!target) return { synced: false };
|
|
281
282
|
|
|
283
|
+
const filePath = path.join(projectRoot, target.file);
|
|
282
284
|
const body = renderConstraintsSection(getEffectiveConstraints(projectRoot), getHarnessVersion());
|
|
283
|
-
const after = content.slice(endIdx + CONSTRAINTS_END_MARKER.length).replace(/^\n+/, '');
|
|
284
|
-
const newContent = content.slice(0, startIdx) + body + (after ? '\n' + after : '');
|
|
285
|
-
if (newContent !== content) {
|
|
286
|
-
fs.writeFileSync(
|
|
285
|
+
const after = target.content.slice(target.endIdx + CONSTRAINTS_END_MARKER.length).replace(/^\n+/, '');
|
|
286
|
+
const newContent = target.content.slice(0, target.startIdx) + body + (after ? '\n' + after : '');
|
|
287
|
+
if (newContent !== target.content) {
|
|
288
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
287
289
|
}
|
|
288
|
-
return true;
|
|
290
|
+
return { synced: true, file: target.file };
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
/**
|
|
@@ -307,7 +309,7 @@ export function retireConstraint(
|
|
|
307
309
|
const landing: RetireResult['landing'] =
|
|
308
310
|
target?.source === 'custom' ? 'custom-constraints.yml' : 'config.yml';
|
|
309
311
|
if (!target) {
|
|
310
|
-
return { id, status: 'unknown_id', isIronLaw: false, landing, stats: emptyStats,
|
|
312
|
+
return { id, status: 'unknown_id', isIronLaw: false, landing, stats: emptyStats, injectionSynced: false };
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
// 已退役保护:内置看 config.yml constraints.<id>.enabled === false;
|
|
@@ -318,10 +320,10 @@ export function retireConstraint(
|
|
|
318
320
|
const loader = new ProjectConfigLoader(projectRoot);
|
|
319
321
|
loader.load();
|
|
320
322
|
if (loader.getCustomConstraints()[id]?.retired || existing?.enabled === false) {
|
|
321
|
-
return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats,
|
|
323
|
+
return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats, injectionSynced: false };
|
|
322
324
|
}
|
|
323
325
|
} else if (existing?.enabled === false) {
|
|
324
|
-
return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats,
|
|
326
|
+
return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats, injectionSynced: false };
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
// 历史统计(来自 traces.log)
|
|
@@ -346,10 +348,19 @@ export function retireConstraint(
|
|
|
346
348
|
// 2. KnowledgeStore
|
|
347
349
|
const knowledgeEntryId = saveRetireKnowledge(projectRoot, id, target, reason, stats, iso);
|
|
348
350
|
|
|
349
|
-
// 3. CLAUDE.md
|
|
350
|
-
const
|
|
351
|
+
// 3. 治理注入段同步(CLAUDE.md / AGENTS.md,路由见 syncGovernanceInjection)
|
|
352
|
+
const injection = syncGovernanceInjection(projectRoot);
|
|
351
353
|
|
|
352
|
-
return {
|
|
354
|
+
return {
|
|
355
|
+
id,
|
|
356
|
+
status: 'retired',
|
|
357
|
+
isIronLaw: target.isIronLaw,
|
|
358
|
+
landing,
|
|
359
|
+
stats,
|
|
360
|
+
injectionSynced: injection.synced,
|
|
361
|
+
injectionFile: injection.file,
|
|
362
|
+
knowledgeEntryId,
|
|
363
|
+
};
|
|
353
364
|
}
|
|
354
365
|
|
|
355
366
|
/**
|
|
@@ -367,8 +378,8 @@ export function printRetireResult(result: RetireResult): void {
|
|
|
367
378
|
console.log(chalk.green(`✅ ${result.id}: 已退役`));
|
|
368
379
|
console.log(` 历史统计: total=${result.stats.total} fail=${result.stats.fail} fail率=${Math.round(result.stats.failRate * 100)}%`);
|
|
369
380
|
console.log(` 知识沉淀: ${result.knowledgeEntryId}(.harness/knowledge)`);
|
|
370
|
-
if (result.
|
|
371
|
-
console.log(
|
|
381
|
+
if (result.injectionSynced) {
|
|
382
|
+
console.log(` 已同步 ${result.injectionFile ?? '治理文档'} 注入段`);
|
|
372
383
|
}
|
|
373
384
|
const recovery =
|
|
374
385
|
result.landing === 'custom-constraints.yml'
|
|
@@ -485,7 +496,7 @@ export async function runRetireInteractive(
|
|
|
485
496
|
}
|
|
486
497
|
console.log(` - KnowledgeStore: 写入 constraint-retired-${p.id}`);
|
|
487
498
|
}
|
|
488
|
-
console.log(' - CLAUDE.md: 若含 HARNESS_CONSTRAINTS 标记段则同步重渲染');
|
|
499
|
+
console.log(' - CLAUDE.md / AGENTS.md: 若含 HARNESS_CONSTRAINTS 标记段则同步重渲染');
|
|
489
500
|
const finalConfirm = await ask('确认执行?(y/N) ');
|
|
490
501
|
if (finalConfirm.toLowerCase() !== 'y' && finalConfirm.toLowerCase() !== 'yes') {
|
|
491
502
|
console.log('已取消');
|