@hunter-harness/workflow-harness 0.2.55 → 0.2.56
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/README.md +3 -3
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -4
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +7 -5
- package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +2 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/claude-code/harness-submit/checklist.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 +6 -6
- package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +276 -67
- package/harness/bundles/general/claude-code/scripts/harness_context.py +165 -41
- package/harness/bundles/general/claude-code/scripts/harness_events.py +46 -4
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +188 -2
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +193 -11
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +3 -3
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -4
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +7 -5
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +6 -6
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +276 -67
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +165 -41
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +46 -4
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +188 -2
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +193 -11
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +3 -3
- package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-run/SKILL.md +7 -5
- package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +2 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +6 -6
- package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-test/reference.md +2 -2
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/codex/scripts/harness_archive.py +276 -67
- package/harness/bundles/general/codex/scripts/harness_context.py +165 -41
- package/harness/bundles/general/codex/scripts/harness_events.py +46 -4
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/codex/scripts/harness_gate.py +188 -2
- package/harness/bundles/general/codex/scripts/harness_ledger.py +193 -11
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +3 -3
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-run/SKILL.md +7 -5
- package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +2 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +6 -6
- package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/cursor/scripts/harness_archive.py +276 -67
- package/harness/bundles/general/cursor/scripts/harness_context.py +165 -41
- package/harness/bundles/general/cursor/scripts/harness_events.py +46 -4
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/cursor/scripts/harness_gate.py +188 -2
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +193 -11
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +3 -3
- package/harness/bundles/java/claude-code/contracts/workflow-policy.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 +21 -33
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -4
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +7 -5
- package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/claude-code/harness-submit/checklist.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 +6 -6
- package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +276 -67
- package/harness/bundles/java/claude-code/scripts/harness_context.py +165 -41
- package/harness/bundles/java/claude-code/scripts/harness_events.py +46 -4
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +188 -2
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +193 -11
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +3 -3
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -4
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +7 -5
- package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +6 -6
- package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +276 -67
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +165 -41
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +46 -4
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +188 -2
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +193 -11
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +3 -3
- package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-run/SKILL.md +7 -5
- package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-run/reference.md +2 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +6 -6
- package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-test/reference.md +1 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/codex/scripts/harness_archive.py +276 -67
- package/harness/bundles/java/codex/scripts/harness_context.py +165 -41
- package/harness/bundles/java/codex/scripts/harness_events.py +46 -4
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/codex/scripts/harness_gate.py +188 -2
- package/harness/bundles/java/codex/scripts/harness_ledger.py +193 -11
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +80 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +3 -3
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-run/SKILL.md +7 -5
- package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +6 -6
- package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/cursor/scripts/harness_archive.py +276 -67
- package/harness/bundles/java/cursor/scripts/harness_context.py +165 -41
- package/harness/bundles/java/cursor/scripts/harness_events.py +46 -4
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +108 -3
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/cursor/scripts/harness_gate.py +188 -2
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +193 -11
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +80 -0
- package/harness/manifests/general/claude-code.json +30 -30
- package/harness/manifests/general/codebuddy.json +30 -30
- package/harness/manifests/general/codex.json +30 -30
- package/harness/manifests/general/cursor.json +30 -30
- package/harness/manifests/java/claude-code.json +33 -33
- package/harness/manifests/java/codebuddy.json +33 -33
- package/harness/manifests/java/codex.json +33 -33
- package/harness/manifests/java/cursor.json +33 -33
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -23,6 +23,18 @@ PHASE_GRAPH = {
|
|
|
23
23
|
"submit": (),
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
WORKFLOW_PHASES = (
|
|
27
|
+
"plan",
|
|
28
|
+
"run",
|
|
29
|
+
"test",
|
|
30
|
+
"review",
|
|
31
|
+
"package",
|
|
32
|
+
"apidoc",
|
|
33
|
+
"submit",
|
|
34
|
+
"merge",
|
|
35
|
+
"archive",
|
|
36
|
+
)
|
|
37
|
+
|
|
26
38
|
DISPLAY_TITLE_MAX_LENGTH = 80
|
|
27
39
|
|
|
28
40
|
|
|
@@ -206,6 +218,107 @@ def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
|
|
|
206
218
|
return contract_root, contract, state_root
|
|
207
219
|
|
|
208
220
|
|
|
221
|
+
def _phase_plan(contract_root: Path) -> tuple[list[str] | None, str]:
|
|
222
|
+
policy_path = contract_root / "meta" / "gate-policy.json"
|
|
223
|
+
if not policy_path.is_file():
|
|
224
|
+
return None, "legacy"
|
|
225
|
+
try:
|
|
226
|
+
policy = _read_json(policy_path)
|
|
227
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
228
|
+
return None, "legacy"
|
|
229
|
+
for field, source in (
|
|
230
|
+
("plannedPhases", "change"),
|
|
231
|
+
("defaultPhases", "policy-default"),
|
|
232
|
+
):
|
|
233
|
+
raw = policy.get(field)
|
|
234
|
+
if not isinstance(raw, list):
|
|
235
|
+
continue
|
|
236
|
+
phases = [str(item).strip() for item in raw if str(item).strip()]
|
|
237
|
+
if (
|
|
238
|
+
phases
|
|
239
|
+
and len(phases) == len(set(phases))
|
|
240
|
+
and all(item in WORKFLOW_PHASES for item in phases)
|
|
241
|
+
):
|
|
242
|
+
return phases, source
|
|
243
|
+
return None, "legacy"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _allowed_next_phases(contract_root: Path, from_phase: str) -> list[str]:
|
|
247
|
+
planned, _source = _phase_plan(contract_root)
|
|
248
|
+
if planned is None:
|
|
249
|
+
return list(PHASE_GRAPH.get(from_phase, ()))
|
|
250
|
+
if from_phase not in planned:
|
|
251
|
+
return []
|
|
252
|
+
index = planned.index(from_phase)
|
|
253
|
+
allowed = planned[index + 1 : index + 2]
|
|
254
|
+
if from_phase in {"test", "review"} and "run" in planned:
|
|
255
|
+
allowed = [*allowed, "run"]
|
|
256
|
+
return list(dict.fromkeys(allowed))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def configure_phase_plan(
|
|
260
|
+
project: Path,
|
|
261
|
+
change: str,
|
|
262
|
+
*,
|
|
263
|
+
phases: list[str],
|
|
264
|
+
operator: str,
|
|
265
|
+
reason: str,
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Persist the single phase plan consumed by Context, Archive and Platform."""
|
|
268
|
+
project = Path(project).resolve()
|
|
269
|
+
try:
|
|
270
|
+
contract_root, _contract_data, _state_root = _contract(project, change)
|
|
271
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
272
|
+
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
273
|
+
normalized = [str(item).strip() for item in phases if str(item).strip()]
|
|
274
|
+
if (
|
|
275
|
+
not normalized
|
|
276
|
+
or len(normalized) != len(set(normalized))
|
|
277
|
+
or any(item not in WORKFLOW_PHASES for item in normalized)
|
|
278
|
+
or normalized[0] != "plan"
|
|
279
|
+
or normalized[-1] != "archive"
|
|
280
|
+
):
|
|
281
|
+
return {
|
|
282
|
+
"ok": False,
|
|
283
|
+
"code": "PHASE_PLAN_INVALID",
|
|
284
|
+
"message": "phase plan must be unique, start with plan, and end with archive",
|
|
285
|
+
"allowedPhases": list(WORKFLOW_PHASES),
|
|
286
|
+
}
|
|
287
|
+
if not operator.strip() or not reason.strip():
|
|
288
|
+
return {
|
|
289
|
+
"ok": False,
|
|
290
|
+
"code": "PHASE_PLAN_JUSTIFICATION_REQUIRED",
|
|
291
|
+
}
|
|
292
|
+
policy_path = contract_root / "meta" / "gate-policy.json"
|
|
293
|
+
policy = _read_json(policy_path) if policy_path.is_file() else {"schemaVersion": 1}
|
|
294
|
+
skipped = [
|
|
295
|
+
{
|
|
296
|
+
"phase": phase,
|
|
297
|
+
"reason": reason.strip(),
|
|
298
|
+
"operator": operator.strip(),
|
|
299
|
+
"decidedAt": _now().isoformat(),
|
|
300
|
+
}
|
|
301
|
+
for phase in WORKFLOW_PHASES
|
|
302
|
+
if phase not in normalized and phase != "merge"
|
|
303
|
+
]
|
|
304
|
+
policy["plannedPhases"] = normalized
|
|
305
|
+
policy["skippedPhases"] = skipped
|
|
306
|
+
policy["phasePlan"] = {
|
|
307
|
+
"source": "change-override",
|
|
308
|
+
"operator": operator.strip(),
|
|
309
|
+
"reason": reason.strip(),
|
|
310
|
+
"updatedAt": _now().isoformat(),
|
|
311
|
+
}
|
|
312
|
+
_write_json_atomic(policy_path, policy)
|
|
313
|
+
return {
|
|
314
|
+
"ok": True,
|
|
315
|
+
"code": "PHASE_PLAN_CONFIGURED",
|
|
316
|
+
"plannedPhases": normalized,
|
|
317
|
+
"skippedPhases": skipped,
|
|
318
|
+
"path": str(policy_path),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
209
322
|
def _active_changes(project: Path) -> list[str]:
|
|
210
323
|
root = project.resolve() / ".harness" / "changes"
|
|
211
324
|
if not root.is_dir():
|
|
@@ -319,6 +432,14 @@ def prepare_context(
|
|
|
319
432
|
and lifecycle.get("status") not in {None, "active"}
|
|
320
433
|
):
|
|
321
434
|
return {"ok": False, "code": "CHANGE_NOT_ACTIVE", "changeName": change}
|
|
435
|
+
planned_phases, phase_plan_source = _phase_plan(contract_root)
|
|
436
|
+
if planned_phases is not None and phase not in planned_phases:
|
|
437
|
+
return {
|
|
438
|
+
"ok": False,
|
|
439
|
+
"code": "PHASE_NOT_PLANNED",
|
|
440
|
+
"phase": phase,
|
|
441
|
+
"plannedPhases": planned_phases,
|
|
442
|
+
}
|
|
322
443
|
paths = _paths(state_root)
|
|
323
444
|
paths["runtime"].mkdir(parents=True, exist_ok=True)
|
|
324
445
|
recovery: dict[str, Any] | None = None
|
|
@@ -395,9 +516,13 @@ def prepare_context(
|
|
|
395
516
|
},
|
|
396
517
|
)
|
|
397
518
|
next_phases = (
|
|
398
|
-
|
|
399
|
-
if
|
|
400
|
-
else
|
|
519
|
+
_allowed_next_phases(contract_root, phase)
|
|
520
|
+
if planned_phases is not None
|
|
521
|
+
else (
|
|
522
|
+
list(PHASE_GRAPH.get(str(latest.get("toPhase")), ()))
|
|
523
|
+
if latest
|
|
524
|
+
else ["plan", "run"]
|
|
525
|
+
)
|
|
401
526
|
)
|
|
402
527
|
return {
|
|
403
528
|
"ok": True,
|
|
@@ -408,6 +533,8 @@ def prepare_context(
|
|
|
408
533
|
"contractRoot": str(contract_root),
|
|
409
534
|
"stateRoot": str(state_root),
|
|
410
535
|
"nextPhases": next_phases,
|
|
536
|
+
"plannedPhases": planned_phases,
|
|
537
|
+
"phasePlanSource": phase_plan_source,
|
|
411
538
|
"legacyBootstrap": not transitions,
|
|
412
539
|
"latestTransition": latest,
|
|
413
540
|
"lease": lease,
|
|
@@ -423,42 +550,19 @@ def _invalidate_for_fixback(
|
|
|
423
550
|
from_phase: str,
|
|
424
551
|
to_phase: str,
|
|
425
552
|
) -> dict[str, Any]:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
ledger = _read_json(ledger_path)
|
|
431
|
-
except (OSError, ValueError, json.JSONDecodeError):
|
|
432
|
-
ledger = {}
|
|
433
|
-
targets = ledger.get("verificationTargets")
|
|
434
|
-
if isinstance(targets, dict):
|
|
435
|
-
for target_id, target in targets.items():
|
|
436
|
-
if not isinstance(target, dict):
|
|
437
|
-
continue
|
|
438
|
-
target["reusable"] = False
|
|
439
|
-
target["invalidation"] = {
|
|
440
|
-
"code": "FIXBACK_INVALIDATED",
|
|
441
|
-
"transitionHash": transition_hash,
|
|
442
|
-
"fromPhase": from_phase,
|
|
443
|
-
"toPhase": to_phase,
|
|
444
|
-
"invalidatedAt": _now().isoformat(),
|
|
445
|
-
}
|
|
446
|
-
invalidated.append(str(target_id))
|
|
447
|
-
validations = ledger.get("validations")
|
|
448
|
-
if isinstance(validations, dict):
|
|
449
|
-
for entry in validations.values():
|
|
450
|
-
if isinstance(entry, dict):
|
|
451
|
-
entry["reusable"] = False
|
|
452
|
-
entry["invalidation"] = {
|
|
453
|
-
"code": "FIXBACK_INVALIDATED",
|
|
454
|
-
"transitionHash": transition_hash,
|
|
455
|
-
}
|
|
456
|
-
_write_json_atomic(ledger_path, ledger)
|
|
553
|
+
# A review→run handoff happens before product files are changed. Invalidating
|
|
554
|
+
# every target here made unrelated API/build evidence unusable. The Fixback
|
|
555
|
+
# resolver now invalidates only entries whose inputsFiles intersect the
|
|
556
|
+
# issue's actual changedFiles.
|
|
457
557
|
record = {
|
|
458
558
|
"schemaVersion": 1,
|
|
459
|
-
"code": "
|
|
559
|
+
"code": "FIXBACK_INVALIDATION_DEFERRED",
|
|
460
560
|
"transitionHash": transition_hash,
|
|
461
|
-
"
|
|
561
|
+
"fromPhase": from_phase,
|
|
562
|
+
"toPhase": to_phase,
|
|
563
|
+
"targetIds": [],
|
|
564
|
+
"deferred": True,
|
|
565
|
+
"reason": "changed-files-not-known",
|
|
462
566
|
"createdAt": _now().isoformat(),
|
|
463
567
|
}
|
|
464
568
|
_append_ndjson(
|
|
@@ -479,17 +583,22 @@ def close_transition(
|
|
|
479
583
|
status: str = "OK",
|
|
480
584
|
) -> dict[str, Any]:
|
|
481
585
|
project = Path(project).resolve()
|
|
482
|
-
|
|
586
|
+
try:
|
|
587
|
+
contract_root, _contract_data, state_root = _contract(project, change)
|
|
588
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
589
|
+
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
590
|
+
allowed_next = _allowed_next_phases(contract_root, from_phase)
|
|
591
|
+
if to_phase not in allowed_next:
|
|
592
|
+
planned_phases, source = _phase_plan(contract_root)
|
|
483
593
|
return {
|
|
484
594
|
"ok": False,
|
|
485
595
|
"code": "TRANSITION_ILLEGAL",
|
|
486
596
|
"fromPhase": from_phase,
|
|
487
597
|
"toPhase": to_phase,
|
|
598
|
+
"allowedNextPhases": allowed_next,
|
|
599
|
+
"plannedPhases": planned_phases,
|
|
600
|
+
"phasePlanSource": source,
|
|
488
601
|
}
|
|
489
|
-
try:
|
|
490
|
-
contract_root, _contract_data, state_root = _contract(project, change)
|
|
491
|
-
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
492
|
-
return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
|
|
493
602
|
paths = _paths(state_root)
|
|
494
603
|
|
|
495
604
|
artifact_entries: list[dict[str, Any]] = []
|
|
@@ -773,6 +882,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
773
882
|
begin.add_argument("--change", required=True)
|
|
774
883
|
begin.add_argument("--phase", required=True)
|
|
775
884
|
begin.add_argument("--executor", required=True)
|
|
885
|
+
configure = sub.add_parser("configure-plan")
|
|
886
|
+
configure.add_argument("--json", action="store_true")
|
|
887
|
+
configure.add_argument("--project", required=True, type=Path)
|
|
888
|
+
configure.add_argument("--change", required=True)
|
|
889
|
+
configure.add_argument("--phases", required=True)
|
|
890
|
+
configure.add_argument("--operator", required=True)
|
|
891
|
+
configure.add_argument("--reason", required=True)
|
|
776
892
|
view = sub.add_parser("view")
|
|
777
893
|
view.add_argument("--json", action="store_true")
|
|
778
894
|
view.add_argument("--project", required=True, type=Path)
|
|
@@ -808,6 +924,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
808
924
|
phase=args.phase,
|
|
809
925
|
executor=args.executor,
|
|
810
926
|
)
|
|
927
|
+
elif args.command == "configure-plan":
|
|
928
|
+
result = configure_phase_plan(
|
|
929
|
+
args.project,
|
|
930
|
+
args.change,
|
|
931
|
+
phases=[item.strip() for item in args.phases.split(",") if item.strip()],
|
|
932
|
+
operator=args.operator,
|
|
933
|
+
reason=args.reason,
|
|
934
|
+
)
|
|
811
935
|
else:
|
|
812
936
|
result = context_view(args.project, args.change)
|
|
813
937
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
@@ -71,6 +71,8 @@ EVENT_TYPES = frozenset(
|
|
|
71
71
|
"phase.start",
|
|
72
72
|
"phase.end",
|
|
73
73
|
"phase.auto_sealed",
|
|
74
|
+
"gate.blocked",
|
|
75
|
+
"gate.recovered",
|
|
74
76
|
"command",
|
|
75
77
|
"verification",
|
|
76
78
|
"artifact",
|
|
@@ -165,6 +167,10 @@ _EVENT_ALLOWED_FIELDS = {
|
|
|
165
167
|
)
|
|
166
168
|
| _PROVENANCE_FIELDS,
|
|
167
169
|
"phase.auto_sealed": frozenset({"status", "reason", "note"}) | _PROVENANCE_FIELDS,
|
|
170
|
+
"gate.blocked": frozenset({"status", "code", "reason", "note"})
|
|
171
|
+
| _PROVENANCE_FIELDS,
|
|
172
|
+
"gate.recovered": frozenset({"status", "code", "reason", "note"})
|
|
173
|
+
| _PROVENANCE_FIELDS,
|
|
168
174
|
"command": frozenset({"command", "exit_code", "duration_ms", "note"})
|
|
169
175
|
| _PROVENANCE_FIELDS,
|
|
170
176
|
"verification": frozenset(
|
|
@@ -1798,18 +1804,45 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1798
1804
|
"warning: verification missing --name or --status",
|
|
1799
1805
|
file=sys.stderr,
|
|
1800
1806
|
)
|
|
1801
|
-
line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
1802
1807
|
phase_closed = False
|
|
1803
1808
|
projection_error: str | None = None
|
|
1809
|
+
attempt_error: str | None = None
|
|
1804
1810
|
auto_sealed_events: list[dict[str, Any]] = []
|
|
1805
1811
|
try:
|
|
1806
1812
|
with event_file_lock(lock_path):
|
|
1807
1813
|
existing_events = (
|
|
1808
1814
|
load_events(path)
|
|
1809
1815
|
if args.type in {"phase.end", "correction", "phase.start"}
|
|
1816
|
+
or event.get("run_id") is not None
|
|
1817
|
+
or event.get("attempt") is not None
|
|
1810
1818
|
else []
|
|
1811
1819
|
)
|
|
1812
|
-
|
|
1820
|
+
run_id = event.get("run_id")
|
|
1821
|
+
if run_id is not None:
|
|
1822
|
+
run_attempts = {
|
|
1823
|
+
int(item["attempt"])
|
|
1824
|
+
for item in existing_events
|
|
1825
|
+
if item.get("phase") == event.get("phase")
|
|
1826
|
+
and item.get("run_id") == run_id
|
|
1827
|
+
and isinstance(item.get("attempt"), int)
|
|
1828
|
+
and int(item["attempt"]) > 0
|
|
1829
|
+
}
|
|
1830
|
+
if run_attempts:
|
|
1831
|
+
expected_attempt = max(run_attempts)
|
|
1832
|
+
explicit_attempt = event.get("attempt")
|
|
1833
|
+
if (
|
|
1834
|
+
isinstance(explicit_attempt, int)
|
|
1835
|
+
and explicit_attempt != expected_attempt
|
|
1836
|
+
):
|
|
1837
|
+
attempt_error = (
|
|
1838
|
+
"EVENT_ATTEMPT_CONFLICT: run_id is already bound to "
|
|
1839
|
+
f"attempt {expected_attempt}, received {explicit_attempt}"
|
|
1840
|
+
)
|
|
1841
|
+
else:
|
|
1842
|
+
event["attempt"] = expected_attempt
|
|
1843
|
+
if attempt_error is not None:
|
|
1844
|
+
pass
|
|
1845
|
+
elif args.type == "phase.end":
|
|
1813
1846
|
phase_closed = phase_end_already_recorded(existing_events, event)
|
|
1814
1847
|
elif args.type == "correction":
|
|
1815
1848
|
try:
|
|
@@ -1830,13 +1863,16 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1830
1863
|
phase=args.phase,
|
|
1831
1864
|
seal_reason=inferred_reason,
|
|
1832
1865
|
)
|
|
1833
|
-
if not phase_closed and projection_error is None:
|
|
1866
|
+
if not phase_closed and projection_error is None and attempt_error is None:
|
|
1834
1867
|
for seal_event in auto_sealed_events:
|
|
1835
1868
|
atomic_append_line(
|
|
1836
1869
|
path,
|
|
1837
1870
|
json.dumps(seal_event, ensure_ascii=False, separators=(",", ":")),
|
|
1838
1871
|
)
|
|
1839
|
-
atomic_append_line(
|
|
1872
|
+
atomic_append_line(
|
|
1873
|
+
path,
|
|
1874
|
+
json.dumps(event, ensure_ascii=False, separators=(",", ":")),
|
|
1875
|
+
)
|
|
1840
1876
|
except (OSError, TimeoutError, ValueError) as exc:
|
|
1841
1877
|
return emit_error(f"append failed: {exc}", as_json=as_json)
|
|
1842
1878
|
if phase_closed:
|
|
@@ -1852,6 +1888,12 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1852
1888
|
as_json=as_json,
|
|
1853
1889
|
error_code=error_code,
|
|
1854
1890
|
)
|
|
1891
|
+
if attempt_error is not None:
|
|
1892
|
+
return emit_error(
|
|
1893
|
+
attempt_error,
|
|
1894
|
+
as_json=as_json,
|
|
1895
|
+
error_code="EVENT_ATTEMPT_CONFLICT",
|
|
1896
|
+
)
|
|
1855
1897
|
|
|
1856
1898
|
_nudge_remote_sync(change_dir)
|
|
1857
1899
|
|
|
@@ -60,6 +60,12 @@ WHITELIST = {
|
|
|
60
60
|
"trigger",
|
|
61
61
|
"from_phase",
|
|
62
62
|
"result_status",
|
|
63
|
+
"planned_phases",
|
|
64
|
+
"skipped_phases",
|
|
65
|
+
"next_phase",
|
|
66
|
+
"phase_plan_source",
|
|
67
|
+
"closure_disposition",
|
|
68
|
+
"closure_reason",
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
MAX_BATCH_SIZE = 100
|
|
@@ -79,6 +85,14 @@ PHASE_LABELS = {
|
|
|
79
85
|
"archive": "归档",
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
REVIEW_REASON_SUMMARIES = {
|
|
89
|
+
"REVIEW_DELEGATED": "已使用独立评审,主会话负责核验结果。",
|
|
90
|
+
"REVIEW_INLINE_UNAVAILABLE": "当前环境没有可用的隔离评审能力,已由主会话完成评审。",
|
|
91
|
+
"REVIEW_INLINE_SPAWN_FAILED": "独立评审启动失败,已由主会话完成评审。",
|
|
92
|
+
"REVIEW_INLINE_INVALID_RESULT": "独立评审未返回有效结果,已由主会话重新完成评审。",
|
|
93
|
+
"INLINE_BY_ADAPTER": "当前适配器未提供隔离评审,已由主会话完成评审。",
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
|
|
83
97
|
def _yaml_scalar(text: str, key: str) -> str | None:
|
|
84
98
|
match = re.search(rf"(?m)^[ \t]*{re.escape(key)}:\s*['\"]?([^'\"\n#]+)", text)
|
|
@@ -143,8 +157,21 @@ def _readable_summary_text(value: Any) -> str:
|
|
|
143
157
|
|
|
144
158
|
|
|
145
159
|
def build_event_summary(event: dict[str, Any]) -> str:
|
|
160
|
+
reason_code = _summary_text(
|
|
161
|
+
event.get("fallback_reason_code") or event.get("decision_reason_code")
|
|
162
|
+
)
|
|
163
|
+
reason_summary = REVIEW_REASON_SUMMARIES.get(reason_code)
|
|
146
164
|
explicit = _readable_summary_text(event.get("summary"))
|
|
147
|
-
if
|
|
165
|
+
if reason_summary and (
|
|
166
|
+
not explicit
|
|
167
|
+
or explicit == reason_code
|
|
168
|
+
or re.search(r"REVIEW_[A-Z0-9_]+|INLINE_BY_ADAPTER", explicit)
|
|
169
|
+
):
|
|
170
|
+
return reason_summary
|
|
171
|
+
if explicit and not re.search(
|
|
172
|
+
r"\b[A-Z][A-Z0-9_]{3,}\b|\b[a-z][\w.-]+\s*=\s*(?:true|false)\b",
|
|
173
|
+
explicit,
|
|
174
|
+
):
|
|
148
175
|
return explicit
|
|
149
176
|
|
|
150
177
|
event_type = _summary_text(event.get("type"))
|
|
@@ -177,6 +204,10 @@ def build_event_summary(event: dict[str, Any]) -> str:
|
|
|
177
204
|
return _summary_text(
|
|
178
205
|
f"{phase_label}阶段已结束:{detail}" if detail else f"{phase_label}阶段已结束。"
|
|
179
206
|
)
|
|
207
|
+
if event_type == "gate.blocked":
|
|
208
|
+
return note or message or "阶段门禁暂未通过,现场已保留,可按提示恢复。"
|
|
209
|
+
if event_type == "gate.recovered":
|
|
210
|
+
return note or message or "阶段门禁已恢复,流程可以继续。"
|
|
180
211
|
if event_type == "command":
|
|
181
212
|
detail = name or note
|
|
182
213
|
return _summary_text(f"执行步骤:{detail}" if detail else "执行了一项工作步骤。")
|
|
@@ -205,6 +236,48 @@ def sanitize(event: dict[str, Any]) -> dict[str, Any]:
|
|
|
205
236
|
return payload
|
|
206
237
|
|
|
207
238
|
|
|
239
|
+
def _workflow_plan(change_dir: Path) -> dict[str, Any] | None:
|
|
240
|
+
policy_path = change_dir / "meta" / "gate-policy.json"
|
|
241
|
+
if not policy_path.is_file():
|
|
242
|
+
return None
|
|
243
|
+
try:
|
|
244
|
+
policy = json.loads(policy_path.read_text(encoding="utf-8-sig"))
|
|
245
|
+
except (OSError, json.JSONDecodeError):
|
|
246
|
+
return None
|
|
247
|
+
if not isinstance(policy, dict):
|
|
248
|
+
return None
|
|
249
|
+
raw = policy.get("plannedPhases")
|
|
250
|
+
source = "change"
|
|
251
|
+
if not isinstance(raw, list) or not raw:
|
|
252
|
+
raw = policy.get("defaultPhases")
|
|
253
|
+
source = "policy-default"
|
|
254
|
+
if not isinstance(raw, list):
|
|
255
|
+
return None
|
|
256
|
+
phases = [str(item).strip() for item in raw if str(item).strip()]
|
|
257
|
+
if not phases:
|
|
258
|
+
return None
|
|
259
|
+
skipped = policy.get("skippedPhases")
|
|
260
|
+
return {
|
|
261
|
+
"planned_phases": list(dict.fromkeys(phases)),
|
|
262
|
+
"skipped_phases": skipped if isinstance(skipped, list) else [],
|
|
263
|
+
"phase_plan_source": source,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _event_payload(event: dict[str, Any], phase_plan: dict[str, Any] | None) -> dict[str, Any]:
|
|
268
|
+
payload = sanitize(event)
|
|
269
|
+
if phase_plan is None:
|
|
270
|
+
return payload
|
|
271
|
+
payload.update(phase_plan)
|
|
272
|
+
if str(event.get("type") or "") in {"phase.end", "phase.auto_sealed"}:
|
|
273
|
+
phase = str(event.get("phase") or "").strip()
|
|
274
|
+
planned = phase_plan["planned_phases"]
|
|
275
|
+
if phase in planned:
|
|
276
|
+
index = planned.index(phase)
|
|
277
|
+
payload["next_phase"] = planned[index + 1] if index + 1 < len(planned) else None
|
|
278
|
+
return payload
|
|
279
|
+
|
|
280
|
+
|
|
208
281
|
def _valid_display_title(value: Any) -> str | None:
|
|
209
282
|
if not isinstance(value, str):
|
|
210
283
|
return None
|
|
@@ -469,6 +542,7 @@ def sync_change(
|
|
|
469
542
|
repair=True,
|
|
470
543
|
)
|
|
471
544
|
pending_raw = lines[acked:]
|
|
545
|
+
phase_plan = _workflow_plan(change_dir)
|
|
472
546
|
uploaded = 0
|
|
473
547
|
quarantined = 0
|
|
474
548
|
last_result: dict[str, Any] | None = None
|
|
@@ -500,7 +574,7 @@ def sync_change(
|
|
|
500
574
|
"producer_seq": line_number,
|
|
501
575
|
"event_type": str(event.get("type") or "unknown"),
|
|
502
576
|
"occurred_at": str(event.get("timestamp") or event.get("ts") or client_time),
|
|
503
|
-
"payload":
|
|
577
|
+
"payload": _event_payload(event, phase_plan),
|
|
504
578
|
}
|
|
505
579
|
if event.get("phase") is not None:
|
|
506
580
|
item["phase"] = event.get("phase")
|
|
@@ -727,6 +801,31 @@ class _CrossProcessLock:
|
|
|
727
801
|
self._stream = None
|
|
728
802
|
|
|
729
803
|
|
|
804
|
+
def _windowless_python_executable(
|
|
805
|
+
executable: str, base_executable: str | None
|
|
806
|
+
) -> str:
|
|
807
|
+
"""Return a Windows interpreter that cannot allocate a console window.
|
|
808
|
+
|
|
809
|
+
Virtual-environment ``python.exe`` files may be forwarding launchers. Even
|
|
810
|
+
when the first process is created with ``CREATE_NO_WINDOW``, such a launcher
|
|
811
|
+
can start the real console interpreter and make Windows Terminal flash. A
|
|
812
|
+
sibling ``pythonw.exe`` preserves the windowless subsystem across that hop.
|
|
813
|
+
"""
|
|
814
|
+
executable_path = Path(executable)
|
|
815
|
+
candidates = [executable_path.with_name("pythonw.exe")]
|
|
816
|
+
if base_executable:
|
|
817
|
+
base_path = Path(base_executable)
|
|
818
|
+
base_windowless = base_path.with_name("pythonw.exe")
|
|
819
|
+
if base_windowless not in candidates:
|
|
820
|
+
candidates.append(base_windowless)
|
|
821
|
+
for candidate in candidates:
|
|
822
|
+
if candidate.is_file():
|
|
823
|
+
return str(candidate)
|
|
824
|
+
if base_executable and Path(base_executable).is_file():
|
|
825
|
+
return str(Path(base_executable))
|
|
826
|
+
return str(executable_path)
|
|
827
|
+
|
|
828
|
+
|
|
730
829
|
def schedule_events_sync(project_root: Path, change_dir: Path) -> bool:
|
|
731
830
|
"""Wake a finite, project-local sync worker after an event is durable.
|
|
732
831
|
|
|
@@ -748,8 +847,14 @@ def schedule_events_sync(project_root: Path, change_dir: Path) -> bool:
|
|
|
748
847
|
temporary = nudge.with_name(f".{nudge.name}.{uuid.uuid4().hex}.tmp")
|
|
749
848
|
temporary.write_text(json.dumps(payload, separators=(",", ":")) + "\n", encoding="utf-8")
|
|
750
849
|
os.replace(temporary, nudge)
|
|
850
|
+
interpreter = sys.executable
|
|
851
|
+
if os.name == "nt":
|
|
852
|
+
interpreter = _windowless_python_executable(
|
|
853
|
+
sys.executable,
|
|
854
|
+
getattr(sys, "_base_executable", None),
|
|
855
|
+
)
|
|
751
856
|
command = [
|
|
752
|
-
|
|
857
|
+
interpreter,
|
|
753
858
|
str(Path(__file__).resolve()),
|
|
754
859
|
"--project",
|
|
755
860
|
str(project_root),
|