@opengsd/gsd-path 1.6.2 → 1.7.0
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/CHANGELOG.md +21 -0
- package/DOCS.md +27 -1
- package/FULL.md +2 -1
- package/HOOKS.md +38 -4
- package/QUICK.md +1 -1
- package/README.md +19 -11
- package/RUNTIME.md +5 -3
- package/WORKFLOW.md +9 -2
- package/package.json +1 -1
- package/scripts/_common.py +76 -2
- package/scripts/archive_milestone.py +129 -7
- package/scripts/build_recovery.py +25 -2
- package/scripts/build_state.py +333 -8
- package/scripts/check_handoffs.py +437 -30
- package/scripts/check_task_briefs.py +135 -11
- package/scripts/discussion_validate.py +46 -10
- package/scripts/dispatch_driver.py +28 -3
- package/scripts/git_guard.py +44 -4
- package/scripts/guard_hook.py +456 -9
- package/scripts/install.py +34 -4
- package/scripts/integration.py +196 -39
- package/scripts/isolation.py +996 -38
- package/scripts/lean_verification.py +28 -13
- package/scripts/loop_run.py +9 -4
- package/scripts/members.py +17 -3
- package/scripts/pipeline_diagnose.py +104 -1
- package/scripts/pipeline_state.py +10 -2
- package/scripts/runtime_store.py +88 -3
- package/scripts/state_checkpoint.py +15 -0
- package/scripts/status_runtime.py +73 -2
- package/scripts/workflow_run.py +10 -3
- package/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path/BUILD.md +3 -1
- package/skills/gsd-path/DEFINE.md +2 -1
- package/skills/gsd-path/PLAN.md +10 -0
- package/skills/gsd-path/SHIP.md +41 -8
- package/skills/gsd-path/SKILL.md +16 -1
- package/skills/gsd-path/references/build-native.md +20 -3
- package/skills/gsd-path/references/coder.md +6 -3
- package/skills/gsd-path/references/reviewer-final.md +10 -0
- package/skills/gsd-path/references/reviewer-wave.md +61 -22
- package/skills/gsd-path/references/reviewer.md +8 -6
- package/skills/gsd-path/references/runtime-selection.md +36 -0
- package/skills/gsd-path/references/spec-probes.md +19 -1
- package/skills/gsd-path/scripts/_common.py +76 -2
- package/skills/gsd-path/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path/scripts/build_recovery.py +25 -2
- package/skills/gsd-path/scripts/build_state.py +333 -8
- package/skills/gsd-path/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path/scripts/dispatch_driver.py +28 -3
- package/skills/gsd-path/scripts/git_guard.py +44 -4
- package/skills/gsd-path/scripts/integration.py +196 -39
- package/skills/gsd-path/scripts/isolation.py +996 -38
- package/skills/gsd-path/scripts/lean_verification.py +28 -13
- package/skills/gsd-path/scripts/loop_run.py +9 -4
- package/skills/gsd-path/scripts/members.py +17 -3
- package/skills/gsd-path/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path/scripts/status_runtime.py +73 -2
- package/skills/gsd-path/scripts/workflow_run.py +10 -3
- package/skills/gsd-path/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path/templates/intent.md +5 -3
- package/skills/gsd-path/templates/task.md +8 -1
- package/skills/gsd-path/templates/wave-review.md +24 -8
- package/skills/gsd-path-build/SKILL.md +3 -1
- package/skills/gsd-path-build/references/build-native.md +20 -3
- package/skills/gsd-path-build/references/coder.md +6 -3
- package/skills/gsd-path-build/references/reviewer-final.md +10 -0
- package/skills/gsd-path-build/references/reviewer-wave.md +61 -22
- package/skills/gsd-path-build/references/reviewer.md +8 -6
- package/skills/gsd-path-build/references/runtime-selection.md +36 -0
- package/skills/gsd-path-build/references/spec-probes.md +19 -1
- package/skills/gsd-path-build/scripts/_common.py +76 -2
- package/skills/gsd-path-build/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-build/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-build/scripts/build_state.py +333 -8
- package/skills/gsd-path-build/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-build/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-build/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-build/scripts/dispatch_driver.py +28 -3
- package/skills/gsd-path-build/scripts/integration.py +196 -39
- package/skills/gsd-path-build/scripts/isolation.py +996 -38
- package/skills/gsd-path-build/scripts/loop_run.py +9 -4
- package/skills/gsd-path-build/scripts/members.py +17 -3
- package/skills/gsd-path-build/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-build/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-build/scripts/workflow_run.py +10 -3
- package/skills/gsd-path-build/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-build/templates/task.md +8 -1
- package/skills/gsd-path-build/templates/wave-review.md +24 -8
- package/skills/gsd-path-decide/references/runtime-selection.md +36 -0
- package/skills/gsd-path-decide/scripts/_common.py +76 -2
- package/skills/gsd-path-decide/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-decide/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-decide/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-decide/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-decide/scripts/integration.py +196 -39
- package/skills/gsd-path-decide/scripts/isolation.py +996 -38
- package/skills/gsd-path-decide/scripts/loop_run.py +9 -4
- package/skills/gsd-path-decide/scripts/members.py +17 -3
- package/skills/gsd-path-decide/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-decide/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-define/SKILL.md +2 -1
- package/skills/gsd-path-define/references/runtime-selection.md +36 -0
- package/skills/gsd-path-define/references/spec-probes.md +19 -1
- package/skills/gsd-path-define/scripts/_common.py +76 -2
- package/skills/gsd-path-define/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-define/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-define/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-define/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-define/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-define/scripts/integration.py +196 -39
- package/skills/gsd-path-define/scripts/isolation.py +996 -38
- package/skills/gsd-path-define/scripts/loop_run.py +9 -4
- package/skills/gsd-path-define/scripts/members.py +17 -3
- package/skills/gsd-path-define/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-define/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-define/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-define/templates/intent.md +5 -3
- package/skills/gsd-path-discuss/references/runtime-selection.md +36 -0
- package/skills/gsd-path-discuss/scripts/_common.py +76 -2
- package/skills/gsd-path-discuss/scripts/loop_run.py +9 -4
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +36 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +76 -2
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-docs-audit/scripts/integration.py +196 -39
- package/skills/gsd-path-docs-audit/scripts/isolation.py +996 -38
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +9 -4
- package/skills/gsd-path-docs-audit/scripts/members.py +17 -3
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-forensics/references/runtime-selection.md +36 -0
- package/skills/gsd-path-forensics/scripts/_common.py +76 -2
- package/skills/gsd-path-forensics/scripts/loop_run.py +9 -4
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +104 -1
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-inspect/references/runtime-selection.md +36 -0
- package/skills/gsd-path-inspect/scripts/_common.py +76 -2
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-inspect/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-inspect/scripts/integration.py +196 -39
- package/skills/gsd-path-inspect/scripts/isolation.py +996 -38
- package/skills/gsd-path-inspect/scripts/loop_run.py +9 -4
- package/skills/gsd-path-inspect/scripts/members.py +17 -3
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-inspect/scripts/workflow_run.py +10 -3
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-loop/references/runtime-selection.md +36 -0
- package/skills/gsd-path-loop/scripts/_common.py +76 -2
- package/skills/gsd-path-loop/scripts/loop_run.py +9 -4
- package/skills/gsd-path-loop/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-migrate/references/runtime-selection.md +36 -0
- package/skills/gsd-path-migrate/scripts/_common.py +76 -2
- package/skills/gsd-path-migrate/scripts/loop_run.py +9 -4
- package/skills/gsd-path-plan/SKILL.md +10 -0
- package/skills/gsd-path-plan/references/runtime-selection.md +36 -0
- package/skills/gsd-path-plan/references/spec-probes.md +19 -1
- package/skills/gsd-path-plan/scripts/_common.py +76 -2
- package/skills/gsd-path-plan/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-plan/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-plan/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-plan/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-plan/scripts/integration.py +196 -39
- package/skills/gsd-path-plan/scripts/isolation.py +996 -38
- package/skills/gsd-path-plan/scripts/loop_run.py +9 -4
- package/skills/gsd-path-plan/scripts/members.py +17 -3
- package/skills/gsd-path-plan/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +10 -3
- package/skills/gsd-path-plan/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-plan/templates/task.md +8 -1
- package/skills/gsd-path-research/references/runtime-selection.md +36 -0
- package/skills/gsd-path-research/scripts/_common.py +76 -2
- package/skills/gsd-path-research/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-research/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-research/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-research/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-research/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-research/scripts/integration.py +196 -39
- package/skills/gsd-path-research/scripts/isolation.py +996 -38
- package/skills/gsd-path-research/scripts/loop_run.py +9 -4
- package/skills/gsd-path-research/scripts/members.py +17 -3
- package/skills/gsd-path-research/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-research/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-research/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-roadmap/references/runtime-selection.md +36 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +76 -2
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-roadmap/scripts/integration.py +196 -39
- package/skills/gsd-path-roadmap/scripts/isolation.py +996 -38
- package/skills/gsd-path-roadmap/scripts/loop_run.py +9 -4
- package/skills/gsd-path-roadmap/scripts/members.py +17 -3
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-ship/SKILL.md +41 -8
- package/skills/gsd-path-ship/references/reviewer-final.md +10 -0
- package/skills/gsd-path-ship/references/reviewer-wave.md +61 -22
- package/skills/gsd-path-ship/references/reviewer.md +8 -6
- package/skills/gsd-path-ship/references/runtime-selection.md +36 -0
- package/skills/gsd-path-ship/references/spec-probes.md +19 -1
- package/skills/gsd-path-ship/scripts/_common.py +76 -2
- package/skills/gsd-path-ship/scripts/archive_milestone.py +129 -7
- package/skills/gsd-path-ship/scripts/build_recovery.py +25 -2
- package/skills/gsd-path-ship/scripts/build_state.py +333 -8
- package/skills/gsd-path-ship/scripts/check_handoffs.py +437 -30
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +135 -11
- package/skills/gsd-path-ship/scripts/discussion_validate.py +46 -10
- package/skills/gsd-path-ship/scripts/integration.py +196 -39
- package/skills/gsd-path-ship/scripts/isolation.py +996 -38
- package/skills/gsd-path-ship/scripts/lean_verification.py +28 -13
- package/skills/gsd-path-ship/scripts/loop_run.py +9 -4
- package/skills/gsd-path-ship/scripts/members.py +17 -3
- package/skills/gsd-path-ship/scripts/pipeline_state.py +10 -2
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +15 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +10 -3
- package/skills/gsd-path-ship/scripts/worktree_paths.py +45 -0
- package/skills/gsd-path-ship/templates/wave-review.md +24 -8
- package/skills/gsd-path-undo/references/runtime-selection.md +36 -0
- package/skills/gsd-path-undo/scripts/_common.py +76 -2
- package/skills/gsd-path-undo/scripts/loop_run.py +9 -4
- package/skills/gsd-path-undo/scripts/worktree_paths.py +45 -0
- package/skills/path/BUILD.md +3 -1
- package/skills/path/DEFINE.md +2 -1
- package/skills/path/PLAN.md +10 -0
- package/skills/path/SHIP.md +41 -8
- package/skills/path/SKILL.md +16 -1
- package/skills/path/references/build-native.md +20 -3
- package/skills/path/references/coder.md +6 -3
- package/skills/path/references/reviewer-final.md +10 -0
- package/skills/path/references/reviewer-wave.md +61 -22
- package/skills/path/references/reviewer.md +8 -6
- package/skills/path/references/runtime-selection.md +36 -0
- package/skills/path/references/spec-probes.md +19 -1
- package/skills/path/scripts/_common.py +76 -2
- package/skills/path/scripts/archive_milestone.py +129 -7
- package/skills/path/scripts/build_recovery.py +25 -2
- package/skills/path/scripts/build_state.py +333 -8
- package/skills/path/scripts/check_handoffs.py +437 -30
- package/skills/path/scripts/check_task_briefs.py +135 -11
- package/skills/path/scripts/discussion_validate.py +46 -10
- package/skills/path/scripts/dispatch_driver.py +28 -3
- package/skills/path/scripts/git_guard.py +44 -4
- package/skills/path/scripts/integration.py +196 -39
- package/skills/path/scripts/isolation.py +996 -38
- package/skills/path/scripts/lean_verification.py +28 -13
- package/skills/path/scripts/loop_run.py +9 -4
- package/skills/path/scripts/members.py +17 -3
- package/skills/path/scripts/pipeline_state.py +10 -2
- package/skills/path/scripts/state_checkpoint.py +15 -0
- package/skills/path/scripts/status_runtime.py +73 -2
- package/skills/path/scripts/workflow_run.py +10 -3
- package/skills/path/scripts/worktree_paths.py +45 -0
- package/skills/path/templates/intent.md +5 -3
- package/skills/path/templates/task.md +8 -1
- package/skills/path/templates/wave-review.md +24 -8
|
@@ -530,10 +530,15 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
|
|
|
530
530
|
except subprocess.TimeoutExpired as drained:
|
|
531
531
|
stdout = drained.stdout or expired.stdout or b""
|
|
532
532
|
stderr = drained.stderr or expired.stderr or b""
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
533
|
+
# Windows communicate() reads in threads that hold each pipe's lock,
|
|
534
|
+
# so close() would block until an orphaned grandchild (bash forks
|
|
535
|
+
# commands it runs from a script file) exits; leave them to the
|
|
536
|
+
# daemon reader threads there.
|
|
537
|
+
if os.name != "nt":
|
|
538
|
+
if process.stdout:
|
|
539
|
+
process.stdout.close()
|
|
540
|
+
if process.stderr:
|
|
541
|
+
process.stderr.close()
|
|
537
542
|
process.kill()
|
|
538
543
|
process.wait(timeout=5)
|
|
539
544
|
output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
|
|
@@ -34,9 +34,17 @@ MEMBERS_FILE = "MEMBERS.md"
|
|
|
34
34
|
MARKER_SCHEMA = "gsd-path/member/v1"
|
|
35
35
|
MARKER_KEYS = {"schema", "coordinator", "project", "name"}
|
|
36
36
|
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
37
|
-
FIELDS = ("Checkout", "Remote", "Integration")
|
|
37
|
+
FIELDS = ("Checkout", "Remote", "Integration", "Prepare")
|
|
38
|
+
# The first three are required; Prepare (isolate provisioning, #463) and
|
|
39
|
+
# Default branch are optional.
|
|
40
|
+
REQUIRED_FIELDS = ("Checkout", "Remote", "Integration")
|
|
38
41
|
# Optional: `add` records it one time; a row without it means main.
|
|
39
42
|
DEFAULT_BRANCH_FIELD = "Default branch"
|
|
43
|
+
# Optional: a shell command run inside each fresh member isolate before any
|
|
44
|
+
# Verify (#463). It provisions what a bare worktree lacks — submodules from
|
|
45
|
+
# the primary checkout's object store, gitignored build inputs — and must
|
|
46
|
+
# copy, never link, outside the isolate (#462).
|
|
47
|
+
PREPARE_FIELD = "Prepare"
|
|
40
48
|
INTEGRATIONS = ("default", "direct", "pull-request")
|
|
41
49
|
GITHUB_REMOTE_RE = re.compile(
|
|
42
50
|
r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)([^/\s]+)/([^/\s]+)"
|
|
@@ -206,10 +214,15 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
|
|
|
206
214
|
return []
|
|
207
215
|
if path.is_symlink() or not path.is_file():
|
|
208
216
|
raise MembersError("MEMBERS.md must be a regular file")
|
|
217
|
+
return parse_members(path.read_text(encoding="utf-8"))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def parse_members(text: str) -> list[dict[str, str]]:
|
|
221
|
+
"""Parse MEMBERS.md text."""
|
|
209
222
|
members: list[dict[str, str]] = []
|
|
210
223
|
current: Optional[dict[str, str]] = None
|
|
211
224
|
preamble: list[str] = []
|
|
212
|
-
for number, line in enumerate(
|
|
225
|
+
for number, line in enumerate(text.splitlines(), 1):
|
|
213
226
|
if line.startswith("## "):
|
|
214
227
|
name = line[3:].strip()
|
|
215
228
|
if not NAME_RE.fullmatch(name):
|
|
@@ -241,7 +254,7 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
|
|
|
241
254
|
if len(set(names)) != len(names):
|
|
242
255
|
raise MembersError("MEMBERS.md repeats a member name")
|
|
243
256
|
for member in members:
|
|
244
|
-
missing = [field for field in
|
|
257
|
+
missing = [field for field in REQUIRED_FIELDS if not member.get(field.lower())]
|
|
245
258
|
if missing:
|
|
246
259
|
raise MembersError(f"MEMBERS.md member {member['name']} lacks {', '.join(missing)}")
|
|
247
260
|
if member["integration"] not in INTEGRATIONS:
|
|
@@ -510,6 +523,7 @@ def render(members: Sequence[dict[str, str]]) -> str:
|
|
|
510
523
|
f"Checkout: {member['checkout']}\n"
|
|
511
524
|
f"Remote: {member['remote']}\n"
|
|
512
525
|
f"Integration: {member['integration']}\n"
|
|
526
|
+
+ (f"{PREPARE_FIELD}: {member['prepare']}\n" if "prepare" in member else "")
|
|
513
527
|
# A row that never had the field keeps none, so old rows are not rewritten.
|
|
514
528
|
+ (f"{DEFAULT_BRANCH_FIELD}: {member['default_branch']}\n" if "default_branch" in member else "")
|
|
515
529
|
for member in members
|
|
@@ -1407,6 +1407,12 @@ def _route_state(repo: Path, project_dir: str = ".project") -> dict[str, object]
|
|
|
1407
1407
|
mode="milestone" if status == "done" else "normal",
|
|
1408
1408
|
reason=f"state is {phase}/{status}",
|
|
1409
1409
|
)
|
|
1410
|
+
if phase == "plan" and status == "blocked":
|
|
1411
|
+
return _route_result(
|
|
1412
|
+
state,
|
|
1413
|
+
"block",
|
|
1414
|
+
reason="plan is blocked; resolve the correction or user decision named by the plan gate before resuming",
|
|
1415
|
+
)
|
|
1410
1416
|
if phase == "plan" and status != "done":
|
|
1411
1417
|
return _route_result(state, "run-phase", phase="plan", reason=f"state is {phase}/{status}")
|
|
1412
1418
|
if phase == "plan":
|
|
@@ -2373,9 +2379,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
2373
2379
|
args.project_dir,
|
|
2374
2380
|
)
|
|
2375
2381
|
elif args.command == "approve" and (args.defer_checkpoint or args.patch):
|
|
2376
|
-
if args.expected_head is not None:
|
|
2382
|
+
if args.defer_checkpoint and args.expected_head is not None:
|
|
2377
2383
|
raise PipelineStateError(
|
|
2378
|
-
"--expected-head is not accepted with --defer-checkpoint
|
|
2384
|
+
"--expected-head is not accepted with --defer-checkpoint"
|
|
2379
2385
|
)
|
|
2380
2386
|
result = state_checkpoint.defer_approval(
|
|
2381
2387
|
args.repo,
|
|
@@ -2384,6 +2390,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
2384
2390
|
args.milestone,
|
|
2385
2391
|
args.patch,
|
|
2386
2392
|
)
|
|
2393
|
+
if args.expected_head is not None:
|
|
2394
|
+
result["expected_head_accepted"] = args.expected_head
|
|
2387
2395
|
elif args.command == "approve":
|
|
2388
2396
|
if args.expected_head is None:
|
|
2389
2397
|
raise PipelineStateError(
|
|
@@ -458,6 +458,21 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
|
|
|
458
458
|
if head.returncode != 0:
|
|
459
459
|
# Pre-Git approval defers base-dependent checks to build.
|
|
460
460
|
return
|
|
461
|
+
if project_dir == ".project":
|
|
462
|
+
# Unsupported .project entries must surface at approval, before build,
|
|
463
|
+
# where removing them is cheapest (#466). Like the checkpoint, judge
|
|
464
|
+
# each git-visible entry by whether it still exists in the worktree.
|
|
465
|
+
project = repo / ".project"
|
|
466
|
+
unsupported = sorted(
|
|
467
|
+
name
|
|
468
|
+
for name in pipeline_state._common.git_visible_entries(project) - PROJECT_ENTRIES
|
|
469
|
+
if (project / name).exists() or (project / name).is_symlink()
|
|
470
|
+
)
|
|
471
|
+
if unsupported:
|
|
472
|
+
raise PipelineStateError(
|
|
473
|
+
"unsupported .project artifacts: " + ", ".join(unsupported)
|
|
474
|
+
+ "; remove or relocate them before approving the plan"
|
|
475
|
+
)
|
|
461
476
|
if __package__:
|
|
462
477
|
from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
463
478
|
else:
|
|
@@ -7,11 +7,13 @@ import sys
|
|
|
7
7
|
# Runtime helpers must not modify their immutable installation.
|
|
8
8
|
sys.dont_write_bytecode = True
|
|
9
9
|
|
|
10
|
+
import contextlib
|
|
10
11
|
import hashlib
|
|
11
12
|
import json
|
|
12
13
|
import os
|
|
13
14
|
import tempfile
|
|
14
15
|
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
15
17
|
|
|
16
18
|
if __package__:
|
|
17
19
|
from . import _common
|
|
@@ -22,7 +24,39 @@ else:
|
|
|
22
24
|
from scripts import _common
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
# Run-scoped cache for read-only consumers (#469): recovery resolves a
|
|
28
|
+
# placement per task, and each resolution lists the whole worktree registry
|
|
29
|
+
# and re-reads the placement receipt. Within one read-only run none of that
|
|
30
|
+
# can change, so the listing and the derived workspace answer once. Flows
|
|
31
|
+
# that create, move, or retire worktrees never enter this scope.
|
|
32
|
+
_PLACEMENT_CACHE: Optional[dict] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextlib.contextmanager
|
|
36
|
+
def readonly_placement_cache():
|
|
37
|
+
global _PLACEMENT_CACHE
|
|
38
|
+
if _PLACEMENT_CACHE is not None: # already inside a read-only run
|
|
39
|
+
yield
|
|
40
|
+
return
|
|
41
|
+
_PLACEMENT_CACHE = {"porcelain": {}, "workspace": {}}
|
|
42
|
+
try:
|
|
43
|
+
yield
|
|
44
|
+
finally:
|
|
45
|
+
_PLACEMENT_CACHE = None
|
|
46
|
+
|
|
47
|
+
|
|
25
48
|
def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
|
|
49
|
+
if _PLACEMENT_CACHE is not None:
|
|
50
|
+
key = str(primary)
|
|
51
|
+
if key in _PLACEMENT_CACHE["porcelain"]:
|
|
52
|
+
return _PLACEMENT_CACHE["porcelain"][key] # type: ignore[index]
|
|
53
|
+
output = _worktree_list_porcelain_uncached(primary)
|
|
54
|
+
_PLACEMENT_CACHE["porcelain"][key] = output
|
|
55
|
+
return output
|
|
56
|
+
return _worktree_list_porcelain_uncached(primary)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _worktree_list_porcelain_uncached(primary: Path) -> tuple[str, bool]:
|
|
26
60
|
try:
|
|
27
61
|
if __package__:
|
|
28
62
|
from . import pipeline_git as pg
|
|
@@ -58,6 +92,17 @@ def _identity(path: Path) -> str:
|
|
|
58
92
|
|
|
59
93
|
|
|
60
94
|
def _workspace(primary: Path, pin: bool) -> Path:
|
|
95
|
+
if _PLACEMENT_CACHE is not None and not pin:
|
|
96
|
+
key = str(primary)
|
|
97
|
+
if key in _PLACEMENT_CACHE["workspace"]:
|
|
98
|
+
return _PLACEMENT_CACHE["workspace"][key] # type: ignore[index]
|
|
99
|
+
workspace = _workspace_uncached(primary, False)
|
|
100
|
+
_PLACEMENT_CACHE["workspace"][key] = workspace
|
|
101
|
+
return workspace
|
|
102
|
+
return _workspace_uncached(primary, pin)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _workspace_uncached(primary: Path, pin: bool) -> Path:
|
|
61
106
|
common = Path(_git(primary, "rev-parse", "--git-common-dir"))
|
|
62
107
|
common = (primary / common).resolve()
|
|
63
108
|
identity = _identity(primary)
|
|
@@ -20,6 +20,42 @@ references to bundled copies of those helpers. Other skill helpers, templates,
|
|
|
20
20
|
and role briefs remain in the installed skill bundle. Pass `-B` to Python helper
|
|
21
21
|
invocations so execution does not create bytecode in the runtime store.
|
|
22
22
|
|
|
23
|
+
When the runtime enforcing an active task cannot finish it, one narrowly
|
|
24
|
+
authorized recovery exists:
|
|
25
|
+
`python3 -B <absolute isolation.py> runtime-hotfix --repo <absolute primary>
|
|
26
|
+
--task-id <id> --runtime <64-hex runtime digest printed by runtime_store.py publish>
|
|
27
|
+
--authorized-by <the owner ruling>` validates the replacement runtime,
|
|
28
|
+
re-runs that task's Verify in a fresh sidecar at the recorded base under the
|
|
29
|
+
replacement pin, and records the receipt twice: a Log line in the task file
|
|
30
|
+
naming the task, base, and both runtime digests, and an identical blob behind
|
|
31
|
+
the local ref `refs/gsd-path/runtime-hotfix/<task>-<base>`. Only a Log line
|
|
32
|
+
whose exact twin that ref holds later admits `.gsd-path/runtime.json` into that
|
|
33
|
+
task's landing, and only when the pin names exactly the receipt's runtime; any
|
|
34
|
+
other task, base, a Log line alone, or a bare mention authorizes nothing, and a
|
|
35
|
+
failing re-run refuses the hotfix. Limits: the command serves only a serial
|
|
36
|
+
task working in the primary and refuses parallel or member tasks; the ref is
|
|
37
|
+
local repository state, so a clone without it cannot land the pin and the
|
|
38
|
+
proof fails closed. The guard applies two rules to agent shell commands. It
|
|
39
|
+
denies any command where `runtime-hotfix` directly follows `isolation.py`, stdin
|
|
40
|
+
`-`, or an `isolation` module operand (`-m isolation`, `-Bm isolation`,
|
|
41
|
+
`-misolation`). It also denies every Git command with a word, including global
|
|
42
|
+
`-c` options and inline variable assignments, that contains `refs/gsd-path/` or
|
|
43
|
+
`runtime-hotfix`. This deliberately blocks read-only uses such as
|
|
44
|
+
`git log refs/gsd-path/<ref>` or `git log --grep runtime-hotfix`, and commit
|
|
45
|
+
messages or branch names that contain `runtime-hotfix`. Limits: it cannot see a copied or renamed
|
|
46
|
+
`isolation.py`, a refspec stored in a Git config file, a ref write made from
|
|
47
|
+
inside another program, a direct edit of files under `.git/`, or shell
|
|
48
|
+
expansion that splits both protected words, such as
|
|
49
|
+
`refs/gs{d,}-path/task-authorizations/<task>`.
|
|
50
|
+
|
|
51
|
+
Publishing a runtime to the shared store is project-free:
|
|
52
|
+
`python3 -B <absolute runtime_store.py> publish --source <absolute package root>`
|
|
53
|
+
verifies and publishes the content-addressed runtime, printing its version,
|
|
54
|
+
digest, and store path as JSON; `--provenance` records a note in that output
|
|
55
|
+
only. Publication alone neither authorizes execution nor changes a project's
|
|
56
|
+
trusted runtime — a project runs the bytes only when its own pin names that
|
|
57
|
+
digest.
|
|
58
|
+
|
|
23
59
|
The selected runtime is authoritative for each helper in its `manifest.json`;
|
|
24
60
|
this includes `review_findings.py` and `task_context.py`. A selected runtime
|
|
25
61
|
that was published before a helper became a runtime file does not list that
|
|
@@ -14,6 +14,7 @@ import sys
|
|
|
14
14
|
# Runtime helpers must not modify their immutable installation.
|
|
15
15
|
sys.dont_write_bytecode = True
|
|
16
16
|
|
|
17
|
+
import atexit
|
|
17
18
|
import contextlib
|
|
18
19
|
import errno
|
|
19
20
|
import json
|
|
@@ -23,9 +24,10 @@ import shlex
|
|
|
23
24
|
import shutil
|
|
24
25
|
import signal
|
|
25
26
|
import subprocess
|
|
27
|
+
import tempfile
|
|
26
28
|
import time
|
|
27
29
|
from pathlib import Path
|
|
28
|
-
from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Tuple
|
|
30
|
+
from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Set, Tuple
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
# Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
|
|
@@ -65,6 +67,23 @@ VERIFY_LEDGER_PATH = ".project/build/verify-ledger.jsonl"
|
|
|
65
67
|
VERIFY_RESULTS = ("pass", "fail")
|
|
66
68
|
VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
|
|
67
69
|
VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
|
|
70
|
+
# The owner citation a withdrawn task tombstone must carry in its Log (#483).
|
|
71
|
+
# One definition shared by every gate that reads the representation.
|
|
72
|
+
WITHDRAWN_CITATION = re.compile(
|
|
73
|
+
r"^- Withdrawn — (?P<date>\d{4}-\d{2}-\d{2}): (?P<ruling>.+)$", re.MULTILINE)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def withdrawn_citations(task_text: str) -> list[str]:
|
|
77
|
+
"""The owner citations in a withdrawn task's Log, as `date: ruling`.
|
|
78
|
+
|
|
79
|
+
The Log is the append-only record of rulings; a Withdrawn line in any
|
|
80
|
+
other section is not the citation the gates accept (#483).
|
|
81
|
+
"""
|
|
82
|
+
body = section_body(task_text, "Log")
|
|
83
|
+
if body is None:
|
|
84
|
+
return []
|
|
85
|
+
return [f"{m.group('date')}: {m.group('ruling')}"
|
|
86
|
+
for m in WITHDRAWN_CITATION.finditer(body)]
|
|
68
87
|
|
|
69
88
|
REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
|
|
70
89
|
REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
|
|
@@ -197,6 +216,19 @@ def section_body(text: str, heading: str) -> Optional[str]:
|
|
|
197
216
|
return match.group("body") if match else None
|
|
198
217
|
|
|
199
218
|
|
|
219
|
+
def contract_shapes(body: str) -> Set[str]:
|
|
220
|
+
"""The whitespace-normalized `- ` entries of an Interface contract body.
|
|
221
|
+
|
|
222
|
+
HTML comments are dropped and continuation lines join their entry. A
|
|
223
|
+
body that says only `None` has no shapes.
|
|
224
|
+
"""
|
|
225
|
+
cleaned = re.sub(r"(?s)<!--.*?-->", "", body).strip()
|
|
226
|
+
if " ".join(cleaned.split()) in {"", "None", "- None"}:
|
|
227
|
+
return set()
|
|
228
|
+
entries = re.split(r"(?m)^-[ \t]+", cleaned)
|
|
229
|
+
return {shape for shape in (" ".join(entry.split()) for entry in entries) if shape}
|
|
230
|
+
|
|
231
|
+
|
|
200
232
|
def overlapping_paths(left: Sequence[str], right: Sequence[str]) -> List[str]:
|
|
201
233
|
"""Return the sorted paths that two task `files` lists both cover.
|
|
202
234
|
|
|
@@ -875,8 +907,50 @@ def find_bash() -> str:
|
|
|
875
907
|
)
|
|
876
908
|
|
|
877
909
|
|
|
910
|
+
def _remove_quietly(path: str) -> None:
|
|
911
|
+
try:
|
|
912
|
+
os.remove(path)
|
|
913
|
+
except OSError:
|
|
914
|
+
pass
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _command_script_dir() -> str:
|
|
918
|
+
"""A temp directory for command scripts that cannot dirty the running tree.
|
|
919
|
+
|
|
920
|
+
TMPDIR/TEMP/TMP may point into the tree being verified, and a script file
|
|
921
|
+
there shows up in git status and blocks landings (#448), so fall back to a
|
|
922
|
+
system directory when the environment temp sits inside the current tree.
|
|
923
|
+
"""
|
|
924
|
+
candidate = os.path.realpath(tempfile.gettempdir())
|
|
925
|
+
tree = os.path.realpath(os.getcwd())
|
|
926
|
+
if candidate != tree and not candidate.startswith(tree + os.sep):
|
|
927
|
+
return candidate
|
|
928
|
+
for fallback in (
|
|
929
|
+
os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "Temp"),
|
|
930
|
+
"/var/tmp",
|
|
931
|
+
"/tmp",
|
|
932
|
+
):
|
|
933
|
+
resolved = os.path.realpath(fallback)
|
|
934
|
+
if os.path.isdir(resolved) and resolved != tree and not resolved.startswith(tree + os.sep):
|
|
935
|
+
return resolved
|
|
936
|
+
raise OSError("no temp directory outside the current worktree for a command script")
|
|
937
|
+
|
|
938
|
+
|
|
878
939
|
def bash_argv(command: str) -> List[str]:
|
|
879
|
-
|
|
940
|
+
if os.name != "nt":
|
|
941
|
+
return [find_bash(), "-c", command]
|
|
942
|
+
# Git for Windows' bash (MSYS2) cuts an argument that carries a double
|
|
943
|
+
# quote near 8 KB before bash sees it, so a long Verify dies mid-script
|
|
944
|
+
# or silently passes when the cut lands in comments (#448). Run the exact
|
|
945
|
+
# bytes from a temp file outside every worktree instead.
|
|
946
|
+
bash = find_bash()
|
|
947
|
+
descriptor, path = tempfile.mkstemp(
|
|
948
|
+
prefix="gsd-path-command-", suffix=".sh", dir=_command_script_dir()
|
|
949
|
+
)
|
|
950
|
+
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
|
|
951
|
+
handle.write(command)
|
|
952
|
+
atexit.register(_remove_quietly, path)
|
|
953
|
+
return [bash, path]
|
|
880
954
|
|
|
881
955
|
|
|
882
956
|
# cmd.exe re-parses the arguments of a .cmd or .bat target, so these would
|
|
@@ -530,10 +530,15 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
|
|
|
530
530
|
except subprocess.TimeoutExpired as drained:
|
|
531
531
|
stdout = drained.stdout or expired.stdout or b""
|
|
532
532
|
stderr = drained.stderr or expired.stderr or b""
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
533
|
+
# Windows communicate() reads in threads that hold each pipe's lock,
|
|
534
|
+
# so close() would block until an orphaned grandchild (bash forks
|
|
535
|
+
# commands it runs from a script file) exits; leave them to the
|
|
536
|
+
# daemon reader threads there.
|
|
537
|
+
if os.name != "nt":
|
|
538
|
+
if process.stdout:
|
|
539
|
+
process.stdout.close()
|
|
540
|
+
if process.stderr:
|
|
541
|
+
process.stderr.close()
|
|
537
542
|
process.kill()
|
|
538
543
|
process.wait(timeout=5)
|
|
539
544
|
output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
|
|
@@ -24,16 +24,20 @@ from typing import Callable, Optional, Sequence
|
|
|
24
24
|
def _load_diagnose_modules():
|
|
25
25
|
if __package__:
|
|
26
26
|
from . import archive_milestone
|
|
27
|
+
from . import build_state
|
|
27
28
|
from . import detect_project
|
|
28
29
|
from . import discussion_records
|
|
30
|
+
from . import discussion_validate
|
|
29
31
|
from . import integration
|
|
30
32
|
from . import isolation
|
|
31
33
|
from . import pipeline_git
|
|
32
34
|
from . import pipeline_state
|
|
33
35
|
return (
|
|
34
36
|
archive_milestone,
|
|
37
|
+
build_state,
|
|
35
38
|
detect_project,
|
|
36
39
|
discussion_records,
|
|
40
|
+
discussion_validate,
|
|
37
41
|
integration,
|
|
38
42
|
isolation,
|
|
39
43
|
pipeline_git,
|
|
@@ -42,16 +46,20 @@ def _load_diagnose_modules():
|
|
|
42
46
|
else:
|
|
43
47
|
try:
|
|
44
48
|
import archive_milestone
|
|
49
|
+
import build_state
|
|
45
50
|
import detect_project
|
|
46
51
|
import discussion_records
|
|
52
|
+
import discussion_validate
|
|
47
53
|
import integration
|
|
48
54
|
import isolation
|
|
49
55
|
import pipeline_git
|
|
50
56
|
import pipeline_state
|
|
51
57
|
return (
|
|
52
58
|
archive_milestone,
|
|
59
|
+
build_state,
|
|
53
60
|
detect_project,
|
|
54
61
|
discussion_records,
|
|
62
|
+
discussion_validate,
|
|
55
63
|
integration,
|
|
56
64
|
isolation,
|
|
57
65
|
pipeline_git,
|
|
@@ -60,8 +68,10 @@ def _load_diagnose_modules():
|
|
|
60
68
|
except ModuleNotFoundError as error:
|
|
61
69
|
if error.name not in {
|
|
62
70
|
"archive_milestone",
|
|
71
|
+
"build_state",
|
|
63
72
|
"detect_project",
|
|
64
73
|
"discussion_records",
|
|
74
|
+
"discussion_validate",
|
|
65
75
|
"integration",
|
|
66
76
|
"isolation",
|
|
67
77
|
"pipeline_git",
|
|
@@ -71,8 +81,10 @@ def _load_diagnose_modules():
|
|
|
71
81
|
try:
|
|
72
82
|
from scripts import (
|
|
73
83
|
archive_milestone,
|
|
84
|
+
build_state,
|
|
74
85
|
detect_project,
|
|
75
86
|
discussion_records,
|
|
87
|
+
discussion_validate,
|
|
76
88
|
integration,
|
|
77
89
|
isolation,
|
|
78
90
|
pipeline_git,
|
|
@@ -80,8 +92,10 @@ def _load_diagnose_modules():
|
|
|
80
92
|
)
|
|
81
93
|
return (
|
|
82
94
|
archive_milestone,
|
|
95
|
+
build_state,
|
|
83
96
|
detect_project,
|
|
84
97
|
discussion_records,
|
|
98
|
+
discussion_validate,
|
|
85
99
|
integration,
|
|
86
100
|
isolation,
|
|
87
101
|
pipeline_git,
|
|
@@ -91,8 +105,10 @@ def _load_diagnose_modules():
|
|
|
91
105
|
if error.name not in {
|
|
92
106
|
"scripts",
|
|
93
107
|
"scripts.archive_milestone",
|
|
108
|
+
"scripts.build_state",
|
|
94
109
|
"scripts.detect_project",
|
|
95
110
|
"scripts.discussion_records",
|
|
111
|
+
"scripts.discussion_validate",
|
|
96
112
|
"scripts.integration",
|
|
97
113
|
"scripts.isolation",
|
|
98
114
|
"scripts.pipeline_git",
|
|
@@ -109,16 +125,20 @@ def _load_diagnose_modules():
|
|
|
109
125
|
shared = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
|
|
110
126
|
sys.path.insert(0, str(shared))
|
|
111
127
|
import archive_milestone
|
|
128
|
+
import build_state
|
|
112
129
|
import detect_project
|
|
113
130
|
import discussion_records
|
|
131
|
+
import discussion_validate
|
|
114
132
|
import integration
|
|
115
133
|
import isolation
|
|
116
134
|
import pipeline_git
|
|
117
135
|
import pipeline_state
|
|
118
136
|
return (
|
|
119
137
|
archive_milestone,
|
|
138
|
+
build_state,
|
|
120
139
|
detect_project,
|
|
121
140
|
discussion_records,
|
|
141
|
+
discussion_validate,
|
|
122
142
|
integration,
|
|
123
143
|
isolation,
|
|
124
144
|
pipeline_git,
|
|
@@ -128,8 +148,10 @@ def _load_diagnose_modules():
|
|
|
128
148
|
|
|
129
149
|
(
|
|
130
150
|
archive_milestone,
|
|
151
|
+
build_state,
|
|
131
152
|
detect_project,
|
|
132
153
|
discussion_records,
|
|
154
|
+
discussion_validate,
|
|
133
155
|
integration,
|
|
134
156
|
isolation,
|
|
135
157
|
pipeline_git,
|
|
@@ -137,6 +159,9 @@ def _load_diagnose_modules():
|
|
|
137
159
|
) = _load_diagnose_modules()
|
|
138
160
|
DetectError = detect_project.DetectError
|
|
139
161
|
classify = detect_project.classify
|
|
162
|
+
BuildStateError = build_state.BuildStateError
|
|
163
|
+
build_ready = build_state.ready
|
|
164
|
+
review_cycle_counts = discussion_validate.review_cycle_counts
|
|
140
165
|
IsolationError = isolation.IsolationError
|
|
141
166
|
_registered_worktrees = isolation._registered_worktrees
|
|
142
167
|
_worktree_report = isolation._worktree_report
|
|
@@ -231,7 +256,15 @@ def _state_template() -> Path:
|
|
|
231
256
|
|
|
232
257
|
|
|
233
258
|
def _command(module: object, *arguments: object) -> str:
|
|
234
|
-
|
|
259
|
+
return _script_command(Path(str(getattr(module, "__file__"))), *arguments)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _dispatch_command(*arguments: object) -> str:
|
|
263
|
+
return _script_command(Path(str(build_state.__file__)).with_name("dispatch_driver.py"), *arguments)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _script_command(path: Path, *arguments: object) -> str:
|
|
267
|
+
script = path.resolve()
|
|
235
268
|
def text(value: object) -> str:
|
|
236
269
|
# Agents paste this into Git Bash on Windows, where unquoted
|
|
237
270
|
# backslashes are escapes; forward slashes need no quoting.
|
|
@@ -354,6 +387,74 @@ def _leftover_worktrees(repo: Path) -> list[dict[str, object]]:
|
|
|
354
387
|
return leftovers
|
|
355
388
|
|
|
356
389
|
|
|
390
|
+
def _build_completion_findings(repo: Path) -> list[dict[str, str]]:
|
|
391
|
+
"""Mirror dispatch complete's build gates so a completion refusal is diagnosable.
|
|
392
|
+
|
|
393
|
+
Runs the same checks `dispatch_driver.py complete` runs, under the same
|
|
394
|
+
precondition (every wave's tasks done, no blocked review holding them), and
|
|
395
|
+
names the review set or typed build state that complete would refuse on.
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
ready_result = build_ready(str(repo), allow_done=True)
|
|
399
|
+
except BuildStateError as error:
|
|
400
|
+
return [
|
|
401
|
+
_finding(
|
|
402
|
+
"review-cycles",
|
|
403
|
+
"stuck",
|
|
404
|
+
f"{error.code}: {error}",
|
|
405
|
+
_dispatch_command("complete", "--repo", repo)
|
|
406
|
+
+ " # reports the same typed blocker; follow its recovery (reconcile "
|
|
407
|
+
"failed or blocked tasks, resolve a dependency deadlock before new dispatch)",
|
|
408
|
+
)
|
|
409
|
+
]
|
|
410
|
+
blocked = ready_result.get("blocked_review")
|
|
411
|
+
if blocked:
|
|
412
|
+
wave, cycle = blocked["wave"], blocked["cycle"]
|
|
413
|
+
waiting = ", ".join(
|
|
414
|
+
str(item) for item in blocked.get("waiting_waves") or []
|
|
415
|
+
)
|
|
416
|
+
if any((repo / ".project" / "tasks").glob(f"*-fix-wave-{wave}-cycle-{cycle}.md")):
|
|
417
|
+
retry = (
|
|
418
|
+
_dispatch_command("review", "--repo", repo, "--wave", wave, "--cycle", cycle + 1)
|
|
419
|
+
+ " # add the owner's --child-command; the source wave's next review cycle "
|
|
420
|
+
"in full at the current HEAD; "
|
|
421
|
+
"pass --repair-evidence when review_findings.py repair-evidence accepts a landed repair"
|
|
422
|
+
)
|
|
423
|
+
else:
|
|
424
|
+
retry = (
|
|
425
|
+
_dispatch_command("fix-tasks", "--repo", repo, "--wave", wave, "--cycle", cycle)
|
|
426
|
+
+ " # then land the repair wave; the source wave's next review cycle judges it"
|
|
427
|
+
)
|
|
428
|
+
return [
|
|
429
|
+
_finding(
|
|
430
|
+
"review-cycles",
|
|
431
|
+
"stuck",
|
|
432
|
+
f"wave {wave} review cycle {cycle} is blocked "
|
|
433
|
+
+ (f"and holds waiting waves {waiting}" if waiting else "and holds completion"),
|
|
434
|
+
retry,
|
|
435
|
+
)
|
|
436
|
+
]
|
|
437
|
+
if ready_result.get("current_wave") is not None:
|
|
438
|
+
return []
|
|
439
|
+
try:
|
|
440
|
+
review_cycle_counts(repo / ".project")
|
|
441
|
+
except archive_milestone.ArchiveError as error:
|
|
442
|
+
return [
|
|
443
|
+
_finding(
|
|
444
|
+
"review-cycles",
|
|
445
|
+
"stuck",
|
|
446
|
+
str(error),
|
|
447
|
+
"compare each PLAN.md wave with the .project/review/wave-N.cycleC.md "
|
|
448
|
+
"files, then run the missing cycle's review: "
|
|
449
|
+
+ _dispatch_command("review", "--repo", repo)
|
|
450
|
+
+ " with that wave's --wave and --cycle and the owner's --child-command; "
|
|
451
|
+
"a repair wave is judged by "
|
|
452
|
+
"its source wave's next cycle",
|
|
453
|
+
)
|
|
454
|
+
]
|
|
455
|
+
return []
|
|
456
|
+
|
|
457
|
+
|
|
357
458
|
def diagnose(repo: Path) -> dict[str, object]:
|
|
358
459
|
resolved = repo.resolve()
|
|
359
460
|
if not resolved.is_dir():
|
|
@@ -567,6 +668,8 @@ def diagnose(repo: Path) -> dict[str, object]:
|
|
|
567
668
|
"$gsd-path-ship",
|
|
568
669
|
)
|
|
569
670
|
)
|
|
671
|
+
elif state.get("phase") == "build" and state.get("status") in {"active", "done"}:
|
|
672
|
+
findings.extend(_build_completion_findings(resolved))
|
|
570
673
|
|
|
571
674
|
undo_transaction = by_name["undo-transaction"]
|
|
572
675
|
route_action = (
|
|
@@ -7,11 +7,13 @@ import sys
|
|
|
7
7
|
# Runtime helpers must not modify their immutable installation.
|
|
8
8
|
sys.dont_write_bytecode = True
|
|
9
9
|
|
|
10
|
+
import contextlib
|
|
10
11
|
import hashlib
|
|
11
12
|
import json
|
|
12
13
|
import os
|
|
13
14
|
import tempfile
|
|
14
15
|
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
15
17
|
|
|
16
18
|
if __package__:
|
|
17
19
|
from . import _common
|
|
@@ -22,7 +24,39 @@ else:
|
|
|
22
24
|
from scripts import _common
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
# Run-scoped cache for read-only consumers (#469): recovery resolves a
|
|
28
|
+
# placement per task, and each resolution lists the whole worktree registry
|
|
29
|
+
# and re-reads the placement receipt. Within one read-only run none of that
|
|
30
|
+
# can change, so the listing and the derived workspace answer once. Flows
|
|
31
|
+
# that create, move, or retire worktrees never enter this scope.
|
|
32
|
+
_PLACEMENT_CACHE: Optional[dict] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextlib.contextmanager
|
|
36
|
+
def readonly_placement_cache():
|
|
37
|
+
global _PLACEMENT_CACHE
|
|
38
|
+
if _PLACEMENT_CACHE is not None: # already inside a read-only run
|
|
39
|
+
yield
|
|
40
|
+
return
|
|
41
|
+
_PLACEMENT_CACHE = {"porcelain": {}, "workspace": {}}
|
|
42
|
+
try:
|
|
43
|
+
yield
|
|
44
|
+
finally:
|
|
45
|
+
_PLACEMENT_CACHE = None
|
|
46
|
+
|
|
47
|
+
|
|
25
48
|
def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
|
|
49
|
+
if _PLACEMENT_CACHE is not None:
|
|
50
|
+
key = str(primary)
|
|
51
|
+
if key in _PLACEMENT_CACHE["porcelain"]:
|
|
52
|
+
return _PLACEMENT_CACHE["porcelain"][key] # type: ignore[index]
|
|
53
|
+
output = _worktree_list_porcelain_uncached(primary)
|
|
54
|
+
_PLACEMENT_CACHE["porcelain"][key] = output
|
|
55
|
+
return output
|
|
56
|
+
return _worktree_list_porcelain_uncached(primary)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _worktree_list_porcelain_uncached(primary: Path) -> tuple[str, bool]:
|
|
26
60
|
try:
|
|
27
61
|
if __package__:
|
|
28
62
|
from . import pipeline_git as pg
|
|
@@ -58,6 +92,17 @@ def _identity(path: Path) -> str:
|
|
|
58
92
|
|
|
59
93
|
|
|
60
94
|
def _workspace(primary: Path, pin: bool) -> Path:
|
|
95
|
+
if _PLACEMENT_CACHE is not None and not pin:
|
|
96
|
+
key = str(primary)
|
|
97
|
+
if key in _PLACEMENT_CACHE["workspace"]:
|
|
98
|
+
return _PLACEMENT_CACHE["workspace"][key] # type: ignore[index]
|
|
99
|
+
workspace = _workspace_uncached(primary, False)
|
|
100
|
+
_PLACEMENT_CACHE["workspace"][key] = workspace
|
|
101
|
+
return workspace
|
|
102
|
+
return _workspace_uncached(primary, pin)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _workspace_uncached(primary: Path, pin: bool) -> Path:
|
|
61
106
|
common = Path(_git(primary, "rev-parse", "--git-common-dir"))
|
|
62
107
|
common = (primary / common).resolve()
|
|
63
108
|
identity = _identity(primary)
|