@hunter-harness/workflow-harness 0.2.39 → 0.2.41
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/manifests/general/claude-code.json +29 -29
- package/harness/manifests/general/codebuddy.json +29 -29
- package/harness/manifests/general/codex.json +29 -29
- package/harness/manifests/general/cursor.json +29 -29
- package/harness/manifests/java/claude-code.json +30 -30
- package/harness/manifests/java/codebuddy.json +30 -30
- package/harness/manifests/java/codex.json +30 -30
- package/harness/manifests/java/cursor.json +30 -30
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -81,7 +81,7 @@ def _table_cells(line: str) -> list[str]:
|
|
|
81
81
|
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def parse_test_scenarios(scenarios_path: Path) -> list[dict[str,
|
|
84
|
+
def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, Any]]:
|
|
85
85
|
"""C9: parse test-scenarios.md tables, extracting scenario rows.
|
|
86
86
|
|
|
87
87
|
Returns a list of dicts with keys: id / priority / scenario / verification /
|
|
@@ -89,7 +89,7 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
89
89
|
"""
|
|
90
90
|
text = Path(scenarios_path).read_text(encoding="utf-8-sig")
|
|
91
91
|
lines = text.splitlines()
|
|
92
|
-
scenarios: list[dict[str,
|
|
92
|
+
scenarios: list[dict[str, Any]] = []
|
|
93
93
|
i = 0
|
|
94
94
|
while i < len(lines):
|
|
95
95
|
line = lines[i].strip()
|
|
@@ -127,6 +127,35 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
127
127
|
"tier",
|
|
128
128
|
)
|
|
129
129
|
expected_index = _column_index(headers, "预期", "expected")
|
|
130
|
+
executable_id_index = _column_index(
|
|
131
|
+
headers,
|
|
132
|
+
"executable test ID",
|
|
133
|
+
"executableTestId",
|
|
134
|
+
"可执行测试 ID",
|
|
135
|
+
"测试 ID",
|
|
136
|
+
)
|
|
137
|
+
test_file_index = _column_index(
|
|
138
|
+
headers,
|
|
139
|
+
"test file",
|
|
140
|
+
"testFile",
|
|
141
|
+
"测试文件",
|
|
142
|
+
"spec path",
|
|
143
|
+
)
|
|
144
|
+
test_title_index = _column_index(
|
|
145
|
+
headers,
|
|
146
|
+
"test title",
|
|
147
|
+
"testTitle",
|
|
148
|
+
"测试标题",
|
|
149
|
+
"用例标题",
|
|
150
|
+
)
|
|
151
|
+
executable_mapping_declared = any(
|
|
152
|
+
index is not None
|
|
153
|
+
for index in (
|
|
154
|
+
executable_id_index,
|
|
155
|
+
test_file_index,
|
|
156
|
+
test_title_index,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
130
159
|
|
|
131
160
|
row_index = i + 2
|
|
132
161
|
while row_index < len(lines):
|
|
@@ -175,6 +204,7 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
175
204
|
priority,
|
|
176
205
|
"advisory",
|
|
177
206
|
),
|
|
207
|
+
"executableMappingDeclared": executable_mapping_declared,
|
|
178
208
|
}
|
|
179
209
|
if category_index is not None and category_index < len(cells):
|
|
180
210
|
if cells[category_index].strip():
|
|
@@ -183,6 +213,9 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
183
213
|
("verification", verification_index),
|
|
184
214
|
("executionTier", execution_tier_index),
|
|
185
215
|
("expected", expected_index),
|
|
216
|
+
("executableTestId", executable_id_index),
|
|
217
|
+
("testFile", test_file_index),
|
|
218
|
+
("testTitle", test_title_index),
|
|
186
219
|
):
|
|
187
220
|
if (
|
|
188
221
|
complete_row
|
|
@@ -199,6 +232,23 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
199
232
|
return scenarios
|
|
200
233
|
|
|
201
234
|
|
|
235
|
+
def scenario_manifest_schema_version(scenarios: list[dict[str, Any]]) -> int:
|
|
236
|
+
required = [
|
|
237
|
+
item
|
|
238
|
+
for item in scenarios
|
|
239
|
+
if str(item.get("requiredEvidenceKind") or "") == "ledger"
|
|
240
|
+
]
|
|
241
|
+
if required and all(
|
|
242
|
+
all(
|
|
243
|
+
str(item.get(field) or "").strip()
|
|
244
|
+
for field in ("executableTestId", "testFile", "testTitle")
|
|
245
|
+
)
|
|
246
|
+
for item in required
|
|
247
|
+
):
|
|
248
|
+
return 2
|
|
249
|
+
return 1
|
|
250
|
+
|
|
251
|
+
|
|
202
252
|
def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
203
253
|
"""C8: parse plan.md task table rows, extracting optional ownerPhase/implementationDoneWhen/verificationPhase columns.
|
|
204
254
|
|
|
@@ -530,6 +580,21 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
530
580
|
"PLAN_SCENARIO_OWNER_PHASE_INVALID",
|
|
531
581
|
f"scenario {scenario.get('id', '?')}: unsupported ownerPhase '{owner}'",
|
|
532
582
|
)
|
|
583
|
+
if (
|
|
584
|
+
scenario.get("requiredEvidenceKind") == "ledger"
|
|
585
|
+
and scenario.get("executableMappingDeclared")
|
|
586
|
+
):
|
|
587
|
+
missing_mapping = [
|
|
588
|
+
field
|
|
589
|
+
for field in ("executableTestId", "testFile", "testTitle")
|
|
590
|
+
if not str(scenario.get(field) or "").strip()
|
|
591
|
+
]
|
|
592
|
+
if missing_mapping:
|
|
593
|
+
return _result_error(
|
|
594
|
+
"PLAN_SCENARIO_EXECUTABLE_MAPPING_MISSING",
|
|
595
|
+
f"scenario {scenario.get('id', '?')}: missing executable mapping fields: "
|
|
596
|
+
+ ", ".join(missing_mapping),
|
|
597
|
+
)
|
|
533
598
|
|
|
534
599
|
digest = hashlib.sha256()
|
|
535
600
|
artifact_names: list[str] = []
|
|
@@ -827,9 +892,16 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
827
892
|
)
|
|
828
893
|
|
|
829
894
|
expected_manifest = {
|
|
830
|
-
"schemaVersion":
|
|
895
|
+
"schemaVersion": scenario_manifest_schema_version(expected_scenarios),
|
|
831
896
|
"changeName": change_name,
|
|
832
|
-
"scenarios":
|
|
897
|
+
"scenarios": [
|
|
898
|
+
{
|
|
899
|
+
key: value
|
|
900
|
+
for key, value in scenario.items()
|
|
901
|
+
if key != "executableMappingDeclared"
|
|
902
|
+
}
|
|
903
|
+
for scenario in expected_scenarios
|
|
904
|
+
],
|
|
833
905
|
}
|
|
834
906
|
if manifest != expected_manifest:
|
|
835
907
|
return _result_error(
|
|
@@ -1138,9 +1210,16 @@ def finalize_plan(
|
|
|
1138
1210
|
scenarios = validation.get("scenarios") or []
|
|
1139
1211
|
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
1140
1212
|
manifest_payload = {
|
|
1141
|
-
"schemaVersion":
|
|
1213
|
+
"schemaVersion": scenario_manifest_schema_version(scenarios),
|
|
1142
1214
|
"changeName": change_name,
|
|
1143
|
-
"scenarios":
|
|
1215
|
+
"scenarios": [
|
|
1216
|
+
{
|
|
1217
|
+
key: value
|
|
1218
|
+
for key, value in scenario.items()
|
|
1219
|
+
if key != "executableMappingDeclared"
|
|
1220
|
+
}
|
|
1221
|
+
for scenario in scenarios
|
|
1222
|
+
],
|
|
1144
1223
|
}
|
|
1145
1224
|
_atomic_write_json(manifest_path, manifest_payload)
|
|
1146
1225
|
created.append(manifest_path)
|
|
@@ -27,6 +27,7 @@ import sys
|
|
|
27
27
|
import time
|
|
28
28
|
import urllib.error
|
|
29
29
|
import urllib.request
|
|
30
|
+
import uuid
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
from typing import Any
|
|
32
33
|
from urllib.parse import urlparse
|
|
@@ -58,6 +59,7 @@ FATAL_KEYWORDS = (
|
|
|
58
59
|
|
|
59
60
|
# Process create-time vs session.startedAt tolerance (seconds).
|
|
60
61
|
IDENTITY_TOLERANCE_SEC = 5.0
|
|
62
|
+
STOP_CONFIRM_TIMEOUT_SEC = 5.0
|
|
61
63
|
|
|
62
64
|
# Windows process flags
|
|
63
65
|
_DETACHED_PROCESS = 0x00000008
|
|
@@ -344,6 +346,35 @@ def get_process_create_time(pid: int) -> dt.datetime | None:
|
|
|
344
346
|
return _posix_process_create_time(pid)
|
|
345
347
|
|
|
346
348
|
|
|
349
|
+
def get_process_executable(pid: int) -> str | None:
|
|
350
|
+
"""Best-effort executable identity for PID-reuse protection."""
|
|
351
|
+
if os.name == "nt":
|
|
352
|
+
try:
|
|
353
|
+
import ctypes
|
|
354
|
+
from ctypes import wintypes
|
|
355
|
+
|
|
356
|
+
kernel32 = ctypes.windll.kernel32
|
|
357
|
+
handle = kernel32.OpenProcess(0x1000, False, pid)
|
|
358
|
+
if not handle:
|
|
359
|
+
return None
|
|
360
|
+
try:
|
|
361
|
+
capacity = wintypes.DWORD(32768)
|
|
362
|
+
buffer = ctypes.create_unicode_buffer(capacity.value)
|
|
363
|
+
if not kernel32.QueryFullProcessImageNameW(
|
|
364
|
+
handle, 0, buffer, ctypes.byref(capacity)
|
|
365
|
+
):
|
|
366
|
+
return None
|
|
367
|
+
return str(Path(buffer.value).resolve())
|
|
368
|
+
finally:
|
|
369
|
+
kernel32.CloseHandle(handle)
|
|
370
|
+
except Exception:
|
|
371
|
+
return None
|
|
372
|
+
try:
|
|
373
|
+
return str(Path(f"/proc/{pid}/exe").resolve(strict=True))
|
|
374
|
+
except OSError:
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
347
378
|
def _windows_process_create_time(pid: int) -> dt.datetime | None:
|
|
348
379
|
try:
|
|
349
380
|
import ctypes
|
|
@@ -454,7 +485,26 @@ def verify_process_identity(session: dict[str, Any]) -> bool | None:
|
|
|
454
485
|
started_utc = started_at.astimezone(dt.timezone.utc)
|
|
455
486
|
create_utc = create_time.astimezone(dt.timezone.utc)
|
|
456
487
|
delta = abs((create_utc - started_utc).total_seconds())
|
|
457
|
-
|
|
488
|
+
if delta > IDENTITY_TOLERANCE_SEC:
|
|
489
|
+
return False
|
|
490
|
+
process_identity = session.get("processIdentity")
|
|
491
|
+
if isinstance(process_identity, dict):
|
|
492
|
+
expected_executable = str(
|
|
493
|
+
process_identity.get("executable") or ""
|
|
494
|
+
).strip()
|
|
495
|
+
if not expected_executable:
|
|
496
|
+
return None
|
|
497
|
+
actual_executable = get_process_executable(pid)
|
|
498
|
+
if actual_executable is None:
|
|
499
|
+
return None
|
|
500
|
+
try:
|
|
501
|
+
if Path(actual_executable).resolve() != Path(
|
|
502
|
+
expected_executable
|
|
503
|
+
).resolve():
|
|
504
|
+
return False
|
|
505
|
+
except OSError:
|
|
506
|
+
return None
|
|
507
|
+
return True
|
|
458
508
|
|
|
459
509
|
|
|
460
510
|
def terminate_process_tree(pid: int) -> None:
|
|
@@ -902,23 +952,83 @@ _WIN_LAUNCHER_SOURCE = """\
|
|
|
902
952
|
\"\"\"Harness Windows service launcher (internal).\"\"\"
|
|
903
953
|
from __future__ import annotations
|
|
904
954
|
|
|
955
|
+
import ctypes
|
|
905
956
|
import subprocess
|
|
906
957
|
import sys
|
|
907
958
|
from pathlib import Path
|
|
908
959
|
|
|
909
960
|
_CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
910
961
|
_CREATE_NO_WINDOW = 0x08000000
|
|
911
|
-
|
|
962
|
+
_JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
|
963
|
+
_JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS = 9
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
class _IoCounters(ctypes.Structure):
|
|
967
|
+
_fields_ = [
|
|
968
|
+
("ReadOperationCount", ctypes.c_uint64),
|
|
969
|
+
("WriteOperationCount", ctypes.c_uint64),
|
|
970
|
+
("OtherOperationCount", ctypes.c_uint64),
|
|
971
|
+
("ReadTransferCount", ctypes.c_uint64),
|
|
972
|
+
("WriteTransferCount", ctypes.c_uint64),
|
|
973
|
+
("OtherTransferCount", ctypes.c_uint64),
|
|
974
|
+
]
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
class _BasicLimitInformation(ctypes.Structure):
|
|
978
|
+
_fields_ = [
|
|
979
|
+
("PerProcessUserTimeLimit", ctypes.c_int64),
|
|
980
|
+
("PerJobUserTimeLimit", ctypes.c_int64),
|
|
981
|
+
("LimitFlags", ctypes.c_uint32),
|
|
982
|
+
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
983
|
+
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
984
|
+
("ActiveProcessLimit", ctypes.c_uint32),
|
|
985
|
+
("Affinity", ctypes.c_size_t),
|
|
986
|
+
("PriorityClass", ctypes.c_uint32),
|
|
987
|
+
("SchedulingClass", ctypes.c_uint32),
|
|
988
|
+
]
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
class _ExtendedLimitInformation(ctypes.Structure):
|
|
992
|
+
_fields_ = [
|
|
993
|
+
("BasicLimitInformation", _BasicLimitInformation),
|
|
994
|
+
("IoInfo", _IoCounters),
|
|
995
|
+
("ProcessMemoryLimit", ctypes.c_size_t),
|
|
996
|
+
("JobMemoryLimit", ctypes.c_size_t),
|
|
997
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
|
998
|
+
("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
999
|
+
]
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _create_job(name: str) -> int:
|
|
1003
|
+
kernel32 = ctypes.windll.kernel32
|
|
1004
|
+
kernel32.CreateJobObjectW.restype = ctypes.c_void_p
|
|
1005
|
+
handle = kernel32.CreateJobObjectW(None, name)
|
|
1006
|
+
if not handle:
|
|
1007
|
+
raise OSError("CreateJobObjectW failed")
|
|
1008
|
+
info = _ExtendedLimitInformation()
|
|
1009
|
+
info.BasicLimitInformation.LimitFlags = _JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
1010
|
+
configured = kernel32.SetInformationJobObject(
|
|
1011
|
+
ctypes.c_void_p(handle),
|
|
1012
|
+
_JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS,
|
|
1013
|
+
ctypes.byref(info),
|
|
1014
|
+
ctypes.sizeof(info),
|
|
1015
|
+
)
|
|
1016
|
+
if not configured:
|
|
1017
|
+
kernel32.CloseHandle(ctypes.c_void_p(handle))
|
|
1018
|
+
raise OSError("SetInformationJobObject failed")
|
|
1019
|
+
return int(handle)
|
|
912
1020
|
|
|
913
1021
|
|
|
914
1022
|
def main() -> int:
|
|
915
1023
|
log_path = Path(sys.argv[1])
|
|
916
1024
|
command_path = Path(sys.argv[2])
|
|
917
1025
|
pid_path = Path(sys.argv[3])
|
|
1026
|
+
job_name = sys.argv[4]
|
|
918
1027
|
command = command_path.read_text(encoding="utf-8").strip()
|
|
919
1028
|
if not command:
|
|
920
1029
|
raise SystemExit("empty service command")
|
|
921
1030
|
|
|
1031
|
+
job_handle = _create_job(job_name)
|
|
922
1032
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
923
1033
|
log_handle = log_path.open("w", encoding="utf-8", errors="replace")
|
|
924
1034
|
try:
|
|
@@ -928,21 +1038,26 @@ def main() -> int:
|
|
|
928
1038
|
stdout=log_handle,
|
|
929
1039
|
stderr=subprocess.STDOUT,
|
|
930
1040
|
stdin=subprocess.DEVNULL,
|
|
931
|
-
creationflags=(
|
|
932
|
-
_CREATE_NEW_PROCESS_GROUP
|
|
933
|
-
| _CREATE_NO_WINDOW
|
|
934
|
-
| _CREATE_BREAKAWAY_FROM_JOB
|
|
935
|
-
),
|
|
1041
|
+
creationflags=(_CREATE_NEW_PROCESS_GROUP | _CREATE_NO_WINDOW),
|
|
936
1042
|
close_fds=False,
|
|
937
1043
|
)
|
|
938
1044
|
finally:
|
|
939
1045
|
log_handle.close()
|
|
940
1046
|
|
|
1047
|
+
kernel32 = ctypes.windll.kernel32
|
|
1048
|
+
assigned = kernel32.AssignProcessToJobObject(
|
|
1049
|
+
ctypes.c_void_p(job_handle),
|
|
1050
|
+
ctypes.c_void_p(int(proc._handle)),
|
|
1051
|
+
)
|
|
1052
|
+
if not assigned:
|
|
1053
|
+
proc.terminate()
|
|
1054
|
+
kernel32.CloseHandle(ctypes.c_void_p(job_handle))
|
|
1055
|
+
raise OSError("AssignProcessToJobObject failed")
|
|
941
1056
|
pid_path.write_text(str(proc.pid), encoding="utf-8")
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1057
|
+
try:
|
|
1058
|
+
return int(proc.wait())
|
|
1059
|
+
finally:
|
|
1060
|
+
kernel32.CloseHandle(ctypes.c_void_p(job_handle))
|
|
946
1061
|
|
|
947
1062
|
|
|
948
1063
|
if __name__ == "__main__":
|
|
@@ -960,6 +1075,14 @@ def _launcher_pid_path(change_dir: Path) -> Path:
|
|
|
960
1075
|
return _runtime_dir(change_dir) / "_harness_service.launcher.pid"
|
|
961
1076
|
|
|
962
1077
|
|
|
1078
|
+
def _job_id_path(change_dir: Path) -> Path:
|
|
1079
|
+
return _runtime_dir(change_dir) / "_harness_service.job.id"
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _child_pid_path(change_dir: Path) -> Path:
|
|
1083
|
+
return _runtime_dir(change_dir) / "_harness_service.child.pid"
|
|
1084
|
+
|
|
1085
|
+
|
|
963
1086
|
def _cleanup_windows_launcher(change_dir: Path) -> None:
|
|
964
1087
|
if os.name != "nt":
|
|
965
1088
|
return
|
|
@@ -994,6 +1117,22 @@ def _wait_for_child_pid(pid_path: Path, *, timeout_sec: float = 15.0) -> int:
|
|
|
994
1117
|
raise TimeoutError(f"service child pid not recorded within {timeout_sec:.0f}s ({pid_path})")
|
|
995
1118
|
|
|
996
1119
|
|
|
1120
|
+
def _read_positive_int(path: Path) -> int | None:
|
|
1121
|
+
try:
|
|
1122
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
1123
|
+
except OSError:
|
|
1124
|
+
return None
|
|
1125
|
+
return int(text) if text.isdigit() and int(text) > 0 else None
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _read_optional_text(path: Path) -> str | None:
|
|
1129
|
+
try:
|
|
1130
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
1131
|
+
except OSError:
|
|
1132
|
+
return None
|
|
1133
|
+
return text or None
|
|
1134
|
+
|
|
1135
|
+
|
|
997
1136
|
def _start_detached_service_windows(
|
|
998
1137
|
command: str,
|
|
999
1138
|
*,
|
|
@@ -1004,8 +1143,10 @@ def _start_detached_service_windows(
|
|
|
1004
1143
|
runtime = _runtime_dir(change_dir)
|
|
1005
1144
|
launcher = _write_windows_launcher(change_dir)
|
|
1006
1145
|
command_path = runtime / "_harness_service.command.txt"
|
|
1007
|
-
pid_path =
|
|
1146
|
+
pid_path = _child_pid_path(change_dir)
|
|
1147
|
+
job_id = f"HunterHarnessService-{uuid.uuid4()}"
|
|
1008
1148
|
command_path.write_text(command, encoding="utf-8", newline="\n")
|
|
1149
|
+
_job_id_path(change_dir).write_text(job_id, encoding="utf-8", newline="\n")
|
|
1009
1150
|
if pid_path.exists():
|
|
1010
1151
|
pid_path.unlink()
|
|
1011
1152
|
|
|
@@ -1015,6 +1156,7 @@ def _start_detached_service_windows(
|
|
|
1015
1156
|
str(log_file),
|
|
1016
1157
|
str(command_path),
|
|
1017
1158
|
str(pid_path),
|
|
1159
|
+
job_id,
|
|
1018
1160
|
]
|
|
1019
1161
|
win_flags = (
|
|
1020
1162
|
_DETACHED_PROCESS
|
|
@@ -1032,13 +1174,15 @@ def _start_detached_service_windows(
|
|
|
1032
1174
|
close_fds=False,
|
|
1033
1175
|
)
|
|
1034
1176
|
_launcher_pid_path(change_dir).write_text(str(launcher_proc.pid), encoding="utf-8")
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1177
|
+
_wait_for_child_pid(pid_path)
|
|
1178
|
+
root_pid = int(launcher_proc.pid)
|
|
1179
|
+
# The launcher intentionally remains alive as the Job Object owner. Close
|
|
1180
|
+
# only this controller's process handle so Popen does not warn at teardown.
|
|
1181
|
+
launcher_handle = getattr(launcher_proc, "_handle", None)
|
|
1182
|
+
if launcher_handle is not None:
|
|
1183
|
+
launcher_handle.Close()
|
|
1184
|
+
launcher_proc.returncode = 0
|
|
1185
|
+
return root_pid
|
|
1042
1186
|
|
|
1043
1187
|
|
|
1044
1188
|
def start_detached_service(
|
|
@@ -1095,9 +1239,28 @@ def build_session(
|
|
|
1095
1239
|
execution_root: Path | None = None,
|
|
1096
1240
|
change_id: str | None = None,
|
|
1097
1241
|
attempt_id: str | None = None,
|
|
1242
|
+
service_pid: int | None = None,
|
|
1243
|
+
job_id: str | None = None,
|
|
1098
1244
|
) -> dict[str, Any]:
|
|
1099
1245
|
profile_name = service_start.get("profile") or "local-dev"
|
|
1100
1246
|
overlay = service_start.get("overlayPath") or ""
|
|
1247
|
+
effective_started_at = started_at or now_iso()
|
|
1248
|
+
executable = get_process_executable(pid) or (
|
|
1249
|
+
sys.executable if os.name == "nt" else os.environ.get("SHELL", "/bin/sh")
|
|
1250
|
+
)
|
|
1251
|
+
owned_ports: set[int] = set()
|
|
1252
|
+
port = extract_port(service_start)
|
|
1253
|
+
if port is not None:
|
|
1254
|
+
owned_ports.add(port)
|
|
1255
|
+
configured_ports = service_start.get("ownedPorts")
|
|
1256
|
+
if isinstance(configured_ports, list):
|
|
1257
|
+
for item in configured_ports:
|
|
1258
|
+
try:
|
|
1259
|
+
candidate = int(item)
|
|
1260
|
+
except (TypeError, ValueError):
|
|
1261
|
+
continue
|
|
1262
|
+
if 0 < candidate < 65536:
|
|
1263
|
+
owned_ports.add(candidate)
|
|
1101
1264
|
session = {
|
|
1102
1265
|
"pid": pid,
|
|
1103
1266
|
"startedBy": "AI",
|
|
@@ -1106,9 +1269,25 @@ def build_session(
|
|
|
1106
1269
|
"profile": profile_name,
|
|
1107
1270
|
"startCommandHash": sha256_text(command),
|
|
1108
1271
|
"overlayPath": overlay,
|
|
1109
|
-
"startedAt":
|
|
1272
|
+
"startedAt": effective_started_at,
|
|
1110
1273
|
"command": command,
|
|
1274
|
+
"ownedPorts": sorted(owned_ports),
|
|
1275
|
+
"processIdentity": {
|
|
1276
|
+
"executable": str(executable),
|
|
1277
|
+
"commandHash": sha256_text(command),
|
|
1278
|
+
"startedAt": effective_started_at,
|
|
1279
|
+
"parentChain": [
|
|
1280
|
+
{
|
|
1281
|
+
"pid": os.getpid(),
|
|
1282
|
+
"executable": str(Path(sys.executable).resolve()),
|
|
1283
|
+
}
|
|
1284
|
+
],
|
|
1285
|
+
},
|
|
1111
1286
|
}
|
|
1287
|
+
if isinstance(service_pid, int) and service_pid > 0:
|
|
1288
|
+
session["servicePid"] = service_pid
|
|
1289
|
+
if job_id:
|
|
1290
|
+
session["jobId"] = str(job_id)
|
|
1112
1291
|
if isinstance(service_start.get("leasedPort"), int):
|
|
1113
1292
|
session["leasedPort"] = service_start["leasedPort"]
|
|
1114
1293
|
session["leaseOwner"] = service_start.get("leaseOwner")
|
|
@@ -1155,6 +1334,57 @@ def needs_user_decision(
|
|
|
1155
1334
|
return 0
|
|
1156
1335
|
|
|
1157
1336
|
|
|
1337
|
+
def _confirm_service_shutdown(session: dict[str, Any]) -> dict[str, Any]:
|
|
1338
|
+
tracked_pids = sorted(
|
|
1339
|
+
{
|
|
1340
|
+
value
|
|
1341
|
+
for value in (session.get("pid"), session.get("servicePid"))
|
|
1342
|
+
if isinstance(value, int) and value > 0
|
|
1343
|
+
}
|
|
1344
|
+
)
|
|
1345
|
+
ports = sorted(
|
|
1346
|
+
{
|
|
1347
|
+
int(value)
|
|
1348
|
+
for value in (session.get("ownedPorts") or [])
|
|
1349
|
+
if isinstance(value, int) and 0 < value < 65536
|
|
1350
|
+
}
|
|
1351
|
+
)
|
|
1352
|
+
deadline = time.monotonic() + STOP_CONFIRM_TIMEOUT_SEC
|
|
1353
|
+
while True:
|
|
1354
|
+
alive_pids = [pid for pid in tracked_pids if is_pid_alive(pid)]
|
|
1355
|
+
occupied_ports = [port for port in ports if is_port_in_use(port)]
|
|
1356
|
+
if not alive_pids and not occupied_ports:
|
|
1357
|
+
return {
|
|
1358
|
+
"ok": True,
|
|
1359
|
+
"alivePids": [],
|
|
1360
|
+
"occupiedPorts": [],
|
|
1361
|
+
}
|
|
1362
|
+
if time.monotonic() >= deadline:
|
|
1363
|
+
return {
|
|
1364
|
+
"ok": False,
|
|
1365
|
+
"alivePids": alive_pids,
|
|
1366
|
+
"occupiedPorts": occupied_ports,
|
|
1367
|
+
}
|
|
1368
|
+
time.sleep(0.05)
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def _remove_windows_service_metadata(change_dir: Path) -> None:
|
|
1372
|
+
if os.name != "nt":
|
|
1373
|
+
return
|
|
1374
|
+
runtime = change_dir / "runtime"
|
|
1375
|
+
for name in (
|
|
1376
|
+
"_harness_service.launcher.pid",
|
|
1377
|
+
"_harness_service.child.pid",
|
|
1378
|
+
"_harness_service.job.id",
|
|
1379
|
+
"_harness_service.command.txt",
|
|
1380
|
+
"_harness_service_launcher.py",
|
|
1381
|
+
):
|
|
1382
|
+
try:
|
|
1383
|
+
(runtime / name).unlink(missing_ok=True)
|
|
1384
|
+
except OSError:
|
|
1385
|
+
pass
|
|
1386
|
+
|
|
1387
|
+
|
|
1158
1388
|
def stop_ai_session(
|
|
1159
1389
|
change_dir: Path,
|
|
1160
1390
|
session: dict[str, Any],
|
|
@@ -1179,14 +1409,40 @@ def stop_ai_session(
|
|
|
1179
1409
|
}
|
|
1180
1410
|
if isinstance(pid, int) and is_pid_alive(pid):
|
|
1181
1411
|
terminate_process_tree(pid)
|
|
1182
|
-
|
|
1412
|
+
confirmation = _confirm_service_shutdown(session)
|
|
1413
|
+
if confirmation["alivePids"]:
|
|
1414
|
+
return {
|
|
1415
|
+
"ok": False,
|
|
1416
|
+
"code": "PROCESS_TREE_EXIT_UNCONFIRMED",
|
|
1417
|
+
"action": "cleanup-unconfirmed",
|
|
1418
|
+
"pid": pid,
|
|
1419
|
+
"alivePids": confirmation["alivePids"],
|
|
1420
|
+
"occupiedPorts": confirmation["occupiedPorts"],
|
|
1421
|
+
"killed": True,
|
|
1422
|
+
"sessionCleared": False,
|
|
1423
|
+
}
|
|
1424
|
+
if confirmation["occupiedPorts"]:
|
|
1425
|
+
return {
|
|
1426
|
+
"ok": False,
|
|
1427
|
+
"code": "OWNED_PORT_RELEASE_UNCONFIRMED",
|
|
1428
|
+
"action": "cleanup-unconfirmed",
|
|
1429
|
+
"pid": pid,
|
|
1430
|
+
"alivePids": [],
|
|
1431
|
+
"occupiedPorts": confirmation["occupiedPorts"],
|
|
1432
|
+
"killed": True,
|
|
1433
|
+
"sessionCleared": False,
|
|
1434
|
+
}
|
|
1435
|
+
_remove_windows_service_metadata(change_dir)
|
|
1183
1436
|
clear_session(change_dir)
|
|
1184
1437
|
return {
|
|
1185
1438
|
"ok": True,
|
|
1439
|
+
"code": "SERVICE_STOP_CONFIRMED",
|
|
1186
1440
|
"action": "stopped",
|
|
1187
1441
|
"pid": pid,
|
|
1188
1442
|
"killed": True,
|
|
1189
1443
|
"sessionCleared": True,
|
|
1444
|
+
"alivePids": [],
|
|
1445
|
+
"occupiedPorts": [],
|
|
1190
1446
|
}
|
|
1191
1447
|
|
|
1192
1448
|
|
|
@@ -1313,6 +1569,9 @@ def cmd_ensure(args: argparse.Namespace) -> int:
|
|
|
1313
1569
|
if stop_result.get("action") == "needs-user-decision":
|
|
1314
1570
|
emit_json(stop_result, as_json=as_json)
|
|
1315
1571
|
return 0
|
|
1572
|
+
if not stop_result.get("ok"):
|
|
1573
|
+
emit_json(stop_result, as_json=as_json)
|
|
1574
|
+
return 1
|
|
1316
1575
|
|
|
1317
1576
|
return _start_and_record(
|
|
1318
1577
|
change_dir=change_dir,
|
|
@@ -1428,6 +1687,16 @@ def _start_and_record(
|
|
|
1428
1687
|
execution_root=execution_root,
|
|
1429
1688
|
change_id=change_id,
|
|
1430
1689
|
attempt_id=attempt_id,
|
|
1690
|
+
service_pid=(
|
|
1691
|
+
_read_positive_int(_child_pid_path(change_dir))
|
|
1692
|
+
if os.name == "nt"
|
|
1693
|
+
else pid
|
|
1694
|
+
),
|
|
1695
|
+
job_id=(
|
|
1696
|
+
_read_optional_text(_job_id_path(change_dir))
|
|
1697
|
+
if os.name == "nt"
|
|
1698
|
+
else None
|
|
1699
|
+
),
|
|
1431
1700
|
)
|
|
1432
1701
|
write_session(change_dir, session)
|
|
1433
1702
|
|
|
@@ -1583,7 +1852,7 @@ def cmd_stop(args: argparse.Namespace) -> int:
|
|
|
1583
1852
|
|
|
1584
1853
|
result = stop_ai_session(change_dir, session, require_identity=True)
|
|
1585
1854
|
emit_json(result, as_json=as_json)
|
|
1586
|
-
return 0
|
|
1855
|
+
return 0 if result.get("ok") else 1
|
|
1587
1856
|
|
|
1588
1857
|
|
|
1589
1858
|
def build_parser() -> argparse.ArgumentParser:
|