agent-project-sdlc 0.1.18 → 0.1.20

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 (41) hide show
  1. package/README.md +11 -9
  2. package/assets/agents/AGENTS_CORE.md +2 -2
  3. package/assets/docs/README.md +12 -10
  4. package/assets/skills/pjsdlc_architect_design/SKILL.md +4 -2
  5. package/assets/skills/pjsdlc_dev_sprint/SKILL.md +11 -8
  6. package/assets/skills/pjsdlc_implementation_doc/SKILL.md +7 -3
  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 +5 -2
  11. package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +5 -2
  12. package/assets/skills/pjsdlc_tester/SKILL.md +5 -4
  13. package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +21 -7
  14. package/assets/templates/PLAN_TEMPLATE.yaml +27 -6
  15. package/assets/templates/RFC_TEMPLATE.md +12 -1
  16. package/assets/templates/TECH_DESIGN_TEMPLATE.md +19 -2
  17. package/assets/tools/build_doc_overviews.py +152 -0
  18. package/assets/tools/harness_utils.py +858 -0
  19. package/assets/tools/impact_analyzer.py +51 -0
  20. package/assets/tools/run_current_gate.py +29 -0
  21. package/assets/tools/status.py +29 -0
  22. package/assets/tools/transition.py +68 -0
  23. package/assets/tools/validate_allowed_paths.py +44 -0
  24. package/assets/tools/validate_design.py +199 -0
  25. package/assets/tools/validate_dev_state.py +20 -0
  26. package/assets/tools/validate_harness.py +60 -0
  27. package/assets/tools/validate_plan.py +24 -0
  28. package/assets/tools/validate_plan_draft.py +19 -0
  29. package/assets/tools/validate_prd.py +27 -0
  30. package/assets/tools/validate_prompt_language.py +138 -0
  31. package/assets/tools/validate_release_plan.py +37 -0
  32. package/assets/tools/validate_review.py +59 -0
  33. package/assets/tools/validate_rfc.py +105 -0
  34. package/assets/tools/validate_task_docs.py +40 -0
  35. package/assets/tools/validate_test_plan.py +82 -0
  36. package/dist/lib/config.js +1 -0
  37. package/dist/lib/migrations.js +3 -0
  38. package/dist/lib/sync-engine.js +4 -0
  39. package/dist/lib/validators.js +351 -17
  40. package/package.json +1 -1
  41. 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"]);
@@ -87,6 +99,10 @@ const RUNNABLE_ENTRY_EXIT_TERMS = [
87
99
  "not applicable"
88
100
  ];
89
101
  const DEVELOPMENT_EVIDENCE_TERMS = ["development evidence", "开发自测证据"];
102
+ const DEVELOPMENT_SELF_TEST_CONTRACT_TERMS = ["development self-test contract", "开发自测合同"];
103
+ const DEVELOPMENT_SELF_TEST_REPORT_TERMS = ["development self-test report", "开发自测报告"];
104
+ const DEVELOPMENT_SELF_TEST_IMPACT_TERMS = ["development self-test impact", "开发自测影响"];
105
+ const MODULE_KEY_TEST_PATH_TERMS = ["module key test path", "模块关键测试路径"];
90
106
  const TESTING_HANDOFF_TERMS = ["testing handoff contract", "测试交接合同"];
91
107
  const EVIDENCE_PLACEHOLDER_TERMS = [
92
108
  "pending",
@@ -217,6 +233,33 @@ const REVIEW_READINESS_FIELDS = [
217
233
  "Config Contract",
218
234
  "Testing Handoff Readiness"
219
235
  ];
236
+ const SELF_TEST_CONTRACT_STATUSES = new Set(["required", "not_applicable"]);
237
+ const RFC_SELF_TEST_TRIGGER_TERMS = [
238
+ "entry/exit",
239
+ "runnable entry",
240
+ "runnable exit",
241
+ "runnable entry/exit",
242
+ "runtime",
243
+ "environment",
244
+ "target_runtime_environment",
245
+ "target runtime",
246
+ "required_gates",
247
+ "gate",
248
+ "handoff",
249
+ "blocker",
250
+ "module key test path",
251
+ "test route",
252
+ "test path",
253
+ "debug path",
254
+ "测试路径",
255
+ "测试链路",
256
+ "自测链路",
257
+ "模块关键测试路径",
258
+ "入口",
259
+ "出口",
260
+ "运行环境",
261
+ "阻塞"
262
+ ];
220
263
  const validators = {
221
264
  "validate-harness": validateHarness,
222
265
  "validate-current": validateCurrent,
@@ -339,26 +382,26 @@ async function validateDesignDraft(projectRoot, root, techPlanFiles) {
339
382
  const availableTechPlans = new Set(techPlanFiles.map((file) => repoRelative(projectRoot, file)));
340
383
  const developmentTasks = [];
341
384
  const primaryRefs = [];
342
- rawTasks.forEach((rawTask, index) => {
385
+ for (const [index, rawTask] of rawTasks.entries()) {
343
386
  if (!isRecord(rawTask)) {
344
387
  errors.push(`Task draft #${index + 1} must be a mapping`);
345
- return;
388
+ continue;
346
389
  }
347
390
  validateDraftTaskShape(rawTask, index, errors);
348
391
  if (rawTask.status !== "pending") {
349
392
  errors.push(`Draft task ${String(rawTask.id ?? "")} should start as pending`);
350
393
  }
351
394
  if (!isDevelopmentDraft(rawTask))
352
- return;
395
+ continue;
353
396
  developmentTasks.push(rawTask);
354
397
  if (!isRecord(rawTask.docs)) {
355
398
  errors.push(`Draft task ${String(rawTask.id ?? "")} docs must be a mapping`);
356
- return;
399
+ continue;
357
400
  }
358
401
  const techRefs = asStringList(rawTask.docs.tech_plan);
359
402
  if (techRefs.length === 0) {
360
403
  errors.push(`Draft task ${String(rawTask.id ?? "")} must reference at least one tech plan slice in docs.tech_plan`);
361
- return;
404
+ continue;
362
405
  }
363
406
  const normalizedRefs = techRefs.map(normalizeDocRef);
364
407
  for (const ref of normalizedRefs) {
@@ -369,8 +412,9 @@ async function validateDesignDraft(projectRoot, root, techPlanFiles) {
369
412
  errors.push(`Draft task ${String(rawTask.id ?? "")} references missing or generated tech plan slice: ${ref}`);
370
413
  }
371
414
  }
415
+ errors.push(...(await validateSelfTestContractTechPlanBinding(projectRoot, rawTask, normalizedRefs)));
372
416
  primaryRefs.push(normalizedRefs[0]);
373
- });
417
+ }
374
418
  if (developmentTasks.length === 0) {
375
419
  errors.push("plan.draft.yaml must contain at least one development task with implementation_doc");
376
420
  }
@@ -611,14 +655,36 @@ async function validateRfc(projectRoot) {
611
655
  errors.push(`Superseded test doc still linked from .docs/INDEX.md: ${superseded}`);
612
656
  }
613
657
  }
614
- const statuses = [...text.matchAll(/status:\s*([a-z_]+)/g)].map((match) => match[1].toUpperCase());
658
+ const statuses = [...text.matchAll(/^\s*-?\s*Status:\s*([A-Z_]+)/gim)].map((match) => match[1].toUpperCase());
615
659
  if (statuses.length === 0)
616
660
  errors.push("RFC must include a Status line");
617
661
  const invalidStatuses = statuses.filter((status) => !["DRAFT", "APPLIED", "VERIFIED", "ARCHIVED"].includes(status));
618
662
  if (invalidStatuses.length > 0)
619
663
  errors.push(`Invalid RFC status: ${invalidStatuses.join(", ")}`);
664
+ errors.push(...(await validateRfcSelfTestImpact(projectRoot, docs)));
620
665
  return { info: [`validate-rfc checked ${docs.length} file(s)`], errors };
621
666
  }
667
+ async function validateRfcSelfTestImpact(projectRoot, docs) {
668
+ const errors = [];
669
+ for (const doc of docs) {
670
+ const relative = repoRelative(projectRoot, doc);
671
+ const basename = path.basename(doc);
672
+ const number = rfcNumber(basename);
673
+ if (number !== undefined && number < 23)
674
+ continue;
675
+ const text = await readText(doc);
676
+ if (!containsAny(text, RFC_SELF_TEST_TRIGGER_TERMS))
677
+ continue;
678
+ if (!containsAny(text, DEVELOPMENT_SELF_TEST_IMPACT_TERMS)) {
679
+ errors.push(`${relative} must include Development Self-Test Impact when RFC changes entry/exit, runtime, gates, handoff, or blockers`);
680
+ }
681
+ }
682
+ return errors;
683
+ }
684
+ function rfcNumber(fileName) {
685
+ const match = fileName.match(/^RFC[_-](\d+)/i);
686
+ return match ? Number(match[1]) : undefined;
687
+ }
622
688
  async function validatePlanState(projectRoot, allowOpen) {
623
689
  const errors = [];
624
690
  const root = await harnessRoot(projectRoot);
@@ -716,7 +782,7 @@ function validateRuntimeEvidenceContract(task) {
716
782
  if (String(task.phase ?? "") !== "SPRINTING")
717
783
  return errors;
718
784
  const context = taskText(task).toLowerCase();
719
- const needsRuntimeContract = containsAny(context, [...APPLICATION_READINESS_TASK_TERMS, ...PAGE_TASK_TERMS]);
785
+ const needsRuntimeContract = needsRunnableTaskContract(task) && !isNotApplicableRuntimeTask(task);
720
786
  const evidenceLevel = task.evidence_level;
721
787
  const targetRuntime = task.target_runtime_environment;
722
788
  if (needsRuntimeContract && !isRecord(evidenceLevel)) {
@@ -769,6 +835,123 @@ function validateRuntimeEvidenceContract(task) {
769
835
  }
770
836
  }
771
837
  }
838
+ errors.push(...validateSelfTestContract(task, needsRuntimeContract));
839
+ return errors;
840
+ }
841
+ function needsRunnableTaskContract(task) {
842
+ const context = taskText(task).toLowerCase();
843
+ return containsAny(context, [...APPLICATION_READINESS_TASK_TERMS, ...PAGE_TASK_TERMS, ...CALLABLE_TASK_TERMS]);
844
+ }
845
+ function isNotApplicableRuntimeTask(task) {
846
+ const evidenceLevel = isRecord(task.evidence_level) ? task.evidence_level : undefined;
847
+ const targetRuntime = isRecord(task.target_runtime_environment) ? task.target_runtime_environment : undefined;
848
+ return String(evidenceLevel?.required ?? "") === "unit" && String(targetRuntime?.kind ?? "") === "not_applicable";
849
+ }
850
+ function validateSelfTestContract(task, requiredForRunnableBoundary) {
851
+ const errors = [];
852
+ const taskId = String(task.id ?? "Task");
853
+ const contract = task.self_test_contract;
854
+ if (requiredForRunnableBoundary && !isRecord(contract)) {
855
+ errors.push(`${taskId} runtime/app task must define self_test_contract`);
856
+ return errors;
857
+ }
858
+ if (contract === undefined)
859
+ return errors;
860
+ if (!isRecord(contract)) {
861
+ errors.push(`${taskId} self_test_contract must be a mapping`);
862
+ return errors;
863
+ }
864
+ const status = String(contract.status ?? "");
865
+ if (!SELF_TEST_CONTRACT_STATUSES.has(status)) {
866
+ errors.push(`${taskId} self_test_contract.status must be required or not_applicable`);
867
+ }
868
+ if (requiredForRunnableBoundary && status !== "required") {
869
+ errors.push(`${taskId} runnable boundary task self_test_contract.status must be required`);
870
+ }
871
+ if (status === "not_applicable") {
872
+ const reason = String(contract.not_applicable_reason ?? "").trim();
873
+ if (reason.length < 24 || isPlaceholderEvidence(reason)) {
874
+ errors.push(`${taskId} self_test_contract.not_applicable_reason must explain why self-test is not applicable`);
875
+ }
876
+ return errors;
877
+ }
878
+ if (status !== "required")
879
+ return errors;
880
+ for (const field of ["source", "runnable_entry", "observable_exit", "module_key_test_path"]) {
881
+ if (typeof contract[field] !== "string" || !String(contract[field]).trim() || isPlaceholderEvidence(String(contract[field]))) {
882
+ errors.push(`${taskId} self_test_contract.${field} must be concrete`);
883
+ }
884
+ }
885
+ if (!Array.isArray(contract.capability_refs) || contract.capability_refs.length === 0) {
886
+ errors.push(`${taskId} self_test_contract.capability_refs must be a non-empty list`);
887
+ }
888
+ const requiredGates = asStringList(contract.required_gates);
889
+ if (requiredGates.length === 0) {
890
+ errors.push(`${taskId} self_test_contract.required_gates must be a non-empty list`);
891
+ }
892
+ const taskGates = new Set(asStringList(task.required_gates));
893
+ for (const gate of requiredGates) {
894
+ if (!taskGates.has(gate)) {
895
+ errors.push(`${taskId} self_test_contract.required_gates must also appear in task required_gates: ${gate}`);
896
+ }
897
+ }
898
+ const scenarios = Array.isArray(contract.scenarios) ? contract.scenarios : [];
899
+ if (scenarios.length === 0) {
900
+ errors.push(`${taskId} self_test_contract.scenarios must be a non-empty list`);
901
+ }
902
+ const seen = new Set();
903
+ scenarios.forEach((scenario, index) => {
904
+ if (!isRecord(scenario)) {
905
+ errors.push(`${taskId} self_test_contract.scenarios[${index}] must be a mapping`);
906
+ return;
907
+ }
908
+ const scenarioId = String(scenario.id ?? "").trim();
909
+ if (!scenarioId) {
910
+ errors.push(`${taskId} self_test_contract.scenarios[${index}].id must be set`);
911
+ }
912
+ else if (seen.has(scenarioId)) {
913
+ errors.push(`${taskId} self_test_contract scenario id must be unique: ${scenarioId}`);
914
+ }
915
+ seen.add(scenarioId);
916
+ for (const field of ["entry", "expected_exit", "evidence"]) {
917
+ if (typeof scenario[field] !== "string" || !String(scenario[field]).trim() || isPlaceholderEvidence(String(scenario[field]))) {
918
+ errors.push(`${taskId} self_test_contract.scenarios[${scenarioId || index}].${field} must be concrete`);
919
+ }
920
+ }
921
+ });
922
+ return errors;
923
+ }
924
+ async function validateSelfTestContractTechPlanBinding(projectRoot, task, normalizedTechRefs) {
925
+ const errors = [];
926
+ const taskId = String(task.id ?? "Task");
927
+ const contract = isRecord(task.self_test_contract) ? task.self_test_contract : undefined;
928
+ if (!contract || String(contract.status ?? "") !== "required")
929
+ return errors;
930
+ const source = normalizeDocRef(String(contract.source ?? ""));
931
+ if (!source)
932
+ return errors;
933
+ if (!normalizedTechRefs.includes(source)) {
934
+ errors.push(`${taskId} self_test_contract.source must be listed in docs.tech_plan: ${source}`);
935
+ return errors;
936
+ }
937
+ const sourcePath = path.join(projectRoot, source);
938
+ if (!(await pathExists(sourcePath)))
939
+ return errors;
940
+ const text = await readText(sourcePath);
941
+ const section = markdownSection(text, DEVELOPMENT_SELF_TEST_CONTRACT_TERMS);
942
+ if (!section) {
943
+ errors.push(`${taskId} self_test_contract.source must contain a Development Self-Test Contract section: ${source}`);
944
+ return errors;
945
+ }
946
+ if (!containsAny(section, MODULE_KEY_TEST_PATH_TERMS)) {
947
+ errors.push(`${taskId} tech plan Development Self-Test Contract must include Module key test path: ${source}`);
948
+ }
949
+ for (const scenario of Array.isArray(contract.scenarios) ? contract.scenarios.filter(isRecord) : []) {
950
+ const scenarioId = String(scenario.id ?? "").trim();
951
+ if (scenarioId && !section.includes(scenarioId)) {
952
+ errors.push(`${taskId} tech plan Development Self-Test Contract must include scenario ${scenarioId}: ${source}`);
953
+ }
954
+ }
772
955
  return errors;
773
956
  }
774
957
  function validateParallelExecutionContract(plan, currentPhase, errors) {
@@ -781,11 +964,19 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
781
964
  }
782
965
  if (contract.enabled !== true)
783
966
  errors.push("parallel_execution.enabled must be true when present");
784
- if (contract.trigger !== "user_requested")
785
- errors.push('parallel_execution.trigger must be "user_requested"');
967
+ if (!PARALLEL_TRIGGERS.has(String(contract.trigger ?? ""))) {
968
+ errors.push("parallel_execution.trigger must be user_requested or workflow_default");
969
+ }
786
970
  if (!PARALLEL_MODES.has(String(contract.mode ?? ""))) {
787
971
  errors.push("parallel_execution.mode must be runtime_managed or user_orchestrated");
788
972
  }
973
+ const provider = parallelRuntimeProvider(contract, errors);
974
+ if (provider && !PARALLEL_RUNTIME_PROVIDERS.has(provider)) {
975
+ errors.push("parallel_execution.runtime.provider must be codex_native_subagents, user_orchestrated, or codex_exec_worktree");
976
+ }
977
+ if (contract.trigger === "workflow_default" && provider !== "codex_native_subagents") {
978
+ errors.push('parallel_execution.runtime.provider must be "codex_native_subagents" when trigger is workflow_default');
979
+ }
789
980
  if ("phase" in contract) {
790
981
  errors.push("parallel_execution must not define phase; lifecycle.yaml is the single source for current_phase");
791
982
  }
@@ -793,7 +984,7 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
793
984
  errors.push("parallel_execution must not define linked_task_id; use plan.yaml current_task_id");
794
985
  }
795
986
  if (!PARALLEL_ALLOWED_PHASES.has(currentPhase)) {
796
- errors.push("parallel_execution is only supported during REQUIREMENT_GATHERING, SPRINTING, or TESTING");
987
+ errors.push("parallel_execution is only supported during REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION");
797
988
  }
798
989
  if (contract.coordinator !== "main_agent")
799
990
  errors.push('parallel_execution.coordinator must be "main_agent"');
@@ -806,6 +997,7 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
806
997
  }
807
998
  else {
808
999
  const seen = new Set();
1000
+ const writeOwnedPaths = [];
809
1001
  workers.forEach((worker, index) => {
810
1002
  const prefix = `parallel_execution.workers[${index}]`;
811
1003
  if (!isRecord(worker)) {
@@ -832,18 +1024,36 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
832
1024
  if (Array.isArray(worker.required_gates) && worker.required_gates.length === 0) {
833
1025
  errors.push(`${prefix}.required_gates must not be empty`);
834
1026
  }
1027
+ if (PARALLEL_READ_ONLY_PHASES.has(currentPhase) && worker.writes_repo !== false) {
1028
+ errors.push(`${prefix}.writes_repo must be false during ${currentPhase}`);
1029
+ }
835
1030
  if (worker.writes_repo === true) {
836
- if (typeof worker.branch !== "string" || !worker.branch.trim()) {
837
- errors.push(`${prefix}.branch is required when writes_repo is true`);
838
- }
839
- if (typeof worker.worktree !== "string" || !worker.worktree.trim()) {
840
- errors.push(`${prefix}.worktree is required when writes_repo is true`);
1031
+ if (provider !== "codex_native_subagents") {
1032
+ if (typeof worker.branch !== "string" || !worker.branch.trim()) {
1033
+ errors.push(`${prefix}.branch is required when writes_repo is true outside codex_native_subagents runtime`);
1034
+ }
1035
+ if (typeof worker.worktree !== "string" || !worker.worktree.trim()) {
1036
+ errors.push(`${prefix}.worktree is required when writes_repo is true outside codex_native_subagents runtime`);
1037
+ }
841
1038
  }
842
1039
  if (!Array.isArray(worker.owned_paths) || worker.owned_paths.length === 0) {
843
1040
  errors.push(`${prefix}.owned_paths must not be empty when writes_repo is true`);
844
1041
  }
1042
+ validateParallelWorkerPathLock(plan, worker, index, errors);
1043
+ for (const owned of stringArray(worker.owned_paths).map(normalizeParallelPattern)) {
1044
+ writeOwnedPaths.push({ index, path: owned });
1045
+ }
845
1046
  }
846
1047
  });
1048
+ for (let left = 0; left < writeOwnedPaths.length; left += 1) {
1049
+ for (let right = left + 1; right < writeOwnedPaths.length; right += 1) {
1050
+ const leftOwned = writeOwnedPaths[left];
1051
+ const rightOwned = writeOwnedPaths[right];
1052
+ if (globPatternsOverlap(leftOwned.path, rightOwned.path)) {
1053
+ errors.push(`parallel_execution write worker owned_paths must not overlap: workers[${leftOwned.index}] ${leftOwned.path} vs workers[${rightOwned.index}] ${rightOwned.path}`);
1054
+ }
1055
+ }
1056
+ }
847
1057
  }
848
1058
  const integration = contract.integration;
849
1059
  if (!isRecord(integration)) {
@@ -862,6 +1072,63 @@ function validateParallelExecutionContract(plan, currentPhase, errors) {
862
1072
  errors.push("parallel_execution.integration.fact_source_updates must be a non-empty list");
863
1073
  }
864
1074
  }
1075
+ function parallelRuntimeProvider(contract, errors) {
1076
+ const runtime = contract.runtime;
1077
+ if (runtime === undefined || runtime === null)
1078
+ return "";
1079
+ if (!isRecord(runtime)) {
1080
+ errors.push("parallel_execution.runtime must be a mapping when present");
1081
+ return "";
1082
+ }
1083
+ return String(runtime.provider ?? "");
1084
+ }
1085
+ function validateParallelWorkerPathLock(plan, worker, index, errors) {
1086
+ const currentTask = currentPlanTask(plan);
1087
+ if (!currentTask)
1088
+ return;
1089
+ const taskAllowed = stringArray(currentTask.allowed_paths).map(normalizeParallelPattern);
1090
+ const workerOwned = stringArray(worker.owned_paths).map(normalizeParallelPattern);
1091
+ const workerForbidden = stringArray(worker.forbidden_paths).map(normalizeParallelPattern);
1092
+ const protectedPatterns = PARALLEL_PROTECTED_WRITE_PATTERNS.map(normalizeParallelPattern);
1093
+ for (const owned of workerOwned) {
1094
+ if (!matchesAny(owned, taskAllowed)) {
1095
+ errors.push(`parallel_execution.workers[${index}].owned_paths must be within current task allowed_paths: ${owned}`);
1096
+ }
1097
+ for (const forbidden of [...workerForbidden, ...protectedPatterns]) {
1098
+ if (globPatternsOverlap(owned, forbidden)) {
1099
+ errors.push(`parallel_execution.workers[${index}].owned_paths must not overlap forbidden paths: ${owned} vs ${forbidden}`);
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ function currentPlanTask(plan) {
1105
+ const currentTaskId = String(plan.current_task_id ?? "");
1106
+ const tasks = Array.isArray(plan.tasks) ? plan.tasks.filter(isRecord) : [];
1107
+ return tasks.find((task) => String(task.id ?? "") === currentTaskId);
1108
+ }
1109
+ function stringArray(value) {
1110
+ return Array.isArray(value) ? value.map((item) => String(item)) : [];
1111
+ }
1112
+ function normalizeParallelPattern(pattern) {
1113
+ return pattern.replace(/\\/g, "/").replaceAll("<harnessRoot>", ".codex");
1114
+ }
1115
+ function globPrefix(pattern) {
1116
+ const normalized = normalizeParallelPattern(pattern);
1117
+ const positions = ["*", "[", "?"].map((token) => normalized.indexOf(token)).filter((index) => index >= 0);
1118
+ const prefix = positions.length > 0 ? normalized.slice(0, Math.min(...positions)) : normalized;
1119
+ return prefix.replace(/\/+$/, "");
1120
+ }
1121
+ function globPatternsOverlap(left, right) {
1122
+ const leftClean = normalizeParallelPattern(left);
1123
+ const rightClean = normalizeParallelPattern(right);
1124
+ if (matchesGlob(leftClean, rightClean) || matchesGlob(rightClean, leftClean))
1125
+ return true;
1126
+ const leftPrefix = globPrefix(leftClean);
1127
+ const rightPrefix = globPrefix(rightClean);
1128
+ if (!leftPrefix || !rightPrefix)
1129
+ return leftPrefix === rightPrefix;
1130
+ return leftPrefix === rightPrefix || leftPrefix.startsWith(`${rightPrefix}/`) || rightPrefix.startsWith(`${leftPrefix}/`);
1131
+ }
865
1132
  async function validateChangedPaths(projectRoot, plan, allowOpen) {
866
1133
  if (!allowOpen)
867
1134
  return [];
@@ -1017,8 +1284,75 @@ function validateDevelopmentEvidenceText(text, task, implementationDoc) {
1017
1284
  }
1018
1285
  }
1019
1286
  errors.push(...validateEvidenceLevelAgainstContract(section, text, task, implementationDoc));
1287
+ errors.push(...validateDevelopmentSelfTestReport(text, section, task, implementationDoc));
1288
+ return errors;
1289
+ }
1290
+ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection, task, implementationDoc) {
1291
+ const errors = [];
1292
+ const taskId = String(task.id ?? "current task");
1293
+ const contract = isRecord(task.self_test_contract) ? task.self_test_contract : undefined;
1294
+ if (!contract || String(contract.status ?? "") !== "required")
1295
+ return errors;
1296
+ const report = markdownSection(fullText, DEVELOPMENT_SELF_TEST_REPORT_TERMS);
1297
+ if (!report) {
1298
+ return [`${taskId} implementation_doc must include Development Self-Test Report for self_test_contract: ${implementationDoc}`];
1299
+ }
1300
+ const basicSelfTest = evidenceFieldValue(developmentEvidenceSection, "Basic Self-test Evidence") ?? "";
1301
+ if (!containsAny(basicSelfTest, ["Development Self-Test Report", "开发自测报告", "self-test report"])) {
1302
+ errors.push(`${taskId} Basic Self-test Evidence must reference the Development Self-Test Report in ${implementationDoc}`);
1303
+ }
1304
+ for (const field of ["Contract Source", "Scenario Results", "Executed Gates", "Module Key Test Path", "Actual Evidence", "Missing / Blockers", "Testing Handoff Readiness"]) {
1305
+ const value = evidenceFieldValue(report, field);
1306
+ const allowsNone = field === "Missing / Blockers";
1307
+ if (!value || (!allowsNone && isPlaceholderEvidence(value))) {
1308
+ errors.push(`${taskId} Development Self-Test Report ${field} must contain executed evidence in ${implementationDoc}`);
1309
+ }
1310
+ }
1311
+ const source = String(contract.source ?? "").trim();
1312
+ if (source && !report.includes(source)) {
1313
+ errors.push(`${taskId} Development Self-Test Report must reference contract source ${source} in ${implementationDoc}`);
1314
+ }
1315
+ for (const gate of asStringList(contract.required_gates)) {
1316
+ if (!report.includes(gate)) {
1317
+ errors.push(`${taskId} Development Self-Test Report must record required gate ${gate} in ${implementationDoc}`);
1318
+ }
1319
+ }
1320
+ const moduleKeyTestPath = evidenceFieldValue(report, "Module Key Test Path") ?? "";
1321
+ const runnableEntry = String(contract.runnable_entry ?? "").trim();
1322
+ if (runnableEntry && !moduleKeyTestPath.includes(runnableEntry)) {
1323
+ errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include runnable entry ${runnableEntry} in ${implementationDoc}`);
1324
+ }
1325
+ const scenarios = Array.isArray(contract.scenarios) ? contract.scenarios.filter(isRecord) : [];
1326
+ for (const scenario of scenarios) {
1327
+ const scenarioId = String(scenario.id ?? "").trim();
1328
+ if (!scenarioId)
1329
+ continue;
1330
+ if (!moduleKeyTestPath.includes(scenarioId)) {
1331
+ errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include scenario ${scenarioId} in ${implementationDoc}`);
1332
+ }
1333
+ const status = scenarioStatus(report, scenarioId);
1334
+ if (!status) {
1335
+ errors.push(`${taskId} Development Self-Test Report must record scenario ${scenarioId} as PASS or BLOCKED in ${implementationDoc}`);
1336
+ }
1337
+ else if (status === "BLOCKED") {
1338
+ errors.push(`${taskId} Development Self-Test Report scenario ${scenarioId} is BLOCKED; keep task open or record a blocker`);
1339
+ }
1340
+ }
1020
1341
  return errors;
1021
1342
  }
1343
+ function scenarioStatus(text, scenarioId) {
1344
+ const escaped = scenarioId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1345
+ const patterns = [
1346
+ new RegExp("^.*" + escaped + ".*\\b(PASS|BLOCKED)\\b.*$", "im"),
1347
+ new RegExp("\\|[^\\n|]*" + escaped + "[^\\n|]*\\|[^\\n|]*\\b(PASS|BLOCKED)\\b[^\\n|]*\\|", "i")
1348
+ ];
1349
+ for (const pattern of patterns) {
1350
+ const match = text.match(pattern);
1351
+ if (match)
1352
+ return match[1].toUpperCase();
1353
+ }
1354
+ return undefined;
1355
+ }
1022
1356
  function hasConcreteDevelopmentEvidenceFields(section) {
1023
1357
  return ["Evidence Level", "Target Runtime Environment", "Runnable Entry", "Observable Exit", "Client / Server Initialization", "Config Contract"].some((field) => {
1024
1358
  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.18",
3
+ "version": "0.1.20",
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"