@opengsd/gsd-path 1.6.2 → 1.7.1
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 +30 -0
- package/DOCS.md +27 -1
- package/FULL.md +4 -3
- package/HOOKS.md +38 -4
- package/QUICK.md +1 -1
- package/README.md +19 -11
- package/RUNTIME.md +5 -3
- package/WORKFLOW.md +11 -4
- 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 +33 -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 +1000 -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 +4 -1
- package/skills/gsd-path/DEFINE.md +2 -1
- package/skills/gsd-path/PLAN.md +13 -3
- package/skills/gsd-path/SHIP.md +41 -8
- package/skills/gsd-path/SKILL.md +16 -1
- package/skills/gsd-path/references/build-native.md +28 -7
- package/skills/gsd-path/references/coder.md +6 -3
- package/skills/gsd-path/references/planner.md +4 -3
- package/skills/gsd-path/references/reviewer-final.md +10 -0
- package/skills/gsd-path/references/reviewer-wave.md +71 -27
- package/skills/gsd-path/references/reviewer.md +21 -8
- 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 +33 -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 +1000 -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/plan.md +3 -2
- 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 +4 -1
- package/skills/gsd-path-build/references/build-native.md +28 -7
- 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 +71 -27
- package/skills/gsd-path-build/references/reviewer.md +21 -8
- 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 +33 -3
- package/skills/gsd-path-build/scripts/integration.py +196 -39
- package/skills/gsd-path-build/scripts/isolation.py +1000 -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 +1000 -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 +1000 -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 +1000 -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 +1000 -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 +13 -3
- package/skills/gsd-path-plan/references/planner.md +4 -3
- 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 +1000 -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/plan.md +3 -2
- 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 +1000 -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 +1000 -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 +71 -27
- package/skills/gsd-path-ship/references/reviewer.md +21 -8
- 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 +1000 -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 +4 -1
- package/skills/path/DEFINE.md +2 -1
- package/skills/path/PLAN.md +13 -3
- package/skills/path/SHIP.md +41 -8
- package/skills/path/SKILL.md +16 -1
- package/skills/path/references/build-native.md +28 -7
- package/skills/path/references/coder.md +6 -3
- package/skills/path/references/planner.md +4 -3
- package/skills/path/references/reviewer-final.md +10 -0
- package/skills/path/references/reviewer-wave.md +71 -27
- package/skills/path/references/reviewer.md +21 -8
- 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 +33 -3
- package/skills/path/scripts/git_guard.py +44 -4
- package/skills/path/scripts/integration.py +196 -39
- package/skills/path/scripts/isolation.py +1000 -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/plan.md +3 -2
- package/skills/path/templates/task.md +8 -1
- package/skills/path/templates/wave-review.md +24 -8
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# gsd-path project runtime
|
|
3
|
-
"""Lint build task briefs against the layer base before dispatch.
|
|
3
|
+
"""Lint build task briefs against the layer base before dispatch.
|
|
4
|
+
|
|
5
|
+
The check is read-only and idempotent: git queries and file reads, no writes.
|
|
6
|
+
Progress goes to stderr (one line every 20 tasks); stdout is exactly the final
|
|
7
|
+
JSON. A host timeout with no output is not a contract verdict — rerun with a
|
|
8
|
+
larger timeout; the checker never hangs on state it changed itself.
|
|
9
|
+
"""
|
|
4
10
|
|
|
5
11
|
import argparse
|
|
12
|
+
import functools
|
|
6
13
|
import json
|
|
7
14
|
import re
|
|
8
15
|
import sys
|
|
16
|
+
import time
|
|
9
17
|
|
|
10
18
|
# Runtime helpers must not modify their immutable installation.
|
|
11
19
|
sys.dont_write_bytecode = True
|
|
12
20
|
from pathlib import Path, PurePosixPath
|
|
13
|
-
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
21
|
+
from typing import Dict, FrozenSet, List, Optional, Sequence, Set, Tuple
|
|
14
22
|
|
|
15
23
|
if __package__:
|
|
16
24
|
from . import _common
|
|
@@ -73,7 +81,57 @@ def _resolve_base(repo: Path, base: str) -> str:
|
|
|
73
81
|
|
|
74
82
|
|
|
75
83
|
def _base_exists(repo: Path, base: str, path: str) -> bool:
|
|
76
|
-
|
|
84
|
+
files, directories = _base_listing(repo, base)
|
|
85
|
+
return path in files or path in directories
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@functools.lru_cache(maxsize=None)
|
|
89
|
+
def _base_listing(repo: Path, base: str) -> Tuple[FrozenSet[str], FrozenSet[str]]:
|
|
90
|
+
"""Every path an object occupies at the immutable `base` commit.
|
|
91
|
+
|
|
92
|
+
One `ls-tree` pair per (repository, base) replaces a `cat-file` subprocess
|
|
93
|
+
per queried path: a commit is fixed, so its listings never change mid-run.
|
|
94
|
+
The file set names blobs and symlinks, plus submodule gitlinks whose commit
|
|
95
|
+
exists in this repository; the directory set names trees.
|
|
96
|
+
`cat-file -e <base>:<path>` succeeded exactly when the path is in either
|
|
97
|
+
set, and the ancestor check treated a path as a non-directory ancestor
|
|
98
|
+
exactly when it is in the file set — a gitlink counted only while its
|
|
99
|
+
commit object was present, so that property is preserved per gitlink.
|
|
100
|
+
"""
|
|
101
|
+
files: Set[str] = set()
|
|
102
|
+
gitlinks: List[Tuple[str, str]] = []
|
|
103
|
+
result = _run_git(repo, "-c", "core.quotepath=false", "ls-tree", "-r", "-z", base)
|
|
104
|
+
if result.returncode != 0:
|
|
105
|
+
raise BriefError(f"cannot list the layer base {base}: {result.stderr.strip()}")
|
|
106
|
+
for entry in result.stdout.split("\0"):
|
|
107
|
+
if not entry:
|
|
108
|
+
continue
|
|
109
|
+
meta, separator, path = entry.partition("\t")
|
|
110
|
+
if not separator:
|
|
111
|
+
continue
|
|
112
|
+
mode, kind, sha = meta.split()
|
|
113
|
+
if kind == "commit":
|
|
114
|
+
gitlinks.append((sha, path))
|
|
115
|
+
else:
|
|
116
|
+
files.add(path)
|
|
117
|
+
present: Dict[str, bool] = {}
|
|
118
|
+
|
|
119
|
+
def gitlink_present(sha: str) -> bool:
|
|
120
|
+
if sha not in present:
|
|
121
|
+
present[sha] = _run_git(repo, "cat-file", "-e", sha).returncode == 0
|
|
122
|
+
return present[sha]
|
|
123
|
+
|
|
124
|
+
files.update(path for sha, path in gitlinks if gitlink_present(sha))
|
|
125
|
+
directories_result = _run_git(
|
|
126
|
+
repo, "-c", "core.quotepath=false", "ls-tree", "-r", "-d", "-z", "--name-only", base)
|
|
127
|
+
if directories_result.returncode != 0:
|
|
128
|
+
raise BriefError(
|
|
129
|
+
f"cannot list the layer base {base}: {directories_result.stderr.strip()}")
|
|
130
|
+
# `ls-tree -d` also lists gitlinks; they are never directories.
|
|
131
|
+
directories = frozenset(
|
|
132
|
+
path for path in directories_result.stdout.split("\0") if path
|
|
133
|
+
) - {path for _, path in gitlinks}
|
|
134
|
+
return frozenset(files), directories
|
|
77
135
|
|
|
78
136
|
|
|
79
137
|
def _project_track(task: Path, repo: Path) -> Optional[str]:
|
|
@@ -326,6 +384,9 @@ def _lint_task(
|
|
|
326
384
|
task_id = path.stem
|
|
327
385
|
if fields and isinstance(fields.get("id"), str):
|
|
328
386
|
task_id = fields["id"] # type: ignore[assignment]
|
|
387
|
+
# The coordinator's own layer base, before any landed or member override: a
|
|
388
|
+
# member task's `coordinator:` reads resolve here (#471).
|
|
389
|
+
coordinator_base = base
|
|
329
390
|
base = (landed_bases or {}).get(task_id, base)
|
|
330
391
|
problems: List[str] = []
|
|
331
392
|
checked = 0
|
|
@@ -370,17 +431,28 @@ def _lint_task(
|
|
|
370
431
|
if ".." in candidate.parts:
|
|
371
432
|
problems.append(f"files entry must not contain '..': {entry}")
|
|
372
433
|
continue
|
|
434
|
+
if ": " in entry or ":" in entry.split("/", 1)[0]:
|
|
435
|
+
problems.append(
|
|
436
|
+
f"files entries are repository-local write scope; a "
|
|
437
|
+
f"repository-qualified reference is a read: {entry}")
|
|
438
|
+
continue
|
|
373
439
|
normalized = str(candidate)
|
|
374
440
|
declared.add(normalized)
|
|
375
441
|
checked += 1
|
|
376
442
|
if ".git" in candidate.parts:
|
|
377
443
|
problems.append(f"files entry must not enter .git: {normalized}")
|
|
378
444
|
continue
|
|
445
|
+
if candidate.parts[0] == ".project":
|
|
446
|
+
# Bookkeeping territory: a landed task file under .project/ is
|
|
447
|
+
# unsupported at ship and cannot be removed during it (#466).
|
|
448
|
+
problems.append(
|
|
449
|
+
f"files entry must stay out of .project/: {normalized}")
|
|
450
|
+
continue
|
|
451
|
+
files_at_base, _ = _base_listing(git_root, base)
|
|
379
452
|
for parent in candidate.parents:
|
|
380
453
|
if str(parent) == ".":
|
|
381
454
|
break
|
|
382
|
-
|
|
383
|
-
if kind.returncode == 0 and kind.stdout.strip() != "tree":
|
|
455
|
+
if str(parent) in files_at_base:
|
|
384
456
|
problems.append(
|
|
385
457
|
f"files entry {normalized} has non-directory ancestor "
|
|
386
458
|
f"{parent} at the layer base"
|
|
@@ -389,6 +461,53 @@ def _lint_task(
|
|
|
389
461
|
|
|
390
462
|
# A landed brief is history that nobody can edit: only an unlanded brief must name present paths.
|
|
391
463
|
landed = task_id in (landed_bases or {})
|
|
464
|
+
# Repository-qualified read-only inputs (#471): `reads:` holds `repo: path`
|
|
465
|
+
# entries resolved in that repository at the same base its own briefs use.
|
|
466
|
+
read_paths: Set[str] = set()
|
|
467
|
+
reads = None if landed else fields.get("reads")
|
|
468
|
+
if reads is not None and not isinstance(reads, list):
|
|
469
|
+
problems.append("frontmatter reads must be a list")
|
|
470
|
+
reads = None
|
|
471
|
+
for entry in reads or []:
|
|
472
|
+
checked += 1
|
|
473
|
+
if not isinstance(entry, str) or ": " not in entry:
|
|
474
|
+
problems.append(f"reads entry must be `repo: path`: {entry!r}")
|
|
475
|
+
continue
|
|
476
|
+
name, _, read_path = entry.partition(": ")
|
|
477
|
+
candidate = PurePosixPath(read_path)
|
|
478
|
+
if candidate.is_absolute() or ".." in candidate.parts:
|
|
479
|
+
problems.append(f"reads entry is not repo-relative: {entry}")
|
|
480
|
+
continue
|
|
481
|
+
normalized = str(candidate)
|
|
482
|
+
read_paths.add(normalized)
|
|
483
|
+
if ".git" in candidate.parts:
|
|
484
|
+
problems.append(f"reads entry must not enter .git: {normalized}")
|
|
485
|
+
continue
|
|
486
|
+
if member is None:
|
|
487
|
+
# A coordinator task may read from any member of this project.
|
|
488
|
+
located = member_base(name) if member_base else None
|
|
489
|
+
if located is None:
|
|
490
|
+
problems.append(
|
|
491
|
+
f"reads entry names no member in MEMBERS.md: {name}: {normalized}")
|
|
492
|
+
continue
|
|
493
|
+
read_root, read_base = located
|
|
494
|
+
elif name == "coordinator":
|
|
495
|
+
# A member task may read the coordinator at its own layer base.
|
|
496
|
+
read_root, read_base = repo, coordinator_base
|
|
497
|
+
else:
|
|
498
|
+
# Cross-repository reads are read-only (#471): any member of the
|
|
499
|
+
# project may be named, resolved at that member's own base.
|
|
500
|
+
located_other = member_base(name) if member_base else None
|
|
501
|
+
if located_other is None:
|
|
502
|
+
problems.append(
|
|
503
|
+
f"reads entry names no member in MEMBERS.md: {name}: {normalized}")
|
|
504
|
+
continue
|
|
505
|
+
read_root, read_base = located_other
|
|
506
|
+
if not _base_exists(read_root, str(read_base), normalized):
|
|
507
|
+
problems.append(
|
|
508
|
+
f"reads entry {name}: {normalized} is missing at repository {name} "
|
|
509
|
+
f"base {read_base}")
|
|
510
|
+
|
|
392
511
|
for name in () if landed else PROSE_SECTIONS:
|
|
393
512
|
body = sections.get(name)
|
|
394
513
|
if body is None or not _clean(body):
|
|
@@ -400,6 +519,7 @@ def _lint_task(
|
|
|
400
519
|
if (
|
|
401
520
|
token not in declared
|
|
402
521
|
and token not in supplied
|
|
522
|
+
and token not in read_paths
|
|
403
523
|
and not _supplied_contract(repo, path, token)
|
|
404
524
|
and not _pipeline_workspace_file(repo, path, token)
|
|
405
525
|
and not _base_exists(git_root, base, token)
|
|
@@ -423,6 +543,7 @@ def _lint_task(
|
|
|
423
543
|
if (
|
|
424
544
|
token not in declared
|
|
425
545
|
and token not in supplied
|
|
546
|
+
and token not in read_paths
|
|
426
547
|
and not _pipeline_workspace_file(repo, path, token)
|
|
427
548
|
and not _base_exists(git_root, base, token)
|
|
428
549
|
):
|
|
@@ -467,16 +588,18 @@ def validate_task_briefs(
|
|
|
467
588
|
raise BriefError(f"no task briefs found in {tasks_dir}")
|
|
468
589
|
if member_base is None:
|
|
469
590
|
member_base = _member_bases(root)
|
|
470
|
-
|
|
591
|
+
started = time.monotonic()
|
|
592
|
+
for number, path in enumerate(task_files, start=1):
|
|
593
|
+
if number % 20 == 0:
|
|
594
|
+
print(f"check_task_briefs: {number}/{len(task_files)} tasks",
|
|
595
|
+
file=sys.stderr, flush=True)
|
|
471
596
|
task_id, task_problems, contract, task_checked = _lint_task(
|
|
472
597
|
root, resolved_base, path, dependency_files, landed_bases, member_base
|
|
473
598
|
)
|
|
474
599
|
problems.extend(f"{task_id}: {problem}" for problem in task_problems)
|
|
475
600
|
if contract is not None:
|
|
476
|
-
for
|
|
477
|
-
shape
|
|
478
|
-
if shape:
|
|
479
|
-
contracts.setdefault(shape, set()).add(task_id)
|
|
601
|
+
for shape in _common.contract_shapes(contract):
|
|
602
|
+
contracts.setdefault(shape, set()).add(task_id)
|
|
480
603
|
checked += task_checked
|
|
481
604
|
|
|
482
605
|
for shape, task_ids in contracts.items():
|
|
@@ -491,7 +614,8 @@ def validate_task_briefs(
|
|
|
491
614
|
|
|
492
615
|
if problems:
|
|
493
616
|
raise BriefError("\n".join(problems))
|
|
494
|
-
return {"base": resolved_base, "checked": checked, "tasks": len(task_files)
|
|
617
|
+
return {"base": resolved_base, "checked": checked, "tasks": len(task_files),
|
|
618
|
+
"elapsed_seconds": round(time.monotonic() - started, 3)}
|
|
495
619
|
|
|
496
620
|
|
|
497
621
|
def _resolve_landed_task_base(
|
|
@@ -1034,6 +1034,19 @@ def review_cycle_counts(archive: Path) -> Sequence[int]:
|
|
|
1034
1034
|
repair_introductions[receipt.task] = (receipt.wave, receipt.cycle)
|
|
1035
1035
|
wave_artifacts = archive_milestone.canonical_wave_files(archive / "review")
|
|
1036
1036
|
archive_milestone.validate_wave_skeptic_files(archive / "review", wave_depths, wave_artifacts)
|
|
1037
|
+
# A repair wave — every task of the wave carries a Review findings section —
|
|
1038
|
+
# is judged by the next review cycle of its source wave, never by an
|
|
1039
|
+
# artifact of its own (build-native.md); an artifact it does have is still
|
|
1040
|
+
# validated below. The heading test matches build_state's parsed-section
|
|
1041
|
+
# rule, so a template comment mentioning the section does not qualify.
|
|
1042
|
+
repair_waves = {
|
|
1043
|
+
wave for wave in wave_numbers
|
|
1044
|
+
if wave_tasks[wave]
|
|
1045
|
+
and all(
|
|
1046
|
+
re.search(r"(?m)^## Review findings\s*$", task_texts[task_id])
|
|
1047
|
+
for task_id, _ in wave_tasks[wave]
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1037
1050
|
for path, wave, cycle, lens in wave_artifacts:
|
|
1038
1051
|
if lens == "panel":
|
|
1039
1052
|
artifacts.setdefault(wave, {}).setdefault(cycle, {})[lens] = path
|
|
@@ -1049,7 +1062,9 @@ def review_cycle_counts(archive: Path) -> Sequence[int]:
|
|
|
1049
1062
|
if verdict not in {"pass", "blocked"}:
|
|
1050
1063
|
raise ArchiveError(f"{path.name} has an invalid wave verdict")
|
|
1051
1064
|
artifacts.setdefault(wave, {}).setdefault(cycle, {})[lens] = path
|
|
1052
|
-
if
|
|
1065
|
+
if any(wave not in wave_numbers for wave in artifacts) or any(
|
|
1066
|
+
wave not in artifacts for wave in wave_numbers if wave not in repair_waves
|
|
1067
|
+
):
|
|
1053
1068
|
raise ArchiveError("review cycle waves do not match PLAN.md")
|
|
1054
1069
|
for receipt in repair_receipts:
|
|
1055
1070
|
if receipt.wave not in artifacts or receipt.cycle not in artifacts[receipt.wave]:
|
|
@@ -1065,7 +1080,7 @@ def review_cycle_counts(archive: Path) -> Sequence[int]:
|
|
|
1065
1080
|
)
|
|
1066
1081
|
|
|
1067
1082
|
counts = []
|
|
1068
|
-
for wave in
|
|
1083
|
+
for wave in sorted(artifacts):
|
|
1069
1084
|
cycles = artifacts[wave]
|
|
1070
1085
|
if sorted(cycles) != list(range(1, max(cycles) + 1)):
|
|
1071
1086
|
raise ArchiveError(f"review cycles for wave {wave} are not contiguous")
|
|
@@ -1128,14 +1143,29 @@ def review_cycle_counts(archive: Path) -> Sequence[int]:
|
|
|
1128
1143
|
is_last_cycle=cycle == max(cycles),
|
|
1129
1144
|
)
|
|
1130
1145
|
)
|
|
1146
|
+
if cycle != max(cycles) and "blocked" not in wave_verdicts[cycle]:
|
|
1147
|
+
# A superseded pass cycle is allowed only when it could not close
|
|
1148
|
+
# the wave: it fails the checks a last cycle must pass.
|
|
1149
|
+
try:
|
|
1150
|
+
for path, depth, lens in expected:
|
|
1151
|
+
validate_wave_review(
|
|
1152
|
+
path,
|
|
1153
|
+
expected_tasks,
|
|
1154
|
+
wave_criteria[wave],
|
|
1155
|
+
depth,
|
|
1156
|
+
lens,
|
|
1157
|
+
task_texts,
|
|
1158
|
+
is_last_cycle=True,
|
|
1159
|
+
)
|
|
1160
|
+
except ArchiveError:
|
|
1161
|
+
pass
|
|
1162
|
+
else:
|
|
1163
|
+
raise ArchiveError(
|
|
1164
|
+
f"superseded review cycle {cycle} for wave {wave} "
|
|
1165
|
+
"must include a blocked Wave verdict"
|
|
1166
|
+
)
|
|
1131
1167
|
|
|
1132
1168
|
last_cycle = max(cycles)
|
|
1133
|
-
for cycle, verdicts in wave_verdicts.items():
|
|
1134
|
-
if cycle != last_cycle and "blocked" not in verdicts:
|
|
1135
|
-
raise ArchiveError(
|
|
1136
|
-
f"superseded review cycle {cycle} for wave {wave} "
|
|
1137
|
-
"must include a blocked Wave verdict"
|
|
1138
|
-
)
|
|
1139
1169
|
if any(verdict != "pass" for verdict in wave_verdicts[last_cycle]):
|
|
1140
1170
|
raise ArchiveError(f"last review cycle for wave {wave} did not pass")
|
|
1141
1171
|
if any(task_verdict != "pass" for task_verdict in task_verdicts[last_cycle]):
|
|
@@ -1211,8 +1241,9 @@ def render_manifest(repo: Path) -> dict:
|
|
|
1211
1241
|
)
|
|
1212
1242
|
listed_contents = "\n".join(f"- {path}" for path in archive_file_inventory(archive))
|
|
1213
1243
|
cycle_text = "/".join(str(cycle) for cycle in cycles)
|
|
1244
|
+
wave_numbers, _wave_depths = plan_wave_depths((archive / "plan" / "PLAN.md").read_text(encoding="utf-8"))
|
|
1214
1245
|
count_text = (
|
|
1215
|
-
f"{len(
|
|
1246
|
+
f"{len(wave_numbers)} Tasks: {len(tasks)} done / {len(tasks)} total "
|
|
1216
1247
|
f"Review cycles used: {cycle_text}"
|
|
1217
1248
|
+ (f" Attested: {attested}" if attested else "")
|
|
1218
1249
|
)
|
|
@@ -1273,7 +1304,12 @@ def validate_manifest(project: Path, archive: Path, state: PipelineState) -> tup
|
|
|
1273
1304
|
validate_gap_reviews(archive, reviewed_head)
|
|
1274
1305
|
task_files, attested = archive_milestone.landed_task_evidence(project, archive / "tasks", reviewed_head)
|
|
1275
1306
|
cycle_counts = review_cycle_counts(archive)
|
|
1276
|
-
|
|
1307
|
+
# Waves counts PLAN waves; an unreviewed repair wave contributes no cycle
|
|
1308
|
+
# but is still a wave, so the count comes from the PLAN, not the cycles.
|
|
1309
|
+
manifest_wave_numbers, _manifest_depths = plan_wave_depths(
|
|
1310
|
+
(archive / "plan" / "PLAN.md").read_text(encoding="utf-8")
|
|
1311
|
+
)
|
|
1312
|
+
wave_count = len(manifest_wave_numbers)
|
|
1277
1313
|
counts = re.fullmatch(
|
|
1278
1314
|
r"(\d+)\s+Tasks:\s+(\d+) done / (\d+) total\s+Review cycles used:\s+([1-9]\d*(?:/[1-9]\d*)*)"
|
|
1279
1315
|
r"(?:\s+Attested:\s+([1-9]\d*))?",
|
|
@@ -247,7 +247,8 @@ def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str,
|
|
|
247
247
|
task_branch: Optional[str], mode: str, wave: Optional[int] = None,
|
|
248
248
|
title: Optional[str] = None, files: Optional[List[str]] = None,
|
|
249
249
|
sidecar: Optional[object] = None, member: Optional[str] = None,
|
|
250
|
-
member_base: Optional[str] = None, contract_file: Optional[str] = None
|
|
250
|
+
member_base: Optional[str] = None, contract_file: Optional[str] = None,
|
|
251
|
+
agent: Optional[str] = None) -> None:
|
|
251
252
|
"""Open a dispatch attempt record for a host-spawned coder before it runs."""
|
|
252
253
|
root = records_root(primary)
|
|
253
254
|
root.mkdir(parents=True, exist_ok=True)
|
|
@@ -277,6 +278,8 @@ def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str,
|
|
|
277
278
|
record["member_base"] = member_base
|
|
278
279
|
if contract_file is not None:
|
|
279
280
|
record["contract_file"] = contract_file
|
|
281
|
+
if agent is not None:
|
|
282
|
+
record["agent"] = agent
|
|
280
283
|
task_dir = root / task_id
|
|
281
284
|
if not should_open_native_shell(root, task_id, base):
|
|
282
285
|
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
@@ -355,6 +358,7 @@ def record_native_member_activation(coordinator: Path, activation: Dict[str, obj
|
|
|
355
358
|
member=str(activation["member"]),
|
|
356
359
|
member_base=str(activation["member_base"]),
|
|
357
360
|
contract_file=task_file,
|
|
361
|
+
agent=str(activation["agent"]),
|
|
358
362
|
)
|
|
359
363
|
|
|
360
364
|
|
|
@@ -720,6 +724,19 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
|
720
724
|
"sidecar": sidecar}
|
|
721
725
|
|
|
722
726
|
|
|
727
|
+
def finish_receipt(receipt: Dict[str, object]) -> Dict[str, object]:
|
|
728
|
+
"""The finish result: landed, question, or blocked — always with a visible cause (#469)."""
|
|
729
|
+
if receipt.get("landed"):
|
|
730
|
+
return {**receipt, "status": "landed"}
|
|
731
|
+
if receipt.get("questions"):
|
|
732
|
+
return {**receipt, "status": "question"}
|
|
733
|
+
result = {**receipt, "status": "blocked"}
|
|
734
|
+
if not receipt.get("blocked"):
|
|
735
|
+
result["reason"] = (
|
|
736
|
+
"task did not land and no blocked entry was recorded; inspect the task Log")
|
|
737
|
+
return result
|
|
738
|
+
|
|
739
|
+
|
|
723
740
|
def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
724
741
|
"""Verify in the isolate, land, record the ledger, retire. Returns the landing receipt."""
|
|
725
742
|
landing = land_task(primary, state)
|
|
@@ -761,15 +778,23 @@ def land_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
761
778
|
except isolation.IsolationError as error:
|
|
762
779
|
raise DriverStop(str(error), task=task_id) from error
|
|
763
780
|
execution, reruns = landing_verify(command, regressions, sidecar_path)
|
|
781
|
+
# The serial Verify runs in the sidecar, not the task checkout; `cwd` names the
|
|
782
|
+
# directory the command saw, `worktree` the task checkout it will land on.
|
|
783
|
+
execution_dir = sidecar_path
|
|
784
|
+
execution_base = base
|
|
764
785
|
location = f"sidecar {sidecar['branch']}"
|
|
765
786
|
isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
|
|
766
787
|
isolation.retire(primary, sidecar_path, str(sidecar["branch"]), False)
|
|
767
788
|
else:
|
|
768
789
|
execution, reruns = landing_verify(command, regressions, worktree)
|
|
790
|
+
execution_dir = worktree
|
|
791
|
+
# A member Verify starts from the member repository's base, not the coordinator's.
|
|
792
|
+
execution_base = str(state["member_base"]) if state["mode"] == "member" else base
|
|
769
793
|
location = f"isolate {state['task_branch']}"
|
|
770
794
|
passed = execution["exit_code"] == 0
|
|
771
795
|
broken = [item for item in reruns if item["exit_code"]]
|
|
772
|
-
execution.update({"worktree": str(worktree), "
|
|
796
|
+
execution.update({"worktree": str(worktree), "cwd": str(execution_dir),
|
|
797
|
+
"base": execution_base, "location": location})
|
|
773
798
|
evidence_dir = (Path(str(state["_path"])).parent if state.get("_path")
|
|
774
799
|
else records_root(primary) / task_id / f"manual-{now().replace(':', '')}")
|
|
775
800
|
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1436,6 +1461,9 @@ def review_brief(state: Dict[str, object], options: argparse.Namespace, primary:
|
|
|
1436
1461
|
f"You are the reviewer for GSD Path wave {wave}, cycle {cycle}; logical task name "
|
|
1437
1462
|
f"{state['task_id']}. Mode: wave" + (f"; lens: {lens}." if lens else "."),
|
|
1438
1463
|
f"Read the reviewer role brief first and follow it exactly: {options.role_brief}",
|
|
1464
|
+
"Validate the agreed acceptance contract. "
|
|
1465
|
+
"Block only on a named unmet requirement or missing required evidence. "
|
|
1466
|
+
"Do not search for improvements or expand the agreed contract.",
|
|
1439
1467
|
f"Template (use only this): {options.template}",
|
|
1440
1468
|
f"AGENTS.md: {agents if agents.exists() else 'absent'}",
|
|
1441
1469
|
f"Repository root, read-only: {primary}",
|
|
@@ -1453,6 +1481,8 @@ def review_brief(state: Dict[str, object], options: argparse.Namespace, primary:
|
|
|
1453
1481
|
f"landing commit {task['commit']}")
|
|
1454
1482
|
if previous is not None:
|
|
1455
1483
|
lines.append(f"Previous cycle review to re-check: {previous}")
|
|
1484
|
+
lines.append("Re-check repaired failures and affected passes only. "
|
|
1485
|
+
"Reuse settled evidence for unaffected criteria.")
|
|
1456
1486
|
if options.repair_evidence:
|
|
1457
1487
|
lines.append(f"Repair evidence receipt (re-review after a proven repair): {options.repair_evidence}")
|
|
1458
1488
|
if final_scope:
|
|
@@ -2591,7 +2621,7 @@ def main(argv=None) -> int:
|
|
|
2591
2621
|
else: # a coder dispatched by hand: derive the isolate from the task frontmatter
|
|
2592
2622
|
current.receipt["landed"].append(
|
|
2593
2623
|
finish_task(primary, native or state_from_task(primary, arguments.task_id)))
|
|
2594
|
-
result =
|
|
2624
|
+
result = finish_receipt(current.receipt)
|
|
2595
2625
|
elif arguments.action == "answer":
|
|
2596
2626
|
result = answer(primary, arguments.task_id, arguments.answer)
|
|
2597
2627
|
elif arguments.action == 'reassign-model':
|