agent-project-sdlc 0.1.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.
Files changed (81) hide show
  1. package/assets/README.md +5 -0
  2. package/assets/agents/.gitkeep +1 -0
  3. package/assets/agents/AGENTS_CORE.md +164 -0
  4. package/assets/github/.gitkeep +1 -0
  5. package/assets/github/harness.yml +45 -0
  6. package/assets/make/.gitkeep +1 -0
  7. package/assets/make/sdlc-harness.mk +78 -0
  8. package/assets/policies/allowed_paths.yaml +55 -0
  9. package/assets/policies/gates.yaml +48 -0
  10. package/assets/policies/phase_contracts.yaml +140 -0
  11. package/assets/policies/risk_matrix.yaml +27 -0
  12. package/assets/skills/architect_design/SKILL.md +62 -0
  13. package/assets/skills/dev_sprint/SKILL.md +90 -0
  14. package/assets/skills/implementation_doc/SKILL.md +58 -0
  15. package/assets/skills/manager/SKILL.md +50 -0
  16. package/assets/skills/pm_prd/SKILL.md +59 -0
  17. package/assets/skills/release_manager/SKILL.md +59 -0
  18. package/assets/skills/reviewer/SKILL.md +60 -0
  19. package/assets/skills/rfc_recalibrate/SKILL.md +61 -0
  20. package/assets/skills/tester/SKILL.md +59 -0
  21. package/assets/templates/ADR_TEMPLATE.md +19 -0
  22. package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +53 -0
  23. package/assets/templates/PLAN_TEMPLATE.yaml +25 -0
  24. package/assets/templates/PRD_TEMPLATE.md +41 -0
  25. package/assets/templates/RELEASE_TEMPLATE.md +34 -0
  26. package/assets/templates/REVIEW_TEMPLATE.md +31 -0
  27. package/assets/templates/RFC_TEMPLATE.md +36 -0
  28. package/assets/templates/SKILL_TEMPLATE.md +52 -0
  29. package/assets/templates/TECH_DESIGN_TEMPLATE.md +54 -0
  30. package/assets/templates/TEST_PLAN_TEMPLATE.md +29 -0
  31. package/dist/cli.d.ts +2 -0
  32. package/dist/cli.js +12 -0
  33. package/dist/commands/doctor.d.ts +1 -0
  34. package/dist/commands/doctor.js +16 -0
  35. package/dist/commands/index.d.ts +3 -0
  36. package/dist/commands/index.js +30 -0
  37. package/dist/commands/init.d.ts +2 -0
  38. package/dist/commands/init.js +56 -0
  39. package/dist/commands/package-source.d.ts +1 -0
  40. package/dist/commands/package-source.js +24 -0
  41. package/dist/commands/sync.d.ts +1 -0
  42. package/dist/commands/sync.js +11 -0
  43. package/dist/commands/upgrade.d.ts +1 -0
  44. package/dist/commands/upgrade.js +7 -0
  45. package/dist/commands/validate.d.ts +1 -0
  46. package/dist/commands/validate.js +14 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +1 -0
  49. package/dist/lib/config.d.ts +5 -0
  50. package/dist/lib/config.js +55 -0
  51. package/dist/lib/doctor.d.ts +6 -0
  52. package/dist/lib/doctor.js +31 -0
  53. package/dist/lib/fs.d.ts +8 -0
  54. package/dist/lib/fs.js +56 -0
  55. package/dist/lib/harness-root.d.ts +9 -0
  56. package/dist/lib/harness-root.js +50 -0
  57. package/dist/lib/init.d.ts +5 -0
  58. package/dist/lib/init.js +76 -0
  59. package/dist/lib/managed-file.d.ts +6 -0
  60. package/dist/lib/managed-file.js +6 -0
  61. package/dist/lib/migrations.d.ts +12 -0
  62. package/dist/lib/migrations.js +176 -0
  63. package/dist/lib/package-json-config.d.ts +2 -0
  64. package/dist/lib/package-json-config.js +37 -0
  65. package/dist/lib/package-source.d.ts +8 -0
  66. package/dist/lib/package-source.js +107 -0
  67. package/dist/lib/paths.d.ts +5 -0
  68. package/dist/lib/paths.js +11 -0
  69. package/dist/lib/sync-engine.d.ts +7 -0
  70. package/dist/lib/sync-engine.js +202 -0
  71. package/dist/lib/types.d.ts +22 -0
  72. package/dist/lib/types.js +1 -0
  73. package/dist/lib/upgrade.d.ts +1 -0
  74. package/dist/lib/upgrade.js +16 -0
  75. package/dist/lib/validators.d.ts +6 -0
  76. package/dist/lib/validators.js +158 -0
  77. package/dist/lib/yaml.d.ts +2 -0
  78. package/dist/lib/yaml.js +7 -0
  79. package/migrations/README.md +3 -0
  80. package/package.json +31 -0
  81. package/source-mappings.yaml +19 -0
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_HARNESS_ROOT, HARNESS_JSON_CONFIG_PATH } from "./paths.js";
3
+ import { pathExists, readText } from "./fs.js";
4
+ export async function readHarnessRootConfig(projectRoot) {
5
+ const explicitConfig = await readJsonConfig(path.join(projectRoot, HARNESS_JSON_CONFIG_PATH));
6
+ const explicitValue = folderNameFromObject(explicitConfig);
7
+ if (explicitValue) {
8
+ return { harnessFolderName: normalizeHarnessFolderName(explicitValue), source: HARNESS_JSON_CONFIG_PATH };
9
+ }
10
+ const packageJson = await readJsonConfig(path.join(projectRoot, "package.json"));
11
+ const packageConfig = packageJson && typeof packageJson === "object" ? packageJson.sdlcHarness : undefined;
12
+ const packageValue = folderNameFromObject(packageConfig);
13
+ if (packageValue) {
14
+ return { harnessFolderName: normalizeHarnessFolderName(packageValue), source: "package.json#sdlcHarness" };
15
+ }
16
+ return { harnessFolderName: DEFAULT_HARNESS_ROOT, source: "default" };
17
+ }
18
+ export async function harnessRoot(projectRoot) {
19
+ return (await readHarnessRootConfig(projectRoot)).harnessFolderName;
20
+ }
21
+ export async function harnessConfigPath(projectRoot) {
22
+ return path.join(await harnessRoot(projectRoot), "config.yaml");
23
+ }
24
+ export function harnessPath(root, ...segments) {
25
+ return path.join(root, ...segments);
26
+ }
27
+ function folderNameFromObject(value) {
28
+ if (!value || typeof value !== "object") {
29
+ return undefined;
30
+ }
31
+ const record = value;
32
+ const folderName = record.harnessFolderName ?? record.harnessFloderName;
33
+ return typeof folderName === "string" && folderName.trim() ? folderName : undefined;
34
+ }
35
+ async function readJsonConfig(filePath) {
36
+ if (!(await pathExists(filePath))) {
37
+ return undefined;
38
+ }
39
+ return JSON.parse(await readText(filePath));
40
+ }
41
+ export function normalizeHarnessFolderName(value) {
42
+ const normalized = value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
43
+ if (!normalized || normalized === "." || normalized === "..") {
44
+ throw new Error("harnessFolderName must be a non-empty relative directory");
45
+ }
46
+ if (path.isAbsolute(normalized) || normalized.includes("..")) {
47
+ throw new Error("harnessFolderName must not be absolute or contain '..'");
48
+ }
49
+ return normalized;
50
+ }
@@ -0,0 +1,5 @@
1
+ export interface InitOptions {
2
+ adopt: boolean;
3
+ force: boolean;
4
+ }
5
+ export declare function runInit(projectRoot: string, options: InitOptions): Promise<string[]>;
@@ -0,0 +1,76 @@
1
+ import path from "node:path";
2
+ import { writeConfigIfMissing } from "./config.js";
3
+ import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
4
+ import { ensureDir, pathExists, writeTextIfChanged } from "./fs.js";
5
+ import { runSync } from "./sync-engine.js";
6
+ const DOC_DIRS = [
7
+ ".docs/00_raw",
8
+ ".docs/01_product",
9
+ ".docs/02_architecture",
10
+ ".docs/03_tech_plan",
11
+ ".docs/04_implementation",
12
+ ".docs/05_decisions",
13
+ ".docs/06_review",
14
+ ".docs/07_test",
15
+ ".docs/08_release",
16
+ ".docs/rfc"
17
+ ];
18
+ export async function runInit(projectRoot, options) {
19
+ const report = [];
20
+ const existingEntries = await projectHasExistingFiles(projectRoot);
21
+ if (existingEntries && !options.adopt && !options.force) {
22
+ report.push("Project is not empty; continuing with non-destructive init. Use --adopt to mark this as an existing project adoption.");
23
+ }
24
+ const root = await harnessRoot(projectRoot);
25
+ const configPath = await harnessConfigPath(projectRoot);
26
+ if (await writeConfigIfMissing(projectRoot)) {
27
+ report.push(`created ${configPath}`);
28
+ }
29
+ else {
30
+ report.push(`kept existing ${configPath}`);
31
+ }
32
+ await createProjectState(projectRoot, root, report);
33
+ await createDocs(projectRoot, report);
34
+ const syncReport = await runSync(projectRoot);
35
+ report.push(`sync changed=${syncReport.changed.length} skipped=${syncReport.skipped.length} blocked=${syncReport.blocked.length}`);
36
+ report.push(options.adopt ? "adopt mode complete" : "init complete");
37
+ return report;
38
+ }
39
+ async function projectHasExistingFiles(projectRoot) {
40
+ const markers = ["README.md", "src", "pyproject.toml", "go.mod"];
41
+ for (const marker of markers) {
42
+ if (await pathExists(path.join(projectRoot, marker))) {
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+ async function createProjectState(projectRoot, root, report) {
49
+ const stateRoot = path.join(projectRoot, root, "state");
50
+ await ensureDir(stateRoot);
51
+ const files = [
52
+ [
53
+ harnessPath(root, "state", "lifecycle.yaml"),
54
+ `project_name: "Project"\nversion: "v0.1"\ncurrent_phase: "REQUIREMENT_GATHERING"\nactive_role: "pm"\nactive_skill: "pm_prd"\ncurrent_milestone: "MVP"\nblocked_reason: ""\nsuspended_phase: ""\nallowed_next_phases:\n - "ARCHITECTING"\nhistory: []\n`
55
+ ],
56
+ [harnessPath(root, "state", "plan.yaml"), `current_phase: "SPRINTING"\ncurrent_task_id: ""\ntasks: []\n`],
57
+ [harnessPath(root, "state", "plan.draft.yaml"), `current_phase: "SPRINTING"\ncurrent_task_id: ""\ntasks: []\n`],
58
+ [harnessPath(root, "state", "gate_results.log"), "# Gate results are appended by sdlc-harness.\n"],
59
+ [harnessPath(root, "state", "memory.md"), "# Project Memory\n\n短期执行计划写入 plan.yaml;长期稳定知识简短记录在这里,并链接到 `.docs/` 正式出处。\n"]
60
+ ];
61
+ for (const [relative, content] of files) {
62
+ if (await writeTextIfChanged(path.join(projectRoot, relative), content)) {
63
+ report.push(`created ${relative}`);
64
+ }
65
+ }
66
+ }
67
+ async function createDocs(projectRoot, report) {
68
+ for (const dir of DOC_DIRS) {
69
+ await ensureDir(path.join(projectRoot, dir));
70
+ await writeTextIfChanged(path.join(projectRoot, dir, ".gitkeep"), "");
71
+ }
72
+ const index = ".docs/INDEX.md";
73
+ if (await writeTextIfChanged(path.join(projectRoot, index), "# Documentation Index\n\n本文件是 AI SDLC Harness 的文档路由表。\n")) {
74
+ report.push(`created ${index}`);
75
+ }
76
+ }
@@ -0,0 +1,6 @@
1
+ export declare const MANAGED_BLOCK_START = "<!-- sdlc-harness:begin -->";
2
+ export declare const MANAGED_BLOCK_END = "<!-- sdlc-harness:end -->";
3
+ export declare const MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
4
+ export declare const MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
5
+ export declare const MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
6
+ export declare const MANAGED_METADATA_END = "-->";
@@ -0,0 +1,6 @@
1
+ export const MANAGED_BLOCK_START = "<!-- sdlc-harness:begin -->";
2
+ export const MANAGED_BLOCK_END = "<!-- sdlc-harness:end -->";
3
+ export const MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
4
+ export const MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
5
+ export const MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
6
+ export const MANAGED_METADATA_END = "-->";
@@ -0,0 +1,12 @@
1
+ export declare const CURRENT_SCHEMA_VERSION = "1";
2
+ export interface Migration {
3
+ from: string;
4
+ to: string;
5
+ description: string;
6
+ }
7
+ export declare const migrations: Migration[];
8
+ export interface MigrationReport {
9
+ changed: string[];
10
+ skipped: string[];
11
+ }
12
+ export declare function runMigrations(projectRoot: string): Promise<MigrationReport>;
@@ -0,0 +1,176 @@
1
+ import path from "node:path";
2
+ import { rm } from "node:fs/promises";
3
+ import { readConfig } from "./config.js";
4
+ import { pathExists, readText, writeTextIfChanged } from "./fs.js";
5
+ import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
6
+ import { parseYaml, stringifyYaml } from "./yaml.js";
7
+ export const CURRENT_SCHEMA_VERSION = "1";
8
+ export const migrations = [];
9
+ export async function runMigrations(projectRoot) {
10
+ const report = { changed: [], skipped: [] };
11
+ const root = await harnessRoot(projectRoot);
12
+ await migrateConfig(projectRoot, root, report);
13
+ await migratePlan(projectRoot, root, report, "plan.yaml", "tasks.yaml");
14
+ await migratePlan(projectRoot, root, report, "plan.draft.yaml", "tasks.draft.yaml");
15
+ await removeLegacyCheckpoints(projectRoot, root, report);
16
+ await ensureMemory(projectRoot, root, report);
17
+ return report;
18
+ }
19
+ async function migrateConfig(projectRoot, root, report) {
20
+ const relativeConfigPath = await harnessConfigPath(projectRoot);
21
+ const configPath = path.join(projectRoot, relativeConfigPath);
22
+ if (!(await pathExists(configPath))) {
23
+ report.skipped.push(relativeConfigPath);
24
+ return;
25
+ }
26
+ const config = await readConfig(projectRoot);
27
+ config.core.schema_version = CURRENT_SCHEMA_VERSION;
28
+ config.managed_files = migrateManagedFiles(config.managed_files, root);
29
+ config.local_overrides = config.local_overrides.map((item) => item === ".harness/policies/*.local.yaml" ? harnessPath(root, "managed", "policies", "*.local.yaml") : item);
30
+ if (await writeTextIfChanged(configPath, stringifyYaml(config))) {
31
+ report.changed.push(relativeConfigPath);
32
+ }
33
+ else {
34
+ report.skipped.push(relativeConfigPath);
35
+ }
36
+ }
37
+ function migrateManagedFiles(managedFiles, root) {
38
+ const migrated = [];
39
+ const seen = new Set();
40
+ const push = (item) => {
41
+ if (seen.has(item.path)) {
42
+ return;
43
+ }
44
+ seen.add(item.path);
45
+ migrated.push(item);
46
+ };
47
+ for (const item of managedFiles) {
48
+ if (item.path === ".agents/skills" || item.path === ".harness/agents/skills") {
49
+ push({ path: harnessPath(root, "skills"), strategy: "managed" });
50
+ continue;
51
+ }
52
+ if (item.path === ".harness/templates") {
53
+ push({ path: harnessPath(root, "managed", "templates"), strategy: "managed" });
54
+ continue;
55
+ }
56
+ if (item.path === ".harness/policies") {
57
+ push({ path: harnessPath(root, "managed", "policies"), strategy: "merge-with-local" });
58
+ continue;
59
+ }
60
+ if (item.path === ".harness/make/sdlc-harness.mk") {
61
+ push({ path: harnessPath(root, "managed", "make", "sdlc-harness.mk"), strategy: "managed" });
62
+ continue;
63
+ }
64
+ push(item);
65
+ }
66
+ if (!seen.has("Makefile")) {
67
+ const agentsIndex = migrated.findIndex((item) => item.path === "AGENTS.md");
68
+ const makefileEntry = { path: "Makefile", strategy: "merge-block" };
69
+ if (agentsIndex >= 0) {
70
+ migrated.splice(agentsIndex + 1, 0, makefileEntry);
71
+ }
72
+ else {
73
+ migrated.unshift(makefileEntry);
74
+ }
75
+ }
76
+ return migrated;
77
+ }
78
+ async function migratePlan(projectRoot, root, report, planFileName, legacyFileName) {
79
+ const relativePlanPath = harnessPath(root, "state", planFileName);
80
+ const planPath = path.join(projectRoot, relativePlanPath);
81
+ const legacyTasksPath = path.join(projectRoot, harnessPath(root, "state", legacyFileName));
82
+ const sourcePath = (await pathExists(planPath)) ? planPath : legacyTasksPath;
83
+ if (!(await pathExists(sourcePath))) {
84
+ report.skipped.push(relativePlanPath);
85
+ return;
86
+ }
87
+ const data = (parseYaml(await readText(sourcePath)) ?? {});
88
+ let changed = false;
89
+ if (!("current_phase" in data)) {
90
+ data.current_phase = "SPRINTING";
91
+ changed = true;
92
+ }
93
+ if (!("current_task_id" in data)) {
94
+ data.current_task_id = "";
95
+ changed = true;
96
+ }
97
+ if (!Array.isArray(data.tasks)) {
98
+ data.tasks = [];
99
+ changed = true;
100
+ }
101
+ if (Array.isArray(data.tasks)) {
102
+ for (const task of data.tasks) {
103
+ if (!isRecord(task))
104
+ continue;
105
+ if ("checkpoint" in task) {
106
+ const checkpoint = String(task.checkpoint ?? "");
107
+ if (isOpenTask(task) && checkpoint) {
108
+ const contract = await readLegacyCheckpointContract(projectRoot, root, checkpoint);
109
+ for (const field of ["allowed_paths", "required_gates", "acceptance_criteria"]) {
110
+ if (!(field in task) && Array.isArray(contract[field])) {
111
+ task[field] = contract[field];
112
+ }
113
+ }
114
+ }
115
+ delete task.checkpoint;
116
+ changed = true;
117
+ }
118
+ }
119
+ }
120
+ if (changed || sourcePath !== planPath) {
121
+ if (await writeTextIfChanged(planPath, stringifyYaml(data))) {
122
+ report.changed.push(relativePlanPath);
123
+ }
124
+ else {
125
+ report.skipped.push(relativePlanPath);
126
+ }
127
+ if (sourcePath !== planPath) {
128
+ await rm(sourcePath, { force: true });
129
+ report.changed.push(path.relative(projectRoot, sourcePath));
130
+ }
131
+ }
132
+ else {
133
+ report.skipped.push(relativePlanPath);
134
+ }
135
+ }
136
+ function isRecord(value) {
137
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
138
+ }
139
+ function isOpenTask(task) {
140
+ return ["pending", "in_progress", "blocked", "pending_revision"].includes(String(task.status));
141
+ }
142
+ async function readLegacyCheckpointContract(projectRoot, root, checkpoint) {
143
+ const relative = checkpoint.replace("<harnessRoot>", root);
144
+ const checkpointPath = path.join(projectRoot, relative);
145
+ if (!(await pathExists(checkpointPath))) {
146
+ return {};
147
+ }
148
+ const text = await readText(checkpointPath);
149
+ const match = text.match(/## Task Contract[\s\S]*?```ya?ml\s*([\s\S]*?)```/i);
150
+ if (!match) {
151
+ return {};
152
+ }
153
+ return (parseYaml(match[1]) ?? {});
154
+ }
155
+ async function removeLegacyCheckpoints(projectRoot, root, report) {
156
+ const relativeCheckpointPath = harnessPath(root, "state", "checkpoints");
157
+ const checkpointPath = path.join(projectRoot, relativeCheckpointPath);
158
+ if (!(await pathExists(checkpointPath))) {
159
+ report.skipped.push(relativeCheckpointPath);
160
+ return;
161
+ }
162
+ await rm(checkpointPath, { recursive: true, force: true });
163
+ report.changed.push(relativeCheckpointPath);
164
+ }
165
+ async function ensureMemory(projectRoot, root, report) {
166
+ const relativeMemoryPath = harnessPath(root, "state", "memory.md");
167
+ const memoryPath = path.join(projectRoot, relativeMemoryPath);
168
+ if (await pathExists(memoryPath)) {
169
+ report.skipped.push(relativeMemoryPath);
170
+ return;
171
+ }
172
+ const content = "# Project Memory\n\n记录跨阶段长期有效的稳定知识,并链接到 `.docs/` 正式出处。\n";
173
+ if (await writeTextIfChanged(memoryPath, content)) {
174
+ report.changed.push(relativeMemoryPath);
175
+ }
176
+ }
@@ -0,0 +1,2 @@
1
+ export declare function packageHarnessRoot(projectRoot: string): Promise<string | undefined>;
2
+ export declare function writePackageHarnessRoot(projectRoot: string, folderName: string): Promise<boolean>;
@@ -0,0 +1,37 @@
1
+ import path from "node:path";
2
+ import { pathExists, readText, writeTextIfChanged } from "./fs.js";
3
+ import { normalizeHarnessFolderName } from "./harness-root.js";
4
+ export async function packageHarnessRoot(projectRoot) {
5
+ const packagePath = path.join(projectRoot, "package.json");
6
+ if (!(await pathExists(packagePath))) {
7
+ return undefined;
8
+ }
9
+ const packageJson = parsePackageJson(await readText(packagePath));
10
+ const config = packageJson.sdlcHarness;
11
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
12
+ return undefined;
13
+ }
14
+ const value = config.harnessFolderName;
15
+ return typeof value === "string" && value.trim() ? normalizeHarnessFolderName(value) : undefined;
16
+ }
17
+ export async function writePackageHarnessRoot(projectRoot, folderName) {
18
+ const normalized = normalizeHarnessFolderName(folderName);
19
+ const packagePath = path.join(projectRoot, "package.json");
20
+ const packageJson = (await pathExists(packagePath)) ? parsePackageJson(await readText(packagePath)) : {};
21
+ const existingConfig = packageJson.sdlcHarness;
22
+ const nextConfig = existingConfig && typeof existingConfig === "object" && !Array.isArray(existingConfig)
23
+ ? { ...existingConfig, harnessFolderName: normalized }
24
+ : { harnessFolderName: normalized };
25
+ const next = {
26
+ ...packageJson,
27
+ sdlcHarness: nextConfig
28
+ };
29
+ return writeTextIfChanged(packagePath, `${JSON.stringify(next, null, 2)}\n`);
30
+ }
31
+ function parsePackageJson(content) {
32
+ const parsed = JSON.parse(content);
33
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34
+ throw new Error("package.json must contain a JSON object");
35
+ }
36
+ return parsed;
37
+ }
@@ -0,0 +1,8 @@
1
+ export interface PackageSourceSyncReport {
2
+ changed: string[];
3
+ }
4
+ export interface PackageSourceCheckReport {
5
+ drift: string[];
6
+ }
7
+ export declare function syncSource(projectRoot: string): Promise<PackageSourceSyncReport>;
8
+ export declare function checkSource(projectRoot: string): Promise<PackageSourceCheckReport>;
@@ -0,0 +1,107 @@
1
+ import { createHash } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
5
+ import { MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
6
+ import { SOURCE_MAPPINGS_PATH } from "./paths.js";
7
+ import { parseYaml } from "./yaml.js";
8
+ export async function syncSource(projectRoot) {
9
+ const report = { changed: [] };
10
+ for (const mapping of await readSourceMappings(projectRoot)) {
11
+ const changed = await applyMapping(projectRoot, mapping);
12
+ report.changed.push(...changed);
13
+ }
14
+ return report;
15
+ }
16
+ export async function checkSource(projectRoot) {
17
+ const drift = [];
18
+ for (const mapping of await readSourceMappings(projectRoot)) {
19
+ const expected = await renderMapping(projectRoot, mapping);
20
+ const target = path.join(projectRoot, mapping.target);
21
+ if (typeof expected === "string") {
22
+ const existing = (await pathExists(target)) ? await readText(target) : "";
23
+ if (normalize(existing) !== normalize(expected)) {
24
+ drift.push(mapping.target);
25
+ }
26
+ continue;
27
+ }
28
+ const sourceHashes = new Map();
29
+ for (const item of expected) {
30
+ sourceHashes.set(item.relative, hash(item.content));
31
+ const targetFile = path.join(target, item.relative);
32
+ const existing = (await pathExists(targetFile)) ? await readText(targetFile) : "";
33
+ if (hash(existing) !== hash(item.content)) {
34
+ drift.push(`${mapping.target}/${item.relative}`);
35
+ }
36
+ }
37
+ const targetFiles = await listFiles(target);
38
+ for (const targetFile of targetFiles) {
39
+ if (path.basename(targetFile) === ".gitkeep") {
40
+ continue;
41
+ }
42
+ const relative = path.relative(target, targetFile);
43
+ if (!sourceHashes.has(relative)) {
44
+ drift.push(`${mapping.target}/${relative}`);
45
+ }
46
+ }
47
+ }
48
+ return { drift };
49
+ }
50
+ async function readSourceMappings(projectRoot) {
51
+ const mappingPath = path.join(projectRoot, SOURCE_MAPPINGS_PATH);
52
+ const parsed = parseYaml(await readText(mappingPath));
53
+ return parsed.source_mappings ?? [];
54
+ }
55
+ async function applyMapping(projectRoot, mapping) {
56
+ const target = path.join(projectRoot, mapping.target);
57
+ const rendered = await renderMapping(projectRoot, mapping);
58
+ if (typeof rendered === "string") {
59
+ return (await writeTextIfChanged(target, rendered)) ? [mapping.target] : [];
60
+ }
61
+ await fs.rm(target, { recursive: true, force: true });
62
+ await ensureDir(target);
63
+ const changed = [];
64
+ for (const item of rendered) {
65
+ const targetFile = path.join(target, item.relative);
66
+ if (await writeTextIfChanged(targetFile, item.content)) {
67
+ changed.push(`${mapping.target}/${item.relative}`);
68
+ }
69
+ }
70
+ return changed;
71
+ }
72
+ async function renderMapping(projectRoot, mapping) {
73
+ const source = path.join(projectRoot, mapping.source);
74
+ if (mapping.mode === "copy-file") {
75
+ return readText(source);
76
+ }
77
+ if (mapping.mode === "copy-tree") {
78
+ const files = await listFiles(source);
79
+ const rendered = [];
80
+ for (const file of files) {
81
+ if (path.basename(file) === ".gitkeep") {
82
+ continue;
83
+ }
84
+ rendered.push({ relative: path.relative(source, file), content: await readText(file) });
85
+ }
86
+ return rendered;
87
+ }
88
+ if (mapping.mode === "extract-managed-block") {
89
+ const content = await readText(source);
90
+ const start = content.indexOf(MANAGED_BLOCK_START);
91
+ const end = content.indexOf(MANAGED_BLOCK_END);
92
+ if (start >= 0 && end > start) {
93
+ return `${content.slice(start + MANAGED_BLOCK_START.length, end).trim()}\n`;
94
+ }
95
+ return content;
96
+ }
97
+ if (mapping.mode === "extract-harness-targets") {
98
+ return readText(source);
99
+ }
100
+ throw new Error(`Unsupported source mapping mode: ${mapping.mode}`);
101
+ }
102
+ function normalize(content) {
103
+ return content.replace(/\r\n/g, "\n").trimEnd();
104
+ }
105
+ function hash(content) {
106
+ return createHash("sha256").update(normalize(content)).digest("hex");
107
+ }
@@ -0,0 +1,5 @@
1
+ export declare const SOURCE_MAPPINGS_PATH = "packages/sdlc-harness/source-mappings.yaml";
2
+ export declare const DEFAULT_HARNESS_ROOT = ".agent";
3
+ export declare const HARNESS_JSON_CONFIG_PATH = "sdlc-harness.config.json";
4
+ export declare function packageRoot(): string;
5
+ export declare function packageAssetPath(...segments: string[]): string;
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export const SOURCE_MAPPINGS_PATH = "packages/sdlc-harness/source-mappings.yaml";
4
+ export const DEFAULT_HARNESS_ROOT = ".agent";
5
+ export const HARNESS_JSON_CONFIG_PATH = "sdlc-harness.config.json";
6
+ export function packageRoot() {
7
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
+ }
9
+ export function packageAssetPath(...segments) {
10
+ return path.join(packageRoot(), "assets", ...segments);
11
+ }
@@ -0,0 +1,7 @@
1
+ export interface SyncReport {
2
+ changed: string[];
3
+ skipped: string[];
4
+ blocked: string[];
5
+ }
6
+ export declare function emptySyncReport(): SyncReport;
7
+ export declare function runSync(projectRoot: string): Promise<SyncReport>;