diff --git a/.gitignore b/.gitignore index bc9691e..184fdd7 100644 --- a/.gitignore +++ b/.gitignore @@ -236,3 +236,12 @@ deps.json .intent/cache/ .venv_test/ .wup/ + +# Repository-local worktrees and operational state +/.worktrees/ +/.subactor/leases/ +/.subactor/sessions/ +/.subactor/recovery/ +/.subactor/receipts/ +/.subactor/cache/ +/.subactor/snapshots/ diff --git a/.governance/docs/LOCAL_CI_PUBLICATION.md b/.governance/docs/LOCAL_CI_PUBLICATION.md new file mode 100644 index 0000000..836e730 --- /dev/null +++ b/.governance/docs/LOCAL_CI_PUBLICATION.md @@ -0,0 +1,28 @@ +# Local CI publication policy — adopted reference + +Canonical policy: [Wellmanifest/new-project 0.20.10](https://github.com/wellmanifest/new-project/blob/d5f77d83b3752477cfb95a535d0e1ce77f148576/docs/information/local-ci-publication.md). +Source revision: `d5f77d83b3752477cfb95a535d0e1ce77f148576`. +Canonical document SHA-256: `44803480f1d51f64eec81a62335b6725747f01b5f2de78105ebfc4017a7922c6`. + +This managed file is an adoption reference. The authored document and its +metadata remain at the canonical Wellmanifest home; do not register this copy +as a new document owned by the adopting repository. + +For Semcod and Subactor, prefer protected local OneDev verification followed +by the independent local Validator App. Resolve the actual protected profile, +observe existing reconciliation, require fresh verification of the PR head +merged with the current base, then invoke the trusted local Validator adapter +under existing publication authorization. The supported local adapter is +`subactor/validator-agent/bin/run-local-direct-pr.sh`; use its protected deployed +checkout and existing key reference as specified by the canonical runbook. +Never self-approve or merge directly. + +A hosted Actions billing or capacity error does not prove local CI is unavailable. +Use hosted dispatch only when explicitly selected by the protected deployment. +Preserve all additional repository checks and required operating systems. +Retire a hosted check only after equivalent deployed local canary evidence and +independent policy review. Missing profiles are gaps, never successful coverage. + +Keep declared, configured, deployed, verified and published evidence separate. +Read the canonical policy for the complete workflow, authority boundaries and +migration requirements. This reference grants no execution or merge authority. diff --git a/.governance/manifest.base.json b/.governance/manifest.base.json index 287feed..b2ee268 100644 --- a/.governance/manifest.base.json +++ b/.governance/manifest.base.json @@ -110,7 +110,7 @@ "stacks": [], "standard": { "id": "wellmanifest/new-project", - "version": "0.20.7" + "version": "0.20.11" }, "ticket": { "activeStatuses": [ diff --git a/.governance/manifest.json b/.governance/manifest.json index 9abc6ca..55aadb5 100644 --- a/.governance/manifest.json +++ b/.governance/manifest.json @@ -194,7 +194,7 @@ ], "standard": { "id": "wellmanifest/new-project", - "version": "0.20.7" + "version": "0.20.11" }, "ticket": { "activeStatuses": [ diff --git a/.governance/manifest.lock.json b/.governance/manifest.lock.json index 4995fec..5b7622d 100644 --- a/.governance/manifest.lock.json +++ b/.governance/manifest.lock.json @@ -24,6 +24,7 @@ ".governance/diagnostics.json": "f132973d5becc5393355f1bb60673c29522eb61f1bea0956d7523214668edd67", ".governance/diagnostics.schema.json": "5c28e6a54319d106c234e63e1f65933533b5bfd5cdf6509a17b28362b56f86e1", ".governance/docs/BRANCH_INTENT_RECONCILIATION.md": "d17163b016e1ad8cf97408d528bf5a6443e931ea694e97191abc7c1e50586c3b", + ".governance/docs/LOCAL_CI_PUBLICATION.md": "f30b778c8dc777be302ff346d3dbd144fc3a02fc957b178cc600bbec1219fc64", ".governance/error/GOV-AGENT-HOST.md": "a3679a63e6346946382dc510a8454b5c1c2851e694f7b3f4b82473764153a90f", ".governance/error/GOV-CHANGE-LEASE.md": "30530e7fd7c2eca9f4db0adec575dbc9b1bea139c2a640bfde0ae7c4b2ace116", ".governance/error/GOV-INTENT.md": "4dc29dbf4c39d18cd11a5ec1eccc257a06d2bea2f95b560aa58085672611d4a9", @@ -41,9 +42,9 @@ ".governance/governance_check.py": "305fc2fbf1c01a8a05e514346aa244977efabedd348379ebffd7e6562949fa17", ".governance/intent.schema.json": "9755f20cd189efe2205629781a6cf6b613bd93e9368416793c4de53256b0a9c0", ".governance/lock.schema.json": "ad80c98f800a4a3310870336dcdaf0aa689cc4988f71084d25d76bea2df1242f", - ".governance/manifest.base.json": "532461b374f6727aadb8ad0a73fe99b3f5bf6c3cbd6ff151d53bee837fce0fff", + ".governance/manifest.base.json": "e49a636499e17b46b399ebb0b0329df5ea79ed1d98f4264f21b666c732925d33", ".governance/manifest.schema.json": "5aa2ccd3f6898834d4e39a78342448145490be56aa132e16ac7c9d64acef8f73", - ".governance/package-manifest.json": "3a148a2396bd8979c61a3df2adc6b0899c29b04e405e0047b73efa4b7d330cb9", + ".governance/package-manifest.json": "a2ea77aca16d59282e1b60105521270ae112ebc52ddb3e08af3eeec26942dc3f", ".governance/precommit_standard_update.py": "c91e2bf9ae9d6ccc77bce0e61450c818a5961edee5bfde3b60426da88e296b0f", ".governance/remediation-intent.schema.json": "ab895e7ca0d83aef4c7d3eab674f14bcb0e0a0375298fdb3496d535dc9184d66", ".governance/remediation-intent.template.dsl.json": "a3eb01c54fe678f3fcebb88103ac4eb02f5dd24016b2ba9552814b5e442dfb34", @@ -66,22 +67,22 @@ ".governance/work-classification.schema.json": "f5c2b518238543589e4f8d3805cc6455e19d6919643644aaeae034abf472a467", ".governance/work-continuity.schema.json": "5134e6884ddaa4fb3a0ffd200d281afb0f3b868b06f71d65e3da6b42f6fe0830", ".governance/work_continuity.py": "2efb720b530cc295b45413a2bc7384a426bd29b5d9b20452035e5943294c18e4", - ".governance/workspace_lifecycle_check.py": "8d9f11ccc7cf86c4b35835b4e5b7a2e44f5626b10466aff0d595da2c27c31ca2", + ".governance/workspace_lifecycle_check.py": "f196f33c4884120ca216e65473f32b163f6c6d262624309cf0cbdc37063d181f", ".governance/worktree_guard.py": "b154f6e67626770ec11c9544d31a27b32d9c215ef73ffc9eb8f52e9c3a9b051b", - ".governance/worktree_overlap_check.py": "f5ad2d2db98a240ca51cc7e2e404a726b521d8830721f1a0318127d4303fc9d9", - ".governance/worktree_path_check.py": "6333676ac045246a655ea6fc042e83d5bbceef5b03b80eca50a12dc98a7aabeb", - ".governance/worktrees.lock.json": "54c5c9ae00dca06cb5ad412a28b033245886c2c5bf45ae0db36e64e3f193ad37", - ".governance/worktrees.schema.json": "5ba905865a72aea6a01afff1d06de028224651d2b444f6ac528cf65499d30148", + ".governance/worktree_overlap_check.py": "86336eef38cfd1c2d421052e089e0523643d48ffa1c6fb7437b69cb78e17c785", + ".governance/worktree_path_check.py": "d3309b76e2c91dd7f046b00322ec8cb48ef744b8b7908d70e475fedf95e5e196", + ".governance/worktrees.lock.json": "9ed607dd339fd4ed30263d7de62231249447568e8e35c8b19fdc0231fca39fde", + ".governance/worktrees.schema.json": "9cc10d126e06cafc87cc117f11b8b095676f461de4d168d2a1c2bb959f0fccd5", ".subactor/.gitignore": "dd223aed5e053f94c6808ac434368c16eeca7e77218f8426e8cf5e46ae441d03", ".subactor/manifest.json": "ab8b1cbe4052a6f0005a3c33a43fa2a70e8837cd32c18b4183d0d448c3a8cee2", - "AGENTS.md": "5a736ee536786caaeaa865fde29d019a0acede9488f9ec1928a628543e40baa0", + "AGENTS.md": "12641a33dcfc24c353834e95d09d4784c42ec035dcf350dd153b55eb02182364", "CLAUDE.md": "48c8159bd17f0a7ab05989ee06e3aeec0aab07f64347fcc271140d9f650d7713", "GEMINI.md": "b66a7d12c4c877110eb263a80b32c065c4c2d581743d72cd6e06ebbe6dc842da", "project/governance-check.bat": "04f4fd3ba15abd6b874bde8fab0dd869402b84b9c83ae72da40c1a804b068045", "project/governance-check.sh": "8eb977ff01a96e47455d227ed5ced949eb53ea19537f870d9016f803840e048e", "project/new-ticket.sh": "82353ad45aee1df6764fd8afac5b950027d89507ea82fd3147c469aa9e556247", "project/readme.sh": "b41a9c88374e6de0439284a4561fb11b1b482039bc5ba1bcf6683fd59b1a3968", - "scripts/install-agent-hosts.sh": "6a0878986f46d3379e12501b2bbdaf7726d71294da6ea3831ad1224d56809ee6", + "scripts/install-agent-hosts.sh": "0e3a59ed654de872c41f810dd2b7aded96a4bebd862db226ac27ffaf5e478ced", "scripts/runtime.sh": "8d5c91808d3c126fc84018a12f0b39dd49194fb8b01c5e024932ff9cf2a7a7e2", "wellmanifest_governance.py": "d6b71f091ffd88fb30c96f54162020d9aeb6e54555328834b68ccb21868ecc07", "worktree-guard.yaml": "bea3d3cda9bd764f9e79b975da8f5360df894fdc04fca0407def88ebd49111b7" @@ -91,7 +92,7 @@ "id": "wellmanifest/new-project", "publicationStatus": "published", "sourceRepository": "wellmanifest/new-project", - "sourceRevision": "9d7af1e63d46e0277da407286699ee50de726d17", - "version": "0.20.7" + "sourceRevision": "2cd39286dc0931870a8a60ee11c96dd6448b39e6", + "version": "0.20.11" } } diff --git a/.governance/package-manifest.json b/.governance/package-manifest.json index d16352d..9944f7f 100644 --- a/.governance/package-manifest.json +++ b/.governance/package-manifest.json @@ -546,6 +546,12 @@ "target": ".governance/docs/BRANCH_INTENT_RECONCILIATION.md", "strategy": "managed", "executable": false + }, + { + "source": "template/files/LOCAL_CI_PUBLICATION.template.md", + "target": ".governance/docs/LOCAL_CI_PUBLICATION.md", + "strategy": "managed", + "executable": false } ] } diff --git a/.governance/workspace_lifecycle_check.py b/.governance/workspace_lifecycle_check.py index a080958..c056089 100755 --- a/.governance/workspace_lifecycle_check.py +++ b/.governance/workspace_lifecycle_check.py @@ -69,10 +69,10 @@ def load_worktrees_contract(): ) source = next((candidate for candidate in candidates if candidate.is_file()), None) if source is None: - raise AuditError("the managed Worktrees v4 conformance module is missing") - spec = importlib.util.spec_from_file_location("workspace_worktrees_v4", source) + raise AuditError("the managed Worktrees conformance module is missing") + spec = importlib.util.spec_from_file_location("workspace_worktrees_contract", source) if spec is None or spec.loader is None: - raise AuditError(f"cannot load Worktrees v4 conformance from {source}") + raise AuditError(f"cannot load Worktrees conformance from {source}") module = importlib.util.module_from_spec(spec) previous = sys.dont_write_bytecode sys.dont_write_bytecode = True @@ -80,7 +80,7 @@ def load_worktrees_contract(): sys.modules[spec.name] = module spec.loader.exec_module(module) except (ImportError, OSError, ValueError) as error: - raise AuditError(f"cannot load Worktrees v4 conformance: {error}") from error + raise AuditError(f"cannot load Worktrees conformance: {error}") from error finally: sys.dont_write_bytecode = previous sys.modules.pop(spec.name, None) @@ -414,9 +414,9 @@ def workspace_inventory(checkouts: list[Checkout]) -> dict[str, Any]: path_style=path_style, ) except (TypeError, ValueError) as error: - raise AuditError(f"Worktrees v4 inventory failed: {error}") from error + raise AuditError(f"Worktrees inventory failed: {error}") from error if observed.get("readOnly") is not True: - raise AuditError("Worktrees v4 inventory did not declare readOnly=true") + raise AuditError("Worktrees inventory did not declare readOnly=true") for entry in observed["entries"]: layout_entries[Path(entry["path"])] = entry @@ -428,7 +428,7 @@ def workspace_inventory(checkouts: list[Checkout]) -> dict[str, Any]: for checkout in sorted(checkouts, key=lambda item: str(item.path)): layout = layout_entries.get(checkout.path) if layout is None: - raise AuditError(f"Worktrees v4 inventory omitted {checkout.path}") + raise AuditError(f"Worktrees inventory omitted {checkout.path}") duplicate = checkout.common_git_dir != authoritative_clone[checkout.identity] anomalies = list(layout["anomalies"]) if duplicate: @@ -446,7 +446,7 @@ def workspace_inventory(checkouts: list[Checkout]) -> dict[str, Any]: "cloneClassification": "duplicate-clone" if duplicate else "registered", "anomalies": sorted(set(anomalies)), }) - return {"schema": "wellmanifest.worktrees/v4", "readOnly": True, "entries": entries} + return {"schema": contract.SCHEMA, "readOnly": True, "entries": entries} def local_branch_findings( @@ -650,7 +650,7 @@ def main(argv: list[str] | None = None) -> int: evidence={"reason": str(error)}, )] inventory = { - "schema": "wellmanifest.worktrees/v4", + "schema": None, "readOnly": True, "entries": [], } diff --git a/.governance/worktree_overlap_check.py b/.governance/worktree_overlap_check.py index 367bf42..1a84603 100755 --- a/.governance/worktree_overlap_check.py +++ b/.governance/worktree_overlap_check.py @@ -98,7 +98,7 @@ class AuditError(RuntimeError): def load_worktrees_contract(): - """Load the exact managed Worktrees v4 inventory without bytecode writes.""" + """Load the exact managed Worktrees inventory without bytecode writes.""" script = Path(__file__).resolve() candidates = ( script.with_name("worktree_path_check.py"), @@ -106,10 +106,10 @@ def load_worktrees_contract(): ) source = next((candidate for candidate in candidates if candidate.is_file()), None) if source is None: - raise AuditError("the managed Worktrees v4 conformance module is missing") - spec = importlib.util.spec_from_file_location("overlap_worktrees_v4", source) + raise AuditError("the managed Worktrees conformance module is missing") + spec = importlib.util.spec_from_file_location("overlap_worktrees_contract", source) if spec is None or spec.loader is None: - raise AuditError(f"cannot load Worktrees v4 conformance from {source}") + raise AuditError(f"cannot load Worktrees conformance from {source}") module = importlib.util.module_from_spec(spec) previous = sys.dont_write_bytecode sys.dont_write_bytecode = True @@ -117,7 +117,7 @@ def load_worktrees_contract(): sys.modules[spec.name] = module spec.loader.exec_module(module) except (ImportError, OSError, ValueError) as error: - raise AuditError(f"cannot load Worktrees v4 conformance: {error}") from error + raise AuditError(f"cannot load Worktrees conformance: {error}") from error finally: sys.dont_write_bytecode = previous sys.modules.pop(spec.name, None) @@ -265,9 +265,9 @@ def workspace_inventory(checkouts: list[Checkout]) -> dict[str, Any]: path_style=path_style, ) except (TypeError, ValueError) as error: - raise AuditError(f"Worktrees v4 inventory failed: {error}") from error + raise AuditError(f"Worktrees inventory failed: {error}") from error if observed.get("readOnly") is not True: - raise AuditError("Worktrees v4 inventory did not declare readOnly=true") + raise AuditError("Worktrees inventory did not declare readOnly=true") for entry in observed["entries"]: layout_entries[Path(entry["path"])] = entry @@ -279,7 +279,7 @@ def workspace_inventory(checkouts: list[Checkout]) -> dict[str, Any]: for checkout in sorted(checkouts, key=lambda item: str(item.path)): layout = layout_entries.get(checkout.path) if layout is None: - raise AuditError(f"Worktrees v4 inventory omitted {checkout.path}") + raise AuditError(f"Worktrees inventory omitted {checkout.path}") duplicate = checkout.common_git_dir != authoritative_clone[checkout.identity] anomalies = list(layout["anomalies"]) if duplicate: @@ -297,7 +297,7 @@ def workspace_inventory(checkouts: list[Checkout]) -> dict[str, Any]: "cloneClassification": "duplicate-clone" if duplicate else "registered", "anomalies": sorted(set(anomalies)), }) - return {"schema": "wellmanifest.worktrees/v4", "readOnly": True, "entries": entries} + return {"schema": contract.SCHEMA, "readOnly": True, "entries": entries} def path_ignored(relative: str, ignore: tuple[str, ...]) -> bool: @@ -883,7 +883,7 @@ def report_payload( "status": "passed" if not findings else "failed", "scope": only_identity or "workspace", "inventory": inventory or { - "schema": "wellmanifest.worktrees/v4", + "schema": None, "readOnly": True, "entries": [], }, @@ -981,7 +981,7 @@ def main(argv: list[str] | None = None) -> int: ] checkouts = [] inventory = { - "schema": "wellmanifest.worktrees/v4", + "schema": None, "readOnly": True, "entries": [], } diff --git a/.governance/worktree_path_check.py b/.governance/worktree_path_check.py index cb4d0a1..a509631 100755 --- a/.governance/worktree_path_check.py +++ b/.governance/worktree_path_check.py @@ -1,4 +1,4 @@ -"""Planner, validator and read-only inventory for wellmanifest.worktrees/v4.""" +"""Planner, validator and read-only inventory for wellmanifest.worktrees/v5.""" from __future__ import annotations @@ -13,7 +13,7 @@ from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath from typing import Any -SCHEMA = "wellmanifest.worktrees/v4" +SCHEMA = "wellmanifest.worktrees/v5" MINIMUM_GIT_VERSION = "2.51.0" NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") TICKET_RE = re.compile(r"^ticket-([0-9]{3,})$") @@ -43,7 +43,7 @@ def plan( primary_checkout: str, path_style: str = "posix", ) -> dict[str, str]: - """Return the canonical v4 layout record for one delivery unit.""" + """Return the canonical v5 layout record for one delivery unit.""" _validate_segment("repositoryName", repository_name) _validate_segment("slug", slug) ticket_match = TICKET_RE.fullmatch(ticket) @@ -55,7 +55,7 @@ def plan( if not primary.is_absolute(): raise ValueError("primaryCheckout must be absolute") stem = f"{ticket}--{slug}" - worktrees_root = primary / "worktrees" + worktrees_root = primary / ".worktrees" lease_root = primary / ".subactor" / "leases" return { "schema": SCHEMA, @@ -111,7 +111,7 @@ def validate_layout(record: dict[str, Any]) -> list[str]: def validate(record: dict[str, Any]) -> list[str]: - """Validate either public v4 record kind.""" + """Validate either public v5 record kind.""" if record.get("kind") == "layout-record": return validate_layout(record) if record.get("kind") == "inventory-record": @@ -247,10 +247,14 @@ def classify_path( if candidate == primary: classification = "primary" + elif ( + value := _direct_child_stem(candidate, primary / ".worktrees") + ) and STEM_RE.fullmatch(value): + classification, layout_version, stem = "canonical-v5", "v5", value elif ( value := _direct_child_stem(candidate, primary / "worktrees") ) and STEM_RE.fullmatch(value): - classification, layout_version, stem = "canonical-v4", "v4", value + classification, layout_version, stem = "legacy-v4", "v4", value elif ( value := _direct_child_stem( candidate, workspace / ".worktrees" / ".branches" / repository_name @@ -424,9 +428,19 @@ def _version_tuple(value: str) -> tuple[int, int, int]: def feature_probe( git: str = "git", runner: Callable[..., subprocess.CompletedProcess[bytes]] = subprocess.run, + *, + from_worktree: str = ".", ) -> dict[str, Any]: - """Probe both the minimum version and required relative-path options.""" - version_result = runner([git, "--version"], check=False, capture_output=True) + """Probe Git in the chosen repository, without mutating caller state.""" + # Hooks and concurrent hosts may inherit selectors for another checkout. + # The explicit cwd owns this read-only observation, not those selectors. + env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")} + context = runner( + [git, "-C", from_worktree, "rev-parse", "--git-dir"], + check=False, capture_output=True, env=env, + ) + context_ok = context.returncode == 0 + version_result = runner([git, "--version"], check=False, capture_output=True, env=env) version_text = (version_result.stdout + version_result.stderr).decode( "utf-8", "replace" ).strip() @@ -440,12 +454,19 @@ def feature_probe( options: dict[str, bool] = {} for command in ("add", "repair"): - result = runner([git, "worktree", command, "-h"], check=False, capture_output=True) - help_text = (result.stdout + result.stderr).decode("utf-8", "replace") - options[command] = "relative-paths" in help_text + options[command] = False + if context_ok: + result = runner( + [git, "-C", from_worktree, "worktree", command, "-h"], + check=False, capture_output=True, env=env, + ) + help_text = (result.stdout + result.stderr).decode("utf-8", "replace") + options[command] = result.returncode in (0, 129) and "relative-paths" in help_text supported = version_ok and all(options.values()) return { "minimumGitVersion": MINIMUM_GIT_VERSION, + "repositoryContextValid": context_ok, + "probeError": None if context_ok else "repository_context_unavailable", "gitVersion": version_text, "versionSupported": version_ok, "worktreeAddRelativePaths": options["add"], @@ -490,6 +511,8 @@ def main() -> int: probe = subparsers.add_parser("feature-probe") probe.add_argument("--git", default="git") + probe.add_argument("--from-worktree", default=".", + help="Repository to probe; independent of the caller cwd") args = parser.parse_args() if args.command == "plan": @@ -524,7 +547,7 @@ def main() -> int: print(json.dumps(record, indent=2)) return 0 - result = feature_probe(args.git) + result = feature_probe(args.git, from_worktree=args.from_worktree) print(json.dumps(result, indent=2)) return 0 if result["supported"] else 1 diff --git a/.governance/worktrees.lock.json b/.governance/worktrees.lock.json index 7abd87c..75cfd1b 100644 --- a/.governance/worktrees.lock.json +++ b/.governance/worktrees.lock.json @@ -2,22 +2,22 @@ "schema": "new-project.worktrees-lock/v1", "dependency": { "id": "wellmanifest/worktrees", - "version": "0.4.1", + "version": "0.5.1", "sourceRepository": "wellmanifest/worktrees", - "sourceRevision": "46db8845637cef2388282b15fe6b17fc76c141d3" + "sourceRevision": "81e0d750f18ecace4436706250bf5deb190a000a" }, "artifacts": [ { "sourcePath": "models/worktrees.schema.json", "packageSourcePath": "subprojects/worktrees/worktrees.schema.json", "managedTargetPath": ".governance/worktrees.schema.json", - "sourceSha256": "5ba905865a72aea6a01afff1d06de028224651d2b444f6ac528cf65499d30148" + "sourceSha256": "9cc10d126e06cafc87cc117f11b8b095676f461de4d168d2a1c2bb959f0fccd5" }, { "sourcePath": "operations/conformance.py", "packageSourcePath": "subprojects/worktrees/conformance.py", "managedTargetPath": ".governance/worktree_path_check.py", - "sourceSha256": "6333676ac045246a655ea6fc042e83d5bbceef5b03b80eca50a12dc98a7aabeb" + "sourceSha256": "d3309b76e2c91dd7f046b00322ec8cb48ef744b8b7908d70e475fedf95e5e196" } ], "installation": { diff --git a/.governance/worktrees.schema.json b/.governance/worktrees.schema.json index 5b07422..e0d5d7c 100644 --- a/.governance/worktrees.schema.json +++ b/.governance/worktrees.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://wellmanifest.org/schemas/worktrees/v4", + "$id": "https://wellmanifest.org/schemas/worktrees/v5", "title": "Wellmanifest worktree records", "oneOf": [ {"$ref": "#/$defs/layoutRecord"}, @@ -67,7 +67,7 @@ "minimumGitVersion" ], "properties": { - "schema": {"const": "wellmanifest.worktrees/v4"}, + "schema": {"const": "wellmanifest.worktrees/v5"}, "kind": {"const": "layout-record"}, "repository": {"type": "string", "minLength": 1}, "repositoryName": {"$ref": "#/$defs/repositoryName"}, @@ -110,7 +110,8 @@ "classification": { "enum": [ "primary", - "canonical-v4", + "canonical-v5", + "legacy-v4", "legacy-v3", "legacy-v2", "legacy-v1", @@ -120,7 +121,7 @@ }, "layoutVersion": { "oneOf": [ - {"enum": ["v1", "v2", "v3", "v4"]}, + {"enum": ["v1", "v2", "v3", "v4", "v5"]}, {"type": "null"} ] }, @@ -160,7 +161,7 @@ "summary" ], "properties": { - "schema": {"const": "wellmanifest.worktrees/v4"}, + "schema": {"const": "wellmanifest.worktrees/v5"}, "kind": {"const": "inventory-record"}, "repository": {"type": "string", "minLength": 1}, "repositoryName": {"$ref": "#/$defs/observedRepositoryName"}, diff --git a/AGENTS.md b/AGENTS.md index 4069436..fef28e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,15 +63,17 @@ Before any multi-step implementation, an agent must: managed `wellmanifest/worktrees` checker. Resolve the primary checkout from Git even when allocation starts inside a linked checkout. The only publishable linked worktree is - `/worktrees/--` with + `/.worktrees/--` with `linkMode=relative`; its lease is `/.subactor/leases/--.json`. Root-ignore - `/worktrees/` and only + `/.worktrees/` and only `/.subactor/{leases,sessions,recovery,receipts,cache,snapshots}/`; keep `.subactor/manifest.json` tracked. Before the first effect, feature-probe `git worktree add --relative-paths` and - `git worktree repair --relative-paths` (minimum Git 2.51.0), and reject a - symlink in any existing canonical path component. Legacy v1/v2/v3, + `git worktree repair --relative-paths` (minimum Git 2.51.0). When the host starts outside the target checkout, pass + `feature-probe --from-worktree ` to the adopted checker; resolve + `repository_context_unavailable` before interpreting feature support. Reject a + symlink in any existing canonical path component. Legacy v1/v2/v3/v4, system-temporary, duplicate and unknown registrations are read-only recovery inventory, never publishable locations. Never automatically move, repair, delete, prune or clean them. A separately authorized exact operation first @@ -92,23 +94,22 @@ Before any multi-step implementation, an agent must: either a `User` login is in protected `trusted-reviewers` or a `Bot` login is in the separate protected `trusted-validator-apps` input. Never trust an arbitrary Bot review. -11a. **INVOKE VALIDATOR-AGENT — do not ask the human.** When publication needs - trusted merge approval (protected default branch, merge blocked on reviews, - or the user asks to publish/merge), the coding agent MUST dispatch - `subactor/validator-agent` itself: +11a. **USE LOCAL ONEDEV AND THE INDEPENDENT VALIDATOR.** For `semcod/*` and + `subactor/*`, follow [.governance/docs/LOCAL_CI_PUBLICATION.md](.governance/docs/LOCAL_CI_PUBLICATION.md). + Resolve the protected repository profile, observe the current OneDev + head/base receipt and reuse any existing local reconciliation result. + Invoke the trusted `subactor/validator-agent/bin/run-local-direct-pr.sh` + with the exact repository, PR, ticket, head SHA and protected key reference; + use `--merge` only for already authorized publication. The deployed local + timer may own this invocation. GitHub Actions dispatch is a separate + transport and is not the default or an unavoidable dependency of local CI. + Do not declare publication blocked by Actions billing before checking the + local route. Retire a hosted check only through protected policy after an + equivalent deployed OneDev canary; preserve uncovered test/platform gates. + Freeze the head through review and merge. Never self-approve, write a fake + status, waive required checks or ask the human to invoke an available + Validator. Scope, pins, deployment and observed success are separate facts. - ```bash - # from a checkout of subactor/validator-agent @ main - ./bin/dispatch-direct-pr.sh \ - --owner --name --pr --ticket ticket-NNN \ - --wait-checks --merge --watch - ``` - - Freeze: re-read `gh pr view --json headRefOid` immediately before - dispatch; do not push after freeze until approve or fail. MUST NOT ask the - human to “use the validator”, MUST NOT self-approve, and MUST NOT treat - chat/Markdown as merge approval. Normative: - `subactor/validator-agent/docs/PUBLICATION_FREEZE.md`. 12. Require merge approval evidence to bind repository, PR, current HEAD, active ticket and actor. The protected resolver creates that evidence outside the PR checkout; repository-authored evidence is untrusted. diff --git a/project/ticket-089/README.md b/project/ticket-089/README.md new file mode 100644 index 0000000..d2f53f2 --- /dev/null +++ b/project/ticket-089/README.md @@ -0,0 +1,15 @@ +# ticket-089: Local CI publication policy + +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION + +SESSION_EXECUTION_AUTHORIZATION: user requests implementation and publication of Wellmanifest standards across Semcod and Subactor. + +## Acceptance criteria + +- [ ] AC-01: Adopt published immutable new-project 0.20.11 through the managed updater. +- [ ] AC-02: Preserve required checks, validate scope and use independent protected publication. + +Canonical fleet evidence: `subactor/docs/architecture/analysis/local-ci-adoption.md`. Full runtime CI coverage is assessed separately. + +Routing correction: existing governance ownership covers this standard adoption; package declarations are verified atomic adoption bindings. The source ownership manifest is preserved. diff --git a/project/ticket-089/intent.json b/project/ticket-089/intent.json new file mode 100644 index 0000000..8799188 --- /dev/null +++ b/project/ticket-089/intent.json @@ -0,0 +1,100 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-089", + "summary": "Adopt local CI publication policy", + "workstream": "governance", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "requested" + }, + "allowedPaths": [ + ".gitignore", + ".governance/docs/LOCAL_CI_PUBLICATION.md", + ".governance/manifest.base.json", + ".governance/manifest.json", + ".governance/manifest.lock.json", + ".governance/package-manifest.json", + ".governance/workspace_lifecycle_check.py", + ".governance/worktree_overlap_check.py", + ".governance/worktree_path_check.py", + ".governance/worktrees.lock.json", + ".governance/worktrees.schema.json", + "AGENTS.md", + "project/ticket-089/**", + "pyproject.toml", + "scripts/install-agent-hosts.sh" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "f454f8083d7ecb9b3580ffce94a90e4d057da184", + "targetBranch": "main", + "outcome": "Adopt immutable published local CI publication instructions while preserving protected checks and custom manifest settings.", + "nonGoals": [ + "No migration of executor profiles, required checks, credentials or runtime deployments." + ], + "complexity": "M", + "estimatedMinutes": 60, + "budgets": { + "maxImplementationFiles": 9, + "maxAffectedComponents": 2, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Use the standard managed updater and its independently verified immutable adoption exception. Route this adoption to the existing governance owner; do not change manifest path ownership.", + "components": [ + { + "name": "governance", + "paths": [ + ".gitignore", + ".governance/docs/LOCAL_CI_PUBLICATION.md", + ".governance/manifest.base.json", + ".governance/manifest.json", + ".governance/manifest.lock.json", + ".governance/package-manifest.json", + ".governance/workspace_lifecycle_check.py", + ".governance/worktree_overlap_check.py", + ".governance/worktree_path_check.py", + ".governance/worktrees.lock.json", + ".governance/worktrees.schema.json", + "AGENTS.md", + "pyproject.toml", + "scripts/install-agent-hosts.sh" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Review a subsequent immutable standard update; preserve previous pins in history." + }, + "runtimeDependencies": [], + "standardAdoption": { + "sourceRepository": "wellmanifest/new-project", + "fromRevision": "9d7af1e63d46e0277da407286699ee50de726d17", + "toRevision": "2cd39286dc0931870a8a60ee11c96dd6448b39e6" + }, + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "./project/governance-check.sh --actor agent" + ], + "evidence": "receipt:local:ticket-089-local-ci-adoption" + } + ] + } +} diff --git a/pyproject.toml b/pyproject.toml index 37e92e9..441a43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,8 +71,8 @@ python_files = ["test_*.py"] addopts = "-p wellmanifest_governance" [tool.wellmanifest] -standard = "0.20.7" -revision = "9d7af1e63d46e0277da407286699ee50de726d17" +standard = "0.20.11" +revision = "2cd39286dc0931870a8a60ee11c96dd6448b39e6" gate = "project/governance-check.sh" [tool.pfix] diff --git a/scripts/install-agent-hosts.sh b/scripts/install-agent-hosts.sh index 97f4b75..9c0ec36 100755 --- a/scripts/install-agent-hosts.sh +++ b/scripts/install-agent-hosts.sh @@ -74,14 +74,22 @@ contract_path() { # Emits "\t" lines for every file the contract governs. contract_targets() { - python3 - "$1" <<'PY' -import json, sys + python3 - "$1" "$2" <<'PY' +import json, pathlib, sys contract = json.load(open(sys.argv[1], encoding="utf-8")) +root = pathlib.Path(sys.argv[2]) +hub = root / "governance/manifest.hub.json" +source_paths = {} +if hub.is_file(): + package = json.loads((root / "governance/package-manifest.json").read_text()) + source_paths = {item["target"]: item["source"] for item in package["files"]} for host in contract["hosts"]: print(f"{host['file']}\t0") print(f"{contract['hook']['path']}\t1") for runtime_file in contract["hook"]["runtimeFiles"]: - print(f"{runtime_file}\t0") + # The hub owns package sources; adopters own the managed target paths. + relative = source_paths.get(runtime_file, runtime_file) + print(f"{relative}\t0") PY } @@ -128,7 +136,7 @@ activate_in_place() { if [[ "$is_hook" == "1" && "$CHECK_ONLY" == false ]]; then chmod +x "$dest/$target" fi - done < <(contract_targets "$contract") + done < <(contract_targets "$contract" "$dest") if [[ "${#missing[@]}" -gt 0 ]]; then printf 'GOV-AGENT-HOST-004: missing host files in %s:\n' "$dest" >&2