agent-project-sdlc 0.1.5 → 0.1.6

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.
@@ -15,6 +15,8 @@ description: Use when ...
15
15
 
16
16
  角色提示词应保持通用,不绑定具体业务项目;如果必须依赖项目事实,应要求读取 `.docs/`、`<harnessRoot>/state/**` 或当前 task contract,而不是写入固定业务假设。
17
17
 
18
+ 项目如果需要补充阶段角色要求,应在 `<harnessRoot>/pjsdlc_managed/override_skills/<skill_name>.md` 写追加提示词,并运行 `sdlc-harness sync` 合成最终 `SKILL.md`;不要直接修改 package-managed Skill 文件。
19
+
18
20
  ## 输入
19
21
 
20
22
  - `<harnessRoot>/state/lifecycle.yaml`
@@ -21,7 +21,10 @@ export function defaultConfig(root) {
21
21
  { path: harnessPath(root, "pjsdlc_managed", "make", "sdlc-harness.mk"), strategy: "managed" },
22
22
  { path: ".github/workflows/harness.yml", strategy: "create-if-missing" }
23
23
  ],
24
- local_overrides: [harnessPath(root, "overrides/**"), harnessPath(root, "pjsdlc_managed", "policies", "*.local.yaml")],
24
+ local_overrides: [
25
+ harnessPath(root, "pjsdlc_managed", "override_skills", "*.md"),
26
+ harnessPath(root, "pjsdlc_managed", "policies", "*.local.yaml")
27
+ ],
25
28
  never_overwrite: [".docs/**", harnessPath(root, "state/**"), "src/**", "tests/**"]
26
29
  };
27
30
  }
@@ -56,6 +56,7 @@ async function migrateLegacyManagedPaths(projectRoot, root, report) {
56
56
  await moveIfDestinationMissing(projectRoot, harnessPath(root, "templates"), harnessPath(root, "pjsdlc_managed", "templates"), report);
57
57
  await moveIfDestinationMissing(projectRoot, harnessPath(root, "policies"), harnessPath(root, "pjsdlc_managed", "policies"), report);
58
58
  await moveIfDestinationMissing(projectRoot, harnessPath(root, "make"), harnessPath(root, "pjsdlc_managed", "make"), report);
59
+ await moveIfDestinationMissing(projectRoot, harnessPath(root, "overrides", "skills"), harnessPath(root, "pjsdlc_managed", "override_skills"), report);
59
60
  }
60
61
  async function movePromptTreeToSkillsIfDestinationMissing(projectRoot, legacyRelativePath, nextRelativePath, report) {
61
62
  const legacyPath = path.join(projectRoot, legacyRelativePath);
@@ -104,6 +105,11 @@ async function moveIfDestinationMissing(projectRoot, legacyRelativePath, nextRel
104
105
  report.changed.push(`${legacyRelativePath} -> ${nextRelativePath}`);
105
106
  }
106
107
  function migrateLocalOverride(item, root) {
108
+ if (item === harnessPath(root, "overrides/**") ||
109
+ item === harnessPath(root, "overrides", "skills", "*.md") ||
110
+ item === harnessPath(root, "overrides", "skills", "**")) {
111
+ return harnessPath(root, "pjsdlc_managed", "override_skills", "*.md");
112
+ }
107
113
  if (item === ".harness/policies/*.local.yaml" ||
108
114
  item === harnessPath(root, "managed", "policies", "*.local.yaml")) {
109
115
  return harnessPath(root, "pjsdlc_managed", "policies", "*.local.yaml");
@@ -30,14 +30,8 @@ async function syncManagedFile(projectRoot, root, managedFile, report) {
30
30
  await syncMakefileInclude(destination, root, report);
31
31
  return;
32
32
  }
33
- if (managedFile.path === path.join(root, "skills")) {
34
- await syncTree(packageAssetPath("skills"), destination, report);
35
- return;
36
- }
37
- if (managedFile.path === path.join(root, "skills") ||
38
- managedFile.path === ".harness/agents/skills" ||
39
- (managedFile.path === ".agents/skills" && root !== ".agents")) {
40
- await syncTree(packageAssetPath("skills"), path.join(projectRoot, root, "skills"), report);
33
+ if (isSkillsManagedPath(managedFile.path, root)) {
34
+ await syncSkillsTree(packageAssetPath("skills"), path.join(projectRoot, root, "skills"), projectRoot, root, report);
41
35
  return;
42
36
  }
43
37
  if (managedFile.path === path.join(root, "pjsdlc_managed", "templates")) {
@@ -62,6 +56,11 @@ async function syncManagedFile(projectRoot, root, managedFile, report) {
62
56
  }
63
57
  report.skipped.push(managedFile.path);
64
58
  }
59
+ function isSkillsManagedPath(managedPath, root) {
60
+ return (managedPath === path.join(root, "skills") ||
61
+ managedPath === ".harness/agents/skills" ||
62
+ (managedPath === ".agents/skills" && root !== ".agents"));
63
+ }
65
64
  async function syncAgentsBlock(destination, root, report) {
66
65
  const corePath = packageAssetPath("agents", "AGENTS_CORE.md");
67
66
  if (!(await pathExists(corePath))) {
@@ -197,6 +196,93 @@ async function syncTree(source, destination, report) {
197
196
  const changed = await copyTree(source, destination, { skipGitkeep: true });
198
197
  report.changed.push(...changed);
199
198
  }
199
+ async function syncSkillsTree(source, destination, projectRoot, root, report) {
200
+ if (!(await pathExists(source))) {
201
+ report.skipped.push(path.basename(destination));
202
+ return;
203
+ }
204
+ const files = await listFiles(source);
205
+ const realFiles = files.filter((file) => !file.endsWith(".gitkeep"));
206
+ if (realFiles.length === 0) {
207
+ report.skipped.push(path.basename(destination));
208
+ return;
209
+ }
210
+ const knownSkills = new Set();
211
+ for (const file of realFiles) {
212
+ const skillName = skillNameForSourceFile(source, file);
213
+ if (skillName) {
214
+ knownSkills.add(skillName);
215
+ }
216
+ }
217
+ const blockedBefore = report.blocked.length;
218
+ const overrides = await readSkillOverrides(projectRoot, root, knownSkills, report);
219
+ if (report.blocked.length > blockedBefore) {
220
+ return;
221
+ }
222
+ for (const file of realFiles) {
223
+ const relative = path.relative(source, file);
224
+ const destinationFile = path.join(destination, relative);
225
+ const skillName = skillNameForSourceFile(source, file);
226
+ const baseContent = await readText(file);
227
+ const content = skillName ? renderSkillWithOverride(baseContent, overrides.get(skillName)) : baseContent;
228
+ if (await writeTextIfChanged(destinationFile, content)) {
229
+ report.changed.push(destinationFile);
230
+ }
231
+ else {
232
+ report.skipped.push(destinationFile);
233
+ }
234
+ }
235
+ }
236
+ async function readSkillOverrides(projectRoot, root, knownSkills, report) {
237
+ const overrides = new Map();
238
+ const overrideRoot = skillOverrideRoot(projectRoot, root);
239
+ if (!(await pathExists(overrideRoot))) {
240
+ return overrides;
241
+ }
242
+ for (const file of await listFiles(overrideRoot)) {
243
+ if (path.basename(file) === ".gitkeep") {
244
+ continue;
245
+ }
246
+ const relativePath = path.relative(overrideRoot, file).split(path.sep).join("/");
247
+ const match = relativePath.match(/^([^/]+)\.md$/);
248
+ if (!match || !knownSkills.has(match[1])) {
249
+ report.blocked.push(`unknown skill override: ${path.join(root, "pjsdlc_managed", "override_skills", relativePath)}`);
250
+ continue;
251
+ }
252
+ const content = await readText(file);
253
+ if (content.trim()) {
254
+ overrides.set(match[1], {
255
+ relativePath: path.join(root, "pjsdlc_managed", "override_skills", relativePath),
256
+ content
257
+ });
258
+ }
259
+ }
260
+ return overrides;
261
+ }
262
+ function skillOverrideRoot(projectRoot, root) {
263
+ return path.join(projectRoot, root, "pjsdlc_managed", "override_skills");
264
+ }
265
+ function skillNameForSourceFile(sourceRoot, file) {
266
+ const relative = path.relative(sourceRoot, file).split(path.sep).join("/");
267
+ const match = relative.match(/^([^/]+)\/SKILL\.md$/);
268
+ return match?.[1];
269
+ }
270
+ function renderSkillWithOverride(baseContent, override) {
271
+ if (!override) {
272
+ return baseContent;
273
+ }
274
+ const header = [
275
+ "",
276
+ "",
277
+ "## Local Override",
278
+ "",
279
+ `Source: \`${override.relativePath.split(path.sep).join("/")}\``,
280
+ "",
281
+ "The following project-local instructions are appended by `sdlc-harness sync`. Keep package-managed Skill files unchanged; edit the override source instead.",
282
+ ""
283
+ ].join("\n");
284
+ return `${baseContent.trimEnd()}${header}${override.content.trim()}\n`;
285
+ }
200
286
  async function syncFile(source, destination, report, missingMode) {
201
287
  if (!(await pathExists(source))) {
202
288
  if (missingMode === "block-if-missing") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-project-sdlc",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CLI and canonical assets for the AI SDLC Harness workflow.",
5
5
  "type": "module",
6
6
  "bin": {