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.
- package/README.md +11 -9
- package/assets/agents/AGENTS_CORE.md +2 -2
- package/assets/docs/README.md +12 -10
- package/assets/skills/pjsdlc_architect_design/SKILL.md +4 -2
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +11 -8
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +7 -3
- package/assets/skills/pjsdlc_manager/SKILL.md +4 -4
- package/assets/skills/pjsdlc_pm_prd/SKILL.md +3 -3
- package/assets/skills/pjsdlc_release_manager/SKILL.md +2 -0
- package/assets/skills/pjsdlc_reviewer/SKILL.md +5 -2
- package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +5 -2
- package/assets/skills/pjsdlc_tester/SKILL.md +5 -4
- package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +21 -7
- package/assets/templates/PLAN_TEMPLATE.yaml +27 -6
- package/assets/templates/RFC_TEMPLATE.md +12 -1
- package/assets/templates/TECH_DESIGN_TEMPLATE.md +19 -2
- package/assets/tools/build_doc_overviews.py +152 -0
- package/assets/tools/harness_utils.py +858 -0
- package/assets/tools/impact_analyzer.py +51 -0
- package/assets/tools/run_current_gate.py +29 -0
- package/assets/tools/status.py +29 -0
- package/assets/tools/transition.py +68 -0
- package/assets/tools/validate_allowed_paths.py +44 -0
- package/assets/tools/validate_design.py +199 -0
- package/assets/tools/validate_dev_state.py +20 -0
- package/assets/tools/validate_harness.py +60 -0
- package/assets/tools/validate_plan.py +24 -0
- package/assets/tools/validate_plan_draft.py +19 -0
- package/assets/tools/validate_prd.py +27 -0
- package/assets/tools/validate_prompt_language.py +138 -0
- package/assets/tools/validate_release_plan.py +37 -0
- package/assets/tools/validate_review.py +59 -0
- package/assets/tools/validate_rfc.py +105 -0
- package/assets/tools/validate_task_docs.py +40 -0
- package/assets/tools/validate_test_plan.py +82 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/migrations.js +3 -0
- package/dist/lib/sync-engine.js +4 -0
- package/dist/lib/validators.js +351 -17
- package/package.json +1 -1
- package/source-mappings.yaml +6 -0
package/dist/lib/validators.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
785
|
-
errors.push(
|
|
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
|
|
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 (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
package/source-mappings.yaml
CHANGED
|
@@ -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"
|