@opengsd/gsd-path 1.6.0 → 1.6.2
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/AGENTS.md +1 -1
- package/CHANGELOG.md +44 -0
- package/FULL.md +5 -4
- package/HOOKS.md +18 -2
- package/QUICK.md +1 -1
- package/README.md +12 -28
- package/WORKFLOW.md +8 -4
- package/package.json +1 -1
- package/scripts/_common.py +43 -0
- package/scripts/check_task_briefs.py +10 -7
- package/scripts/dispatch_driver.py +17 -5
- package/scripts/git_guard.py +18 -2
- package/scripts/integration.py +17 -4
- package/scripts/isolation.py +66 -14
- package/scripts/lean_verification.py +5 -1
- package/scripts/model_policy.py +8 -1
- package/scripts/pipeline_git.py +19 -5
- package/scripts/pipeline_state.py +3 -1
- package/scripts/state_promote.py +77 -1
- package/skills/gsd-path/BUILD.md +3 -1
- package/skills/gsd-path/PLAN.md +3 -2
- package/skills/gsd-path/SHIP.md +14 -8
- package/skills/gsd-path/references/model-policy.md +3 -1
- package/skills/gsd-path/references/operating-rules.md +3 -2
- package/skills/gsd-path/references/runtime-selection.md +2 -1
- package/skills/gsd-path/scripts/_common.py +43 -0
- package/skills/gsd-path/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path/scripts/dispatch_driver.py +17 -5
- package/skills/gsd-path/scripts/git_guard.py +18 -2
- package/skills/gsd-path/scripts/integration.py +17 -4
- package/skills/gsd-path/scripts/isolation.py +66 -14
- package/skills/gsd-path/scripts/lean_verification.py +5 -1
- package/skills/gsd-path/scripts/model_policy.py +8 -1
- package/skills/gsd-path/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path/scripts/state_promote.py +77 -1
- package/skills/gsd-path-build/SKILL.md +3 -1
- package/skills/gsd-path-build/references/model-policy.md +3 -1
- package/skills/gsd-path-build/references/operating-rules.md +3 -2
- package/skills/gsd-path-build/references/runtime-selection.md +2 -1
- package/skills/gsd-path-build/scripts/_common.py +43 -0
- package/skills/gsd-path-build/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-build/scripts/dispatch_driver.py +17 -5
- package/skills/gsd-path-build/scripts/integration.py +17 -4
- package/skills/gsd-path-build/scripts/isolation.py +66 -14
- package/skills/gsd-path-build/scripts/model_policy.py +8 -1
- package/skills/gsd-path-build/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-build/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-build/scripts/state_promote.py +77 -1
- package/skills/gsd-path-decide/references/model-policy.md +3 -1
- package/skills/gsd-path-decide/references/operating-rules.md +3 -2
- package/skills/gsd-path-decide/references/runtime-selection.md +2 -1
- package/skills/gsd-path-decide/scripts/_common.py +43 -0
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-decide/scripts/integration.py +17 -4
- package/skills/gsd-path-decide/scripts/isolation.py +66 -14
- package/skills/gsd-path-decide/scripts/model_policy.py +8 -1
- package/skills/gsd-path-decide/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-decide/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-decide/scripts/state_promote.py +77 -1
- package/skills/gsd-path-define/references/operating-rules.md +3 -2
- package/skills/gsd-path-define/references/runtime-selection.md +2 -1
- package/skills/gsd-path-define/scripts/_common.py +43 -0
- package/skills/gsd-path-define/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-define/scripts/integration.py +17 -4
- package/skills/gsd-path-define/scripts/isolation.py +66 -14
- package/skills/gsd-path-define/scripts/model_policy.py +8 -1
- package/skills/gsd-path-define/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-define/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-define/scripts/state_promote.py +77 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +3 -2
- package/skills/gsd-path-discuss/references/runtime-selection.md +2 -1
- package/skills/gsd-path-discuss/scripts/_common.py +43 -0
- package/skills/gsd-path-discuss/scripts/model_policy.py +8 -1
- package/skills/gsd-path-docs-audit/references/model-policy.md +3 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +3 -2
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +2 -1
- package/skills/gsd-path-docs-audit/scripts/_common.py +43 -0
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-docs-audit/scripts/integration.py +17 -4
- package/skills/gsd-path-docs-audit/scripts/isolation.py +66 -14
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +8 -1
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +77 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +3 -2
- package/skills/gsd-path-forensics/references/runtime-selection.md +2 -1
- package/skills/gsd-path-forensics/scripts/_common.py +43 -0
- package/skills/gsd-path-forensics/scripts/model_policy.py +8 -1
- package/skills/gsd-path-inspect/references/model-policy.md +3 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +3 -2
- package/skills/gsd-path-inspect/references/runtime-selection.md +2 -1
- package/skills/gsd-path-inspect/scripts/_common.py +43 -0
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-inspect/scripts/integration.py +17 -4
- package/skills/gsd-path-inspect/scripts/isolation.py +66 -14
- package/skills/gsd-path-inspect/scripts/model_policy.py +8 -1
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-inspect/scripts/state_promote.py +77 -1
- package/skills/gsd-path-loop/references/operating-rules.md +3 -2
- package/skills/gsd-path-loop/references/runtime-selection.md +2 -1
- package/skills/gsd-path-loop/scripts/_common.py +43 -0
- package/skills/gsd-path-loop/scripts/model_policy.py +8 -1
- package/skills/gsd-path-migrate/references/operating-rules.md +3 -2
- package/skills/gsd-path-migrate/references/runtime-selection.md +2 -1
- package/skills/gsd-path-migrate/scripts/_common.py +43 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +8 -1
- package/skills/gsd-path-plan/SKILL.md +3 -2
- package/skills/gsd-path-plan/references/model-policy.md +3 -1
- package/skills/gsd-path-plan/references/operating-rules.md +3 -2
- package/skills/gsd-path-plan/references/runtime-selection.md +2 -1
- package/skills/gsd-path-plan/scripts/_common.py +43 -0
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-plan/scripts/integration.py +17 -4
- package/skills/gsd-path-plan/scripts/isolation.py +66 -14
- package/skills/gsd-path-plan/scripts/model_policy.py +8 -1
- package/skills/gsd-path-plan/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-plan/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-plan/scripts/state_promote.py +77 -1
- package/skills/gsd-path-research/references/model-policy.md +3 -1
- package/skills/gsd-path-research/references/operating-rules.md +3 -2
- package/skills/gsd-path-research/references/runtime-selection.md +2 -1
- package/skills/gsd-path-research/scripts/_common.py +43 -0
- package/skills/gsd-path-research/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-research/scripts/integration.py +17 -4
- package/skills/gsd-path-research/scripts/isolation.py +66 -14
- package/skills/gsd-path-research/scripts/model_policy.py +8 -1
- package/skills/gsd-path-research/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-research/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-research/scripts/state_promote.py +77 -1
- package/skills/gsd-path-roadmap/references/model-policy.md +3 -1
- package/skills/gsd-path-roadmap/references/operating-rules.md +3 -2
- package/skills/gsd-path-roadmap/references/runtime-selection.md +2 -1
- package/skills/gsd-path-roadmap/scripts/_common.py +43 -0
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-roadmap/scripts/integration.py +17 -4
- package/skills/gsd-path-roadmap/scripts/isolation.py +66 -14
- package/skills/gsd-path-roadmap/scripts/model_policy.py +8 -1
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-roadmap/scripts/state_promote.py +77 -1
- package/skills/gsd-path-ship/SKILL.md +14 -8
- package/skills/gsd-path-ship/references/model-policy.md +3 -1
- package/skills/gsd-path-ship/references/operating-rules.md +3 -2
- package/skills/gsd-path-ship/references/runtime-selection.md +2 -1
- package/skills/gsd-path-ship/scripts/_common.py +43 -0
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-ship/scripts/integration.py +17 -4
- package/skills/gsd-path-ship/scripts/isolation.py +66 -14
- package/skills/gsd-path-ship/scripts/lean_verification.py +5 -1
- package/skills/gsd-path-ship/scripts/model_policy.py +8 -1
- package/skills/gsd-path-ship/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-ship/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-ship/scripts/state_promote.py +77 -1
- package/skills/gsd-path-undo/references/operating-rules.md +3 -2
- package/skills/gsd-path-undo/references/runtime-selection.md +2 -1
- package/skills/gsd-path-undo/scripts/_common.py +43 -0
- package/skills/gsd-path-undo/scripts/model_policy.py +8 -1
- package/skills/path/BUILD.md +3 -1
- package/skills/path/PLAN.md +3 -2
- package/skills/path/SHIP.md +14 -8
- package/skills/path/references/model-policy.md +3 -1
- package/skills/path/references/operating-rules.md +3 -2
- package/skills/path/references/runtime-selection.md +2 -1
- package/skills/path/scripts/_common.py +43 -0
- package/skills/path/scripts/check_task_briefs.py +10 -7
- package/skills/path/scripts/dispatch_driver.py +17 -5
- package/skills/path/scripts/git_guard.py +18 -2
- package/skills/path/scripts/integration.py +17 -4
- package/skills/path/scripts/isolation.py +66 -14
- package/skills/path/scripts/lean_verification.py +5 -1
- package/skills/path/scripts/model_policy.py +8 -1
- package/skills/path/scripts/pipeline_git.py +19 -5
- package/skills/path/scripts/pipeline_state.py +3 -1
- package/skills/path/scripts/state_promote.py +77 -1
package/scripts/model_policy.py
CHANGED
|
@@ -196,7 +196,14 @@ def task_settings(path):
|
|
|
196
196
|
|
|
197
197
|
def native_record(args):
|
|
198
198
|
"""The parent owns lifecycle; release requires its recorded terminal-result receipt."""
|
|
199
|
+
# A relative path would resolve against cwd, which may be the immutable runtime store.
|
|
200
|
+
if not args.record.is_absolute():
|
|
201
|
+
raise PolicyError(f'--record must be an absolute path: {args.record}')
|
|
199
202
|
record = args.record.resolve()
|
|
203
|
+
missing = f'release requires an existing record and terminal-result evidence: {record}'
|
|
204
|
+
# Check before log_lock creates the record directory and its lock file.
|
|
205
|
+
if args.action == 'release' and not record.exists():
|
|
206
|
+
raise PolicyError(missing)
|
|
200
207
|
with log_lock({'log': str(record)}):
|
|
201
208
|
data = read_json(record) if record.exists() else None
|
|
202
209
|
identity = {'repo': str(args.repo.resolve()), 'project_dir': args.project_dir,
|
|
@@ -209,7 +216,7 @@ def native_record(args):
|
|
|
209
216
|
raise PolicyError(f'record belongs to another assignment or milestone/track/cycle ({differing})')
|
|
210
217
|
if args.action == 'release':
|
|
211
218
|
if data is None or not args.terminal_result:
|
|
212
|
-
raise PolicyError(
|
|
219
|
+
raise PolicyError(missing)
|
|
213
220
|
data.update(active=False, terminal_result=args.terminal_result)
|
|
214
221
|
else:
|
|
215
222
|
if args.capabilities is None:
|
package/scripts/pipeline_git.py
CHANGED
|
@@ -484,7 +484,12 @@ def _require_tag_integration_proof(
|
|
|
484
484
|
"1",
|
|
485
485
|
landing,
|
|
486
486
|
).stdout.split()
|
|
487
|
-
|
|
487
|
+
rebased = (
|
|
488
|
+
mode == "pull-request"
|
|
489
|
+
and len(parents) == 2
|
|
490
|
+
and _common.rebase_landing_problem(repo, ship, landing) is None
|
|
491
|
+
)
|
|
492
|
+
if not rebased and (len(parents) != 3 or parents[2] != ship):
|
|
488
493
|
raise PipelineGitError(f"{mode} integration proof has invalid merge topology")
|
|
489
494
|
first_parent = _run_git(repo, "rev-list", "--first-parent", base).stdout.splitlines()
|
|
490
495
|
if landing not in first_parent:
|
|
@@ -694,6 +699,7 @@ def retire_previous_branch(
|
|
|
694
699
|
expected_sha: str,
|
|
695
700
|
*,
|
|
696
701
|
allow_remote_absent: bool = False,
|
|
702
|
+
rebased: bool = False,
|
|
697
703
|
) -> None:
|
|
698
704
|
"""Delete an integrated previous bound branch, locally and on origin.
|
|
699
705
|
|
|
@@ -741,7 +747,8 @@ def retire_previous_branch(
|
|
|
741
747
|
if _ref_exists(repo, tracking_ref):
|
|
742
748
|
_run_git(repo, "update-ref", "-d", tracking_ref)
|
|
743
749
|
if _ref_exists(repo, local_ref):
|
|
744
|
-
|
|
750
|
+
# A rebased ship is not an ancestor of HEAD, so -d would refuse it.
|
|
751
|
+
_run_git(repo, "branch", "-D" if rebased else "-d", previous_branch)
|
|
745
752
|
|
|
746
753
|
|
|
747
754
|
def _retire_members(repo: Path, ship_sha: str) -> None:
|
|
@@ -840,9 +847,15 @@ def bind_next_milestone_branch(
|
|
|
840
847
|
landing_sha,
|
|
841
848
|
check=False,
|
|
842
849
|
)
|
|
843
|
-
|
|
850
|
+
# A forge rebase merge lands copies of the ship commits (#418); the
|
|
851
|
+
# pull-request milestone tag must then prove the landing.
|
|
852
|
+
rebased = (
|
|
853
|
+
integrated.returncode == 1
|
|
854
|
+
and _common.rebase_landing_problem(repo, ship_sha, landing_sha) is None
|
|
855
|
+
)
|
|
856
|
+
if integrated.returncode == 1 and not rebased:
|
|
844
857
|
raise PipelineGitError("milestone landing does not contain the ship commit")
|
|
845
|
-
if integrated.returncode
|
|
858
|
+
if integrated.returncode not in (0, 1):
|
|
846
859
|
detail = integrated.stderr.strip() or integrated.stdout.strip()
|
|
847
860
|
raise PipelineGitError(f"could not verify milestone landing: {detail}")
|
|
848
861
|
landing_integrated = _run_git(
|
|
@@ -858,7 +871,7 @@ def bind_next_milestone_branch(
|
|
|
858
871
|
if landing_integrated.returncode != 0:
|
|
859
872
|
detail = landing_integrated.stderr.strip() or landing_integrated.stdout.strip()
|
|
860
873
|
raise PipelineGitError(f"could not verify integrated branch: {detail}")
|
|
861
|
-
if allow_remote_absent:
|
|
874
|
+
if allow_remote_absent or rebased:
|
|
862
875
|
proven_landing = _require_tag_integration_proof(
|
|
863
876
|
repo,
|
|
864
877
|
ship_sha,
|
|
@@ -994,6 +1007,7 @@ def bind_next_milestone_branch(
|
|
|
994
1007
|
allow_remote_absent=(
|
|
995
1008
|
allow_remote_absent or journal["stage"] in {"switched", "retired"}
|
|
996
1009
|
),
|
|
1010
|
+
rebased=rebased,
|
|
997
1011
|
)
|
|
998
1012
|
_retire_members(repo, ship_sha)
|
|
999
1013
|
journal["stage"] = "retired"
|
|
@@ -1024,7 +1024,9 @@ def _bind_next_journal_recovery(
|
|
|
1024
1024
|
)
|
|
1025
1025
|
if resolved_landing.returncode != 0 or resolved_landing.stdout.strip() != landing:
|
|
1026
1026
|
raise PipelineStateError("bind-next journal landing is not an existing full SHA")
|
|
1027
|
-
if not _is_ancestor(repo, ship, landing)
|
|
1027
|
+
if not _is_ancestor(repo, ship, landing) and _common.rebase_landing_problem(
|
|
1028
|
+
repo, ship, landing
|
|
1029
|
+
) is not None:
|
|
1028
1030
|
raise PipelineStateError("bind-next journal landing does not contain ship")
|
|
1029
1031
|
if not _is_ancestor(repo, landing, base):
|
|
1030
1032
|
raise PipelineStateError("bind-next journal landing is not an ancestor of base")
|
package/scripts/state_promote.py
CHANGED
|
@@ -343,6 +343,67 @@ def _completed_promotion(
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
def _ruling_rows(text: str) -> dict[str, list[str]]:
|
|
347
|
+
"""Map Queue # to the cells of each `## User rulings` table row."""
|
|
348
|
+
rows: dict[str, list[str]] = {}
|
|
349
|
+
in_rulings = False
|
|
350
|
+
for line in text.splitlines():
|
|
351
|
+
if line == "## User rulings":
|
|
352
|
+
in_rulings = True
|
|
353
|
+
continue
|
|
354
|
+
if in_rulings and line.startswith("## "):
|
|
355
|
+
break
|
|
356
|
+
if in_rulings and line.lstrip().startswith("|"):
|
|
357
|
+
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
358
|
+
if len(cells) == 4 and cells[0].isdigit():
|
|
359
|
+
rows[cells[0]] = cells
|
|
360
|
+
return rows
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _superseded_carry_forward(
|
|
364
|
+
repo: Path,
|
|
365
|
+
active_state: PipelineState,
|
|
366
|
+
source: Path,
|
|
367
|
+
destination: Path,
|
|
368
|
+
) -> bool:
|
|
369
|
+
"""True when active research is only the shipped DOCS-AUDIT carry-forward
|
|
370
|
+
and the lookahead audit keeps every one of its rulings."""
|
|
371
|
+
if not active_state.archive or destination.is_symlink() or not destination.is_dir():
|
|
372
|
+
return False
|
|
373
|
+
archived = repo / active_state.archive / "research" / "DOCS-AUDIT.md"
|
|
374
|
+
active = destination / "DOCS-AUDIT.md"
|
|
375
|
+
lookahead = source / "DOCS-AUDIT.md"
|
|
376
|
+
entries = [
|
|
377
|
+
path for path in destination.iterdir() if not state_checkpoint._is_junk_file(path)
|
|
378
|
+
]
|
|
379
|
+
if entries != [active] or any(
|
|
380
|
+
path.is_symlink() or not path.is_file() for path in (archived, active, lookahead)
|
|
381
|
+
):
|
|
382
|
+
return False
|
|
383
|
+
carried = active.read_bytes()
|
|
384
|
+
if carried != archived.read_bytes():
|
|
385
|
+
return False
|
|
386
|
+
rulings = _ruling_rows(carried.decode("utf-8"))
|
|
387
|
+
if not any(cells[3] == "no" for cells in rulings.values()):
|
|
388
|
+
return False
|
|
389
|
+
kept = _ruling_rows(lookahead.read_text(encoding="utf-8"))
|
|
390
|
+
for number, cells in rulings.items():
|
|
391
|
+
other = kept.get(number)
|
|
392
|
+
# Plan may record a T### for a pending ruling; nothing else may change.
|
|
393
|
+
if (
|
|
394
|
+
other is None
|
|
395
|
+
or other[:3] != cells[:3]
|
|
396
|
+
or (
|
|
397
|
+
other[3] != cells[3]
|
|
398
|
+
and not (cells[3] == "no" and re.fullmatch(r"T\d{3}", other[3]))
|
|
399
|
+
)
|
|
400
|
+
):
|
|
401
|
+
raise PipelineStateError(
|
|
402
|
+
f"lookahead DOCS-AUDIT.md does not keep carry-forward ruling #{number}"
|
|
403
|
+
)
|
|
404
|
+
return True
|
|
405
|
+
|
|
406
|
+
|
|
346
407
|
def _prepare_promotion(
|
|
347
408
|
repo: Path,
|
|
348
409
|
project: Path,
|
|
@@ -475,12 +536,17 @@ def _prepare_promotion(
|
|
|
475
536
|
raise PipelineStateError("lookahead STATE branch and archive must be null")
|
|
476
537
|
|
|
477
538
|
tracks: dict[str, str] = {}
|
|
539
|
+
carry_forward: Optional[str] = None
|
|
478
540
|
for name in PROMOTION_TRACKS:
|
|
479
541
|
source = next_root / name
|
|
480
542
|
destination = project / name
|
|
481
543
|
if source.exists() or source.is_symlink():
|
|
482
544
|
if destination.exists() or destination.is_symlink():
|
|
483
|
-
|
|
545
|
+
if name != "research" or not _superseded_carry_forward(
|
|
546
|
+
repo, active_state, source, destination
|
|
547
|
+
):
|
|
548
|
+
raise PipelineStateError(f"promotion destination already exists: {destination}")
|
|
549
|
+
carry_forward = state_checkpoint._tree_digest(destination)
|
|
484
550
|
tracks[name] = state_checkpoint._tree_digest(source)
|
|
485
551
|
if not tracks:
|
|
486
552
|
raise PipelineStateError("lookahead track has no promotable artifacts")
|
|
@@ -507,6 +573,7 @@ def _prepare_promotion(
|
|
|
507
573
|
"landing": landing,
|
|
508
574
|
"base": base,
|
|
509
575
|
"tracks": tracks,
|
|
576
|
+
"carry_forward_sha256": carry_forward,
|
|
510
577
|
"residual_sha256": state_checkpoint._tree_digest(next_root, tuple(tracks)),
|
|
511
578
|
"next_state_sha256": state_checkpoint._sha256(next_text),
|
|
512
579
|
"active_state_sha256": state_checkpoint._sha256(active_text),
|
|
@@ -559,6 +626,15 @@ def _resume_track_moves(project: Path, journal: dict[str, object], path: Path) -
|
|
|
559
626
|
destination = project / name
|
|
560
627
|
source_exists = source.exists() or source.is_symlink()
|
|
561
628
|
destination_exists = destination.exists() or destination.is_symlink()
|
|
629
|
+
carry_forward = journal.get("carry_forward_sha256")
|
|
630
|
+
if name == "research" and source_exists and destination_exists and carry_forward:
|
|
631
|
+
if state_checkpoint._tree_digest(destination) != carry_forward:
|
|
632
|
+
raise PipelineStateError("DOCS-AUDIT carry-forward drifted during promotion")
|
|
633
|
+
# The archive and the ship commit keep the carry-forward copy.
|
|
634
|
+
shutil.rmtree(destination)
|
|
635
|
+
journal["stage"] = "removed:carry-forward"
|
|
636
|
+
pipeline_state._write_json(path, journal)
|
|
637
|
+
destination_exists = False
|
|
562
638
|
if source_exists and destination_exists:
|
|
563
639
|
raise PipelineStateError(f"promotion has both source and destination: {name}")
|
|
564
640
|
if not source_exists and not destination_exists:
|
package/skills/gsd-path/BUILD.md
CHANGED
|
@@ -130,7 +130,9 @@ STATE.md-only "finalize" checkpoint for work already checkpointed. The helper
|
|
|
130
130
|
refuses a `build:` checkpoint that deletes or renames a `.project/tasks/`
|
|
131
131
|
brief unless the canonical `build: abandon milestone <slug>` checkpoint moves
|
|
132
132
|
that same brief into `.project/archive/`; build adds fix tasks and repairs
|
|
133
|
-
briefs, it never replans.
|
|
133
|
+
briefs, it never replans. It also refuses a `build:` checkpoint while the
|
|
134
|
+
committed STATE is `ship/active`: final-review outputs stay uncommitted until
|
|
135
|
+
the ship commit.
|
|
134
136
|
|
|
135
137
|
## Execute from runtime receipts
|
|
136
138
|
|
package/skills/gsd-path/PLAN.md
CHANGED
|
@@ -263,8 +263,9 @@ as stated in Lookahead mode.
|
|
|
263
263
|
|
|
264
264
|
Before approval, the helper validates the task graph and brief paths.
|
|
265
265
|
Pending briefs may reference files supplied by transitive dependencies.
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
A landed brief is immutable history: the helper checks its `files` scope
|
|
267
|
+
at the recorded historical base and does not check the paths that its
|
|
268
|
+
prose and Verify name.
|
|
268
269
|
This applies to plan re-approval too; the dispatch CLI remains HEAD-only
|
|
269
270
|
with no overrides.
|
|
270
271
|
|
package/skills/gsd-path/SHIP.md
CHANGED
|
@@ -69,7 +69,8 @@ so the position is unchanged. Then:
|
|
|
69
69
|
task logs prove it is the immediately preceding `ship/blocked` finding
|
|
70
70
|
set, the approved patch tasks copied every finding verbatim, and those tasks
|
|
71
71
|
are now done; otherwise it blocks. Git history preserves the superseded
|
|
72
|
-
finding, while final review itself still creates no pre-ship commit
|
|
72
|
+
finding, while final review itself still creates no pre-ship commit
|
|
73
|
+
(`isolation.py checkpoint` refuses a `build:` checkpoint at `ship/active`).
|
|
73
74
|
2. Derive a stable numbered list of cross-wave integration risks from
|
|
74
75
|
interfaces and flows that span waves. List only genuine risks that could
|
|
75
76
|
plausibly fail; never pad the list. Include PLAN.md's project Verify as
|
|
@@ -341,13 +342,18 @@ The persisted `STATE.archive` field is the transaction identity.
|
|
|
341
342
|
PR is open, the helper returns `status: awaiting-merge`. Present
|
|
342
343
|
**Outcome**, link
|
|
343
344
|
the returned PR as **Review**, and state in **Next** that the user must
|
|
344
|
-
merge it with GitHub's merge-commit method. Stop this ship
|
|
345
|
-
Path never enables auto-merge or merges the PR. On rerun after
|
|
346
|
-
GitHub user merges it, the helper requires the PR head to remain
|
|
347
|
-
commit and its landing to be
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
345
|
+
merge it with GitHub's merge-commit or rebase method. Stop this ship
|
|
346
|
+
invocation; Path never enables auto-merge or merges the PR. On rerun after
|
|
347
|
+
a human GitHub user merges it, the helper requires the PR head to remain
|
|
348
|
+
the ship commit and its landing to be on `origin/main` first-parent
|
|
349
|
+
history as either a two-parent merge with that ship commit as second
|
|
350
|
+
parent, or a rebase merge: the last of a first-parent run of single-parent
|
|
351
|
+
commits whose `git patch-id --stable` values match, in order, the
|
|
352
|
+
non-merge commits from the landing to the ship commit (with equal trees
|
|
353
|
+
when `main` did not move). It then writes and pushes the annotated
|
|
354
|
+
milestone tag containing the PR URL, ship SHA, and landing SHA. A
|
|
355
|
+
milestone that was rebase-merged before this support existed recovers by
|
|
356
|
+
rerunning ship. A closed unmerged PR, squash, merge queue, moved
|
|
351
357
|
head, duplicate PR, or non-GitHub.com origin blocks. The remote bound
|
|
352
358
|
branch may be absent after a valid merge because GitHub may auto-delete it.
|
|
353
359
|
- `external-landing` is for an origin where Path cannot open the review
|
|
@@ -70,7 +70,9 @@ settings block; their default inheritance remains usable.
|
|
|
70
70
|
Resolve the bundled helper to an absolute path. Use one record under the Git
|
|
71
71
|
common directory's `gsd-path/model-selection/` directory per assignment. Scope
|
|
72
72
|
the path and `--scope` to the milestone, active/lookahead track, and review cycle
|
|
73
|
-
where applicable; later work must not reuse an earlier record.
|
|
73
|
+
where applicable; later work must not reuse an earlier record. The helper
|
|
74
|
+
refuses a relative `--record`, and `release` of a missing record names the
|
|
75
|
+
path and creates nothing.
|
|
74
76
|
|
|
75
77
|
```text
|
|
76
78
|
python3 -B <helper> resolve --repo <primary> --scope <milestone/track/cycle> --assignment <logical-name> --role <role> --record <absolute-record> --capabilities <advertised-json>
|
|
@@ -136,8 +136,9 @@ bundles this file; spawned agents follow their role brief instead.
|
|
|
136
136
|
for the current STATE.branch milestone must be an ancestor of origin/main;
|
|
137
137
|
if it is not, that milestone's integration is incomplete and control routes
|
|
138
138
|
to ship, not build. Direct mode requires the matching canonical `integrate:`
|
|
139
|
-
commit; PR mode requires tag metadata and a two-parent merge whose
|
|
140
|
-
parent is the ship commit
|
|
139
|
+
commit; PR mode requires tag metadata and either a two-parent merge whose
|
|
140
|
+
second parent is the ship commit or a proven rebase merge of the ship
|
|
141
|
+
commits. A null branch returns
|
|
141
142
|
to the router; build never creates, selects, switches, or rebinds it.
|
|
142
143
|
- The ship phase makes exactly one commit on the bound branch — the
|
|
143
144
|
`.project/`-only ship commit recording
|
|
@@ -27,7 +27,8 @@ helper: run the bundled copy until the project runtime is upgraded.
|
|
|
27
27
|
`dispatch_driver.py` and `workflow_run.py` always run from the installed skill
|
|
28
28
|
bundle, because they read role briefs and templates from it; a skill update
|
|
29
29
|
replaces them, a runtime upgrade does not. The driver reloads the helper modules
|
|
30
|
-
that it imports from the selected runtime
|
|
30
|
+
that it imports from the selected runtime, and runs the subprocess helpers that
|
|
31
|
+
the runtime lists (such as the fix-task brief lint) from it.
|
|
31
32
|
|
|
32
33
|
With no declaration, retain the legacy helper lookup. An existing
|
|
33
34
|
`.gsd-path/runtime/` requires the explicit `--runtime-migrate --project PATH`
|
|
@@ -361,6 +361,49 @@ def run_git(
|
|
|
361
361
|
return _run_exact(git_command(repo, *arguments), input=input)
|
|
362
362
|
|
|
363
363
|
|
|
364
|
+
def _patch_ids(repo: Path, commits: Sequence[str]) -> list[Optional[str]]:
|
|
365
|
+
"""`git patch-id --stable` per commit, in order; None for an empty commit."""
|
|
366
|
+
log = run_git(repo, "log", "--no-walk=unsorted", "-p", "--binary", "--no-color",
|
|
367
|
+
"--no-ext-diff", *commits)
|
|
368
|
+
if log.returncode != 0:
|
|
369
|
+
raise ValueError("could not read commit patches")
|
|
370
|
+
ids = run_git(repo, "patch-id", "--stable", input=log.stdout)
|
|
371
|
+
if ids.returncode != 0:
|
|
372
|
+
raise ValueError("could not compute patch ids")
|
|
373
|
+
by_commit = {commit: patch for patch, commit in (line.split() for line in ids.stdout.splitlines())}
|
|
374
|
+
return [by_commit.get(commit) for commit in commits]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def rebase_landing_problem(repo: Path, ship: str, landing: str) -> Optional[str]:
|
|
378
|
+
"""Why landing is not a forge rebase merge of ship, or None when it is (#418).
|
|
379
|
+
|
|
380
|
+
A rebase merge replays ship's own commits onto the default branch. Proof:
|
|
381
|
+
the first-parent run that ends at landing has one non-merge commit per
|
|
382
|
+
non-merge commit of landing..ship, with equal patch ids in the same order;
|
|
383
|
+
when that run starts at ship's fork point, landing's tree equals ship's.
|
|
384
|
+
"""
|
|
385
|
+
listed = run_git(repo, "rev-list", "--reverse", "--topo-order", "--no-merges", f"{landing}..{ship}")
|
|
386
|
+
if listed.returncode != 0:
|
|
387
|
+
return "could not list the ship commits"
|
|
388
|
+
originals = listed.stdout.split()
|
|
389
|
+
if not originals:
|
|
390
|
+
return "landing already contains the ship commit"
|
|
391
|
+
walked = run_git(repo, "rev-list", "--first-parent", "--parents", "-n", str(len(originals)), landing)
|
|
392
|
+
rows = [line.split() for line in walked.stdout.splitlines()][::-1]
|
|
393
|
+
if walked.returncode != 0 or len(rows) != len(originals) or any(len(row) != 2 for row in rows):
|
|
394
|
+
return "landing is not a linear run of rebased commits"
|
|
395
|
+
try:
|
|
396
|
+
if _patch_ids(repo, originals) != _patch_ids(repo, [row[0] for row in rows]):
|
|
397
|
+
return "rebased commits do not replay the ship commits in order"
|
|
398
|
+
except ValueError as error:
|
|
399
|
+
return str(error)
|
|
400
|
+
fork = run_git(repo, "merge-base", ship, landing).stdout.strip()
|
|
401
|
+
trees = run_git(repo, "rev-parse", f"{ship}^{{tree}}", f"{landing}^{{tree}}").stdout.split()
|
|
402
|
+
if rows[0][1] == fork and (len(trees) != 2 or trees[0] != trees[1]):
|
|
403
|
+
return "rebased tree differs from the ship tree"
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
364
407
|
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
365
408
|
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
366
409
|
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
@@ -387,7 +387,9 @@ def _lint_task(
|
|
|
387
387
|
)
|
|
388
388
|
break
|
|
389
389
|
|
|
390
|
-
|
|
390
|
+
# A landed brief is history that nobody can edit: only an unlanded brief must name present paths.
|
|
391
|
+
landed = task_id in (landed_bases or {})
|
|
392
|
+
for name in () if landed else PROSE_SECTIONS:
|
|
391
393
|
body = sections.get(name)
|
|
392
394
|
if body is None or not _clean(body):
|
|
393
395
|
continue
|
|
@@ -400,8 +402,6 @@ def _lint_task(
|
|
|
400
402
|
and token not in supplied
|
|
401
403
|
and not _supplied_contract(repo, path, token)
|
|
402
404
|
and not _pipeline_workspace_file(repo, path, token)
|
|
403
|
-
# A landed brief is history: the pipeline moves its own files (build recovery, archive) later.
|
|
404
|
-
and not (task_id in (landed_bases or {}) and _pipeline_workspace_path(repo, path, token))
|
|
405
405
|
and not _base_exists(git_root, base, token)
|
|
406
406
|
):
|
|
407
407
|
problems.append(f"## {name} names a path missing at the layer base: {token}")
|
|
@@ -411,7 +411,7 @@ def _lint_task(
|
|
|
411
411
|
block = VERIFY_BLOCK_PATTERN.search(verify_body)
|
|
412
412
|
if block is None or not block.group("block").strip():
|
|
413
413
|
problems.append("## Verify must contain a non-empty fenced bash block")
|
|
414
|
-
|
|
414
|
+
elif not landed:
|
|
415
415
|
for token in _verify_tokens(
|
|
416
416
|
block.group("block"),
|
|
417
417
|
repo=repo,
|
|
@@ -448,9 +448,10 @@ def validate_task_briefs(
|
|
|
448
448
|
|
|
449
449
|
Plain lint uses the layer base without overrides. Plan approval and dispatch
|
|
450
450
|
supply transitive dependency files for unlanded tasks and resolved historical
|
|
451
|
-
bases for landed tasks
|
|
451
|
+
bases for landed tasks. A landed brief is immutable: its `files` scope is
|
|
452
|
+
checked at that base, and the paths its prose and Verify name are not checked.
|
|
452
453
|
The caller validates the task graph and landed metadata before supplying
|
|
453
|
-
these mappings
|
|
454
|
+
these mappings.
|
|
454
455
|
"""
|
|
455
456
|
|
|
456
457
|
resolved_base = _resolve_base(root, base)
|
|
@@ -481,6 +482,9 @@ def validate_task_briefs(
|
|
|
481
482
|
for shape, task_ids in contracts.items():
|
|
482
483
|
if len(task_ids) == 1:
|
|
483
484
|
task_id = next(iter(task_ids))
|
|
485
|
+
# A landed brief is history: a later repair may reword the bullet in its partner.
|
|
486
|
+
if task_id in (landed_bases or {}):
|
|
487
|
+
continue
|
|
484
488
|
problems.append(
|
|
485
489
|
f"{task_id}: interface contract is not shared by any other task: {shape}"
|
|
486
490
|
)
|
|
@@ -592,7 +596,6 @@ def plan_validation_bases(
|
|
|
592
596
|
)
|
|
593
597
|
except IsolationError as error:
|
|
594
598
|
raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
|
|
595
|
-
dependency_files[task_id] = set()
|
|
596
599
|
except check_handoffs.HandoffError as error:
|
|
597
600
|
raise BriefError(str(error)) from error
|
|
598
601
|
return tasks, dependency_files, landed_bases
|
|
@@ -117,9 +117,21 @@ def _refresh_stop_errors() -> None:
|
|
|
117
117
|
model_policy.PolicyError)
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
# The selected Project runtime directory once _reload_pinned_modules ran; None keeps bundled helpers.
|
|
121
|
+
_RUNTIME_ROOT: Optional[Path] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def helper_script(name: str) -> Path:
|
|
125
|
+
"""A subprocess helper from the selected runtime when it ships one, else the bundled sibling."""
|
|
126
|
+
if _RUNTIME_ROOT is not None and (_RUNTIME_ROOT / name).is_file():
|
|
127
|
+
return _RUNTIME_ROOT / name
|
|
128
|
+
return Path(__file__).resolve().parent / name
|
|
129
|
+
|
|
130
|
+
|
|
120
131
|
def _reload_pinned_modules(runtime_root: Path) -> None:
|
|
121
132
|
global isolation, build_state, task_context, contracts, pipeline_state
|
|
122
|
-
global archive_milestone, review_findings, model_policy
|
|
133
|
+
global archive_milestone, review_findings, model_policy, _RUNTIME_ROOT
|
|
134
|
+
_RUNTIME_ROOT = runtime_root
|
|
123
135
|
if _bundled_helpers():
|
|
124
136
|
path = str(runtime_root)
|
|
125
137
|
if path not in sys.path:
|
|
@@ -981,7 +993,7 @@ class Round:
|
|
|
981
993
|
diagnose = shlex.join([
|
|
982
994
|
sys.executable,
|
|
983
995
|
"-B",
|
|
984
|
-
str(
|
|
996
|
+
str(helper_script("pipeline_diagnose.py")),
|
|
985
997
|
"diagnose",
|
|
986
998
|
"--repo",
|
|
987
999
|
str(self.primary),
|
|
@@ -1677,8 +1689,8 @@ class Review:
|
|
|
1677
1689
|
|
|
1678
1690
|
|
|
1679
1691
|
def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
|
|
1680
|
-
"""Run a bundled
|
|
1681
|
-
command = [sys.executable, "-B", str(
|
|
1692
|
+
"""Run a helper (runtime-selected, else bundled) and return its JSON stdout; a non-zero exit is a typed stop."""
|
|
1693
|
+
command = [sys.executable, "-B", str(helper_script(script)), *arguments]
|
|
1682
1694
|
completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
|
|
1683
1695
|
step = {"script": script, "command": command, "exit_code": completed.returncode,
|
|
1684
1696
|
"stdout": completed.stdout, "stderr": completed.stderr}
|
|
@@ -2290,7 +2302,7 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
2290
2302
|
if plan_changed:
|
|
2291
2303
|
_common.atomic_write(plan_path, updated)
|
|
2292
2304
|
receipt["plan_wave"] = fix_wave
|
|
2293
|
-
lint = subprocess.run([sys.executable, "-B", str(
|
|
2305
|
+
lint = subprocess.run([sys.executable, "-B", str(helper_script("check_task_briefs.py")),
|
|
2294
2306
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
2295
2307
|
"--tasks-dir", f"{options.project_dir}/tasks",
|
|
2296
2308
|
"--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
|
|
@@ -69,6 +69,11 @@ PUBLICATION_HINT = (
|
|
|
69
69
|
"(STATE shipped/done) through archive_milestone.py integrate; findings during "
|
|
70
70
|
"ship reopen through the patch plan (see SHIP.md)"
|
|
71
71
|
)
|
|
72
|
+
AUTHORIZED_PUSH_HINT = (
|
|
73
|
+
"an owner-approved push of one exact commit to a branch other than the bound "
|
|
74
|
+
"or default branch needs refs/gsd-path/authorizations/<project>/push/<ref> "
|
|
75
|
+
"at that commit (see HOOKS.md)"
|
|
76
|
+
)
|
|
72
77
|
LEGACY_STATE_FIELDS = {
|
|
73
78
|
"pipeline",
|
|
74
79
|
"project",
|
|
@@ -621,14 +626,25 @@ def pre_push_violations(lines):
|
|
|
621
626
|
continue
|
|
622
627
|
state = state_at(local_sha)
|
|
623
628
|
owed = unshipped_bound_branch(state) if state is not None else None
|
|
624
|
-
if owed is not None:
|
|
629
|
+
if owed is not None and not authorized_publication(state, remote_ref, local_sha):
|
|
625
630
|
found.append(
|
|
626
631
|
f"{remote_ref} <- {local_sha[:12]} carries unshipped {owed} work "
|
|
627
|
-
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}"
|
|
632
|
+
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}; "
|
|
633
|
+
f"{AUTHORIZED_PUSH_HINT}"
|
|
628
634
|
)
|
|
629
635
|
return found
|
|
630
636
|
|
|
631
637
|
|
|
638
|
+
def authorized_publication(state, remote_ref, sha):
|
|
639
|
+
"""The owner authorized this exact commit on this one non-default branch."""
|
|
640
|
+
project = state.get("project")
|
|
641
|
+
if not isinstance(project, str) or not members.NAME_RE.fullmatch(project):
|
|
642
|
+
return False
|
|
643
|
+
if not remote_ref.startswith("refs/heads/") or remote_ref == f"refs/heads/{default_branch()}":
|
|
644
|
+
return False
|
|
645
|
+
return members.authorized(repo_root(), project, "push", remote_ref, sha)
|
|
646
|
+
|
|
647
|
+
|
|
632
648
|
def is_ship_commit(subject):
|
|
633
649
|
return bool(subject and subject.strip().casefold().startswith("ship:"))
|
|
634
650
|
|
|
@@ -845,6 +845,8 @@ def require_pull_request_merge(
|
|
|
845
845
|
merge_commit: str,
|
|
846
846
|
ship_commit: str,
|
|
847
847
|
remote_default: str,
|
|
848
|
+
*,
|
|
849
|
+
allow_rebase: bool = False,
|
|
848
850
|
) -> None:
|
|
849
851
|
resolved = optional_ref(project, merge_commit)
|
|
850
852
|
if resolved != merge_commit:
|
|
@@ -853,9 +855,16 @@ def require_pull_request_merge(
|
|
|
853
855
|
run_git(project, "rev-list", "--parents", "-n", "1", merge_commit),
|
|
854
856
|
"inspect pull-request merge parents",
|
|
855
857
|
).split()
|
|
856
|
-
if len(parents)
|
|
858
|
+
if allow_rebase and len(parents) == 2:
|
|
859
|
+
problem = _common.rebase_landing_problem(project, ship_commit, merge_commit)
|
|
860
|
+
if problem is not None:
|
|
861
|
+
raise ArchiveError(
|
|
862
|
+
"pull-request integration must use a two-parent merge commit "
|
|
863
|
+
f"or a rebase merge of the ship commits: {problem}"
|
|
864
|
+
)
|
|
865
|
+
elif len(parents) != 3:
|
|
857
866
|
raise ArchiveError("pull-request integration must use a two-parent merge commit")
|
|
858
|
-
|
|
867
|
+
elif parents[2] != ship_commit:
|
|
859
868
|
raise ArchiveError("pull-request merge second parent is not the ship commit")
|
|
860
869
|
first_parent = archive_milestone.require_git_success(
|
|
861
870
|
run_git(project, "rev-list", "--first-parent", remote_default),
|
|
@@ -1026,7 +1035,9 @@ def integrate_pull_request(
|
|
|
1026
1035
|
merge_commit,
|
|
1027
1036
|
)
|
|
1028
1037
|
remote_default = refresh_origin(project)["remote_default"]
|
|
1029
|
-
require_pull_request_merge(
|
|
1038
|
+
require_pull_request_merge(
|
|
1039
|
+
project, merge_commit, ship_commit, remote_default, allow_rebase=True
|
|
1040
|
+
)
|
|
1030
1041
|
pull = update_pull_request_body(
|
|
1031
1042
|
repository,
|
|
1032
1043
|
pull,
|
|
@@ -1473,7 +1484,9 @@ def validate_integrated(
|
|
|
1473
1484
|
pull["number"],
|
|
1474
1485
|
merge_commit,
|
|
1475
1486
|
)
|
|
1476
|
-
require_pull_request_merge(
|
|
1487
|
+
require_pull_request_merge(
|
|
1488
|
+
project, merge_commit, ship_commit, remote_default, allow_rebase=True
|
|
1489
|
+
)
|
|
1477
1490
|
require_annotated_tag(
|
|
1478
1491
|
project,
|
|
1479
1492
|
tag_ref,
|