@dommaker/harness 1.2.0 → 1.2.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cli/commands/check.js +1 -1
  3. package/dist/cli/commands/check.js.map +1 -1
  4. package/dist/cli/commands/constraints-report.js +1 -1
  5. package/dist/cli/commands/constraints-report.js.map +1 -1
  6. package/dist/cli/commands/constraints-retire.d.ts +6 -3
  7. package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
  8. package/dist/cli/commands/constraints-retire.js +34 -25
  9. package/dist/cli/commands/constraints-retire.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts +21 -1
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +112 -3
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/core/constraints/checkers/governance-presence.d.ts +20 -0
  15. package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -0
  16. package/dist/core/constraints/checkers/governance-presence.js +76 -0
  17. package/dist/core/constraints/checkers/governance-presence.js.map +1 -0
  18. package/dist/core/constraints/checkers/index.d.ts.map +1 -1
  19. package/dist/core/constraints/checkers/index.js +2 -0
  20. package/dist/core/constraints/checkers/index.js.map +1 -1
  21. package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
  22. package/dist/core/constraints/definitions/guidelines.js +21 -0
  23. package/dist/core/constraints/definitions/guidelines.js.map +1 -1
  24. package/dist/core/constraints/injection-drift.d.ts +29 -4
  25. package/dist/core/constraints/injection-drift.d.ts.map +1 -1
  26. package/dist/core/constraints/injection-drift.js +52 -20
  27. package/dist/core/constraints/injection-drift.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/__tests__/governance-presence.test.ts +129 -0
  30. package/src/__tests__/iron-laws.test.ts +7 -7
  31. package/src/cli/commands/CONTEXT.md +4 -1
  32. package/src/cli/commands/__tests__/constraints-report.test.ts +2 -2
  33. package/src/cli/commands/__tests__/constraints-retire.test.ts +45 -4
  34. package/src/cli/commands/__tests__/init-injection.test.ts +202 -0
  35. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +28 -0
  36. package/src/cli/commands/check.ts +1 -1
  37. package/src/cli/commands/constraints-report.ts +1 -1
  38. package/src/cli/commands/constraints-retire.ts +38 -27
  39. package/src/cli/commands/init.ts +123 -3
  40. package/src/core/CONTEXT.md +1 -1
  41. package/src/core/constraints/__tests__/injection-drift.test.ts +90 -0
  42. package/src/core/constraints/checkers/governance-presence.ts +74 -0
  43. package/src/core/constraints/checkers/index.ts +2 -0
  44. package/src/core/constraints/definitions/guidelines.ts +22 -0
  45. package/src/core/constraints/injection-drift.ts +60 -21
@@ -2,7 +2,7 @@
2
2
  * harness constraints retire 测试(ADR-0001 P5)
3
3
  *
4
4
  * 执行逻辑(retireConstraint,纯函数化):config.yml 写入形态、
5
- * KnowledgeStore 记录、CLAUDE.md 注入段同步、重复 retire / 未知 id 保护。
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.claudeMdSynced).toBe(true);
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.claudeMdSynced).toBe(false);
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.claudeMdSynced).toBe(true);
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
+ });
@@ -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('⚠️ 检测到 CLAUDE.md 约束注入漂移(仅警告,不阻断):'));
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 无约束注入标记段(未注入,不算漂移);可运行 npx @dommaker/harness init 注入');
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
- * 并同步 CLAUDE.md 注入段(存在 HARNESS_CONSTRAINTS 标记时)。
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, CONSTRAINTS_START_MARKER, CONSTRAINTS_END_MARKER } from '../../core/constraints/injection-renderer';
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
- claudeMdSynced: boolean;
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
- * 同步 CLAUDE.md 注入段(存在 HARNESS_CONSTRAINTS 标记时)
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 syncClaudeMdInjection(projectRoot: string): boolean {
274
- const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
275
- if (!fs.existsSync(claudeMdPath)) return false;
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(claudeMdPath, newContent, 'utf-8');
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, claudeMdSynced: false };
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, claudeMdSynced: false };
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, claudeMdSynced: false };
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 claudeMdSynced = syncClaudeMdInjection(projectRoot);
351
+ // 3. 治理注入段同步(CLAUDE.md / AGENTS.md,路由见 syncGovernanceInjection)
352
+ const injection = syncGovernanceInjection(projectRoot);
351
353
 
352
- return { id, status: 'retired', isIronLaw: target.isIronLaw, landing, stats, claudeMdSynced, knowledgeEntryId };
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.claudeMdSynced) {
371
- console.log(' 已同步 CLAUDE.md 注入段');
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('已取消');
@@ -569,7 +569,102 @@ export async function setupClaudeMdOutputStyle(projectPath: string): Promise<voi
569
569
  }
570
570
 
571
571
  /**
572
- * CLAUDE.md 中写入/更新 Governance Rules 约束段
572
+ * 治理契约 PRESERVE 段标记(studio #302,ADR 2026-08-21 落点模型:
573
+ * 治理契约正本住 AGENTS.md 手写 PRESERVE 段,sync-docs 重新生成时原样保留)
574
+ */
575
+ const GOVERNANCE_PRESERVE_BEGIN = '<!-- PRESERVE:governance -->';
576
+ const GOVERNANCE_PRESERVE_END = '<!-- /PRESERVE:governance -->';
577
+
578
+ /**
579
+ * 在 AGENTS.md 的 PRESERVE:governance 段写入/更新 Governance Rules 约束段(新落点模型)
580
+ *
581
+ * - AGENTS.md 不存在:创建最小骨架(标题 + 说明 + PRESERVE:governance 段),
582
+ * 完整导读由 `harness sync-docs --agents` 生成,PRESERVE 段在重新生成时原样保留
583
+ * - 已有 PRESERVE:governance 段:段内机器管理的只有 HARNESS_CONSTRAINTS 标记区间——
584
+ * 有标记则只替换标记区间,段内其余手写内容(治理契约引言/流程/纪律等)原样保留;
585
+ * 无标记(纯手写段)则在段尾追加注入段,不动手写内容
586
+ * - 无该段:在文件末尾追加
587
+ * - 段标记残缺(只有单边标记):不写入,告警交由人工修复(防二次损坏)
588
+ */
589
+ export async function setupAgentsMdConstraints(projectPath: string): Promise<void> {
590
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
591
+ const version = getPackageVersion();
592
+
593
+ const constraints = getEffectiveConstraints(projectPath);
594
+ const bodyOnly = renderConstraintsSection(constraints, version);
595
+ const block = `${GOVERNANCE_PRESERVE_BEGIN}\n## Governance Rules\n${bodyOnly}${GOVERNANCE_PRESERVE_END}\n`;
596
+
597
+ let existingContent: string | null = null;
598
+ try {
599
+ existingContent = await fs.readFile(agentsMdPath, 'utf-8');
600
+ } catch {
601
+ // AGENTS.md 不存在
602
+ }
603
+
604
+ if (existingContent === null) {
605
+ const skeleton = [
606
+ '# AGENTS.md',
607
+ '',
608
+ '> 机器生成部分由 `harness sync-docs --agents` 维护;`PRESERVE:governance` 段是治理契约正本(手写/治理变更流程管控),重新生成时原样保留。',
609
+ '',
610
+ block,
611
+ ].join('\n');
612
+ await fs.writeFile(agentsMdPath, skeleton, 'utf-8');
613
+ console.log(chalk.green(`✅ 已创建 AGENTS.md 并写入治理契约 PRESERVE:governance 段 (v${version})`));
614
+ return;
615
+ }
616
+
617
+ const startIdx = existingContent.indexOf(GOVERNANCE_PRESERVE_BEGIN);
618
+ const endIdx = existingContent.indexOf(GOVERNANCE_PRESERVE_END);
619
+
620
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
621
+ // 段内机器管理的只有 HARNESS_CONSTRAINTS 标记区间;其余手写内容原样保留
622
+ const before = existingContent.slice(0, startIdx);
623
+ const blockContent = existingContent.slice(startIdx + GOVERNANCE_PRESERVE_BEGIN.length, endIdx);
624
+ const after = existingContent.slice(endIdx + GOVERNANCE_PRESERVE_END.length).replace(/^\n+/, '');
625
+
626
+ const csIdx = blockContent.indexOf(CONSTRAINTS_START_MARKER);
627
+ const ceIdx = blockContent.indexOf(CONSTRAINTS_END_MARKER);
628
+ let newBlockContent: string;
629
+ if (csIdx !== -1 && ceIdx !== -1 && ceIdx > csIdx) {
630
+ // 只替换标记区间。bodyOnly 自带结尾换行,故剥掉尾部恰好一个前导换行
631
+ // (END 标记行的行尾换行),其余手写内容逐字保留,保证幂等。
632
+ newBlockContent =
633
+ blockContent.slice(0, csIdx) +
634
+ bodyOnly +
635
+ blockContent.slice(ceIdx + CONSTRAINTS_END_MARKER.length).replace(/^\n/, '');
636
+ } else {
637
+ // 纯手写段(无约束标记):段尾追加注入段,手写内容不动
638
+ newBlockContent = blockContent.trimEnd() + '\n\n## Governance Rules\n' + bodyOnly;
639
+ }
640
+
641
+ const newContent =
642
+ before +
643
+ GOVERNANCE_PRESERVE_BEGIN +
644
+ newBlockContent +
645
+ GOVERNANCE_PRESERVE_END +
646
+ '\n' +
647
+ (after ? '\n' + after : '');
648
+ if (newContent !== existingContent) {
649
+ await fs.writeFile(agentsMdPath, newContent, 'utf-8');
650
+ console.log(chalk.green(`✅ 已更新 AGENTS.md 治理契约 PRESERVE:governance 段 (v${version})`));
651
+ }
652
+ return;
653
+ }
654
+
655
+ if (startIdx !== -1 || endIdx !== -1) {
656
+ console.log(chalk.yellow('⚠️ AGENTS.md 中 PRESERVE:governance 标记残缺(只有单边),跳过治理契约写入,请人工修复'));
657
+ return;
658
+ }
659
+
660
+ // 无该段:文件末尾追加
661
+ const newContent = existingContent.trimEnd() + '\n\n' + block;
662
+ await fs.writeFile(agentsMdPath, newContent, 'utf-8');
663
+ console.log(chalk.green(`✅ 已追加治理契约 PRESERVE:governance 段到 AGENTS.md (v${version})`));
664
+ }
665
+
666
+ /**
667
+ * 在 CLAUDE.md 中写入/更新 Governance Rules 约束段(旧落点模型,向后兼容保留)
573
668
  *
574
669
  * - 约束集来自 getEffectiveConstraints(ADR-0001):preset 裁剪、config.yml
575
670
  * 禁用、custom 追加、scenes 过滤全部反映在注入文本里
@@ -632,6 +727,30 @@ export async function setupClaudeMdConstraints(projectPath: string): Promise<voi
632
727
  }
633
728
  }
634
729
 
730
+ /**
731
+ * 治理约束段写入落点路由(studio #302,ADR 2026-08-21 落点模型)
732
+ *
733
+ * - 旧模型仓(CLAUDE.md 已有 HARNESS_CONSTRAINTS 标记或 `## Governance Rules` 块):
734
+ * 继续写 CLAUDE.md——init 幂等重跑不破坏既有仓,不制造双份约束正本
735
+ * - 其余(新仓初始化):写 AGENTS.md PRESERVE:governance 段(入库公共面正本)
736
+ */
737
+ export async function setupGovernanceConstraints(projectPath: string): Promise<void> {
738
+ let claudeContent: string | null = null;
739
+ try {
740
+ claudeContent = await fs.readFile(path.join(projectPath, 'CLAUDE.md'), 'utf-8');
741
+ } catch {
742
+ // CLAUDE.md 不存在 → 新仓
743
+ }
744
+
745
+ if (
746
+ claudeContent !== null &&
747
+ (claudeContent.includes(CONSTRAINTS_START_MARKER) || /^##\s+Governance Rules/m.test(claudeContent))
748
+ ) {
749
+ return setupClaudeMdConstraints(projectPath);
750
+ }
751
+ return setupAgentsMdConstraints(projectPath);
752
+ }
753
+
635
754
  /**
636
755
  * 设置治理相关文件
637
756
  */
@@ -648,8 +767,9 @@ async function setupGovernance(projectPath: string, level: string): Promise<void
648
767
  // 2. 在 CLAUDE.md 中写入 Output Style 段(仅在不存在时创建)
649
768
  await setupClaudeMdOutputStyle(projectPath);
650
769
 
651
- // 3. CLAUDE.md 中写入/更新 Governance Rules 约束段
652
- await setupClaudeMdConstraints(projectPath);
770
+ // 3. 写入/更新 Governance Rules 约束段(新仓 → AGENTS.md PRESERVE:governance;
771
+ // 旧模型仓 → CLAUDE.md,落点路由见 setupGovernanceConstraints)
772
+ await setupGovernanceConstraints(projectPath);
653
773
 
654
774
  // 4. 生成 CONTEXT.md 文件
655
775
  const contextConfig = governance.context_files as Record<string, unknown> | undefined;
@@ -4,7 +4,7 @@
4
4
  约束引擎核心:check/prompt 二元约束系统(ADR-0001)、生效集合并(effective-constraints)、检查点验证器(CSO/passes-gate)、会话管理、Spec 验证器、项目配置加载。
5
5
 
6
6
  ## 核心导出
7
- - `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS) + 检查引擎(ConstraintChecker,拦截统一由 checkBeforeExecution 承担,ADR-0004) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 注入渲染(injection-renderer) + Agent prompt 渲染(agent-prompt-renderer:trigger 参数化分组渲染,role 路由留 studio,H6/G6)/漂移校验(injection-drift)/使用统计(usage-report);CheckEnv 生产侧唯一构造点 `buildCheckEnv(context, providers|'none')`(checkers/types.ts:providers = 证据接线,'none' = 显式不接 → evidence flag checker 按契约 skip)
7
+ - `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS) + 检查引擎(ConstraintChecker,拦截统一由 checkBeforeExecution 承担,ADR-0004) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 注入渲染(injection-renderer) + Agent prompt 渲染(agent-prompt-renderer:trigger 参数化分组渲染,role 路由留 studio,H6/G6)/漂移校验(injection-drift:含注入段落点路由 resolveInjectionTarget,CLAUDE.md 有标记优先、否则 AGENTS.md PRESERVE:governance,studio #307)/使用统计(usage-report);CheckEnv 生产侧唯一构造点 `buildCheckEnv(context, providers|'none')`(checkers/types.ts:providers = 证据接线,'none' = 显式不接 → evidence flag checker 按契约 skip)
8
8
  - `effective-constraints.ts` — `getEffectiveConstraints(projectRoot, {preset?})`:全仓唯一生效集来源(内置 → preset → config.yml 禁用(内置与 custom 同效)→ custom 追加(禁用/已退役的不追加)→ scenes 过滤);`getMergedConstraintsConfig(projectRoot, {preset?})`:同链路的完整 MergedConstraintsConfig 形状(含 disabled/custom/unknownIds),内置工单 23 优先级规则(--preset 仅在无项目自定义配置时生效),check 命令经此入口;`lintEffectiveConfig` 配置诊断
9
9
  - `effective-set.ts` — `filterEnabledEntries(knownIds, entries, {onUnknownId})`:约束侧(collect)与门禁侧(throw)共用的 config 条目筛选器
10
10
  - `validators/` — checkpoints、passes-gate、CSO 验证器