@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
|
@@ -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"
|
|
@@ -4470,6 +4502,20 @@ def attest(
|
|
|
4470
4502
|
}
|
|
4471
4503
|
|
|
4472
4504
|
|
|
4505
|
+
def _committed_ship_active(repo: Path) -> bool:
|
|
4506
|
+
"""True when the STATE.md committed at HEAD is ship/active."""
|
|
4507
|
+
|
|
4508
|
+
result = run_git(repo, "show", "HEAD:.project/STATE.md")
|
|
4509
|
+
if result.returncode != 0:
|
|
4510
|
+
return False
|
|
4511
|
+
frontmatter = result.stdout.split("\n---", 1)[0]
|
|
4512
|
+
fields = {
|
|
4513
|
+
key: _unquote(value)
|
|
4514
|
+
for key, value in re.findall(r"(?m)^(phase|status):(.*)$", frontmatter)
|
|
4515
|
+
}
|
|
4516
|
+
return fields.get("phase") == "ship" and fields.get("status") == "active"
|
|
4517
|
+
|
|
4518
|
+
|
|
4473
4519
|
def checkpoint(
|
|
4474
4520
|
repo: Path,
|
|
4475
4521
|
expected_head: str,
|
|
@@ -4519,6 +4565,12 @@ def checkpoint(
|
|
|
4519
4565
|
"subject": subject.strip(),
|
|
4520
4566
|
}
|
|
4521
4567
|
raise IsolationError("primary HEAD differs from --expected-head")
|
|
4568
|
+
if subject.strip().startswith("build:") and _committed_ship_active(repo):
|
|
4569
|
+
raise IsolationError(
|
|
4570
|
+
"STATE is ship/active: final review creates no pre-ship commit; leave its "
|
|
4571
|
+
"outputs uncommitted for archive_milestone.py, and send findings through "
|
|
4572
|
+
"the patch plan (see SHIP.md)"
|
|
4573
|
+
)
|
|
4522
4574
|
pending = uncommitted_paths(repo)
|
|
4523
4575
|
forced: Set[str] = set()
|
|
4524
4576
|
# Build only: an approval undo resets to a parent without the ledger and would drop it.
|
|
@@ -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:
|
|
@@ -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:
|
|
@@ -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
|
|
|
@@ -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`
|