@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
|
@@ -144,7 +144,13 @@ def settled_tasks(repo: Path) -> dict[str, tuple[str, dict]]:
|
|
|
144
144
|
raise state.PipelineStateError(f"invalid recovery task path: {path.name}")
|
|
145
145
|
text = state._read_real_file(path, path.name)
|
|
146
146
|
fields = task_fields(text, path.name)
|
|
147
|
-
if fields.get("status")
|
|
147
|
+
if fields.get("status") == "withdrawn":
|
|
148
|
+
# A cited tombstone is settled inventory; recovery skips it (#483).
|
|
149
|
+
if not state._common.withdrawn_citations(text):
|
|
150
|
+
raise state.PipelineStateError(
|
|
151
|
+
f"withdrawn task lacks an owner citation: {path.name}"
|
|
152
|
+
)
|
|
153
|
+
elif fields.get("status") not in {"pending", "done"}:
|
|
148
154
|
raise state.PipelineStateError(f"settle task ownership before recovery: {path.name}")
|
|
149
155
|
if fields["status"] == "pending" and any(
|
|
150
156
|
fields.get(key) != "null" for key in ("agent", "base", "worktree", "task_branch")
|
|
@@ -250,7 +256,24 @@ def validate_review_backup(repo: Path, recovery: dict, directory: Path) -> None:
|
|
|
250
256
|
for path in directory.rglob("*")
|
|
251
257
|
if path.is_file() and not state._common.is_ignored_junk(path)} if directory.exists() else {}
|
|
252
258
|
if actual != expected:
|
|
253
|
-
|
|
259
|
+
extra = sorted(set(actual) - set(expected))
|
|
260
|
+
changed = sorted(
|
|
261
|
+
name for name in set(actual) & set(expected) if actual[name] != expected[name]
|
|
262
|
+
)
|
|
263
|
+
missing = sorted(set(expected) - set(actual))
|
|
264
|
+
detail = ""
|
|
265
|
+
if extra:
|
|
266
|
+
detail += f" files in the review backup that the recovery base does not have: {', '.join(extra)}."
|
|
267
|
+
if changed:
|
|
268
|
+
detail += f" files differing from the recovery base: {', '.join(changed)}."
|
|
269
|
+
if missing:
|
|
270
|
+
detail += f" files at the recovery base missing from the review backup: {', '.join(missing)}."
|
|
271
|
+
raise state.PipelineStateError(
|
|
272
|
+
"review backup differs from the recovery base." + detail
|
|
273
|
+
+ " prepare-build-recovery must run before re-planning touches any recovery"
|
|
274
|
+
" artifact; remove the extra files and restore the missing or changed ones"
|
|
275
|
+
" to the recovery base's content, then rerun prepare"
|
|
276
|
+
)
|
|
254
277
|
|
|
255
278
|
|
|
256
279
|
def prepare(repo: Path) -> dict:
|
|
@@ -21,6 +21,7 @@ if __package__:
|
|
|
21
21
|
IsolationError,
|
|
22
22
|
recover as recover_isolation,
|
|
23
23
|
verify_landed_task_files,
|
|
24
|
+
withdrawn_citations,
|
|
24
25
|
)
|
|
25
26
|
from .pipeline_git import task_commit_subject
|
|
26
27
|
from .pipeline_state import PipelineStateError, load_state
|
|
@@ -33,6 +34,7 @@ else:
|
|
|
33
34
|
IsolationError,
|
|
34
35
|
recover as recover_isolation,
|
|
35
36
|
verify_landed_task_files,
|
|
37
|
+
withdrawn_citations,
|
|
36
38
|
)
|
|
37
39
|
from pipeline_git import task_commit_subject
|
|
38
40
|
from pipeline_state import PipelineStateError, load_state
|
|
@@ -45,6 +47,7 @@ else:
|
|
|
45
47
|
IsolationError,
|
|
46
48
|
recover as recover_isolation,
|
|
47
49
|
verify_landed_task_files,
|
|
50
|
+
withdrawn_citations,
|
|
48
51
|
)
|
|
49
52
|
from scripts.pipeline_git import task_commit_subject
|
|
50
53
|
from scripts.pipeline_state import PipelineStateError, load_state
|
|
@@ -63,7 +66,7 @@ REVIEW_VERDICT_RE = re.compile(
|
|
|
63
66
|
)
|
|
64
67
|
REVIEW_FINDINGS_HEADING = "Review findings"
|
|
65
68
|
VERIFY_HEAVY_RE = re.compile(r"(?m)^Heavy:\s*(?P<value>yes|no)\s*(?:<!--.*-->)?\s*$")
|
|
66
|
-
VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked"}
|
|
69
|
+
VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked", "withdrawn"}
|
|
67
70
|
VERIFY_RESULTS = _common.VERIFY_RESULTS
|
|
68
71
|
|
|
69
72
|
|
|
@@ -271,6 +274,16 @@ def _validate_graph(tasks: Sequence[Task]) -> None:
|
|
|
271
274
|
f"{task.task_id} depends on later-wave {dependency}",
|
|
272
275
|
{"task": task.task_id, "dependency": dependency},
|
|
273
276
|
)
|
|
277
|
+
if by_id[dependency].status == "withdrawn" and task.status not in (
|
|
278
|
+
"done",
|
|
279
|
+
"withdrawn",
|
|
280
|
+
):
|
|
281
|
+
raise BuildStateError(
|
|
282
|
+
"withdrawn-dependency",
|
|
283
|
+
f"{task.task_id} depends on withdrawn {dependency}; re-point "
|
|
284
|
+
"the dependency or withdraw this task too (#483)",
|
|
285
|
+
{"task": task.task_id, "dependency": dependency},
|
|
286
|
+
)
|
|
274
287
|
|
|
275
288
|
visiting: List[str] = []
|
|
276
289
|
visited = set()
|
|
@@ -342,6 +355,15 @@ def _load_tasks(
|
|
|
342
355
|
"plan-task-mismatch",
|
|
343
356
|
f"{task.task_id} names missing PLAN.md Wave {task.wave}",
|
|
344
357
|
)
|
|
358
|
+
if task.status == "withdrawn" and not withdrawn_citations(
|
|
359
|
+
path.read_text(encoding="utf-8")):
|
|
360
|
+
raise BuildStateError(
|
|
361
|
+
"uncited-withdrawn",
|
|
362
|
+
f"{task.task_id} is withdrawn without an owner citation; append "
|
|
363
|
+
"a `- Withdrawn — <YYYY-MM-DD>: <owner ruling verbatim>` Log "
|
|
364
|
+
"entry (#483)",
|
|
365
|
+
{"task": task.task_id},
|
|
366
|
+
)
|
|
345
367
|
parsed.append(task)
|
|
346
368
|
missing_waves = sorted(set(plan_waves) - {task.wave for task in parsed})
|
|
347
369
|
if missing_waves:
|
|
@@ -431,6 +453,10 @@ def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]
|
|
|
431
453
|
seen_worktrees: Dict[str, str] = {}
|
|
432
454
|
seen_branches: Dict[str, str] = {}
|
|
433
455
|
for task in project.tasks:
|
|
456
|
+
if task.status == "withdrawn":
|
|
457
|
+
# A tombstone is inventory, not work: no dispatch metadata is
|
|
458
|
+
# expected and recovery reports it as none (#483).
|
|
459
|
+
continue
|
|
434
460
|
if task.status == "pending":
|
|
435
461
|
if any(
|
|
436
462
|
value is not None
|
|
@@ -479,7 +505,10 @@ def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]
|
|
|
479
505
|
_invalid_state(task, f"worktree is also owned by {previous}")
|
|
480
506
|
continue
|
|
481
507
|
if task.status == "done":
|
|
482
|
-
if any(
|
|
508
|
+
if any(
|
|
509
|
+
by_id[dependency].status not in ("done", "withdrawn")
|
|
510
|
+
for dependency in task.deps
|
|
511
|
+
):
|
|
483
512
|
_invalid_state(task, "done task has an incomplete dependency")
|
|
484
513
|
if task.agent is None or task.base is None:
|
|
485
514
|
_invalid_state(task, "done task is missing agent or base")
|
|
@@ -679,7 +708,11 @@ def _unreviewed_waves(project: Project, waves: Sequence[int]) -> List[int]:
|
|
|
679
708
|
review_dir = project.tasks_dir.parent / "review"
|
|
680
709
|
reviewed = {
|
|
681
710
|
wave for wave in waves
|
|
682
|
-
if all(
|
|
711
|
+
if all(
|
|
712
|
+
task.review_findings
|
|
713
|
+
for task in project.tasks
|
|
714
|
+
if task.wave == wave and task.status != "withdrawn"
|
|
715
|
+
)
|
|
683
716
|
}
|
|
684
717
|
if review_dir.is_dir() and not review_dir.is_symlink():
|
|
685
718
|
for path in review_dir.iterdir():
|
|
@@ -696,7 +729,12 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
696
729
|
project = _load_project(repo, project_dir, ("active", "done") if allow_done else ("active",))
|
|
697
730
|
reports = _validate_ready_metadata(project)
|
|
698
731
|
by_id = {task.task_id: task for task in project.tasks}
|
|
699
|
-
|
|
732
|
+
# A withdrawn tombstone carries no obligation: its wave is finished
|
|
733
|
+
# unless live work remains in it (#483).
|
|
734
|
+
unfinished_waves = sorted({
|
|
735
|
+
task.wave for task in project.tasks
|
|
736
|
+
if task.status != "done" and task.status != "withdrawn"
|
|
737
|
+
})
|
|
700
738
|
current_wave = unfinished_waves[0] if unfinished_waves else None
|
|
701
739
|
blocked_reviews = _blocked_wave_reviews(project)
|
|
702
740
|
blocked_wave = min(
|
|
@@ -705,7 +743,10 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
705
743
|
for wave in blocked_reviews
|
|
706
744
|
if (current_wave is None or wave < current_wave)
|
|
707
745
|
and any(task.wave == wave for task in project.tasks)
|
|
708
|
-
and all(
|
|
746
|
+
and all(
|
|
747
|
+
task.status in ("done", "withdrawn")
|
|
748
|
+
for task in project.tasks if task.wave == wave
|
|
749
|
+
)
|
|
709
750
|
),
|
|
710
751
|
default=None,
|
|
711
752
|
)
|
|
@@ -716,7 +757,7 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
716
757
|
repairs = [
|
|
717
758
|
task
|
|
718
759
|
for task in project.tasks
|
|
719
|
-
if task.status != "done"
|
|
760
|
+
if task.status != "done" and task.status != "withdrawn"
|
|
720
761
|
and task.wave > blocked_wave
|
|
721
762
|
and task.review_findings
|
|
722
763
|
and source_ids.intersection(task.deps)
|
|
@@ -785,7 +826,8 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
785
826
|
],
|
|
786
827
|
}
|
|
787
828
|
for task in project.tasks
|
|
788
|
-
if task.wave == current_wave
|
|
829
|
+
if task.wave == current_wave
|
|
830
|
+
and task.status != "done" and task.status != "withdrawn"
|
|
789
831
|
]
|
|
790
832
|
raise BuildStateError(
|
|
791
833
|
"dependency-deadlock",
|
|
@@ -946,6 +988,8 @@ def verify_landed_tasks(
|
|
|
946
988
|
canonical_project_dir=canonical_dir,
|
|
947
989
|
)
|
|
948
990
|
for task in tasks:
|
|
991
|
+
if task.status == "withdrawn":
|
|
992
|
+
continue # A cited tombstone ships without a landing (#483)
|
|
949
993
|
if task.status != "done":
|
|
950
994
|
_invalid_state(task, "task must have status: done before shipping")
|
|
951
995
|
try:
|
|
@@ -965,6 +1009,8 @@ def verify_landed_tasks(
|
|
|
965
1009
|
}
|
|
966
1010
|
evidence = []
|
|
967
1011
|
for task in tasks:
|
|
1012
|
+
if task.status == "withdrawn":
|
|
1013
|
+
continue # A cited tombstone has no landing to prove (#483)
|
|
968
1014
|
report = reports.get(task.task_id)
|
|
969
1015
|
if report is None or report.get("verdict") not in LANDED_VERDICTS:
|
|
970
1016
|
_invalid_state(task, "task landing is not canonical")
|
|
@@ -1044,10 +1090,13 @@ def _ledger_entries(path: Path) -> List[Dict[str, object]]:
|
|
|
1044
1090
|
|
|
1045
1091
|
def verify_record(repo: str, command: str, commit: str, result: str,
|
|
1046
1092
|
execution: Optional[Dict[str, object]] = None,
|
|
1047
|
-
member: Optional[str] = None
|
|
1093
|
+
member: Optional[str] = None,
|
|
1094
|
+
rerun: bool = False) -> Dict[str, object]:
|
|
1048
1095
|
"""Append one verify run (command, commit, result, timestamp) to the ledger.
|
|
1049
1096
|
|
|
1050
1097
|
A member run names its member and a commit in that member's repository.
|
|
1098
|
+
``rerun`` marks an owner-requested re-execution at the same command and
|
|
1099
|
+
commit, so the ledger shows the earlier entry was superseded, not erased.
|
|
1051
1100
|
"""
|
|
1052
1101
|
|
|
1053
1102
|
repository = _repo_root(repo)
|
|
@@ -1066,6 +1115,7 @@ def verify_record(repo: str, command: str, commit: str, result: str,
|
|
|
1066
1115
|
"result": result,
|
|
1067
1116
|
"recorded_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
1068
1117
|
**({"repo": member} if member else {}),
|
|
1118
|
+
**({"rerun": True} if rerun else {}),
|
|
1069
1119
|
}
|
|
1070
1120
|
if execution is not None:
|
|
1071
1121
|
if (not isinstance(execution.get("stdout"), str)
|
|
@@ -1073,6 +1123,18 @@ def verify_record(repo: str, command: str, commit: str, result: str,
|
|
|
1073
1123
|
or type(execution.get("exit_code")) is not int
|
|
1074
1124
|
or (execution["exit_code"] == 0) != (result == "pass")):
|
|
1075
1125
|
raise BuildStateError("invalid-execution", "verification output and result disagree")
|
|
1126
|
+
if "cwd" in execution and not isinstance(execution["cwd"], str):
|
|
1127
|
+
raise BuildStateError("invalid-execution", "execution cwd must be a path string")
|
|
1128
|
+
if "base" in execution and not re.fullmatch(r"[0-9a-f]{40}", str(execution["base"])):
|
|
1129
|
+
raise BuildStateError("invalid-execution", "execution base must be a commit sha")
|
|
1130
|
+
location = execution.get("location")
|
|
1131
|
+
if isinstance(location, str) and "cwd" in execution and "worktree" in execution:
|
|
1132
|
+
if location.startswith("sidecar") and execution["cwd"] == execution["worktree"]:
|
|
1133
|
+
raise BuildStateError("invalid-execution",
|
|
1134
|
+
"a sidecar Verify cannot name the task checkout as its directory")
|
|
1135
|
+
if location.startswith("isolate") and execution["cwd"] != execution["worktree"]:
|
|
1136
|
+
raise BuildStateError("invalid-execution",
|
|
1137
|
+
"an isolate Verify must name its task worktree as its directory")
|
|
1076
1138
|
entry["execution"] = execution
|
|
1077
1139
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1078
1140
|
previous = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
@@ -1095,6 +1157,255 @@ def verify_lookup(repo: str, command: str, commit: str, member: Optional[str] =
|
|
|
1095
1157
|
}
|
|
1096
1158
|
|
|
1097
1159
|
|
|
1160
|
+
VERIFY_LOG_RE = re.compile(r"orchestrator Verify \((?:isolate|sidecar) [^)]+\): (pass|fail), exit \d+")
|
|
1161
|
+
MEMBER_RECORD_RE = re.compile(r"^Member: (\S+) ([0-9a-f]{40}) ([0-9a-f]{40})$", re.MULTILINE)
|
|
1162
|
+
GAP_MEMBER_RE = re.compile(r"^Member reviewed HEAD: (\S+) ([0-9a-f]{40})$", re.MULTILINE)
|
|
1163
|
+
GAP_HEAD_RE = re.compile(r"^Reviewed HEAD: ([0-9a-f]{40})$", re.MULTILINE)
|
|
1164
|
+
GAP_VERDICT_RE = re.compile(r"^Gap verdict: (pass|blocked)$", re.MULTILINE)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _proven_landings(
|
|
1168
|
+
repository: Path,
|
|
1169
|
+
done_paths: Dict[str, Path],
|
|
1170
|
+
canonical_tasks_dir: str,
|
|
1171
|
+
head: str,
|
|
1172
|
+
) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
1173
|
+
"""Map each provably landed task id to its landing commit and record proof failures."""
|
|
1174
|
+
|
|
1175
|
+
def collect(payload: Mapping[str, object]) -> Dict[str, str]:
|
|
1176
|
+
proven: Dict[str, str] = {}
|
|
1177
|
+
for item in payload["tasks"]: # type: ignore[index]
|
|
1178
|
+
if not isinstance(item, Mapping):
|
|
1179
|
+
continue
|
|
1180
|
+
if str(item.get("verdict")) in LANDED_VERDICTS and item.get("commit"):
|
|
1181
|
+
proven[str(item.get("task_id"))] = str(item["commit"])
|
|
1182
|
+
return proven
|
|
1183
|
+
|
|
1184
|
+
try:
|
|
1185
|
+
batch = verify_landed_task_files(repository, list(done_paths.values()), canonical_tasks_dir, head)
|
|
1186
|
+
return collect(batch), {}
|
|
1187
|
+
except IsolationError:
|
|
1188
|
+
pass
|
|
1189
|
+
proven: Dict[str, str] = {}
|
|
1190
|
+
errors: Dict[str, str] = {}
|
|
1191
|
+
for task_id, path in sorted(done_paths.items()):
|
|
1192
|
+
try:
|
|
1193
|
+
single = verify_landed_task_files(repository, [path], canonical_tasks_dir, head)
|
|
1194
|
+
except IsolationError as error:
|
|
1195
|
+
errors[task_id] = str(error)
|
|
1196
|
+
continue
|
|
1197
|
+
found = collect(single)
|
|
1198
|
+
if task_id in found:
|
|
1199
|
+
proven[task_id] = found[task_id]
|
|
1200
|
+
return proven, errors
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _ledger_absence_reason(
|
|
1204
|
+
repository: Path,
|
|
1205
|
+
task: Task,
|
|
1206
|
+
landing: str,
|
|
1207
|
+
member_landing: Optional[str],
|
|
1208
|
+
member_base: Optional[str],
|
|
1209
|
+
) -> str:
|
|
1210
|
+
"""Explain why a passing task has no reusable ledger row."""
|
|
1211
|
+
|
|
1212
|
+
if task.repo:
|
|
1213
|
+
if not member_landing or not member_base:
|
|
1214
|
+
return "member record does not name the member landing"
|
|
1215
|
+
try:
|
|
1216
|
+
checkout = _member_checkout(repository, task.repo)
|
|
1217
|
+
parent = _git_output(checkout, "rev-parse", f"{member_landing}^")
|
|
1218
|
+
except BuildStateError:
|
|
1219
|
+
return "member checkout unavailable — the record condition cannot be re-derived"
|
|
1220
|
+
return ("ledger row absent although the member landing meets the record condition"
|
|
1221
|
+
if parent == member_base else
|
|
1222
|
+
"member landing parent differs from the member base — row deliberately not written")
|
|
1223
|
+
parent = _git_output(repository, "rev-parse", f"{landing}^")
|
|
1224
|
+
return ("ledger row absent although the landing meets the record condition"
|
|
1225
|
+
if parent == task.base else
|
|
1226
|
+
"landing parent differs from the recorded base — row deliberately not written")
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def evidence_manifest(
|
|
1230
|
+
repo: str,
|
|
1231
|
+
project_dir: str,
|
|
1232
|
+
head: str,
|
|
1233
|
+
canonical_project_dir: str = DEFAULT_PROJECT_DIR,
|
|
1234
|
+
) -> Dict[str, object]:
|
|
1235
|
+
"""Join every task to its authoritative accepted evidence and classify the result.
|
|
1236
|
+
|
|
1237
|
+
States: `ledger` (a reusable verify-ledger row matches the task's Verify command
|
|
1238
|
+
at its proven landing — for member tasks, at the member landing recorded in the
|
|
1239
|
+
coordinator record body), `log-only` (the retained task Log records the isolated
|
|
1240
|
+
Verify pass and the landing is proven, but no reusable row exists), `rejected`
|
|
1241
|
+
(the Log records a failed Verify, or a ledger row at the landing failed), and
|
|
1242
|
+
`superseded` / `missing` (a plan-approved supersedes retired the Verify, or no
|
|
1243
|
+
durable evidence exists at this head), plus `withdrawn` (a cited owner-approved
|
|
1244
|
+
tombstone that never owed a landing — distinct from every completed state). A pass is never inferred from a Log label
|
|
1245
|
+
alone: done tasks require a proven first-parent landing, and member rows must
|
|
1246
|
+
match the recorded member landing. The project section reports the member
|
|
1247
|
+
milestone project Verify only when the project Verify gap (final-gap-1.md)
|
|
1248
|
+
records a passing verdict at exactly this head (`gap-embedded`), or a reusable ledger row matches PLAN's
|
|
1249
|
+
project Verify command at this head (`ledger`); a blocked gap verdict or a
|
|
1250
|
+
different reviewed HEAD is listed but never accepted. Limits: failed-attempt
|
|
1251
|
+
executions kept only in uncommitted dispatch records are not consulted, and a
|
|
1252
|
+
noncanonical task artifact aborts with `invalid-task-file` naming the file — the
|
|
1253
|
+
same strictness as every other build_state command; fix or flag the file first.
|
|
1254
|
+
"""
|
|
1255
|
+
|
|
1256
|
+
repository = _repo_root(repo)
|
|
1257
|
+
artifact_dir, artifact_path = _project_path(repository, project_dir)
|
|
1258
|
+
canonical_dir, _ = _project_path(repository, canonical_project_dir)
|
|
1259
|
+
if not FULL_SHA_RE.fullmatch(head) or not _commit_resolves(repository, head):
|
|
1260
|
+
raise BuildStateError("invalid-head", "--head must be a full, existing commit SHA")
|
|
1261
|
+
if _git_output(repository, "rev-parse", f"{head}^{{commit}}") != head:
|
|
1262
|
+
raise BuildStateError("invalid-head", "--head must resolve to itself")
|
|
1263
|
+
tasks = _load_tasks(
|
|
1264
|
+
repository, artifact_dir, artifact_path, canonical_project_dir=canonical_dir
|
|
1265
|
+
)
|
|
1266
|
+
superseded_by: Dict[str, str] = {}
|
|
1267
|
+
for task in tasks:
|
|
1268
|
+
if task.status != "done":
|
|
1269
|
+
continue
|
|
1270
|
+
for earlier in task.supersedes:
|
|
1271
|
+
superseded_by.setdefault(earlier, task.task_id)
|
|
1272
|
+
done_paths = {
|
|
1273
|
+
task.task_id: artifact_path / "tasks" / Path(task.task_file).name
|
|
1274
|
+
for task in tasks if task.status == "done"
|
|
1275
|
+
}
|
|
1276
|
+
canonical_tasks_dir = str(PurePosixPath(canonical_dir) / "tasks")
|
|
1277
|
+
proven, proof_errors = (
|
|
1278
|
+
_proven_landings(repository, done_paths, canonical_tasks_dir, head) if done_paths else ({}, {})
|
|
1279
|
+
)
|
|
1280
|
+
ledger_path = _ledger_path(repository)
|
|
1281
|
+
entries = _ledger_entries(ledger_path) if ledger_path.exists() else []
|
|
1282
|
+
manifest: List[Dict[str, object]] = []
|
|
1283
|
+
for task in tasks:
|
|
1284
|
+
text = _read_file(
|
|
1285
|
+
artifact_path / "tasks" / Path(task.task_file).name,
|
|
1286
|
+
f"{artifact_dir}/tasks/{Path(task.task_file).name}",
|
|
1287
|
+
)
|
|
1288
|
+
results = VERIFY_LOG_RE.findall(text)
|
|
1289
|
+
entry: Dict[str, object] = {
|
|
1290
|
+
"task": task.task_id,
|
|
1291
|
+
"title": task.title,
|
|
1292
|
+
"status": task.status,
|
|
1293
|
+
"repo": task.repo or None,
|
|
1294
|
+
"verify_command": _common.task_verify_command(text),
|
|
1295
|
+
"superseded_by": superseded_by.get(task.task_id),
|
|
1296
|
+
}
|
|
1297
|
+
manifest.append(entry)
|
|
1298
|
+
if task.status == "withdrawn":
|
|
1299
|
+
# The tombstone stays distinct from completed acceptance: the
|
|
1300
|
+
# manifest records the withdrawal and its owner citations (#483).
|
|
1301
|
+
entry["state"] = "withdrawn"
|
|
1302
|
+
entry["reason"] = "; ".join(withdrawn_citations(text))
|
|
1303
|
+
continue
|
|
1304
|
+
if task.status != "done":
|
|
1305
|
+
entry["state"] = "rejected" if "fail" in results else "missing"
|
|
1306
|
+
entry["reason"] = "verify failed" if "fail" in results else "task never landed"
|
|
1307
|
+
if "fail" in results:
|
|
1308
|
+
entry["receipt"] = {"source": "task-log", "path": task.task_file}
|
|
1309
|
+
continue
|
|
1310
|
+
landing = proven.get(task.task_id)
|
|
1311
|
+
if landing is None:
|
|
1312
|
+
entry["state"] = "missing"
|
|
1313
|
+
entry["reason"] = ("landing proof failed: " + proof_errors[task.task_id]
|
|
1314
|
+
if task.task_id in proof_errors
|
|
1315
|
+
else "done task without a provable first-parent landing at this head")
|
|
1316
|
+
continue
|
|
1317
|
+
entry["landing"] = landing
|
|
1318
|
+
member_landing: Optional[str] = None
|
|
1319
|
+
member_base: Optional[str] = None
|
|
1320
|
+
if task.repo:
|
|
1321
|
+
body = _git_output(repository, "log", "-1", "--format=%b", landing)
|
|
1322
|
+
record = MEMBER_RECORD_RE.search(body)
|
|
1323
|
+
if record is None or record.group(1) != task.repo:
|
|
1324
|
+
entry["state"] = "missing"
|
|
1325
|
+
entry["reason"] = "member record commit does not name this member's landing"
|
|
1326
|
+
continue
|
|
1327
|
+
member_landing, member_base = record.group(2), record.group(3)
|
|
1328
|
+
lookup_commit = member_landing or landing
|
|
1329
|
+
if not entry["verify_command"]:
|
|
1330
|
+
entry["state"] = "missing"
|
|
1331
|
+
entry["reason"] = "landed task without a Verify command"
|
|
1332
|
+
continue
|
|
1333
|
+
try:
|
|
1334
|
+
lookup = verify_lookup(str(repository), str(entry["verify_command"]), lookup_commit,
|
|
1335
|
+
task.repo or None)
|
|
1336
|
+
except BuildStateError:
|
|
1337
|
+
lookup = None
|
|
1338
|
+
if lookup is not None and lookup["hit"]:
|
|
1339
|
+
entry["state"] = "ledger" if lookup["reuse"] else "rejected"
|
|
1340
|
+
entry["receipt"] = {"source": "verify-ledger", "commit": lookup_commit}
|
|
1341
|
+
elif "pass" in results:
|
|
1342
|
+
entry["state"] = "log-only"
|
|
1343
|
+
entry["receipt"] = {"source": "task-log", "path": task.task_file}
|
|
1344
|
+
entry["reason"] = _ledger_absence_reason(
|
|
1345
|
+
repository, task, landing, member_landing, member_base)
|
|
1346
|
+
elif entry["superseded_by"] in proven:
|
|
1347
|
+
entry["state"] = "superseded"
|
|
1348
|
+
entry["reason"] = (f"verify retired by {entry['superseded_by']}; "
|
|
1349
|
+
"the superseding task carries the evidence")
|
|
1350
|
+
else:
|
|
1351
|
+
entry["state"] = "missing"
|
|
1352
|
+
entry["reason"] = "proven landing without a recorded Verify result"
|
|
1353
|
+
if entry["superseded_by"] and entry["superseded_by"] not in proven:
|
|
1354
|
+
note = f"superseding task {entry['superseded_by']} is not landed at this head"
|
|
1355
|
+
entry["reason"] = f"{entry['reason']}; {note}" if entry.get("reason") else note
|
|
1356
|
+
gap_members: List[Dict[str, object]] = []
|
|
1357
|
+
gap_accepted = False
|
|
1358
|
+
gap_reasons: List[str] = []
|
|
1359
|
+
gap_path = repository.joinpath(*PurePosixPath(canonical_dir).parts) / "review" / "final-gap-1.md"
|
|
1360
|
+
if gap_path.is_file() and not gap_path.is_symlink():
|
|
1361
|
+
text = gap_path.read_text(encoding="utf-8")
|
|
1362
|
+
verdict = GAP_VERDICT_RE.findall(text)
|
|
1363
|
+
reviewed = GAP_HEAD_RE.findall(text)
|
|
1364
|
+
for name, sha in GAP_MEMBER_RE.findall(text):
|
|
1365
|
+
gap_members.append({
|
|
1366
|
+
"member": name,
|
|
1367
|
+
"head": sha,
|
|
1368
|
+
"verdict": verdict[-1] if verdict else None,
|
|
1369
|
+
"source": f"{canonical_dir}/review/{gap_path.name}",
|
|
1370
|
+
})
|
|
1371
|
+
if gap_members:
|
|
1372
|
+
if verdict and verdict[-1] == "pass" and reviewed and reviewed[-1] == head:
|
|
1373
|
+
gap_accepted = True
|
|
1374
|
+
else:
|
|
1375
|
+
gap_reasons.append(
|
|
1376
|
+
f"{gap_path.name}: verdict {verdict[-1] if verdict else 'unreadable'}"
|
|
1377
|
+
f" at reviewed HEAD {reviewed[-1] if reviewed else 'unreadable'}")
|
|
1378
|
+
plan = _read_file(artifact_path / "plan" / "PLAN.md", f"{artifact_dir}/plan/PLAN.md")
|
|
1379
|
+
try:
|
|
1380
|
+
project_command = check_handoffs._line_value(plan, "Project verify:")
|
|
1381
|
+
except check_handoffs.HandoffError:
|
|
1382
|
+
project_command = ""
|
|
1383
|
+
project_state = "missing"
|
|
1384
|
+
project_reason: Optional[str] = None
|
|
1385
|
+
if gap_accepted:
|
|
1386
|
+
project_state = "gap-embedded"
|
|
1387
|
+
elif gap_reasons:
|
|
1388
|
+
project_reason = "; ".join(gap_reasons)
|
|
1389
|
+
elif project_command:
|
|
1390
|
+
project_lookup = verify_lookup(str(repository), project_command, head)
|
|
1391
|
+
if project_lookup["reuse"]:
|
|
1392
|
+
project_state = "ledger"
|
|
1393
|
+
elif project_lookup["hit"]:
|
|
1394
|
+
project_reason = "the recorded project Verify row at this head failed"
|
|
1395
|
+
return {
|
|
1396
|
+
"command": "evidence-manifest",
|
|
1397
|
+
"project_dir": artifact_dir,
|
|
1398
|
+
"canonical_project_dir": canonical_dir,
|
|
1399
|
+
"head": head,
|
|
1400
|
+
"tasks": manifest,
|
|
1401
|
+
"project": {
|
|
1402
|
+
"members": gap_members or None,
|
|
1403
|
+
"state": project_state,
|
|
1404
|
+
**({"reason": project_reason} if project_reason else {}),
|
|
1405
|
+
},
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
|
|
1098
1409
|
def _parser() -> argparse.ArgumentParser:
|
|
1099
1410
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1100
1411
|
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
@@ -1127,6 +1438,13 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
1127
1438
|
lookup_parser.add_argument("--repo", required=True)
|
|
1128
1439
|
lookup_parser.add_argument("--command", required=True, dest="verify_command")
|
|
1129
1440
|
lookup_parser.add_argument("--commit", required=True)
|
|
1441
|
+
manifest_parser = subcommands.add_parser(
|
|
1442
|
+
"evidence-manifest", help="join every task to its authoritative accepted evidence"
|
|
1443
|
+
)
|
|
1444
|
+
manifest_parser.add_argument("--repo", required=True)
|
|
1445
|
+
manifest_parser.add_argument("--project-dir", default=DEFAULT_PROJECT_DIR)
|
|
1446
|
+
manifest_parser.add_argument("--head", required=True)
|
|
1447
|
+
manifest_parser.add_argument("--canonical-project-dir", default=DEFAULT_PROJECT_DIR)
|
|
1130
1448
|
return parser
|
|
1131
1449
|
|
|
1132
1450
|
|
|
@@ -1143,6 +1461,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
1143
1461
|
)
|
|
1144
1462
|
elif arguments.command == "verify-lookup":
|
|
1145
1463
|
result = verify_lookup(arguments.repo, arguments.verify_command, arguments.commit)
|
|
1464
|
+
elif arguments.command == "evidence-manifest":
|
|
1465
|
+
result = evidence_manifest(
|
|
1466
|
+
arguments.repo,
|
|
1467
|
+
arguments.project_dir,
|
|
1468
|
+
arguments.head,
|
|
1469
|
+
arguments.canonical_project_dir,
|
|
1470
|
+
)
|
|
1146
1471
|
else:
|
|
1147
1472
|
result = verify_landed_tasks(
|
|
1148
1473
|
arguments.repo,
|