agent-project-sdlc 0.1.19 → 0.1.21

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 (39) hide show
  1. package/README.md +9 -7
  2. package/assets/agents/AGENTS_CORE.md +2 -2
  3. package/assets/docs/README.md +10 -8
  4. package/assets/skills/pjsdlc_architect_design/SKILL.md +2 -0
  5. package/assets/skills/pjsdlc_dev_sprint/SKILL.md +7 -5
  6. package/assets/skills/pjsdlc_implementation_doc/SKILL.md +2 -2
  7. package/assets/skills/pjsdlc_manager/SKILL.md +4 -4
  8. package/assets/skills/pjsdlc_pm_prd/SKILL.md +3 -3
  9. package/assets/skills/pjsdlc_release_manager/SKILL.md +2 -0
  10. package/assets/skills/pjsdlc_reviewer/SKILL.md +2 -0
  11. package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +2 -0
  12. package/assets/skills/pjsdlc_tester/SKILL.md +2 -2
  13. package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +1 -1
  14. package/assets/templates/PLAN_TEMPLATE.yaml +9 -6
  15. package/assets/tools/build_doc_overviews.py +152 -0
  16. package/assets/tools/harness_utils.py +858 -0
  17. package/assets/tools/impact_analyzer.py +51 -0
  18. package/assets/tools/run_current_gate.py +29 -0
  19. package/assets/tools/status.py +29 -0
  20. package/assets/tools/transition.py +68 -0
  21. package/assets/tools/validate_allowed_paths.py +44 -0
  22. package/assets/tools/validate_design.py +199 -0
  23. package/assets/tools/validate_dev_state.py +20 -0
  24. package/assets/tools/validate_harness.py +60 -0
  25. package/assets/tools/validate_plan.py +24 -0
  26. package/assets/tools/validate_plan_draft.py +19 -0
  27. package/assets/tools/validate_prd.py +27 -0
  28. package/assets/tools/validate_prompt_language.py +138 -0
  29. package/assets/tools/validate_release_plan.py +37 -0
  30. package/assets/tools/validate_review.py +59 -0
  31. package/assets/tools/validate_rfc.py +105 -0
  32. package/assets/tools/validate_task_docs.py +40 -0
  33. package/assets/tools/validate_test_plan.py +82 -0
  34. package/dist/lib/config.js +1 -0
  35. package/dist/lib/migrations.js +3 -0
  36. package/dist/lib/sync-engine.js +4 -0
  37. package/dist/lib/validators.js +227 -17
  38. package/package.json +1 -1
  39. package/source-mappings.yaml +6 -0
@@ -6,8 +6,20 @@ import { listFiles, pathExists, readText } from "./fs.js";
6
6
  import { parseYaml } from "./yaml.js";
7
7
  const execFileAsync = promisify(execFile);
8
8
  const PARALLEL_MODES = new Set(["runtime_managed", "user_orchestrated"]);
9
+ const PARALLEL_TRIGGERS = new Set(["user_requested", "workflow_default"]);
10
+ const PARALLEL_RUNTIME_PROVIDERS = new Set(["codex_native_subagents", "user_orchestrated", "codex_exec_worktree"]);
9
11
  const TASK_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "SPRINTING", "REVIEWING", "TESTING", "RELEASING", "RFC_RECALIBRATION"]);
10
- const PARALLEL_ALLOWED_PHASES = new Set(["REQUIREMENT_GATHERING", "SPRINTING", "TESTING"]);
12
+ const PARALLEL_ALLOWED_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "SPRINTING", "REVIEWING", "TESTING", "RELEASING", "RFC_RECALIBRATION"]);
13
+ const PARALLEL_READ_ONLY_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"]);
14
+ const PARALLEL_PROTECTED_WRITE_PATTERNS = [
15
+ ".codex/state/**",
16
+ "<harnessRoot>/state/**",
17
+ ".docs/INDEX.md",
18
+ ".docs/**/overview.md",
19
+ ".docs/04_implementation/**",
20
+ ".docs/06_review/**",
21
+ ".docs/08_release/**"
22
+ ];
11
23
  const TASK_STATUSES = new Set(["pending", "in_progress", "done", "blocked", "pending_revision", "cancelled"]);
12
24
  const OPEN_TASK_STATUSES = new Set(["pending", "in_progress", "blocked", "pending_revision"]);
13
25
  const EVIDENCE_LEVELS = new Set(["unit", "local_runtime", "external_provider_live", "deployed_runtime", "business_handoff_ready"]);
@@ -101,6 +113,36 @@ const EVIDENCE_PLACEHOLDER_TERMS = [
101
113
  "待补",
102
114
  "待确认"
103
115
  ];
116
+ const SELF_TEST_REPORT_PLACEHOLDER_TERMS = [
117
+ "pass / blocked",
118
+ "pass or blocked",
119
+ "pass/block",
120
+ "pass/blocker",
121
+ "local start / invocation",
122
+ "all self-test scenarios",
123
+ "all task/module promised runnable entries",
124
+ "actual internal key paths",
125
+ "observable completion evidence"
126
+ ];
127
+ const SELF_TEST_OBSERVABLE_EVIDENCE_TERMS = [
128
+ "pass output",
129
+ "response",
130
+ "output",
131
+ "side effect",
132
+ "log",
133
+ "artifact",
134
+ "health",
135
+ "status",
136
+ "audit",
137
+ "rendered",
138
+ "page state",
139
+ "screenshot",
140
+ "browser check",
141
+ "playwright",
142
+ "command output",
143
+ "queue",
144
+ "file"
145
+ ];
104
146
  const PAGE_TASK_TERMS = ["frontend", "front-end", "browser", "page", "页面", "前端", "按钮", "表单", "跳转"];
105
147
  const PAGE_ENTRY_TERMS = ["http://", "https://", "localhost", "127.0.0.1", "page url", "页面 url", "dev server"];
106
148
  const PAGE_BROWSER_CHECK_TERMS = ["browser check", "playwright", "screenshot", "click", "button", "form", "页面可加载", "浏览器验证"];
@@ -952,11 +994,19 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
952
994
  }
953
995
  if (contract.enabled !== true)
954
996
  errors.push("parallel_execution.enabled must be true when present");
955
- if (contract.trigger !== "user_requested")
956
- errors.push('parallel_execution.trigger must be "user_requested"');
997
+ if (!PARALLEL_TRIGGERS.has(String(contract.trigger ?? ""))) {
998
+ errors.push("parallel_execution.trigger must be user_requested or workflow_default");
999
+ }
957
1000
  if (!PARALLEL_MODES.has(String(contract.mode ?? ""))) {
958
1001
  errors.push("parallel_execution.mode must be runtime_managed or user_orchestrated");
959
1002
  }
1003
+ const provider = parallelRuntimeProvider(contract, errors);
1004
+ if (provider && !PARALLEL_RUNTIME_PROVIDERS.has(provider)) {
1005
+ errors.push("parallel_execution.runtime.provider must be codex_native_subagents, user_orchestrated, or codex_exec_worktree");
1006
+ }
1007
+ if (contract.trigger === "workflow_default" && provider !== "codex_native_subagents") {
1008
+ errors.push('parallel_execution.runtime.provider must be "codex_native_subagents" when trigger is workflow_default');
1009
+ }
960
1010
  if ("phase" in contract) {
961
1011
  errors.push("parallel_execution must not define phase; lifecycle.yaml is the single source for current_phase");
962
1012
  }
@@ -964,7 +1014,7 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
964
1014
  errors.push("parallel_execution must not define linked_task_id; use plan.yaml current_task_id");
965
1015
  }
966
1016
  if (!PARALLEL_ALLOWED_PHASES.has(currentPhase)) {
967
- errors.push("parallel_execution is only supported during REQUIREMENT_GATHERING, SPRINTING, or TESTING");
1017
+ errors.push("parallel_execution is only supported during REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION");
968
1018
  }
969
1019
  if (contract.coordinator !== "main_agent")
970
1020
  errors.push('parallel_execution.coordinator must be "main_agent"');
@@ -977,6 +1027,7 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
977
1027
  }
978
1028
  else {
979
1029
  const seen = new Set();
1030
+ const writeOwnedPaths = [];
980
1031
  workers.forEach((worker, index) => {
981
1032
  const prefix = `parallel_execution.workers[${index}]`;
982
1033
  if (!isRecord(worker)) {
@@ -1003,18 +1054,36 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
1003
1054
  if (Array.isArray(worker.required_gates) && worker.required_gates.length === 0) {
1004
1055
  errors.push(`${prefix}.required_gates must not be empty`);
1005
1056
  }
1057
+ if (PARALLEL_READ_ONLY_PHASES.has(currentPhase) && worker.writes_repo !== false) {
1058
+ errors.push(`${prefix}.writes_repo must be false during ${currentPhase}`);
1059
+ }
1006
1060
  if (worker.writes_repo === true) {
1007
- if (typeof worker.branch !== "string" || !worker.branch.trim()) {
1008
- errors.push(`${prefix}.branch is required when writes_repo is true`);
1009
- }
1010
- if (typeof worker.worktree !== "string" || !worker.worktree.trim()) {
1011
- errors.push(`${prefix}.worktree is required when writes_repo is true`);
1061
+ if (provider !== "codex_native_subagents") {
1062
+ if (typeof worker.branch !== "string" || !worker.branch.trim()) {
1063
+ errors.push(`${prefix}.branch is required when writes_repo is true outside codex_native_subagents runtime`);
1064
+ }
1065
+ if (typeof worker.worktree !== "string" || !worker.worktree.trim()) {
1066
+ errors.push(`${prefix}.worktree is required when writes_repo is true outside codex_native_subagents runtime`);
1067
+ }
1012
1068
  }
1013
1069
  if (!Array.isArray(worker.owned_paths) || worker.owned_paths.length === 0) {
1014
1070
  errors.push(`${prefix}.owned_paths must not be empty when writes_repo is true`);
1015
1071
  }
1072
+ validateParallelWorkerPathLock(plan, worker, index, errors);
1073
+ for (const owned of stringArray(worker.owned_paths).map(normalizeParallelPattern)) {
1074
+ writeOwnedPaths.push({ index, path: owned });
1075
+ }
1016
1076
  }
1017
1077
  });
1078
+ for (let left = 0; left < writeOwnedPaths.length; left += 1) {
1079
+ for (let right = left + 1; right < writeOwnedPaths.length; right += 1) {
1080
+ const leftOwned = writeOwnedPaths[left];
1081
+ const rightOwned = writeOwnedPaths[right];
1082
+ if (globPatternsOverlap(leftOwned.path, rightOwned.path)) {
1083
+ errors.push(`parallel_execution write worker owned_paths must not overlap: workers[${leftOwned.index}] ${leftOwned.path} vs workers[${rightOwned.index}] ${rightOwned.path}`);
1084
+ }
1085
+ }
1086
+ }
1018
1087
  }
1019
1088
  const integration = contract.integration;
1020
1089
  if (!isRecord(integration)) {
@@ -1033,6 +1102,63 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
1033
1102
  errors.push("parallel_execution.integration.fact_source_updates must be a non-empty list");
1034
1103
  }
1035
1104
  }
1105
+ function parallelRuntimeProvider(contract, errors) {
1106
+ const runtime = contract.runtime;
1107
+ if (runtime === undefined || runtime === null)
1108
+ return "";
1109
+ if (!isRecord(runtime)) {
1110
+ errors.push("parallel_execution.runtime must be a mapping when present");
1111
+ return "";
1112
+ }
1113
+ return String(runtime.provider ?? "");
1114
+ }
1115
+ function validateParallelWorkerPathLock(plan, worker, index, errors) {
1116
+ const currentTask = currentPlanTask(plan);
1117
+ if (!currentTask)
1118
+ return;
1119
+ const taskAllowed = stringArray(currentTask.allowed_paths).map(normalizeParallelPattern);
1120
+ const workerOwned = stringArray(worker.owned_paths).map(normalizeParallelPattern);
1121
+ const workerForbidden = stringArray(worker.forbidden_paths).map(normalizeParallelPattern);
1122
+ const protectedPatterns = PARALLEL_PROTECTED_WRITE_PATTERNS.map(normalizeParallelPattern);
1123
+ for (const owned of workerOwned) {
1124
+ if (!matchesAny(owned, taskAllowed)) {
1125
+ errors.push(`parallel_execution.workers[${index}].owned_paths must be within current task allowed_paths: ${owned}`);
1126
+ }
1127
+ for (const forbidden of [...workerForbidden, ...protectedPatterns]) {
1128
+ if (globPatternsOverlap(owned, forbidden)) {
1129
+ errors.push(`parallel_execution.workers[${index}].owned_paths must not overlap forbidden paths: ${owned} vs ${forbidden}`);
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ function currentPlanTask(plan) {
1135
+ const currentTaskId = String(plan.current_task_id ?? "");
1136
+ const tasks = Array.isArray(plan.tasks) ? plan.tasks.filter(isRecord) : [];
1137
+ return tasks.find((task) => String(task.id ?? "") === currentTaskId);
1138
+ }
1139
+ function stringArray(value) {
1140
+ return Array.isArray(value) ? value.map((item) => String(item)) : [];
1141
+ }
1142
+ function normalizeParallelPattern(pattern) {
1143
+ return pattern.replace(/\\/g, "/").replaceAll("<harnessRoot>", ".codex");
1144
+ }
1145
+ function globPrefix(pattern) {
1146
+ const normalized = normalizeParallelPattern(pattern);
1147
+ const positions = ["*", "[", "?"].map((token) => normalized.indexOf(token)).filter((index) => index >= 0);
1148
+ const prefix = positions.length > 0 ? normalized.slice(0, Math.min(...positions)) : normalized;
1149
+ return prefix.replace(/\/+$/, "");
1150
+ }
1151
+ function globPatternsOverlap(left, right) {
1152
+ const leftClean = normalizeParallelPattern(left);
1153
+ const rightClean = normalizeParallelPattern(right);
1154
+ if (matchesGlob(leftClean, rightClean) || matchesGlob(rightClean, leftClean))
1155
+ return true;
1156
+ const leftPrefix = globPrefix(leftClean);
1157
+ const rightPrefix = globPrefix(rightClean);
1158
+ if (!leftPrefix || !rightPrefix)
1159
+ return leftPrefix === rightPrefix;
1160
+ return leftPrefix === rightPrefix || leftPrefix.startsWith(`${rightPrefix}/`) || rightPrefix.startsWith(`${leftPrefix}/`);
1161
+ }
1036
1162
  async function validateChangedPaths(projectRoot, plan, allowOpen) {
1037
1163
  if (!allowOpen)
1038
1164
  return [];
@@ -1222,11 +1348,26 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
1222
1348
  }
1223
1349
  }
1224
1350
  const moduleKeyTestPath = evidenceFieldValue(report, "Module Key Test Path") ?? "";
1351
+ if (isPlaceholderSelfTestReportValue(moduleKeyTestPath) || isTemplateModuleKeyTestPath(moduleKeyTestPath)) {
1352
+ errors.push(`${taskId} Development Self-Test Report Module Key Test Path must replace template placeholders with actual executed path evidence in ${implementationDoc}`);
1353
+ }
1225
1354
  const runnableEntry = String(contract.runnable_entry ?? "").trim();
1226
1355
  if (runnableEntry && !moduleKeyTestPath.includes(runnableEntry)) {
1227
1356
  errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include runnable entry ${runnableEntry} in ${implementationDoc}`);
1228
1357
  }
1229
1358
  const scenarios = Array.isArray(contract.scenarios) ? contract.scenarios.filter(isRecord) : [];
1359
+ const exitEvidenceTerms = [
1360
+ String(contract.observable_exit ?? "").trim(),
1361
+ ...scenarios.flatMap((scenario) => [
1362
+ String(scenario.expected_exit ?? "").trim(),
1363
+ String(scenario.evidence ?? "").trim()
1364
+ ])
1365
+ ].filter(Boolean);
1366
+ if (exitEvidenceTerms.length > 0
1367
+ && !exitEvidenceTerms.some((term) => normalizedIncludes(moduleKeyTestPath, term))
1368
+ && !containsAny(moduleKeyTestPath, SELF_TEST_OBSERVABLE_EVIDENCE_TERMS)) {
1369
+ errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include observable exit or evidence from self_test_contract in ${implementationDoc}`);
1370
+ }
1230
1371
  for (const scenario of scenarios) {
1231
1372
  const scenarioId = String(scenario.id ?? "").trim();
1232
1373
  if (!scenarioId)
@@ -1238,25 +1379,94 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
1238
1379
  if (!status) {
1239
1380
  errors.push(`${taskId} Development Self-Test Report must record scenario ${scenarioId} as PASS or BLOCKED in ${implementationDoc}`);
1240
1381
  }
1382
+ else if (status === "AMBIGUOUS") {
1383
+ errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} must choose exactly one of PASS or BLOCKED in ${implementationDoc}`);
1384
+ }
1241
1385
  else if (status === "BLOCKED") {
1242
1386
  errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} is BLOCKED; keep task open or record a blocker`);
1243
1387
  }
1388
+ errors.push(...validateScenarioTableEvidence(report, scenarioId, taskId, implementationDoc));
1389
+ }
1390
+ const targetRuntime = isRecord(task.target_runtime_environment) ? task.target_runtime_environment : undefined;
1391
+ const reportContext = `${taskText(task)}\n${report}\n${Object.values(contract).map((value) => String(value ?? "")).join("\n")}`;
1392
+ if (String(targetRuntime?.kind ?? "") === "browser" || containsAny(reportContext, PAGE_TASK_TERMS)) {
1393
+ const loweredReport = report.toLowerCase();
1394
+ if (!containsAny(loweredReport, PAGE_ENTRY_TERMS)) {
1395
+ errors.push(`${taskId} page Development Self-Test Report must include a dev server or page URL in ${implementationDoc}`);
1396
+ }
1397
+ if (!containsAny(loweredReport, PAGE_BROWSER_CHECK_TERMS)) {
1398
+ errors.push(`${taskId} page Development Self-Test Report must include browser, Playwright, screenshot, or equivalent interaction evidence in ${implementationDoc}`);
1399
+ }
1244
1400
  }
1245
1401
  return errors;
1246
1402
  }
1247
1403
  function scenarioStatus(text, scenarioId) {
1248
1404
  const escaped = scenarioId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1249
- const patterns = [
1250
- new RegExp("^.*" + escaped + ".*\\b(PASS|BLOCKED)\\b.*$", "im"),
1251
- new RegExp("\\|[^\\n|]*" + escaped + "[^\\n|]*\\|[^\\n|]*\\b(PASS|BLOCKED)\\b[^\\n|]*\\|", "i")
1252
- ];
1253
- for (const pattern of patterns) {
1254
- const match = text.match(pattern);
1255
- if (match)
1256
- return match[1].toUpperCase();
1405
+ const pattern = new RegExp("^.*" + escaped + ".*$", "gim");
1406
+ for (const match of text.matchAll(pattern)) {
1407
+ const line = match[0];
1408
+ const hasPass = /\bPASS\b/i.test(line);
1409
+ const hasBlocked = /\bBLOCKED\b/i.test(line);
1410
+ if (hasPass && hasBlocked)
1411
+ return "AMBIGUOUS";
1412
+ if (hasPass)
1413
+ return "PASS";
1414
+ if (hasBlocked)
1415
+ return "BLOCKED";
1257
1416
  }
1258
1417
  return undefined;
1259
1418
  }
1419
+ function validateScenarioTableEvidence(report, scenarioId, taskId, implementationDoc) {
1420
+ const errors = [];
1421
+ const rows = markdownTableRows(report).filter((cells) => cells.some((cell) => normalizeCell(cell) === scenarioId));
1422
+ for (const cells of rows) {
1423
+ const [id, result, executedEntry, actualExit, evidence] = cells;
1424
+ if (!id || normalizeCell(id) !== scenarioId)
1425
+ continue;
1426
+ const requiredCells = [
1427
+ ["Result", result],
1428
+ ["Executed Entry", executedEntry],
1429
+ ["Actual Exit", actualExit],
1430
+ ["Evidence", evidence]
1431
+ ];
1432
+ for (const [label, value] of requiredCells) {
1433
+ if (!value || isPlaceholderSelfTestReportValue(value)) {
1434
+ errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} table ${label} must contain concrete evidence in ${implementationDoc}`);
1435
+ }
1436
+ }
1437
+ if (result && scenarioStatus(`| ${cells.join(" | ")} |`, scenarioId) === "AMBIGUOUS") {
1438
+ errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} table Result must choose exactly one of PASS or BLOCKED in ${implementationDoc}`);
1439
+ }
1440
+ }
1441
+ return errors;
1442
+ }
1443
+ function markdownTableRows(section) {
1444
+ return section
1445
+ .split(/\r?\n/)
1446
+ .map((line) => line.trim())
1447
+ .filter((line) => line.startsWith("|") && line.endsWith("|") && !/^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|$/.test(line))
1448
+ .map((line) => line.slice(1, -1).split("|").map((cell) => cell.trim()));
1449
+ }
1450
+ function normalizeCell(value) {
1451
+ return value.replace(/`/g, "").trim();
1452
+ }
1453
+ function isTemplateModuleKeyTestPath(value) {
1454
+ const lowered = value.toLowerCase();
1455
+ return [
1456
+ "local start / invocation",
1457
+ "all self-test scenarios",
1458
+ "all task/module promised runnable entries",
1459
+ "actual internal key paths",
1460
+ "observable completion evidence"
1461
+ ].some((term) => lowered.includes(term));
1462
+ }
1463
+ function isPlaceholderSelfTestReportValue(value) {
1464
+ const normalized = value.trim().toLowerCase();
1465
+ return isPlaceholderEvidence(value) || SELF_TEST_REPORT_PLACEHOLDER_TERMS.some((term) => normalized.includes(term));
1466
+ }
1467
+ function normalizedIncludes(text, needle) {
1468
+ return text.toLowerCase().includes(needle.toLowerCase());
1469
+ }
1260
1470
  function hasConcreteDevelopmentEvidenceFields(section) {
1261
1471
  return ["Evidence Level", "Target Runtime Environment", "Runnable Entry", "Observable Exit", "Client / Server Initialization", "Config Contract"].some((field) => {
1262
1472
  const value = evidenceFieldValue(section, field);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-project-sdlc",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "CLI and canonical assets for the AI SDLC Harness workflow.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,12 @@ source_mappings:
19
19
  - source: ".codex/pjsdlc_managed/make/sdlc-harness.mk"
20
20
  target: "packages/sdlc-harness/assets/make/sdlc-harness.mk"
21
21
  mode: "copy-file"
22
+ - source: "tools"
23
+ target: "packages/sdlc-harness/assets/tools"
24
+ mode: "copy-tree"
25
+ exclude:
26
+ - "consumer_lab_full_test.mjs"
27
+ - "release_npm.mjs"
22
28
  - source: ".github/workflows/harness.yml"
23
29
  target: "packages/sdlc-harness/assets/github/harness.yml"
24
30
  mode: "copy-file"