agent-project-sdlc 0.1.15 → 0.1.16

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 CHANGED
@@ -22,8 +22,8 @@ npx sdlc-harness init --adopt
22
22
  | Project initialization | `npx sdlc-harness init` | Creates `AGENTS.md`, `<harnessRoot>/state/**`, workflow skills, managed templates/policies, `.docs/**` and a Makefile include. |
23
23
  | Existing project adoption | `npx sdlc-harness init --adopt` | Adds Harness non-destructively to an existing repository. |
24
24
  | Configurable Harness root | `--harness-folder`, `package.json#sdlcHarness.harnessFolderName`, `sdlc-harness.config.json` | Supports Codex `.codex`, Claude `.claude`, Cursor `.cursor`, Cline `.cline`, Roo `.roo`, Gemini `.gemini` or a custom folder. |
25
- | Managed file sync | `npx sdlc-harness sync` | Materializes package canonical assets into the configured Harness root while preserving project state, docs and local overrides. |
26
- | Upgrade | `npx sdlc-harness upgrade` | Runs migrations and sync for already-adopted projects. |
25
+ | Managed file sync | `npx sdlc-harness sync` | Materializes package canonical assets and safely updates package-managed guidance sections inside user-owned Markdown files while preserving project state, docs and local overrides. |
26
+ | Upgrade | `npx sdlc-harness upgrade` | Runs migrations and sync for already-adopted projects, including legacy seed guidance migration. |
27
27
  | Diagnostics | `npx sdlc-harness doctor` | Reports Harness root, package version, schema version and key managed paths. |
28
28
  | Validators | `npx sdlc-harness validate-*`, `make validate-current`, `make validate-harness` | Checks product, design slicing, development, review, test, release, RFC, active plan shape, prompt language contract and generated overview freshness. |
29
29
  | Lifecycle workflow | `<harnessRoot>/state/lifecycle.yaml`, `<harnessRoot>/state/plan.yaml`, `.docs/**` | Tracks REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING and RFC_RECALIBRATION facts. |
@@ -98,6 +98,8 @@ Do not create formal `.docs/07_test/**` test cases or reports before development
98
98
 
99
99
  `<harnessRoot>/state/memory.md` is only a short cross-stage reminder and navigation surface. It answers what an agent should remember next time and where to find the source. Memory may link to ADRs, PRDs, tech plans or implementation docs; full context, alternatives, tradeoffs and long-term consequences belong in `.docs/05_decisions/` ADRs or other formal `.docs/**` fact sources.
100
100
 
101
+ `sync` and `upgrade` also maintain fixed package-managed sections inside user-owned Markdown files: `## Harness Guidance` in `<harnessRoot>/state/memory.md` and `## Harness Maintenance Rules` in `.docs/INDEX.md`. User memory entries, artifact maps and links stay outside those sections and are preserved. Legacy untitled seed guidance is migrated into the titled section to avoid duplicates. `.github/workflows/harness.yml` is updated only when it has `pjsdlc:sdlc-harness:github-workflow:*` markers or exactly matches the old generated workflow; customized workflows without markers are skipped and reported as `customized`.
102
+
101
103
  ## Common Commands
102
104
 
103
105
  ```sh
@@ -57,8 +57,8 @@ npx sdlc-harness init --adopt
57
57
  | 新项目初始化 | `npx sdlc-harness init` | 选择目标 Agent,生成 Harness 根目录、状态文件、workflow skills、模板、策略、`.docs/**` 和 Makefile include |
58
58
  | 已有项目接入 | `npx sdlc-harness init --adopt` | 非破坏性接入已有仓库,不覆盖业务代码和已有项目事实源 |
59
59
  | 可配置 Harness 根目录 | `--harness-folder`、`package.json#sdlcHarness.harnessFolderName`、`sdlc-harness.config.json` | 支持 `.codex`、`.claude`、`.cursor`、`.cline`、`.roo`、`.gemini` 或自定义目录 |
60
- | 同步 managed workflow 文件 | `npx sdlc-harness sync` | 从包内 canonical assets 物化 `AGENTS.md` managed block、workflow skills、templates、policies、Makefile 片段和 GitHub workflow |
61
- | 升级已接入项目 | `npx sdlc-harness upgrade` | 执行迁移并自动 `sync`,保留 state、docs、业务代码和本地 override |
60
+ | 同步 managed workflow 文件 | `npx sdlc-harness sync` | 从包内 canonical assets 物化 `AGENTS.md` managed block、workflow skills、templates、policies、Makefile 片段、GitHub workflow,并安全更新 user-owned Markdown guidance sections |
61
+ | 升级已接入项目 | `npx sdlc-harness upgrade` | 执行迁移并自动 `sync`,保留 state、docs、业务代码和本地 override,同时迁移旧 seed guidance |
62
62
  | 接入诊断 | `npx sdlc-harness doctor` | 检查 harness root、版本、schema、关键文件和 managed paths |
63
63
  | 阶段 gate | `npx sdlc-harness validate-*`、`make validate-current`、`make validate-harness` | 校验需求、设计切片、开发、Review、测试、发布、RFC、Harness 骨架、提示词语言契约和 overview freshness |
64
64
  | 生命周期工作流 | `lifecycle.yaml`、`plan.yaml`、`.docs/**` | 固定 REQUIREMENT_GATHERING、ARCHITECTING、SPRINTING、REVIEWING、TESTING、RELEASING、RFC_RECALIBRATION 等阶段事实链 |
@@ -119,6 +119,8 @@ SPRINTING 的 Definition of Done 包含可运行入口/出口:技术方案或
119
119
 
120
120
  `<harnessRoot>/state/memory.md` 只做跨阶段快捷提示和导航,回答“下次进来要先记住什么、去哪里找”。memory 可以链接到 ADR、PRD、tech plan 或 implementation doc;完整背景、备选方案、取舍和长期后果放在 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源里。
121
121
 
122
+ `sync` / `upgrade` 会维护用户耦合文件里的固定 package-managed sections:`<harnessRoot>/state/memory.md` 的 `## Harness Guidance` 和 `.docs/INDEX.md` 的 `## Harness Maintenance Rules`。用户自己的 memory 条目、文档产物地图和链接保留在这些标题区块之外;如果旧项目只有早期无标题 seed 文案,升级会把它迁移到固定标题区块,避免重复。`.github/workflows/harness.yml` 只在文件带 `pjsdlc:sdlc-harness:github-workflow:*` marker 或内容等于旧版 generated workflow 时自动更新;自定义且无 marker 的 workflow 会被跳过并报告 `customized`。
123
+
122
124
  ### Workflow skill 如何生效
123
125
 
124
126
  `<harnessRoot>/skills/<name>/SKILL.md` 是 Harness 的 workflow skill 事实源,也是稳定的 hard file index。它有两种使用方式:
@@ -1,3 +1,4 @@
1
+ # pjsdlc:sdlc-harness:github-workflow:begin
1
2
  name: Harness Gates
2
3
 
3
4
  on:
@@ -43,3 +44,4 @@ jobs:
43
44
  run: make "${HARNESS_GATE}"
44
45
  env:
45
46
  HARNESS_GATE: ${{ github.event.inputs.gate || 'validate-harness' }}
47
+ # pjsdlc:sdlc-harness:github-workflow:end
@@ -2,6 +2,9 @@ import { runSync } from "../lib/sync-engine.js";
2
2
  export async function sync() {
3
3
  const report = await runSync(process.cwd());
4
4
  console.log(`sync changed=${report.changed.length} skipped=${report.skipped.length} blocked=${report.blocked.length}`);
5
+ for (const skipped of report.skipped.filter((line) => line.includes("customized"))) {
6
+ console.log(`skipped: ${skipped}`);
7
+ }
5
8
  for (const blocked of report.blocked) {
6
9
  console.error(`blocked: ${blocked}`);
7
10
  }
package/dist/lib/init.js CHANGED
@@ -3,6 +3,7 @@ import { writeConfigIfMissing } from "./config.js";
3
3
  import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
4
4
  import { ensureDir, pathExists, writeTextIfChanged } from "./fs.js";
5
5
  import { runSync } from "./sync-engine.js";
6
+ import { syncDocsIndexMaintenanceSection, syncMemoryGuidanceSection } from "./user-owned-sections.js";
6
7
  const DOC_DIRS = [
7
8
  ".docs/00_raw",
8
9
  ".docs/01_product",
@@ -55,24 +56,24 @@ async function createProjectState(projectRoot, root, report) {
55
56
  ],
56
57
  [harnessPath(root, "state", "plan.yaml"), `current_task_id: ""\nnext_task_sequence: 1\ntasks: []\n`],
57
58
  [harnessPath(root, "state", "plan.draft.yaml"), `next_task_sequence: 1\ntasks: []\n`],
58
- [
59
- harnessPath(root, "state", "memory.md"),
60
- "# Project Memory\n\n短期执行计划写入 plan.yaml;长期稳定知识只在这里记录简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。\n"
61
- ]
62
59
  ];
63
60
  for (const [relative, content] of files) {
64
61
  if (await writeTextIfChanged(path.join(projectRoot, relative), content)) {
65
62
  report.push(`created ${relative}`);
66
63
  }
67
64
  }
65
+ await syncMemoryGuidanceSection(projectRoot, root, {
66
+ changed: report,
67
+ skipped: []
68
+ });
68
69
  }
69
70
  async function createDocs(projectRoot, report) {
70
71
  for (const dir of DOC_DIRS) {
71
72
  await ensureDir(path.join(projectRoot, dir));
72
73
  await writeTextIfChanged(path.join(projectRoot, dir, ".gitkeep"), "");
73
74
  }
74
- const index = ".docs/INDEX.md";
75
- if (await writeTextIfChanged(path.join(projectRoot, index), "# Documentation Index\n\n本文件是 AI SDLC Harness 的文档路由表。\n")) {
76
- report.push(`created ${index}`);
77
- }
75
+ await syncDocsIndexMaintenanceSection(projectRoot, {
76
+ changed: report,
77
+ skipped: []
78
+ });
78
79
  }
@@ -10,6 +10,8 @@ export declare const MAKEFILE_BLOCK_START = "# pjsdlc:sdlc-harness:make:begin";
10
10
  export declare const MAKEFILE_BLOCK_END = "# pjsdlc:sdlc-harness:make:end";
11
11
  export declare const LEGACY_MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
12
12
  export declare const LEGACY_MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
13
+ export declare const GITHUB_WORKFLOW_BLOCK_START = "# pjsdlc:sdlc-harness:github-workflow:begin";
14
+ export declare const GITHUB_WORKFLOW_BLOCK_END = "# pjsdlc:sdlc-harness:github-workflow:end";
13
15
  export declare const MANAGED_METADATA_START = "<!-- pjsdlc:sdlc-harness-managed";
14
16
  export declare const LEGACY_MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
15
17
  export declare const MANAGED_METADATA_END = "-->";
@@ -6,6 +6,8 @@ export const MAKEFILE_BLOCK_START = "# pjsdlc:sdlc-harness:make:begin";
6
6
  export const MAKEFILE_BLOCK_END = "# pjsdlc:sdlc-harness:make:end";
7
7
  export const LEGACY_MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
8
8
  export const LEGACY_MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
9
+ export const GITHUB_WORKFLOW_BLOCK_START = "# pjsdlc:sdlc-harness:github-workflow:begin";
10
+ export const GITHUB_WORKFLOW_BLOCK_END = "# pjsdlc:sdlc-harness:github-workflow:end";
9
11
  export const MANAGED_METADATA_START = "<!-- pjsdlc:sdlc-harness-managed";
10
12
  export const LEGACY_MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
11
13
  export const MANAGED_METADATA_END = "-->";
@@ -3,6 +3,7 @@ import { readdir, rename, rm } from "node:fs/promises";
3
3
  import { defaultConfig, readConfig } from "./config.js";
4
4
  import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
5
5
  import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
6
+ import { syncDocsIndexMaintenanceSection, syncMemoryGuidanceSection } from "./user-owned-sections.js";
6
7
  import { parseYaml, stringifyYaml } from "./yaml.js";
7
8
  export const CURRENT_SCHEMA_VERSION = "1";
8
9
  export const migrations = [];
@@ -27,7 +28,7 @@ export async function runMigrations(projectRoot) {
27
28
  await migratePlan(projectRoot, root, report, "plan.draft.yaml", "tasks.draft.yaml", { activePlan: false });
28
29
  await removeLegacyGateResults(projectRoot, root, report);
29
30
  await removeLegacyCheckpoints(projectRoot, root, report);
30
- await ensureMemory(projectRoot, root, report);
31
+ await ensureUserOwnedGuidanceSections(projectRoot, root, report);
31
32
  return report;
32
33
  }
33
34
  async function migrateConfig(projectRoot, root, report) {
@@ -161,6 +162,9 @@ function migrateManagedFiles(managedFiles, root) {
161
162
  migrated.unshift(makefileEntry);
162
163
  }
163
164
  }
165
+ if (!seen.has(".github/workflows/harness.yml")) {
166
+ push({ path: ".github/workflows/harness.yml", strategy: "create-if-missing" });
167
+ }
164
168
  return migrated;
165
169
  }
166
170
  async function migrateLifecycle(projectRoot, root, report) {
@@ -349,15 +353,7 @@ async function removeLegacyGateResults(projectRoot, root, report) {
349
353
  await rm(gateResultsPath, { force: true });
350
354
  report.changed.push(relativeGateResultsPath);
351
355
  }
352
- async function ensureMemory(projectRoot, root, report) {
353
- const relativeMemoryPath = harnessPath(root, "state", "memory.md");
354
- const memoryPath = path.join(projectRoot, relativeMemoryPath);
355
- if (await pathExists(memoryPath)) {
356
- report.skipped.push(relativeMemoryPath);
357
- return;
358
- }
359
- const content = "# Project Memory\n\n记录跨阶段长期有效知识的简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。\n";
360
- if (await writeTextIfChanged(memoryPath, content)) {
361
- report.changed.push(relativeMemoryPath);
362
- }
356
+ async function ensureUserOwnedGuidanceSections(projectRoot, root, report) {
357
+ await syncMemoryGuidanceSection(projectRoot, root, report);
358
+ await syncDocsIndexMaintenanceSection(projectRoot, report);
363
359
  }
@@ -2,8 +2,9 @@ import path from "node:path";
2
2
  import { readConfig } from "./config.js";
3
3
  import { harnessRoot } from "./harness-root.js";
4
4
  import { copyTree, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
5
- import { AGENTS_BLOCK_MARKERS, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
5
+ import { AGENTS_BLOCK_MARKERS, GITHUB_WORKFLOW_BLOCK_END, GITHUB_WORKFLOW_BLOCK_START, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
6
6
  import { packageAssetPath } from "./paths.js";
7
+ import { syncProjectGuidanceSections } from "./user-owned-sections.js";
7
8
  import { parseYaml, stringifyYaml } from "./yaml.js";
8
9
  export function emptySyncReport() {
9
10
  return {
@@ -19,6 +20,7 @@ export async function runSync(projectRoot) {
19
20
  for (const managedFile of config.managed_files) {
20
21
  await syncManagedFile(projectRoot, root, managedFile, report);
21
22
  }
23
+ await syncProjectGuidanceSections(projectRoot, root, report);
22
24
  return report;
23
25
  }
24
26
  async function syncManagedFile(projectRoot, root, managedFile, report) {
@@ -48,11 +50,7 @@ async function syncManagedFile(projectRoot, root, managedFile, report) {
48
50
  return;
49
51
  }
50
52
  if (managedFile.path === ".github/workflows/harness.yml") {
51
- if (await pathExists(destination)) {
52
- report.skipped.push(managedFile.path);
53
- return;
54
- }
55
- await syncFile(packageAssetPath("github", "harness.yml"), destination, report, "skip-if-missing");
53
+ await syncGithubWorkflow(packageAssetPath("github", "harness.yml"), destination, managedFile.path, report);
56
54
  return;
57
55
  }
58
56
  report.skipped.push(managedFile.path);
@@ -368,3 +366,61 @@ async function syncFile(source, destination, report, missingMode) {
368
366
  report.skipped.push(destination);
369
367
  }
370
368
  }
369
+ async function syncGithubWorkflow(source, destination, relativePath, report) {
370
+ if (!(await pathExists(source))) {
371
+ report.skipped.push(relativePath);
372
+ return;
373
+ }
374
+ const sourceContent = await readText(source);
375
+ if (!(await pathExists(destination))) {
376
+ if (await writeTextIfChanged(destination, sourceContent)) {
377
+ report.changed.push(relativePath);
378
+ }
379
+ else {
380
+ report.skipped.push(relativePath);
381
+ }
382
+ return;
383
+ }
384
+ const existing = await readText(destination);
385
+ const markerState = workflowMarkerState(existing);
386
+ if (markerState === "invalid") {
387
+ report.blocked.push(`${relativePath}: incomplete managed workflow markers`);
388
+ return;
389
+ }
390
+ if (markerState === "managed" || normalizeWorkflow(existing) === normalizeWorkflow(stripWorkflowMarkers(sourceContent))) {
391
+ if (await writeTextIfChanged(destination, sourceContent)) {
392
+ report.changed.push(relativePath);
393
+ }
394
+ else {
395
+ report.skipped.push(relativePath);
396
+ }
397
+ return;
398
+ }
399
+ report.skipped.push(`${relativePath}: customized`);
400
+ }
401
+ function workflowMarkerState(content) {
402
+ const startIndex = content.indexOf(GITHUB_WORKFLOW_BLOCK_START);
403
+ const endIndex = content.indexOf(GITHUB_WORKFLOW_BLOCK_END);
404
+ const hasStart = startIndex >= 0;
405
+ const hasEnd = endIndex >= 0;
406
+ if (!hasStart && !hasEnd) {
407
+ return "missing";
408
+ }
409
+ if (hasStart !== hasEnd || endIndex < startIndex) {
410
+ return "invalid";
411
+ }
412
+ if (content.indexOf(GITHUB_WORKFLOW_BLOCK_START, startIndex + GITHUB_WORKFLOW_BLOCK_START.length) >= 0 ||
413
+ content.indexOf(GITHUB_WORKFLOW_BLOCK_END, endIndex + GITHUB_WORKFLOW_BLOCK_END.length) >= 0) {
414
+ return "invalid";
415
+ }
416
+ return "managed";
417
+ }
418
+ function stripWorkflowMarkers(content) {
419
+ return content
420
+ .split(/\r?\n/)
421
+ .filter((line) => line.trim() !== GITHUB_WORKFLOW_BLOCK_START && line.trim() !== GITHUB_WORKFLOW_BLOCK_END)
422
+ .join("\n");
423
+ }
424
+ function normalizeWorkflow(content) {
425
+ return content.replace(/\r\n/g, "\n").trim();
426
+ }
@@ -7,6 +7,9 @@ export async function runUpgrade(projectRoot) {
7
7
  lines.push(`migrations changed=${migrationReport.changed.length} skipped=${migrationReport.skipped.length}`);
8
8
  const syncReport = await runSync(projectRoot);
9
9
  lines.push(`sync changed=${syncReport.changed.length} skipped=${syncReport.skipped.length} blocked=${syncReport.blocked.length}`);
10
+ for (const skipped of syncReport.skipped.filter((line) => line.includes("customized"))) {
11
+ lines.push(`sync skipped: ${skipped}`);
12
+ }
10
13
  const doctor = await runDoctor(projectRoot);
11
14
  lines.push(`doctor warnings=${doctor.warnings.length} errors=${doctor.errors.length}`);
12
15
  if (syncReport.blocked.length > 0 || doctor.errors.length > 0) {
@@ -0,0 +1,7 @@
1
+ export interface UserOwnedSectionReport {
2
+ changed: string[];
3
+ skipped: string[];
4
+ }
5
+ export declare function syncProjectGuidanceSections(projectRoot: string, root: string, report: UserOwnedSectionReport): Promise<void>;
6
+ export declare function syncMemoryGuidanceSection(projectRoot: string, root: string, report: UserOwnedSectionReport): Promise<void>;
7
+ export declare function syncDocsIndexMaintenanceSection(projectRoot: string, report: UserOwnedSectionReport): Promise<void>;
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import { pathExists, readText, writeTextIfChanged } from "./fs.js";
3
+ import { harnessPath } from "./harness-root.js";
4
+ const MEMORY_GUIDANCE_HEADING = "## Harness Guidance";
5
+ const DOCS_INDEX_RULES_HEADING = "## Harness Maintenance Rules";
6
+ const LEGACY_MEMORY_PARAGRAPHS = [
7
+ "短期执行计划写入 plan.yaml;长期稳定知识只在这里记录简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。",
8
+ "记录跨阶段长期有效知识的简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。",
9
+ "内容保持简短,详细说明链接到 `.docs/` 下的对应文档。完整决策背景、备选方案、取舍和后果应写入 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源。"
10
+ ];
11
+ const LEGACY_INDEX_MAINTENANCE_SECTION = [
12
+ "## 维护规则",
13
+ "",
14
+ "- 每个新增产物都要从本索引链接。",
15
+ "- 仍属于产品、架构、实现、测试或 RFC 事实源的过时产物标记为 superseded;短期执行计划和历史发布流水以 git、tag、registry、CI 或外部 release 系统追溯。",
16
+ "- task/release 的历史动作记录以 git commit、tag 或外部 release 系统为准,不再维护 `<harnessRoot>/archive/` 常规归档。",
17
+ "- implementation docs 必须对齐真实代码,而不只是原始技术方案。"
18
+ ].join("\n");
19
+ export async function syncProjectGuidanceSections(projectRoot, root, report) {
20
+ await syncMemoryGuidanceSection(projectRoot, root, report);
21
+ await syncDocsIndexMaintenanceSection(projectRoot, report);
22
+ }
23
+ export async function syncMemoryGuidanceSection(projectRoot, root, report) {
24
+ const relativePath = harnessPath(root, "state", "memory.md");
25
+ const targetPath = path.join(projectRoot, relativePath);
26
+ const existing = (await pathExists(targetPath)) ? await readText(targetPath) : "# Project Memory\n";
27
+ const next = mergeMarkdownSection(removeLegacyMemoryGuidance(existing), MEMORY_GUIDANCE_HEADING, renderMemoryGuidanceSection(root));
28
+ await writeSectionIfChanged(targetPath, relativePath, next, report);
29
+ }
30
+ export async function syncDocsIndexMaintenanceSection(projectRoot, report) {
31
+ const relativePath = ".docs/INDEX.md";
32
+ const targetPath = path.join(projectRoot, relativePath);
33
+ const existing = (await pathExists(targetPath))
34
+ ? await readText(targetPath)
35
+ : "# Documentation Index\n\n本文件是 AI SDLC Harness 的文档路由表。\n";
36
+ const next = mergeMarkdownSection(removeLegacyIndexMaintenanceSection(existing), DOCS_INDEX_RULES_HEADING, renderDocsIndexMaintenanceSection());
37
+ await writeSectionIfChanged(targetPath, relativePath, next, report);
38
+ }
39
+ function renderMemoryGuidanceSection(root) {
40
+ const planPath = harnessPath(root, "state", "plan.yaml");
41
+ return [
42
+ MEMORY_GUIDANCE_HEADING,
43
+ "",
44
+ "- 内容保持简短,详细说明链接到 `.docs/` 下的对应文档。",
45
+ `- 短期执行计划写入 \`${planPath}\`;长期稳定知识只在这里记录简短摘要和链接。`,
46
+ "- 完整决策背景、备选方案、取舍和后果应写入 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源。"
47
+ ].join("\n");
48
+ }
49
+ function renderDocsIndexMaintenanceSection() {
50
+ return [
51
+ DOCS_INDEX_RULES_HEADING,
52
+ "",
53
+ "- `overview.md` 是 generated artifact,用于浏览和阶段交接;不要手写或局部编辑。",
54
+ "- Markdown slices 和 `.docs/INDEX.md` 是事实源。",
55
+ "- 任意 `.docs/<stage>/**/*.md` 新增、修改、拆分、合并或废弃后,运行 `make docs-overview`。",
56
+ "- 提交或阶段交付前,运行 `make validate-doc-overviews` 或 `make validate-harness` 确认 overview 未过期。",
57
+ "- 每个新增产物都要从本索引链接;implementation docs 必须对齐真实代码。"
58
+ ].join("\n");
59
+ }
60
+ function removeLegacyMemoryGuidance(content) {
61
+ let next = content;
62
+ for (const paragraph of LEGACY_MEMORY_PARAGRAPHS) {
63
+ next = next.replace(paragraph, "");
64
+ }
65
+ return compactBlankLines(next);
66
+ }
67
+ function removeLegacyIndexMaintenanceSection(content) {
68
+ return compactBlankLines(content.replace(LEGACY_INDEX_MAINTENANCE_SECTION, ""));
69
+ }
70
+ function mergeMarkdownSection(existing, heading, section) {
71
+ const normalizedExisting = existing.replace(/\r\n/g, "\n").trimEnd();
72
+ const normalizedSection = section.trimEnd();
73
+ if (!normalizedExisting) {
74
+ return `${normalizedSection}\n`;
75
+ }
76
+ const lines = normalizedExisting.split("\n");
77
+ const startIndex = lines.findIndex((line) => line.trim() === heading);
78
+ if (startIndex < 0) {
79
+ return `${normalizedExisting}\n\n${normalizedSection}\n`;
80
+ }
81
+ const headingLevel = heading.match(/^#+/)?.[0].length ?? 2;
82
+ let endIndex = lines.length;
83
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
84
+ const match = lines[index].match(/^(#{1,6})\s+/);
85
+ if (match && match[1].length <= headingLevel) {
86
+ endIndex = index;
87
+ break;
88
+ }
89
+ }
90
+ const before = lines.slice(0, startIndex).join("\n").trimEnd();
91
+ const after = lines.slice(endIndex).join("\n").trimStart();
92
+ const parts = [before, normalizedSection, after].filter((part) => part.trim());
93
+ return `${parts.join("\n\n")}\n`;
94
+ }
95
+ async function writeSectionIfChanged(targetPath, relativePath, content, report) {
96
+ if (await writeTextIfChanged(targetPath, content)) {
97
+ report.changed.push(relativePath);
98
+ }
99
+ else {
100
+ report.skipped.push(relativePath);
101
+ }
102
+ }
103
+ function compactBlankLines(content) {
104
+ return content.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n");
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-project-sdlc",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "CLI and canonical assets for the AI SDLC Harness workflow.",
5
5
  "type": "module",
6
6
  "bin": {