@opengsd/gsd-path 1.6.1 → 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.
Files changed (275) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/DOCS.md +27 -1
  3. package/FULL.md +2 -1
  4. package/HOOKS.md +38 -4
  5. package/QUICK.md +1 -1
  6. package/README.md +20 -29
  7. package/RUNTIME.md +5 -3
  8. package/WORKFLOW.md +9 -2
  9. package/package.json +1 -1
  10. package/scripts/_common.py +76 -2
  11. package/scripts/archive_milestone.py +129 -7
  12. package/scripts/build_recovery.py +25 -2
  13. package/scripts/build_state.py +333 -8
  14. package/scripts/check_handoffs.py +437 -30
  15. package/scripts/check_task_briefs.py +135 -11
  16. package/scripts/discussion_validate.py +46 -10
  17. package/scripts/dispatch_driver.py +28 -3
  18. package/scripts/git_guard.py +44 -4
  19. package/scripts/guard_hook.py +456 -9
  20. package/scripts/install.py +34 -4
  21. package/scripts/integration.py +196 -39
  22. package/scripts/isolation.py +1015 -37
  23. package/scripts/lean_verification.py +33 -14
  24. package/scripts/loop_run.py +9 -4
  25. package/scripts/members.py +17 -3
  26. package/scripts/pipeline_diagnose.py +104 -1
  27. package/scripts/pipeline_state.py +10 -2
  28. package/scripts/runtime_store.py +88 -3
  29. package/scripts/state_checkpoint.py +15 -0
  30. package/scripts/status_runtime.py +73 -2
  31. package/scripts/workflow_run.py +10 -3
  32. package/scripts/worktree_paths.py +45 -0
  33. package/skills/gsd-path/BUILD.md +5 -1
  34. package/skills/gsd-path/DEFINE.md +2 -1
  35. package/skills/gsd-path/PLAN.md +10 -0
  36. package/skills/gsd-path/SHIP.md +43 -9
  37. package/skills/gsd-path/SKILL.md +16 -1
  38. package/skills/gsd-path/references/build-native.md +20 -3
  39. package/skills/gsd-path/references/coder.md +6 -3
  40. package/skills/gsd-path/references/reviewer-final.md +10 -0
  41. package/skills/gsd-path/references/reviewer-wave.md +61 -22
  42. package/skills/gsd-path/references/reviewer.md +8 -6
  43. package/skills/gsd-path/references/runtime-selection.md +36 -0
  44. package/skills/gsd-path/references/spec-probes.md +19 -1
  45. package/skills/gsd-path/scripts/_common.py +76 -2
  46. package/skills/gsd-path/scripts/archive_milestone.py +129 -7
  47. package/skills/gsd-path/scripts/build_recovery.py +25 -2
  48. package/skills/gsd-path/scripts/build_state.py +333 -8
  49. package/skills/gsd-path/scripts/check_handoffs.py +437 -30
  50. package/skills/gsd-path/scripts/check_task_briefs.py +135 -11
  51. package/skills/gsd-path/scripts/discussion_validate.py +46 -10
  52. package/skills/gsd-path/scripts/dispatch_driver.py +28 -3
  53. package/skills/gsd-path/scripts/git_guard.py +44 -4
  54. package/skills/gsd-path/scripts/integration.py +196 -39
  55. package/skills/gsd-path/scripts/isolation.py +1015 -37
  56. package/skills/gsd-path/scripts/lean_verification.py +33 -14
  57. package/skills/gsd-path/scripts/loop_run.py +9 -4
  58. package/skills/gsd-path/scripts/members.py +17 -3
  59. package/skills/gsd-path/scripts/pipeline_state.py +10 -2
  60. package/skills/gsd-path/scripts/state_checkpoint.py +15 -0
  61. package/skills/gsd-path/scripts/status_runtime.py +73 -2
  62. package/skills/gsd-path/scripts/workflow_run.py +10 -3
  63. package/skills/gsd-path/scripts/worktree_paths.py +45 -0
  64. package/skills/gsd-path/templates/intent.md +5 -3
  65. package/skills/gsd-path/templates/task.md +8 -1
  66. package/skills/gsd-path/templates/wave-review.md +24 -8
  67. package/skills/gsd-path-build/SKILL.md +5 -1
  68. package/skills/gsd-path-build/references/build-native.md +20 -3
  69. package/skills/gsd-path-build/references/coder.md +6 -3
  70. package/skills/gsd-path-build/references/reviewer-final.md +10 -0
  71. package/skills/gsd-path-build/references/reviewer-wave.md +61 -22
  72. package/skills/gsd-path-build/references/reviewer.md +8 -6
  73. package/skills/gsd-path-build/references/runtime-selection.md +36 -0
  74. package/skills/gsd-path-build/references/spec-probes.md +19 -1
  75. package/skills/gsd-path-build/scripts/_common.py +76 -2
  76. package/skills/gsd-path-build/scripts/archive_milestone.py +129 -7
  77. package/skills/gsd-path-build/scripts/build_recovery.py +25 -2
  78. package/skills/gsd-path-build/scripts/build_state.py +333 -8
  79. package/skills/gsd-path-build/scripts/check_handoffs.py +437 -30
  80. package/skills/gsd-path-build/scripts/check_task_briefs.py +135 -11
  81. package/skills/gsd-path-build/scripts/discussion_validate.py +46 -10
  82. package/skills/gsd-path-build/scripts/dispatch_driver.py +28 -3
  83. package/skills/gsd-path-build/scripts/integration.py +196 -39
  84. package/skills/gsd-path-build/scripts/isolation.py +1015 -37
  85. package/skills/gsd-path-build/scripts/loop_run.py +9 -4
  86. package/skills/gsd-path-build/scripts/members.py +17 -3
  87. package/skills/gsd-path-build/scripts/pipeline_state.py +10 -2
  88. package/skills/gsd-path-build/scripts/state_checkpoint.py +15 -0
  89. package/skills/gsd-path-build/scripts/workflow_run.py +10 -3
  90. package/skills/gsd-path-build/scripts/worktree_paths.py +45 -0
  91. package/skills/gsd-path-build/templates/task.md +8 -1
  92. package/skills/gsd-path-build/templates/wave-review.md +24 -8
  93. package/skills/gsd-path-decide/references/runtime-selection.md +36 -0
  94. package/skills/gsd-path-decide/scripts/_common.py +76 -2
  95. package/skills/gsd-path-decide/scripts/archive_milestone.py +129 -7
  96. package/skills/gsd-path-decide/scripts/build_recovery.py +25 -2
  97. package/skills/gsd-path-decide/scripts/check_handoffs.py +437 -30
  98. package/skills/gsd-path-decide/scripts/check_task_briefs.py +135 -11
  99. package/skills/gsd-path-decide/scripts/discussion_validate.py +46 -10
  100. package/skills/gsd-path-decide/scripts/integration.py +196 -39
  101. package/skills/gsd-path-decide/scripts/isolation.py +1015 -37
  102. package/skills/gsd-path-decide/scripts/loop_run.py +9 -4
  103. package/skills/gsd-path-decide/scripts/members.py +17 -3
  104. package/skills/gsd-path-decide/scripts/pipeline_state.py +10 -2
  105. package/skills/gsd-path-decide/scripts/state_checkpoint.py +15 -0
  106. package/skills/gsd-path-decide/scripts/worktree_paths.py +45 -0
  107. package/skills/gsd-path-define/SKILL.md +2 -1
  108. package/skills/gsd-path-define/references/runtime-selection.md +36 -0
  109. package/skills/gsd-path-define/references/spec-probes.md +19 -1
  110. package/skills/gsd-path-define/scripts/_common.py +76 -2
  111. package/skills/gsd-path-define/scripts/archive_milestone.py +129 -7
  112. package/skills/gsd-path-define/scripts/build_recovery.py +25 -2
  113. package/skills/gsd-path-define/scripts/check_handoffs.py +437 -30
  114. package/skills/gsd-path-define/scripts/check_task_briefs.py +135 -11
  115. package/skills/gsd-path-define/scripts/discussion_validate.py +46 -10
  116. package/skills/gsd-path-define/scripts/integration.py +196 -39
  117. package/skills/gsd-path-define/scripts/isolation.py +1015 -37
  118. package/skills/gsd-path-define/scripts/loop_run.py +9 -4
  119. package/skills/gsd-path-define/scripts/members.py +17 -3
  120. package/skills/gsd-path-define/scripts/pipeline_state.py +10 -2
  121. package/skills/gsd-path-define/scripts/state_checkpoint.py +15 -0
  122. package/skills/gsd-path-define/scripts/worktree_paths.py +45 -0
  123. package/skills/gsd-path-define/templates/intent.md +5 -3
  124. package/skills/gsd-path-discuss/references/runtime-selection.md +36 -0
  125. package/skills/gsd-path-discuss/scripts/_common.py +76 -2
  126. package/skills/gsd-path-discuss/scripts/loop_run.py +9 -4
  127. package/skills/gsd-path-discuss/scripts/worktree_paths.py +45 -0
  128. package/skills/gsd-path-docs-audit/references/runtime-selection.md +36 -0
  129. package/skills/gsd-path-docs-audit/scripts/_common.py +76 -2
  130. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +129 -7
  131. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +25 -2
  132. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +437 -30
  133. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +135 -11
  134. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +46 -10
  135. package/skills/gsd-path-docs-audit/scripts/integration.py +196 -39
  136. package/skills/gsd-path-docs-audit/scripts/isolation.py +1015 -37
  137. package/skills/gsd-path-docs-audit/scripts/loop_run.py +9 -4
  138. package/skills/gsd-path-docs-audit/scripts/members.py +17 -3
  139. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +10 -2
  140. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +15 -0
  141. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +45 -0
  142. package/skills/gsd-path-forensics/references/runtime-selection.md +36 -0
  143. package/skills/gsd-path-forensics/scripts/_common.py +76 -2
  144. package/skills/gsd-path-forensics/scripts/loop_run.py +9 -4
  145. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +104 -1
  146. package/skills/gsd-path-forensics/scripts/worktree_paths.py +45 -0
  147. package/skills/gsd-path-inspect/references/runtime-selection.md +36 -0
  148. package/skills/gsd-path-inspect/scripts/_common.py +76 -2
  149. package/skills/gsd-path-inspect/scripts/archive_milestone.py +129 -7
  150. package/skills/gsd-path-inspect/scripts/build_recovery.py +25 -2
  151. package/skills/gsd-path-inspect/scripts/check_handoffs.py +437 -30
  152. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +135 -11
  153. package/skills/gsd-path-inspect/scripts/discussion_validate.py +46 -10
  154. package/skills/gsd-path-inspect/scripts/integration.py +196 -39
  155. package/skills/gsd-path-inspect/scripts/isolation.py +1015 -37
  156. package/skills/gsd-path-inspect/scripts/loop_run.py +9 -4
  157. package/skills/gsd-path-inspect/scripts/members.py +17 -3
  158. package/skills/gsd-path-inspect/scripts/pipeline_state.py +10 -2
  159. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +15 -0
  160. package/skills/gsd-path-inspect/scripts/workflow_run.py +10 -3
  161. package/skills/gsd-path-inspect/scripts/worktree_paths.py +45 -0
  162. package/skills/gsd-path-loop/references/runtime-selection.md +36 -0
  163. package/skills/gsd-path-loop/scripts/_common.py +76 -2
  164. package/skills/gsd-path-loop/scripts/loop_run.py +9 -4
  165. package/skills/gsd-path-loop/scripts/worktree_paths.py +45 -0
  166. package/skills/gsd-path-migrate/references/runtime-selection.md +36 -0
  167. package/skills/gsd-path-migrate/scripts/_common.py +76 -2
  168. package/skills/gsd-path-migrate/scripts/loop_run.py +9 -4
  169. package/skills/gsd-path-plan/SKILL.md +10 -0
  170. package/skills/gsd-path-plan/references/runtime-selection.md +36 -0
  171. package/skills/gsd-path-plan/references/spec-probes.md +19 -1
  172. package/skills/gsd-path-plan/scripts/_common.py +76 -2
  173. package/skills/gsd-path-plan/scripts/archive_milestone.py +129 -7
  174. package/skills/gsd-path-plan/scripts/build_recovery.py +25 -2
  175. package/skills/gsd-path-plan/scripts/check_handoffs.py +437 -30
  176. package/skills/gsd-path-plan/scripts/check_task_briefs.py +135 -11
  177. package/skills/gsd-path-plan/scripts/discussion_validate.py +46 -10
  178. package/skills/gsd-path-plan/scripts/integration.py +196 -39
  179. package/skills/gsd-path-plan/scripts/isolation.py +1015 -37
  180. package/skills/gsd-path-plan/scripts/loop_run.py +9 -4
  181. package/skills/gsd-path-plan/scripts/members.py +17 -3
  182. package/skills/gsd-path-plan/scripts/pipeline_state.py +10 -2
  183. package/skills/gsd-path-plan/scripts/state_checkpoint.py +15 -0
  184. package/skills/gsd-path-plan/scripts/workflow_run.py +10 -3
  185. package/skills/gsd-path-plan/scripts/worktree_paths.py +45 -0
  186. package/skills/gsd-path-plan/templates/task.md +8 -1
  187. package/skills/gsd-path-research/references/runtime-selection.md +36 -0
  188. package/skills/gsd-path-research/scripts/_common.py +76 -2
  189. package/skills/gsd-path-research/scripts/archive_milestone.py +129 -7
  190. package/skills/gsd-path-research/scripts/build_recovery.py +25 -2
  191. package/skills/gsd-path-research/scripts/check_handoffs.py +437 -30
  192. package/skills/gsd-path-research/scripts/check_task_briefs.py +135 -11
  193. package/skills/gsd-path-research/scripts/discussion_validate.py +46 -10
  194. package/skills/gsd-path-research/scripts/integration.py +196 -39
  195. package/skills/gsd-path-research/scripts/isolation.py +1015 -37
  196. package/skills/gsd-path-research/scripts/loop_run.py +9 -4
  197. package/skills/gsd-path-research/scripts/members.py +17 -3
  198. package/skills/gsd-path-research/scripts/pipeline_state.py +10 -2
  199. package/skills/gsd-path-research/scripts/state_checkpoint.py +15 -0
  200. package/skills/gsd-path-research/scripts/worktree_paths.py +45 -0
  201. package/skills/gsd-path-roadmap/references/runtime-selection.md +36 -0
  202. package/skills/gsd-path-roadmap/scripts/_common.py +76 -2
  203. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +129 -7
  204. package/skills/gsd-path-roadmap/scripts/build_recovery.py +25 -2
  205. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +437 -30
  206. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +135 -11
  207. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +46 -10
  208. package/skills/gsd-path-roadmap/scripts/integration.py +196 -39
  209. package/skills/gsd-path-roadmap/scripts/isolation.py +1015 -37
  210. package/skills/gsd-path-roadmap/scripts/loop_run.py +9 -4
  211. package/skills/gsd-path-roadmap/scripts/members.py +17 -3
  212. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +10 -2
  213. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +15 -0
  214. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +45 -0
  215. package/skills/gsd-path-ship/SKILL.md +43 -9
  216. package/skills/gsd-path-ship/references/reviewer-final.md +10 -0
  217. package/skills/gsd-path-ship/references/reviewer-wave.md +61 -22
  218. package/skills/gsd-path-ship/references/reviewer.md +8 -6
  219. package/skills/gsd-path-ship/references/runtime-selection.md +36 -0
  220. package/skills/gsd-path-ship/references/spec-probes.md +19 -1
  221. package/skills/gsd-path-ship/scripts/_common.py +76 -2
  222. package/skills/gsd-path-ship/scripts/archive_milestone.py +129 -7
  223. package/skills/gsd-path-ship/scripts/build_recovery.py +25 -2
  224. package/skills/gsd-path-ship/scripts/build_state.py +333 -8
  225. package/skills/gsd-path-ship/scripts/check_handoffs.py +437 -30
  226. package/skills/gsd-path-ship/scripts/check_task_briefs.py +135 -11
  227. package/skills/gsd-path-ship/scripts/discussion_validate.py +46 -10
  228. package/skills/gsd-path-ship/scripts/integration.py +196 -39
  229. package/skills/gsd-path-ship/scripts/isolation.py +1015 -37
  230. package/skills/gsd-path-ship/scripts/lean_verification.py +33 -14
  231. package/skills/gsd-path-ship/scripts/loop_run.py +9 -4
  232. package/skills/gsd-path-ship/scripts/members.py +17 -3
  233. package/skills/gsd-path-ship/scripts/pipeline_state.py +10 -2
  234. package/skills/gsd-path-ship/scripts/state_checkpoint.py +15 -0
  235. package/skills/gsd-path-ship/scripts/workflow_run.py +10 -3
  236. package/skills/gsd-path-ship/scripts/worktree_paths.py +45 -0
  237. package/skills/gsd-path-ship/templates/wave-review.md +24 -8
  238. package/skills/gsd-path-undo/references/runtime-selection.md +36 -0
  239. package/skills/gsd-path-undo/scripts/_common.py +76 -2
  240. package/skills/gsd-path-undo/scripts/loop_run.py +9 -4
  241. package/skills/gsd-path-undo/scripts/worktree_paths.py +45 -0
  242. package/skills/path/BUILD.md +5 -1
  243. package/skills/path/DEFINE.md +2 -1
  244. package/skills/path/PLAN.md +10 -0
  245. package/skills/path/SHIP.md +43 -9
  246. package/skills/path/SKILL.md +16 -1
  247. package/skills/path/references/build-native.md +20 -3
  248. package/skills/path/references/coder.md +6 -3
  249. package/skills/path/references/reviewer-final.md +10 -0
  250. package/skills/path/references/reviewer-wave.md +61 -22
  251. package/skills/path/references/reviewer.md +8 -6
  252. package/skills/path/references/runtime-selection.md +36 -0
  253. package/skills/path/references/spec-probes.md +19 -1
  254. package/skills/path/scripts/_common.py +76 -2
  255. package/skills/path/scripts/archive_milestone.py +129 -7
  256. package/skills/path/scripts/build_recovery.py +25 -2
  257. package/skills/path/scripts/build_state.py +333 -8
  258. package/skills/path/scripts/check_handoffs.py +437 -30
  259. package/skills/path/scripts/check_task_briefs.py +135 -11
  260. package/skills/path/scripts/discussion_validate.py +46 -10
  261. package/skills/path/scripts/dispatch_driver.py +28 -3
  262. package/skills/path/scripts/git_guard.py +44 -4
  263. package/skills/path/scripts/integration.py +196 -39
  264. package/skills/path/scripts/isolation.py +1015 -37
  265. package/skills/path/scripts/lean_verification.py +33 -14
  266. package/skills/path/scripts/loop_run.py +9 -4
  267. package/skills/path/scripts/members.py +17 -3
  268. package/skills/path/scripts/pipeline_state.py +10 -2
  269. package/skills/path/scripts/state_checkpoint.py +15 -0
  270. package/skills/path/scripts/status_runtime.py +73 -2
  271. package/skills/path/scripts/workflow_run.py +10 -3
  272. package/skills/path/scripts/worktree_paths.py +45 -0
  273. package/skills/path/templates/intent.md +5 -3
  274. package/skills/path/templates/task.md +8 -1
  275. 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
- return _run_git(repo, "cat-file", "-e", f"{base}:{path}").returncode == 0
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
- kind = _run_git(git_root, "cat-file", "-t", f"{base}:{parent}")
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
- for path in task_files:
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 entry in re.split(r"(?m)^-[ \t]+", contract):
477
- shape = " ".join(entry.split())
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 sorted(artifacts) != wave_numbers:
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 wave_numbers:
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(cycles)} Tasks: {len(tasks)} done / {len(tasks)} total "
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
- wave_count = len(cycle_counts)
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) -> 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), "location": location})
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)
@@ -2591,7 +2616,7 @@ def main(argv=None) -> int:
2591
2616
  else: # a coder dispatched by hand: derive the isolate from the task frontmatter
2592
2617
  current.receipt["landed"].append(
2593
2618
  finish_task(primary, native or state_from_task(primary, arguments.task_id)))
2594
- result = {**current.receipt, "status": "landed" if current.receipt["landed"] else "blocked"}
2619
+ result = finish_receipt(current.receipt)
2595
2620
  elif arguments.action == "answer":
2596
2621
  result = answer(primary, arguments.task_id, arguments.answer)
2597
2622
  elif arguments.action == 'reassign-model':