@opengsd/gsd-path 1.6.0 → 1.6.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/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -0
- package/FULL.md +5 -4
- package/HOOKS.md +18 -2
- package/QUICK.md +1 -1
- package/README.md +10 -9
- 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 +46 -14
- 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/PLAN.md +3 -2
- package/skills/gsd-path/SHIP.md +12 -7
- 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 +46 -14
- 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/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 +46 -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 +46 -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 +46 -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 +46 -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 +46 -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 +46 -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 +46 -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 +46 -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 +12 -7
- 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 +46 -14
- 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/PLAN.md +3 -2
- package/skills/path/SHIP.md +12 -7
- 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 +46 -14
- 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
|
@@ -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
|
|
@@ -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,
|
|
@@ -1219,9 +1219,17 @@ def _commit_member_source(sidecar: Path, journal: Dict[str, object], allowed: Se
|
|
|
1219
1219
|
undeclared = undeclared_paths(sidecar, member_base, pending, allowed)
|
|
1220
1220
|
if undeclared:
|
|
1221
1221
|
raise IsolationError("undeclared paths: " + ", ".join(undeclared))
|
|
1222
|
-
git_output(sidecar, "add", "-A", "--", *sorted(pending))
|
|
1223
1222
|
body = member_commit_body(str(journal["task_file"]), sorted(pending), member_base, str(journal["base"]))
|
|
1224
|
-
|
|
1223
|
+
# The paths and the body grow with the change set, so both go through
|
|
1224
|
+
# stdin: a Windows command line holds about 32 KiB.
|
|
1225
|
+
for arguments, text in (
|
|
1226
|
+
(("add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"),
|
|
1227
|
+
"".join(f"{path}\0" for path in sorted(pending))),
|
|
1228
|
+
(("commit", "-q", "-F", "-"), _commit_message(str(journal["subject"]), body)),
|
|
1229
|
+
):
|
|
1230
|
+
result = run_git(sidecar, *arguments, input=text)
|
|
1231
|
+
if result.returncode != 0:
|
|
1232
|
+
raise IsolationError((result.stderr or result.stdout).strip() or "git command failed")
|
|
1225
1233
|
source = current_sha(sidecar)
|
|
1226
1234
|
if source == member_base:
|
|
1227
1235
|
raise IsolationError("no changes to land")
|
|
@@ -1669,6 +1677,25 @@ def committed_paths_since(repo: Path, base: str) -> Set[str]:
|
|
|
1669
1677
|
)
|
|
1670
1678
|
|
|
1671
1679
|
|
|
1680
|
+
def _stage_paths(repo: Path, mode: str, paths: Set[str]) -> None:
|
|
1681
|
+
"""Stage `paths` with one `git add`.
|
|
1682
|
+
|
|
1683
|
+
One call for each path fails when a file replaces a tracked directory: the
|
|
1684
|
+
first call drops the files below the directory from the index, and then
|
|
1685
|
+
their pathspecs match no file. The `./` prefix stops pathspec magic, so a
|
|
1686
|
+
name such as `:!b` cannot exclude another path of the set. Git names the
|
|
1687
|
+
path in its error text.
|
|
1688
|
+
"""
|
|
1689
|
+
if not paths:
|
|
1690
|
+
return
|
|
1691
|
+
added = run_git(
|
|
1692
|
+
repo, "add", mode, "--pathspec-from-file=-", "--pathspec-file-nul",
|
|
1693
|
+
input="".join(f"./{path}\0" for path in sorted(paths)),
|
|
1694
|
+
)
|
|
1695
|
+
if added.returncode != 0:
|
|
1696
|
+
raise IsolationError(f"could not stage the change set: {added.stderr.strip()}")
|
|
1697
|
+
|
|
1698
|
+
|
|
1672
1699
|
def ignored_legacy_ledger(repo: Path) -> bool:
|
|
1673
1700
|
"""True when a verify ledger absent from HEAD is hidden by an ignore rule.
|
|
1674
1701
|
|
|
@@ -1684,17 +1711,25 @@ def ignored_legacy_ledger(repo: Path) -> bool:
|
|
|
1684
1711
|
return run_git(repo, "check-ignore", "-q", "--no-index", "--", VERIFY_LEDGER_PATH).returncode == 0
|
|
1685
1712
|
|
|
1686
1713
|
|
|
1714
|
+
def _commit_message(subject: str, body: str) -> str:
|
|
1715
|
+
"""The message for `git commit -F -`.
|
|
1716
|
+
|
|
1717
|
+
A landing body lists every changed path. As a `-m` argument it can exceed
|
|
1718
|
+
the Windows command-line limit, so the message goes through stdin. Like
|
|
1719
|
+
`-m`, the message ends its last line.
|
|
1720
|
+
"""
|
|
1721
|
+
message = f"{subject}\n\n{body}"
|
|
1722
|
+
return message if message.endswith("\n") else message + "\n"
|
|
1723
|
+
|
|
1724
|
+
|
|
1687
1725
|
def _commit_pending(
|
|
1688
1726
|
repo: Path, pending: Set[str], subject: str, body: str, forced: Set[str] = frozenset()
|
|
1689
1727
|
) -> str:
|
|
1690
1728
|
reset = run_git(repo, "reset", "-q", "HEAD")
|
|
1691
1729
|
if reset.returncode != 0:
|
|
1692
1730
|
raise IsolationError("could not clear the index before commit")
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
added = run_git(repo, "add", mode, "--", path)
|
|
1696
|
-
if added.returncode != 0:
|
|
1697
|
-
raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
|
|
1731
|
+
_stage_paths(repo, "-A", pending - forced)
|
|
1732
|
+
_stage_paths(repo, "-f", pending & forced)
|
|
1698
1733
|
staged = _split_paths(
|
|
1699
1734
|
git_output(
|
|
1700
1735
|
repo,
|
|
@@ -1707,7 +1742,7 @@ def _commit_pending(
|
|
|
1707
1742
|
)
|
|
1708
1743
|
if staged != pending:
|
|
1709
1744
|
raise IsolationError("staged paths do not match the uncommitted change set")
|
|
1710
|
-
committed = run_git(repo, "commit", "-q", "-
|
|
1745
|
+
committed = run_git(repo, "commit", "-q", "-F", "-", input=_commit_message(subject, body))
|
|
1711
1746
|
if committed.returncode != 0:
|
|
1712
1747
|
raise IsolationError((committed.stderr or committed.stdout).strip() or "commit failed")
|
|
1713
1748
|
return current_sha(repo)
|
|
@@ -1786,18 +1821,15 @@ def commit_allowed_changes(
|
|
|
1786
1821
|
reset = run_git(repo, "reset", "-q", "HEAD")
|
|
1787
1822
|
if reset.returncode != 0:
|
|
1788
1823
|
raise IsolationError("could not clear the index before landing")
|
|
1789
|
-
|
|
1790
|
-
added = run_git(repo, "add", "-A", "--", path)
|
|
1791
|
-
if added.returncode != 0:
|
|
1792
|
-
raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
|
|
1824
|
+
_stage_paths(repo, "-A", staging)
|
|
1793
1825
|
staged = _split_paths(
|
|
1794
|
-
git_output(repo, "diff", "--cached", "--name-only", "--relative")
|
|
1826
|
+
git_output(repo, "diff", "--no-renames", "--cached", "--name-only", "--relative")
|
|
1795
1827
|
)
|
|
1796
1828
|
if staged != staging:
|
|
1797
1829
|
raise IsolationError("staged paths do not match the uncommitted change set")
|
|
1798
1830
|
verified_tree = git_output(repo, "write-tree")
|
|
1799
1831
|
verified_delta = _tree_delta(repo, base, verified_tree)
|
|
1800
|
-
committed = run_git(repo, "commit", "-q", "-
|
|
1832
|
+
committed = run_git(repo, "commit", "-q", "-F", "-", input=_commit_message(subject, body))
|
|
1801
1833
|
if committed.returncode != 0:
|
|
1802
1834
|
raise IsolationError(
|
|
1803
1835
|
(committed.stderr or committed.stdout).strip() or "commit failed"
|
|
@@ -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:
|
|
@@ -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")
|
|
@@ -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:
|
|
@@ -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)
|
|
@@ -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:
|
|
@@ -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`
|