From 2330a433ff5b2ed94b4441b25461edad9a416904 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 19 Sep 2026 18:54:29 +0200 Subject: [PATCH] chore(ticket-077): adopt wellmanifest/new-project 0.20.32 --- .aider.conf.yml | 12 + .cursor/rules/new-project-standard.mdc | 19 ++ .github/copilot-instructions.md | 19 ++ .governance/agent-hosts.json | 50 +++- .governance/agent-hosts.schema.json | 98 ++++++- .governance/agent_host_check.py | 333 +++++++++++++++++++++- .governance/error/GOV-AGENT-HOST.md | 7 + .governance/error/GOV-WORK-START.md | 33 +++ .governance/manifest.base.json | 2 +- .governance/manifest.json | 59 ++-- .governance/manifest.lock.json | 34 +-- .governance/work-start-report.schema.json | 12 +- .governance/work_start_check.py | 64 ++++- AGENTS.md | 33 +++ CLAUDE.md | 19 ++ GEMINI.md | 19 ++ project/TICKETS.md | 7 + project/ticket-077/README.md | 21 ++ project/ticket-077/intent.json | 111 ++++++++ pyproject.toml | 6 +- scripts/install-agent-hosts.sh | 48 +++- 21 files changed, 921 insertions(+), 85 deletions(-) create mode 100644 project/ticket-077/README.md create mode 100644 project/ticket-077/intent.json diff --git a/.aider.conf.yml b/.aider.conf.yml index 348a7c7..f133379 100644 --- a/.aider.conf.yml +++ b/.aider.conf.yml @@ -1,3 +1,11 @@ +# +# Managed standard sources: local .governance/manifest.json, +# .governance/manifest.lock.json and .governance/package-manifest.json are +# authoritative. Remote links are navigation only and are never fetched at runtime. +# Canonical instructions: https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md +# Host contract: https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json +# Immutable adoption/updater: https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py +# # wellmanifest/new-project — fail-closed contract for aider. # aider loads these files into every session, so the same rules apply here as # in Cursor, Claude Code or Gemini. The pre-commit hook enforces them. @@ -8,3 +16,7 @@ read: # Never let the tool create commits the governance hook has not seen. auto-commits: false attribute-commit-message-author: true + +# Bounded session controls: respect the ticket's maxActiveMinutes, create a +# checkpoint before a context or tool boundary, and leave a handoff then stop +# after a deterministic failure instead of retrying indefinitely. diff --git a/.cursor/rules/new-project-standard.mdc b/.cursor/rules/new-project-standard.mdc index 42e4aad..8298ad6 100644 --- a/.cursor/rules/new-project-standard.mdc +++ b/.cursor/rules/new-project-standard.mdc @@ -3,6 +3,21 @@ description: wellmanifest/new-project fail-closed ticket contract for every Curs alwaysApply: true --- + +## Managed standard sources + +Local adoption files are authoritative. Remote links are navigation only and +must not be fetched or executed at runtime. + +- Local adoption manifest: [.governance/manifest.json](.governance/manifest.json) +- Local adoption lock: [.governance/manifest.lock.json](.governance/manifest.lock.json) +- Local package map: [.governance/package-manifest.json](.governance/package-manifest.json) +- Canonical instructions: [AGENTS template](https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md) +- Host contract: [agent-hosts.json](https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json) +- Immutable adoption/updater: [create_adoption_lock.py](https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py) + + + # new-project standard (host-agnostic) Before writing code in this repository: @@ -15,3 +30,7 @@ Before writing code in this repository: 6. Run `./project/governance-check.sh` before claiming done. The pre-commit hook rejects commits that are not bound to an `IN_PROGRESS` ticket. Do not invent ticket numbers. Do not ask the human to approve a merge; invoke validator-agent when publication needs trusted approval. + +Bounded session controls: respect the ticket's `maxActiveMinutes`, create a +`checkpoint` before a context or tool boundary, and leave a `handoff` then +`stop` after a deterministic failure instead of retrying indefinitely. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8c2e556..1b5cdd1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,5 +1,20 @@ # GitHub Copilot instructions + +## Managed standard sources + +The local adoption manifest, lock and package are authoritative. Remote links +are navigation only and must not be fetched or executed at runtime. + +- Local adoption manifest: [.governance/manifest.json](.governance/manifest.json) +- Local adoption lock: [.governance/manifest.lock.json](.governance/manifest.lock.json) +- Local package map: [.governance/package-manifest.json](.governance/package-manifest.json) +- Canonical instructions: [AGENTS template](https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md) +- Host contract: [agent-hosts.json](https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json) +- Immutable adoption/updater: [create_adoption_lock.py](https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py) + + + This repository follows the `wellmanifest/new-project` policy-as-code standard. Same fail-closed contract as `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` and the Cursor rule. Copilot Chat and the Copilot coding agent load this file automatically. @@ -15,3 +30,7 @@ rule. Copilot Chat and the Copilot coding agent load this file automatically. Suggestions that skip these steps are rejected by the pre-commit hook and by the `governance / enforce` CI job. Markdown is not a substitute for either. + +Bounded session controls: respect the ticket's `maxActiveMinutes`, create a +`checkpoint` before a context or tool boundary, and leave a `handoff` then +`stop` after a deterministic failure instead of retrying indefinitely. diff --git a/.governance/agent-hosts.json b/.governance/agent-hosts.json index 2eb64f6..eb58915 100644 --- a/.governance/agent-hosts.json +++ b/.governance/agent-hosts.json @@ -1,6 +1,6 @@ { "schema": "new-project.agent-hosts/v1", - "note": "Single source of truth for the host-agnostic agent contract (AGENTS.md rule 22). scripts/agent_host_check.py proves these files are present, that the fail-closed hook is installed and active, and that the packaging metadata a repository already carries actually runs the gate. Instruction files are advisory to a model; the hook, the CI job and the packaging lifecycle bindings declared here are the parts that are not. Adding a host here without adding it to governance/package-manifest.json makes the requirement unshippable, so both change together.", + "note": "Single source of truth for the host-agnostic agent contract (AGENTS.md rule 22). scripts/agent_host_check.py proves these files are present, that the fail-closed hook is installed and active, that source links point at the local adoption contract and concrete remote standard files, that bounded-session controls are present, and that the packaging metadata a repository already carries actually runs the gate. Instruction files are advisory to a model; the hook, the CI job and the packaging lifecycle bindings declared here are the parts that are not. Adding a host here without adding it to governance/package-manifest.json makes the requirement unshippable, so both change together.", "hook": { "path": ".githooks/pre-commit", "hooksPathConfig": ".githooks", @@ -58,6 +58,54 @@ ] } ], + "sourceLinks": { + "schema": "new-project.agent-source-links/v1", + "authority": "Local adoption lock, local managed-file digests and protected validation are authoritative. Remote main URLs are navigation only; host instructions never fetch or execute them.", + "local": [ + {"id": "hub-manifest", "path": "governance/manifest.hub.json"}, + {"id": "hub-package", "path": "governance/package-manifest.json"}, + {"id": "adopter-manifest", "path": ".governance/manifest.json"}, + {"id": "adopter-lock", "path": ".governance/manifest.lock.json"}, + {"id": "adopter-package", "path": ".governance/package-manifest.json"} + ], + "remote": [ + {"id": "new-project-agents", "repository": "wellmanifest/new-project", "path": "template/files/AGENTS.template.md", "url": "https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md"}, + {"id": "new-project-hosts", "repository": "wellmanifest/new-project", "path": "governance/agent-hosts.json", "url": "https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json"}, + {"id": "new-project-adoption", "repository": "wellmanifest/new-project", "path": "scripts/create_adoption_lock.py", "url": "https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py"}, + {"id": "worktrees-schema", "repository": "wellmanifest/worktrees", "path": "models/worktrees.schema.json", "url": "https://github.com/wellmanifest/worktrees/blob/main/models/worktrees.schema.json"}, + {"id": "git-lifecycle-schema", "repository": "wellmanifest/git-lifecycle", "path": "standard/git-lifecycle.schema.json", "url": "https://github.com/wellmanifest/git-lifecycle/blob/main/standard/git-lifecycle.schema.json"}, + {"id": "ticket-lifecycle-schema", "repository": "wellmanifest/ticket-lifecycle", "path": "standard/ticket-lifecycle.schema.json", "url": "https://github.com/wellmanifest/ticket-lifecycle/blob/main/standard/ticket-lifecycle.schema.json"}, + {"id": "policy-dsl", "repository": "wellmanifest/policy-dsl", "path": "spec/POLICY_DSL.md", "url": "https://github.com/wellmanifest/policy-dsl/blob/main/spec/POLICY_DSL.md"}, + {"id": "logs-contract", "repository": "wellmanifest/logs", "path": "contracts/logs.contract.json", "url": "https://github.com/wellmanifest/logs/blob/main/contracts/logs.contract.json"}, + {"id": "agent-schema", "repository": "wellmanifest/agent", "path": "standard/agent.schema.json", "url": "https://github.com/wellmanifest/agent/blob/main/standard/agent.schema.json"}, + {"id": "llm-policy", "repository": "wellmanifest/llm", "path": "README.md", "url": "https://github.com/wellmanifest/llm/blob/main/README.md"}, + {"id": "offer-pointer", "repository": "wellmanifest/offer", "path": "README.md", "url": "https://github.com/wellmanifest/offer/blob/main/README.md"}, + {"id": "brand-pointer", "repository": "wellmanifest/brand", "path": "README.md", "url": "https://github.com/wellmanifest/brand/blob/main/README.md"} + ], + "requiredInEveryHost": ["new-project-agents", "new-project-hosts", "new-project-adoption"], + "requiredInAgents": ["new-project-agents", "new-project-hosts", "new-project-adoption", "worktrees-schema", "git-lifecycle-schema", "ticket-lifecycle-schema", "policy-dsl", "logs-contract", "agent-schema", "llm-policy", "offer-pointer", "brand-pointer"] + }, + "anomalyChecks": { + "schema": "new-project.agent-guidance-audit/v1", + "maxInstructionBytes": 65536, + "requiredTerms": ["checkpoint", "handoff", "stop", "maxActiveMinutes"], + "contradictions": [ + { + "id": "direct-default-branch-delivery", + "patterns": ["push directly to main", "never push directly to main"] + }, + { + "id": "self-merge", + "patterns": ["merge directly", "never merge directly"] + } + ], + "ci": { + "requiredChecksCandidates": [ + "governance/required-checks.json", + ".governance/required-checks.json" + ] + } + }, "packaging": { "python": { "marker": "pyproject.toml", diff --git a/.governance/agent-hosts.schema.json b/.governance/agent-hosts.schema.json index a1b3d77..19d1d13 100644 --- a/.governance/agent-hosts.schema.json +++ b/.governance/agent-hosts.schema.json @@ -5,7 +5,7 @@ "description": "Declares the instruction files every LLM host loads, the fail-closed git hook, and the packaging touchpoints that make the contract executable rather than advisory.", "type": "object", "additionalProperties": false, - "required": ["schema", "hook", "hosts", "packaging", "declarationFields"], + "required": ["schema", "hook", "hosts", "sourceLinks", "anomalyChecks", "packaging", "declarationFields"], "properties": { "schema": { "const": "new-project.agent-hosts/v1" }, "note": { "type": "string" }, @@ -42,6 +42,102 @@ } } }, + "sourceLinks": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "authority", "local", "remote", "requiredInEveryHost", "requiredInAgents"], + "properties": { + "schema": { "const": "new-project.agent-source-links/v1" }, + "authority": { "type": "string", "minLength": 1 }, + "local": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "path"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, + "path": { "type": "string", "pattern": "^[^/][^\\\\]*$" } + } + } + }, + "remote": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "repository", "path", "url"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, + "repository": { "type": "string", "pattern": "^wellmanifest/[a-z0-9.-]+$" }, + "path": { "type": "string", "pattern": "^[^/][^\\\\]*$" }, + "url": { "type": "string", "pattern": "^https://github\\.com/wellmanifest/[a-z0-9.-]+/blob/main/.+$" } + } + } + }, + "requiredInEveryHost": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" } + }, + "requiredInAgents": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" } + } + } + }, + "anomalyChecks": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "maxInstructionBytes", "requiredTerms", "contradictions", "ci"], + "properties": { + "schema": { "const": "new-project.agent-guidance-audit/v1" }, + "maxInstructionBytes": { "type": "integer", "minimum": 1 }, + "requiredTerms": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "contradictions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "patterns"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, + "patterns": { + "type": "array", + "minItems": 2, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + } + } + } + }, + "ci": { + "type": "object", + "additionalProperties": false, + "required": ["requiredChecksCandidates"], + "properties": { + "requiredChecksCandidates": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + } + } + } + } + }, "packaging": { "type": "object", "minProperties": 1, diff --git a/.governance/agent_host_check.py b/.governance/agent_host_check.py index de9feca..aedf998 100644 --- a/.governance/agent_host_check.py +++ b/.governance/agent_host_check.py @@ -13,6 +13,7 @@ import argparse import json import os +import re import subprocess import sys from dataclasses import dataclass, field @@ -34,6 +35,7 @@ SCHEMA = "new-project.agent-hosts/v1" CONTRACT_CANDIDATES = ("governance/agent-hosts.json", ".governance/agent-hosts.json") LOCK_CANDIDATES = ("governance/manifest.lock.json", ".governance/manifest.lock.json") +SOURCE_LINK_MARKER = "" @dataclass(order=True) @@ -98,6 +100,133 @@ def check_hosts(root: Path, contract: dict[str, Any]) -> list[Finding]: return findings +def check_source_links(root: Path, contract: dict[str, Any]) -> list[Finding]: + """Require every managed host projection to expose its bounded sources. + + The local files prove which package is adopted. Remote links are deliberately + navigation-only and point to concrete files on the current standard branch; + no validator fetches them and they never replace the local lock/digests. + """ + findings: list[Finding] = [] + source_links = contract.get("sourceLinks") + if not isinstance(source_links, dict): + return [Finding( + "GOV-AGENT-HOST-004", + "Agent host contract has no source-links declaration.", + "Adopt the current standard package with its managed source-links contract.", + ["sourceLinks"], + )] + + local = source_links.get("local", []) + remote = source_links.get("remote", []) + remote_ids = [ + item.get("id") for item in remote + if isinstance(item, dict) and isinstance(item.get("id"), str) + ] + duplicate_ids = sorted({identifier for identifier in remote_ids if remote_ids.count(identifier) > 1}) + if duplicate_ids: + return [Finding( + "GOV-AGENT-HOST-004", + "Agent host source-links declaration contains duplicate remote ids: " + + ", ".join(duplicate_ids), + "Restore unique remote source identifiers in the managed host contract.", + ["sourceLinks"], + )] + by_id = { + item.get("id"): item for item in remote + if isinstance(item, dict) and isinstance(item.get("id"), str) + } + required_every = source_links.get("requiredInEveryHost", []) + required_agents = source_links.get("requiredInAgents", []) + required_ids = set(required_every) | set(required_agents) + missing_ids = sorted(identifier for identifier in required_ids if identifier not in by_id) + if missing_ids: + findings.append(Finding( + "GOV-AGENT-HOST-004", + "Agent host source-links declaration references unknown remote ids: " + + ", ".join(missing_ids), + "Restore the managed source-links contract from the standard package.", + ["sourceLinks"], + )) + return findings + + malformed_urls = [] + for identifier, item in by_id.items(): + repository = item.get("repository") + path = item.get("path") + url = item.get("url") + expected = f"https://github.com/{repository}/blob/main/{path}" + if not isinstance(url, str) or url != expected: + malformed_urls.append(identifier) + if malformed_urls: + findings.append(Finding( + "GOV-AGENT-HOST-004", + "Agent host source-links declaration contains non-canonical URLs: " + + ", ".join(sorted(malformed_urls)), + "Use the concrete main-branch URL derived from each declared repository and path.", + ["sourceLinks"], + )) + return findings + + local_paths = [ + str(item["path"]) + for item in local + if isinstance(item, dict) + and isinstance(item.get("path"), str) + and (root / str(item["path"])).is_file() + ] + if not local_paths: + findings.append(Finding( + "GOV-AGENT-HOST-004", + "No declared local source link resolves in this checkout.", + "Restore the local adoption lock/package or the hub manifest/package before using host instructions.", + ["sourceLinks"], + )) + return findings + + for host in contract["hosts"]: + relative = str(host["file"]) + path = root / relative + if not path.is_file(): + continue + try: + content = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as error: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host source links are unreadable in {relative}: {error}", + "Restore the managed host projection through standard adoption.", + [relative], + )) + continue + if SOURCE_LINK_MARKER not in content: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host instruction file has no managed source-links marker: {relative}", + "Regenerate managed host instructions from the pinned standard package.", + [relative], + )) + for local_path in local_paths: + if local_path not in content: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host instruction file omits local source link {local_path}: {relative}", + "Regenerate managed host instructions from the pinned standard package.", + [relative, local_path], + )) + remote_ids = required_agents if relative == "AGENTS.md" else required_every + for identifier in remote_ids: + url = by_id[identifier].get("url") + if not isinstance(url, str) or url not in content: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host instruction file omits remote source link {identifier}: {relative}", + "Regenerate managed host instructions from the pinned standard package.", + [relative, identifier], + )) + return findings + + def check_hook(root: Path, contract: dict[str, Any], actor: str) -> list[Finding]: findings: list[Finding] = [] hook_relative = str(contract["hook"]["path"]) @@ -251,6 +380,206 @@ def check_declaration( return findings +def workflow_job_names(path: Path) -> list[str]: + """Parse the small, stable subset of GitHub workflow YAML we need. + + The required-checks validator owns the complete workflow contract. This + deliberately remains a narrow, dependency-free preflight so an agent-host + audit can flag an impossible CI declaration before a long session starts. + """ + lines = path.read_text(encoding="utf-8").splitlines() + in_jobs = False + jobs: list[str] = [] + current_key: str | None = None + current_name: str | None = None + + def flush() -> None: + nonlocal current_key, current_name + if current_key is not None: + jobs.append(current_name or current_key) + current_key = None + current_name = None + + for line in lines: + if re.match(r"^jobs:\s*(?:#.*)?$", line): + in_jobs = True + continue + if not in_jobs: + continue + if (line and not line.startswith((" ", "\t")) + and line.strip() and not line.lstrip().startswith("#")): + break + match = re.match(r"^ ([A-Za-z0-9][A-Za-z0-9_-]*):\s*(?:#.*)?$", line) + if match: + flush() + current_key = match.group(1) + continue + name = re.match(r"^ name:\s*(.+?)\s*$", line) + if name and current_key is not None and current_name is None: + value = name.group(1).strip() + if " #" in value: + value = value.split(" #", 1)[0].rstrip() + if len(value) >= 2 and value[0] in {"'", '"'} and value[-1] == value[0]: + value = value[1:-1] + current_name = value + flush() + return jobs + + +def check_guidance_anomalies(root: Path, contract: dict[str, Any]) -> list[Finding]: + """Catch bounded-session and impossible-CI hazards before model work begins. + + This is intentionally static and offline. It does not fetch remote links, + infer intent from prose, or retry a failed command. A finding is a stop + signal with a concrete path, not an invitation to keep experimenting. + """ + config = contract.get("anomalyChecks") + if not isinstance(config, dict): + return [Finding( + "GOV-AGENT-HOST-004", + "Agent host contract has no deterministic anomaly-check declaration.", + "Adopt the current standard package with its bounded-session audit contract.", + ["anomalyChecks"], + )] + + findings: list[Finding] = [] + max_bytes = config.get("maxInstructionBytes") + required_terms = config.get("requiredTerms", []) + contradictions = config.get("contradictions", []) + if not isinstance(max_bytes, int) or max_bytes < 1: + return [Finding( + "GOV-AGENT-HOST-004", + "Agent host anomaly contract has an invalid instruction-size limit.", + "Declare a positive maxInstructionBytes value in the managed contract.", + ["anomalyChecks"], + )] + + for host in contract["hosts"]: + relative = str(host["file"]) + path = root / relative + if not path.is_file(): + continue # check_hosts emits the more direct missing-file finding. + try: + content = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as error: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host guidance cannot be audited in {relative}: {error}", + "Restore the managed host projection from the pinned package.", + [relative], + )) + continue + size = len(content.encode("utf-8")) + if size > max_bytes: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host guidance exceeds the bounded instruction size ({size} > {max_bytes} bytes): {relative}", + "Split or shorten the managed guidance before the host truncates its instruction chain.", + [relative], + )) + folded = content.casefold() + missing = [ + str(term) for term in required_terms + if isinstance(term, str) and term.casefold() not in folded + ] + if missing: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host guidance lacks bounded-session controls {', '.join(missing)}: {relative}", + "Restore checkpoint, handoff and stop conditions so a blocked session cannot retry indefinitely.", + [relative], + )) + for rule in contradictions: + if not isinstance(rule, dict): + continue + patterns = rule.get("patterns") + if not isinstance(patterns, list) or len(patterns) < 2: + continue + present = [str(pattern) for pattern in patterns + if isinstance(pattern, str) and pattern.casefold() in folded] + if len(present) == len(patterns): + identifier = str(rule.get("id", "unnamed")) + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"Host guidance contains contradictory directives ({identifier}): {relative}", + "Remove one directive or split the rules by an explicit, machine-checkable scope.", + [relative], + )) + + ci = config.get("ci") + if not isinstance(ci, dict): + return findings + candidates = ci.get("requiredChecksCandidates", []) + checks_path = next( + (root / str(candidate) for candidate in candidates + if isinstance(candidate, str) and (root / candidate).is_file()), + None, + ) + if checks_path is None: + findings.append(Finding( + "GOV-AGENT-HOST-004", + "CI anomaly audit cannot find a required-checks declaration.", + "Restore governance/required-checks.json or .governance/required-checks.json.", + ["required-checks"], + )) + return findings + try: + declaration = load_json(checks_path) + except (OSError, json.JSONDecodeError) as error: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"CI required-checks declaration is unreadable: {error}", + "Restore a valid managed required-checks declaration.", + [str(checks_path.relative_to(root))], + )) + return findings + if not isinstance(declaration, dict): + return findings + pairs: list[tuple[str, str]] = [] + bound = declaration.get("requiredChecks") + if isinstance(bound, list) and bound: + for item in bound: + if isinstance(item, dict) and isinstance(item.get("name"), str) and isinstance(item.get("workflowFile"), str): + pairs.append((item["name"], item["workflowFile"])) + elif isinstance(declaration.get("requiredCheckNames"), list) and isinstance(declaration.get("workflowFile"), str): + pairs = [(str(name), declaration["workflowFile"]) + for name in declaration["requiredCheckNames"] + if isinstance(name, str)] + if not pairs: + findings.append(Finding( + "GOV-AGENT-HOST-004", + "CI required-checks declaration has no usable check/workflow pairs.", + "Declare requiredCheckNames with workflowFile, or bound requiredChecks entries.", + [str(checks_path.relative_to(root))], + )) + return findings + for workflow in sorted({workflow for _, workflow in pairs}): + workflow_path = root / workflow + if not workflow_path.is_file(): + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"CI required-checks declaration names a missing workflow: {workflow}", + "Point workflowFile at a workflow present in this checkout.", + [workflow, str(checks_path.relative_to(root))], + )) + continue + try: + published = workflow_job_names(workflow_path) + except (OSError, UnicodeDecodeError): + published = [] + for name, declared_workflow in pairs: + if declared_workflow != workflow: + continue + if name not in published: + findings.append(Finding( + "GOV-AGENT-HOST-004", + f"CI required check {name!r} is not published by {workflow}.", + "Add the job or change the declaration to a job this workflow actually publishes; do not retry a permanently impossible gate.", + [workflow, str(checks_path.relative_to(root))], + )) + return findings + + def load_contract(root: Path, explicit: str | None) -> tuple[dict[str, Any] | None, Finding | None]: if explicit is not None: path = root / explicit if not Path(explicit).is_absolute() else Path(explicit) @@ -280,7 +609,7 @@ def load_contract(root: Path, explicit: str | None) -> tuple[dict[str, Any] | No "GOV-AGENT-HOST-004", f"Agent host contract must declare schema {SCHEMA}.", "Restore the pinned host contract through a standard upgrade.", [str(path.name)], ) - for key in ("hook", "hosts", "packaging"): + for key in ("hook", "hosts", "sourceLinks", "packaging", "anomalyChecks"): if key not in contract: return None, Finding( "GOV-AGENT-HOST-004", f"Agent host contract has no '{key}' section.", @@ -296,8 +625,10 @@ def audit(root: Path, actor: str = "agent", contract_path: str | None = None) -> else: findings = ( check_hosts(root, contract) + + check_source_links(root, contract) + check_hook(root, contract, actor) + check_packaging(root, contract) + + check_guidance_anomalies(root, contract) ) findings.sort() return { diff --git a/.governance/error/GOV-AGENT-HOST.md b/.governance/error/GOV-AGENT-HOST.md index d74f488..d3aeb44 100644 --- a/.governance/error/GOV-AGENT-HOST.md +++ b/.governance/error/GOV-AGENT-HOST.md @@ -9,6 +9,11 @@ emitowane przez `.githooks/pre-commit`, oraz `GOV-AGENT-HOST-004`, również `.githooks`, więc kod emitowany przez hooka nie może już wypaść z katalogu niezauważony. +Ten sam audyt emituje `GOV-AGENT-HOST-004`, gdy instrukcje przekraczają limit +hosta, tracą `checkpoint`/`handoff`/`stop`, zawierają skonfigurowaną sprzeczność +albo deklarują required check, którego workflow nie publikuje. To są blokery +przed rozpoczęciem długiej sesji, a nie sygnały do kolejnych ślepych retry. + ## Situation `001`–`003` oraz `007` pojawiają się przy commicie: branch nie jest związany z ticketem @@ -43,6 +48,8 @@ rzeczywistości sprawdzany. registry. Dla `003` nie twórz commita: chroniony kontroler dostawy zapisuje terminalny receipt poza checkoutem autora. 4. Potwierdź stan: `python3 scripts/agent_host_check.py --root .`. + Przy findingu anomalii napraw źródłowy kontrakt/projekcję lub CI, a potem + uruchom audyt ponownie; nie obchodź go przez `--no-verify`. ## Verification diff --git a/.governance/error/GOV-WORK-START.md b/.governance/error/GOV-WORK-START.md index c5afb63..04a5744 100644 --- a/.governance/error/GOV-WORK-START.md +++ b/.governance/error/GOV-WORK-START.md @@ -59,6 +59,39 @@ copies without renaming or deleting them. Matching HEAD alone, matching paths, or matching patch IDs is insufficient. An unmatched intermediate commit or later new work still routes to reconciliation. Missing history fails closed. +### Stale carrier of an integrated ticket + +Blocker `integrated-ticket-carrier` names an active-projected ticket whose +carrier is dirty in a checkout while its directory is already on the observed +target and no `ticket/NNN` branch lies outside that target. A typical source is +an allocation-time copy left in a primary checkout that is behind the target. +The conservative `status-projection` activity is unchanged, so the copy still +counts toward the workstream limit; admission routes to RECONCILE instead of an +unexplained SERIALIZE. Resolve it without discarding unknown work: + +1. Compare each dirty carrier with the target version and confirm no process or + session is still editing it. +2. Store a content-addressed, secret-scanned snapshot of every dirty file in + ignored receipt storage, recording base HEAD and target SHA. +3. Recheck the file digests, restore only the snapshotted carrier paths and + fast-forward the checkout; leave unrelated dirty work in place. +4. Reobserve admission. A differing carrier that records real continuation work + needs a new or reused ticket, not a restore. + +### Writes in the selected checkout + +The selected checkout of REUSE_EXISTING is not a competing peer, yet another +writer may have left uncommitted changes in it. Every registered checkout +reports `dirtyNewestModifiedAt`, the newest modification time of its dirty +paths: recency evidence only, never writer identity or authority. When dirty +paths of the selected checkout overlap the requested scope, `requiredBeforeWrite` +asks the caller to confirm they belong to this session. A caller that observed +the checkout earlier passes that report's `dirtyDigest` with +`--ticket ticket-NNN --expect-dirty-digest `; any change since then adds +blocker `selected-checkout-changed` and removes REUSE_EXISTING. This is a +clone-local compare-and-swap on content, not a lease or cross-clone lock. +Disjoint dirty work of another writer may continue beside authorized work. + Registered checkout observations, dirty paths, active scopes and WIP limits are unchanged. Historical content inclusion is not current behavior, owner consent, a merge receipt, ticket closure or permission to discard history. diff --git a/.governance/manifest.base.json b/.governance/manifest.base.json index f30da28..13ec025 100644 --- a/.governance/manifest.base.json +++ b/.governance/manifest.base.json @@ -110,7 +110,7 @@ "stacks": [], "standard": { "id": "wellmanifest/new-project", - "version": "0.20.31" + "version": "0.20.32" }, "ticket": { "activeStatuses": [ diff --git a/.governance/manifest.json b/.governance/manifest.json index b26658b..9e63cb3 100644 --- a/.governance/manifest.json +++ b/.governance/manifest.json @@ -15,24 +15,12 @@ "coordination": { "integration": { "requiredForPaths": [ - ".governance/manifest.base.json", - ".governance/manifest.json", - ".governance/manifest.lock.json", - ".governance/package-manifest.json", - ".governance/required-checks.json", - ".governance/ticket-allocation.json", - ".subactor/manifest.json", - "AGENTS.md", - "package.json", - "pyproject.toml", - "go.mod", - "Cargo.toml", - "pom.xml", "operations/**", "events/**", "error/**", "models/**", - "proto/**" + "proto/**", + ".planfile/**" ], "workstream": "integration" }, @@ -52,9 +40,17 @@ }, "governance": { "ownedPaths": [ + ".aider.conf.yml", + ".cursor/rules/**", + ".githooks/**", + ".github/copilot-instructions.md", + ".github/workflows/new-project-governance.yml", + ".gitignore", ".governance/**", ".subactor/**", "AGENTS.md", + "CLAUDE.md", + "GEMINI.md", "README.md", "TODO.md", "CHANGELOG.md", @@ -63,7 +59,11 @@ "project.sh", "project.bat", "project/**", - "scripts/runtime.sh" + "scripts/runtime.sh", + "scripts/install-agent-hosts.sh", + "wellmanifest_governance.py", + "worktree-guard.yaml", + "pyproject.toml" ] }, "infrastructure": { @@ -77,42 +77,17 @@ }, "integration": { "ownedPaths": [ - ".governance/manifest.base.json", - ".governance/manifest.json", - ".governance/manifest.lock.json", - ".governance/package-manifest.json", - ".governance/required-checks.json", - ".governance/ticket-allocation.json", - ".governance/ticket-activity.override.json", - ".governance/docs.json", - ".governance/managed-copies.json", - ".subactor/manifest.json", - "AGENTS.md", ".planfile/**", "VERSION", - "package.json", - "pyproject.toml", - "go.mod", - "Cargo.toml", - "pom.xml", "docs/**", "operations/**", "events/**", "error/**", "models/**", "proto/**", - "src/monag/**", - "project.sh", - "project.bat", - "tests/**", "Makefile", "MANIFEST.in", - "LICENSE", - "README.md", - "goal.yaml", - "CHANGELOG.md", - "scripts/**", - "wellmanifest_governance.py" + "LICENSE" ] }, "interfaces": { @@ -224,7 +199,7 @@ ], "standard": { "id": "wellmanifest/new-project", - "version": "0.20.31" + "version": "0.20.32" }, "ticket": { "activeStatuses": [ diff --git a/.governance/manifest.lock.json b/.governance/manifest.lock.json index 440f6f4..e666d2b 100644 --- a/.governance/manifest.lock.json +++ b/.governance/manifest.lock.json @@ -1,16 +1,16 @@ { "managedFiles": { - ".aider.conf.yml": "756af4477d7a0d39361919c957ee954807259f15cabf4dcabde81677eac6efd6", - ".cursor/rules/new-project-standard.mdc": "72699a5cb3718be9a50603eda9c8a6d960f21b1949fdfe2b35013385b22f9afe", + ".aider.conf.yml": "6de9c74f752e8a879419698531ac575a1f2a3b93efd38a257ac1c48682a26bdd", + ".cursor/rules/new-project-standard.mdc": "8884003b3a4470e4677ca5128645f466021432ceb3f018a9f1aa743210f4181d", ".githooks/pre-commit": "bde7345b3a1a726eaf6dfd18403a8e917c0544ddfb929e4f8f10c39e58eb8a0f", - ".github/copilot-instructions.md": "b31f9ba957ce108a83851f48f8dbff8b4301d1c744a77fe76914e6bb8499aca3", + ".github/copilot-instructions.md": "08330b67c95c3c175573fe1bec06c21a2f49e6c419164d04980d4f81b4efb8a8", ".github/workflows/new-project-governance.yml": "b41d0df4cb11de5bd1458976e0a1f69ee6733593cd9bb93d68fbbb7fe2e52929", ".governance/AGENT_DECISIONS.md": "853c5282e44321cd5cd895f41627fc4c5bb98774cd6e726ee1ccf319582f8407", ".governance/adoption-bindings.json": "9a6015fde26226d4c55764c81c1ab8e8d42fcf64661533a71d90fcc488edaeb3", ".governance/adoption-bindings.schema.json": "0fbad765ca67924b7b3340dcad09f262b62c979740a291da7dacd8e48d28241c", - ".governance/agent-hosts.json": "d4565102018a634307d941173fdea5da04c2cce494a979199a463d318dab41a0", - ".governance/agent-hosts.schema.json": "46a3e39eb35f5ad34363a5aa1394cc0db2304f48ee8d816c80615dc565c3c360", - ".governance/agent_host_check.py": "a6d2103122163688b263a86bb0dfd9c8ec04274d59d2308b2afa44b89d92b0cb", + ".governance/agent-hosts.json": "37562366bcd3a1d3a232f258adcdde02c66bc50ae9b4da38c96196321797b3db", + ".governance/agent-hosts.schema.json": "2e069dc7af1ecd62ba3e0846ad2c050208d465bc8adaf9be13d350c77310cc2a", + ".governance/agent_host_check.py": "ffdd5802c01bb2f5d71218e4cea0544e332788508b3e7bf2b6aec856422ac46d", ".governance/approval-evidence.schema.json": "e0f79eb7bdb534ec17e1a94a56b06a371df14b6f6b9f34db6cf4f8ee059718d5", ".governance/branch-intent-reconciliation.schema.json": "8d770fc7c81884844c3218cc1f18c11ed9c9f7851f2769e040eec676e8ea8006", ".governance/branch_intent_reconciliation.py": "cf316043eaff77021183d64f5b6f8b291fb0c38b62a9bae7203845a8b35592ca", @@ -26,7 +26,7 @@ ".governance/docs/BRANCH_INTENT_RECONCILIATION.md": "d17163b016e1ad8cf97408d528bf5a6443e931ea694e97191abc7c1e50586c3b", ".governance/docs/LOCAL_CI_PUBLICATION.md": "f30b778c8dc777be302ff346d3dbd144fc3a02fc957b178cc600bbec1219fc64", ".governance/docs/SNAPSHOT_MIGRATION.md": "fdf7601dc1f513b7d5bc2468f5b1dcea6dd5923d7bc32b50b147ab2620d768af", - ".governance/error/GOV-AGENT-HOST.md": "a3679a63e6346946382dc510a8454b5c1c2851e694f7b3f4b82473764153a90f", + ".governance/error/GOV-AGENT-HOST.md": "ba4ad66e68705a9ab0b43a9864ffaaa2399ddaddc91c5f25202803c058e320d9", ".governance/error/GOV-APPROVAL.md": "3b508f23491f2ad93e115481699fab84ef3def844f7580736c29c8afafb1ec47", ".governance/error/GOV-ARCHITECTURE-001.md": "c2b9e480ccb5e15023c1592d9d81a6c8532a87fdd5ab93719fccd2c5d75e7a08", ".governance/error/GOV-CHANGE-LEASE.md": "30530e7fd7c2eca9f4db0adec575dbc9b1bea139c2a640bfde0ae7c4b2ace116", @@ -39,7 +39,7 @@ ".governance/error/GOV-TICKET-ACTIVITY.md": "f1bc9cab86c36028eed449f1c40a229131cb49141cbea35620580e2ef2d2b642", ".governance/error/GOV-TICKET-ALLOCATION.md": "09f92cac24bbbbe5c2967221497fb6b68b02bcd3bf4f56afe36d36ac7d7b0a58", ".governance/error/GOV-WORK-CONTINUITY.md": "de46a4adc51e8dd5880a83a6d9585ecdbbea4ae30a44ee6506c207ea2e8bc8eb", - ".governance/error/GOV-WORK-START.md": "93213ed96593bfa458b645e58c5e88fd805ce94655579e046e94d01dbde502f6", + ".governance/error/GOV-WORK-START.md": "6dcec8198e98a23c34c1d34cf8fb14c25b973b7f25217a6cb0554959b6fbba3f", ".governance/error/GOV-WORKSPACE-LIFECYCLE.md": "392b9f484ee26eff04a77d6c24d283413aa25d7e23e0d295ab3d1542f3e1ef3d", ".governance/error/GOV-WORKTREE-OVERLAP.md": "65a2533f13e63d6ebeeb63c07adc0794ea9e04075a91e873eff2d79910239b0e", ".governance/error/README.md": "e8486dd29f52ca3fee96ed6881a62c38141864cde5aa1adea2b16d22b2feefaa", @@ -47,7 +47,7 @@ ".governance/governance_check.py": "024ca8b795e8d459131ba325d5edd66b5dd103740bc0c04f7c0de4accc971300", ".governance/intent.schema.json": "c70b7f210c9f4f549870e2bda0f765e882b85cc75dd481250b36b7be8d3d2ef9", ".governance/lock.schema.json": "ad80c98f800a4a3310870336dcdaf0aa689cc4988f71084d25d76bea2df1242f", - ".governance/manifest.base.json": "2e861ba675d829990160537e68d7de2c5208ea469390855d127ddcc93dcfa134", + ".governance/manifest.base.json": "1c67c22baccf0b8fae5e2596db4d294e6f8232ba2f16c07523acf090c86042a2", ".governance/manifest.schema.json": "5aa2ccd3f6898834d4e39a78342448145490be56aa132e16ac7c9d64acef8f73", ".governance/package-manifest.json": "fdaa684e66bf2d1b94c2b59839dd179a9d5468c42b89fc41f29c99727da4301b", ".governance/precommit_standard_update.py": "c91e2bf9ae9d6ccc77bce0e61450c818a5961edee5bfde3b60426da88e296b0f", @@ -76,9 +76,9 @@ ".governance/work-classification.dsl.json": "3a947c41938c0b8ef1717957f313ff9248764252735182de30b1f2d6878748b6", ".governance/work-classification.schema.json": "f5c2b518238543589e4f8d3805cc6455e19d6919643644aaeae034abf472a467", ".governance/work-continuity.schema.json": "5134e6884ddaa4fb3a0ffd200d281afb0f3b868b06f71d65e3da6b42f6fe0830", - ".governance/work-start-report.schema.json": "754052876760ab11204a7991aa750241e60e21bec92b9544f93a76d3c3d0f5d0", + ".governance/work-start-report.schema.json": "af3a86ad2bd6e40c3c770ec2c879e1e78f3d37d70583b734ffd58d2a14b2e8d6", ".governance/work_continuity.py": "43402de7e0a899bdeb284dbf2535032b59517d697eafc057c38d03691afd5e92", - ".governance/work_start_check.py": "ebe2795f8a790be46a4d766f8a2315af7c7ea8f311ca1f7715488fd8dd00301d", + ".governance/work_start_check.py": "5d04ff79e51102dbc443b8f676b890a72c0220d38255577417f61ae5df98db50", ".governance/workspace_lifecycle_check.py": "9b800de09bf4c518f41f9c6d79c9145f488cdf2b9183eed1ccf68298fb33cc0e", ".governance/worktree_guard.py": "b154f6e67626770ec11c9544d31a27b32d9c215ef73ffc9eb8f52e9c3a9b051b", ".governance/worktree_overlap_check.py": "a7d17aa36344cbf644437e5f5d3b9dc4d8a264863bae2e80198b147f21d4b84d", @@ -87,14 +87,14 @@ ".governance/worktrees.schema.json": "bb5989c19ee33d9beafa34576ef568ef70384a664ccf763ac2e29dde3a464756", ".subactor/.gitignore": "dd223aed5e053f94c6808ac434368c16eeca7e77218f8426e8cf5e46ae441d03", ".subactor/manifest.json": "ab8b1cbe4052a6f0005a3c33a43fa2a70e8837cd32c18b4183d0d448c3a8cee2", - "AGENTS.md": "ba1e764c1faff4560ed881ffdbb3bfebb1a5ab113ff998ee77d43155bc56a5d3", - "CLAUDE.md": "48c8159bd17f0a7ab05989ee06e3aeec0aab07f64347fcc271140d9f650d7713", - "GEMINI.md": "b66a7d12c4c877110eb263a80b32c065c4c2d581743d72cd6e06ebbe6dc842da", + "AGENTS.md": "77519c98acc5d4480d6829185857c88a4a0afe6b50e01d0f784e74c0fedaac26", + "CLAUDE.md": "628e743294cd4e23531eece8053595fec526080b20f2fac74248e43e5227442b", + "GEMINI.md": "f72a35f8a888b1727f4829fa33410e75a1e830a363cc7bf7c465e5c519535725", "project/governance-check.bat": "04f4fd3ba15abd6b874bde8fab0dd869402b84b9c83ae72da40c1a804b068045", "project/governance-check.sh": "8eb977ff01a96e47455d227ed5ced949eb53ea19537f870d9016f803840e048e", "project/new-ticket.sh": "1eb5784f229c8c68417788e1753f013c0ca3d1164e510cd5ee294bf326044dd7", "project/readme.sh": "b41a9c88374e6de0439284a4561fb11b1b482039bc5ba1bcf6683fd59b1a3968", - "scripts/install-agent-hosts.sh": "316a4dd6c877ba8f79a10bb761b188378deb8aea70fb7fcc54a729c664987f44", + "scripts/install-agent-hosts.sh": "c242127001e362a5748e096807de1a001ec9e4cca1d8e79d173769bc0b3804e6", "scripts/runtime.sh": "27ec7c0ff9ba3e16be5438ce2fd938a0e1dd34cc3a3627bf97155a83f4304306", "wellmanifest_governance.py": "d6b71f091ffd88fb30c96f54162020d9aeb6e54555328834b68ccb21868ecc07", "worktree-guard.yaml": "bea3d3cda9bd764f9e79b975da8f5360df894fdc04fca0407def88ebd49111b7" @@ -104,7 +104,7 @@ "id": "wellmanifest/new-project", "publicationStatus": "published", "sourceRepository": "wellmanifest/new-project", - "sourceRevision": "2b016654cff1a1ccef2c0d6126a9c2550ae6b37a", - "version": "0.20.31" + "sourceRevision": "b6ba9c21a65a6a5648ecf904b64c3b75295e136f", + "version": "0.20.32" } } diff --git a/.governance/work-start-report.schema.json b/.governance/work-start-report.schema.json index c4f76d4..022f3b3 100644 --- a/.governance/work-start-report.schema.json +++ b/.governance/work-start-report.schema.json @@ -104,6 +104,7 @@ "behind", "dirtyPaths", "dirtyDigest", + "dirtyNewestModifiedAt", "pending", "ticket", "workstream", @@ -146,6 +147,13 @@ "dirtyDigest": { "type": "string" }, + "dirtyNewestModifiedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, "pending": { "type": "boolean" }, @@ -266,7 +274,9 @@ "scope-reservation", "pending-delta", "unassigned-branch-delta", - "unassigned-ticket" + "unassigned-ticket", + "integrated-ticket-carrier", + "selected-checkout-changed" ] } } diff --git a/.governance/work_start_check.py b/.governance/work_start_check.py index fce2811..4ec6f2b 100755 --- a/.governance/work_start_check.py +++ b/.governance/work_start_check.py @@ -13,7 +13,7 @@ import sys sys.dont_write_bytecode = True -from ticket_activity import ActivityError, resolve as resolve_activity +from ticket_activity import ActivityError, delivery_landed, resolve as resolve_activity from ticket_input import configured_mode, load_input, primary_database from worktree_overlap_check import globs_may_overlap, path_ignored @@ -154,6 +154,26 @@ def dirty_observation(root): return material(sorted(paths)), digest({"status": status, "files": hashes}), sorted(paths) +def dirty_modified(root, paths): + """Newest modification time of dirty paths: a recency observation, never writer identity.""" + newest = None + for rel in paths: + try: + stamp = (root / rel).lstat().st_mtime + except OSError: + continue + newest = stamp if newest is None or stamp > newest else newest + return None if newest is None else datetime.fromtimestamp(newest, timezone.utc).isoformat(timespec="seconds") + + +def landed(path, ticket, target): + """Whether the ticket directory is on the observed target and no ticket branch is outside it.""" + try: + return delivery_landed(path, path / "project" / ticket, target) + except subprocess.SubprocessError as error: + raise ObservationError("Target ancestry observation failed") from error + + def remote_heads(root): """Read advertisements, never fetch or print URLs/credential diagnostics.""" result = {} @@ -256,7 +276,7 @@ def contains(head, remote_sha): def inspect(root, workstream, requested_paths=(), ticket=None, storage=None, - observe_publication=False): + observe_publication=False, expected_dirty_digest=None): root = Path(git(root, "rev-parse", "--show-toplevel").strip()).resolve() manifest = manifest_at(root) coordination = manifest["coordination"] @@ -336,6 +356,7 @@ def inspect(root, workstream, requested_paths=(), ticket=None, storage=None, entries.append({"path": str(path.resolve()), "branch": branch or None, "headSha": head, "ahead": ahead, "behind": behind, "dirtyPaths": dirty, "allDirtyPaths": all_dirty, "dirtyDigest": dirty_hash, + "dirtyNewestModifiedAt": dirty_modified(path, all_dirty), "pending": pending, "ticket": ticket_id, "workstream": intent.get("workstream") if intent else None, "allowedPaths": material(intent["allowedPaths"]) if intent else [], @@ -389,7 +410,14 @@ def inspect(root, workstream, requested_paths=(), ticket=None, storage=None, unassigned[key] = True if resolution.active and intent.get("workstream") == workstream: active_tickets.add(key) - if resolution.active and (intersects(requested, scope) or (not scope and intent.get("workstream") == workstream)): + relevant = intersects(requested, scope) or intent.get("workstream") == workstream + if resolution.active and mode == "files" and relevant and landed(path, key, target_sha): + # A dirty carrier copy of a ticket already on the observed target + # still projects activity (the conservative default is kept). + # Name it instead of silently holding the workstream limit. + blockers.append({"path": str(path), "branch": entry["branch"], "ticket": key, + "active": resolution.active, "reason": "integrated-ticket-carrier"}) + elif resolution.active and (intersects(requested, scope) or (not scope and intent.get("workstream") == workstream)): blockers.append({"path": str(path), "branch": entry["branch"], "ticket": key, "active": resolution.active, "reason": "unassigned-ticket"}) for entry in entries: @@ -426,9 +454,26 @@ def inspect(root, workstream, requested_paths=(), ticket=None, storage=None, continue blockers.append({"path": None, "branch": ref, "ticket": None, "active": False, "reason": "unassigned-branch-delta"}) + required = ["current intent and session authority", "verified owner or accepted handoff", + "controller lease CAS and fencing", "fresh preflight and governance gate"] + if selected: + # The selected checkout is excluded from peer contention, yet another + # writer may have left uncommitted changes in it. Recency is evidence + # only; the opt-in digest CAS detects any change since the caller's + # previous observation. + overlap = [p for p in selected["dirtyPaths"] if path_ignored(p, tuple(requested))] + if expected_dirty_digest is not None and expected_dirty_digest != selected["dirtyDigest"]: + blockers.append({"path": selected["path"], "branch": selected["branch"], + "ticket": selected["ticket"], "active": selected["active"], + "reason": "selected-checkout-changed"}) + elif expected_dirty_digest is None and overlap: + required.append(f"confirm that {len(overlap)} uncommitted requested path(s) in the selected checkout " + f"(newest {selected['dirtyNewestModifiedAt']}) belong to this session, then pass " + f"--expect-dirty-digest {selected['dirtyDigest']}") route = "NEW_TICKET_CANDIDATE" if blockers: - route = ("RECONCILE" if any(b["ticket"] is None or b["reason"] == "unassigned-ticket" for b in blockers) else + route = ("RECONCILE" if any(b["ticket"] is None or b["reason"] in {"unassigned-ticket", "integrated-ticket-carrier"} + for b in blockers) else "ASSIST_READ_ONLY" if any(b["active"] for b in blockers) else "HANDOFF_REQUIRED") elif selected: route = "REUSE_EXISTING" @@ -442,8 +487,7 @@ def inspect(root, workstream, requested_paths=(), ticket=None, storage=None, "requestedTicket": ticket, "route": route, "diagnostic": None, "worktrees": entries, "uncheckedBranches": branches, "blockers": blockers, "activeTicketCount": len(active_tickets), "workstreamLimit": limit, - "requiredBeforeWrite": ["current intent and session authority", "verified owner or accepted handoff", - "controller lease CAS and fencing", "fresh preflight and governance gate"], + "requiredBeforeWrite": required, "observationDigest": ""} storage_digest = digest({key: {"revision": row["revision"], "files": {name: hashlib.sha256(value[0]).hexdigest() @@ -476,10 +520,16 @@ def main(argv=None): parser.add_argument("--storage", choices=["files", "sqlite"]) parser.add_argument("--observe-publication", action="store_true", help="Read origin refs twice without fetching; distinguish remote code from integration/release authority.") + parser.add_argument("--expect-dirty-digest", metavar="SHA256", + help="With --ticket: dirtyDigest of the selected checkout from this session's previous observation; " + "a mismatch blocks reuse (clone-local CAS, not a lease).") args = parser.parse_args(argv) + if args.expect_dirty_digest is not None and ( + not args.ticket or not re.fullmatch(r"[0-9a-f]{64}", args.expect_dirty_digest)): + parser.error("--expect-dirty-digest requires --ticket and a lowercase SHA-256 digest") try: payload = inspect(args.root, args.workstream, args.path, args.ticket, args.storage, - args.observe_publication) + args.observe_publication, args.expect_dirty_digest) except (ObservationError, ActivityError, KeyError, TypeError, ValueError, OSError, StopIteration): # No exception content: remote URLs or secret-bearing input never leak. print(json.dumps({"schema": SCHEMA, "readOnly": True, "grantsAuthority": False, diff --git a/AGENTS.md b/AGENTS.md index f2cf998..9c6f9ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,30 @@ # AGENTS.md + +## Managed standard sources + +This managed projection follows the local adoption contract. The local lock, +manifest and managed-file digests are authoritative; remote `main` links are +navigation only and are never fetched or executed by an agent. + +- Local adoption manifest: [.governance/manifest.json](.governance/manifest.json) +- Local adoption lock: [.governance/manifest.lock.json](.governance/manifest.lock.json) +- Local package map: [.governance/package-manifest.json](.governance/package-manifest.json) +- Canonical instructions: [AGENTS template](https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md) +- Host contract: [agent-hosts.json](https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json) +- Immutable adoption/updater: [create_adoption_lock.py](https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py) +- Worktree contract: [worktrees.schema.json](https://github.com/wellmanifest/worktrees/blob/main/models/worktrees.schema.json) +- Git lifecycle: [git-lifecycle.schema.json](https://github.com/wellmanifest/git-lifecycle/blob/main/standard/git-lifecycle.schema.json) +- Ticket lifecycle: [ticket-lifecycle.schema.json](https://github.com/wellmanifest/ticket-lifecycle/blob/main/standard/ticket-lifecycle.schema.json) +- Policy DSL: [POLICY_DSL.md](https://github.com/wellmanifest/policy-dsl/blob/main/spec/POLICY_DSL.md) +- Logs contract: [logs.contract.json](https://github.com/wellmanifest/logs/blob/main/contracts/logs.contract.json) +- Agent contract: [agent.schema.json](https://github.com/wellmanifest/agent/blob/main/standard/agent.schema.json) +- LLM policy boundary: [wellmanifest/llm README](https://github.com/wellmanifest/llm/blob/main/README.md) +- Offer pointer: [wellmanifest/offer README](https://github.com/wellmanifest/offer/blob/main/README.md) +- Brand pointer: [wellmanifest/brand README](https://github.com/wellmanifest/brand/blob/main/README.md) + + + ## Opted-in SQLite ticket storage When `git config --local --get new-project.ticketStorage` is `sqlite`, the @@ -241,3 +266,11 @@ Unknown ownership or remote/independent-clone state must not be guessed. Markdown approval is an audit note, not trusted merge approval. Required merge approval comes from the repository's protected review, attestation and ruleset boundary. + +## Bounded session controls + +Every implementation session is bounded by the ticket's `maxActiveMinutes` and +must create a `checkpoint` before a context, tool or process boundary. On a +deterministic failure or unresolved ownership, write a concise `handoff` with +the next observable action and `stop`; do not retry the same failed path +indefinitely. diff --git a/CLAUDE.md b/CLAUDE.md index 924417d..d3946b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,20 @@ # CLAUDE.md + +## Managed standard sources + +Read the local adoption manifest, lock and package before using this host +projection. The remote links are navigation only; do not fetch them at runtime. + +- Local adoption manifest: [.governance/manifest.json](.governance/manifest.json) +- Local adoption lock: [.governance/manifest.lock.json](.governance/manifest.lock.json) +- Local package map: [.governance/package-manifest.json](.governance/package-manifest.json) +- Canonical instructions: [AGENTS template](https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md) +- Host contract: [agent-hosts.json](https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json) +- Immutable adoption/updater: [create_adoption_lock.py](https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py) + + + This repository follows the `wellmanifest/new-project` policy-as-code standard. Same contract as `AGENTS.md`, `GEMINI.md`, `.cursor/rules/new-project-standard.mdc`, `.aider.conf.yml` and `.github/copilot-instructions.md`. Claude Code must follow @@ -19,3 +34,7 @@ The pre-commit hook rejects commits that are not bound to an `IN_PROGRESS` `ticket-NNN`, and the `governance / enforce` CI job rejects a pull request whose host contract or packaging declaration drifted. Markdown is not a substitute for either gate. + +Bounded session controls: respect the ticket's `maxActiveMinutes`, create a +`checkpoint` before a context or tool boundary, and leave a `handoff` then +`stop` after a deterministic failure instead of retrying indefinitely. diff --git a/GEMINI.md b/GEMINI.md index f797201..e700b4c 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,5 +1,20 @@ # GEMINI.md + +## Managed standard sources + +The local adoption manifest, lock and package are authoritative. These remote +links are navigation only and must not be fetched or executed at runtime. + +- Local adoption manifest: [.governance/manifest.json](.governance/manifest.json) +- Local adoption lock: [.governance/manifest.lock.json](.governance/manifest.lock.json) +- Local package map: [.governance/package-manifest.json](.governance/package-manifest.json) +- Canonical instructions: [AGENTS template](https://github.com/wellmanifest/new-project/blob/main/template/files/AGENTS.template.md) +- Host contract: [agent-hosts.json](https://github.com/wellmanifest/new-project/blob/main/governance/agent-hosts.json) +- Immutable adoption/updater: [create_adoption_lock.py](https://github.com/wellmanifest/new-project/blob/main/scripts/create_adoption_lock.py) + + + This repository follows the `wellmanifest/new-project` policy-as-code standard. This file is the Gemini / Antigravity entry; the same rules are in `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/new-project-standard.mdc`, `.aider.conf.yml` and @@ -19,3 +34,7 @@ Fail-closed. Do not write code until this contract is followed. If authority or ownership remains unclear, pause the dependent effect and follow [.governance/AGENT_DECISIONS.md](.governance/AGENT_DECISIONS.md) to inspect evidence and existing authorization. Continue disjoint authorized work. Do not invent a ticket number. + +Bounded session controls: respect the ticket's `maxActiveMinutes`, create a +`checkpoint` before a context or tool boundary, and leave a `handoff` then +`stop` after a deterministic failure instead of retrying indefinitely. diff --git a/project/TICKETS.md b/project/TICKETS.md index 00d5513..543d744 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -60,4 +60,11 @@ | **ticket-067** | [`README.md`](./ticket-067/README.md) | - | - | - | - | - | | **ticket-068** | [`README.md`](./ticket-068/README.md) | - | - | - | - | - | | **ticket-069** | [`README.md`](./ticket-069/README.md) | - | - | - | - | - | +| **ticket-070** | [`README.md`](./ticket-070/README.md) | - | - | - | - | - | +| **ticket-071** | [`README.md`](./ticket-071/README.md) | - | - | - | - | - | +| **ticket-072** | [`README.md`](./ticket-072/README.md) | - | - | - | - | - | +| **ticket-073** | [`README.md`](./ticket-073/README.md) | - | - | - | - | - | +| **ticket-074** | [`README.md`](./ticket-074/README.md) | - | - | - | - | - | +| **ticket-075** | [`README.md`](./ticket-075/README.md) | - | - | - | - | - | +| **ticket-077** | [`README.md`](./ticket-077/README.md) | - | - | - | - | - | diff --git a/project/ticket-077/README.md b/project/ticket-077/README.md new file mode 100644 index 0000000..29bbeae --- /dev/null +++ b/project/ticket-077/README.md @@ -0,0 +1,21 @@ +# Ticket 077: Adopt wellmanifest/new-project 0.20.32 + +- **ID**: ticket-077 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-09-19 + +## Goal and scope + +SESSION_EXECUTION_AUTHORIZATION: adopt, test, push and protected merge of wellmanifest/new-project 0.20.32 in semcod/monag. + +Adopt published standard `0.20.32` (`b6ba9c21a65a6a5648ecf904b64c3b75295e136f`), replacing `0.20.31` (`2b016654cff1a1ccef2c0d6126a9c2550ae6b37a`). Fix pytest `pythonpath` in `pyproject.toml` to include the repository root `"."` so `wellmanifest_governance` imports cleanly under standard pytest invocations. + +## Acceptance criteria + +- [x] AC-01: Adopt standard revision `b6ba9c21a65a6a5648ecf904b64c3b75295e136f`, regenerating lock and package manifests. +- [x] AC-02: Synchronize `[tool.wellmanifest]` in `pyproject.toml` to standard `0.20.32` and revision `b6ba9c21a65a6a5648ecf904b64c3b75295e136f`. +- [x] AC-03: Add `"."` to `[tool.pytest.ini_options].pythonpath` in `pyproject.toml`. +- [x] AC-04: Harmonize coordination workstream definitions in `.governance/manifest.json`. +- [x] AC-05: Pass test suite and governance check with `GOV-PASS`. diff --git a/project/ticket-077/intent.json b/project/ticket-077/intent.json new file mode 100644 index 0000000..f7d2cec --- /dev/null +++ b/project/ticket-077/intent.json @@ -0,0 +1,111 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-077", + "summary": "Adopt wellmanifest/new-project 0.20.32", + "workstream": "governance", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-077/**", + "project/TICKETS.md", + ".governance/**", + "pyproject.toml", + "AGENTS.md", + "CLAUDE.md", + "GEMINI.md", + ".aider.conf.yml", + ".cursor/rules/**", + ".github/copilot-instructions.md", + "scripts/install-agent-hosts.sh", + "project/new-ticket.sh" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md", + ".env" + ], + "stacks": [ + "python" + ], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "fb5259da3fb739cb0d138bd9008d422b59232381", + "targetBranch": "main", + "standardAdoption": { + "sourceRepository": "wellmanifest/new-project", + "fromRevision": "2b016654cff1a1ccef2c0d6126a9c2550ae6b37a", + "toRevision": "b6ba9c21a65a6a5648ecf904b64c3b75295e136f" + }, + "outcome": "This repository runs the verified wellmanifest/new-project 0.20.32 standard with synchronized pyproject.toml packaging pin, repaired pytest pythonpath including repository root, and harmonized coordination workstreams.", + "nonGoals": [ + "No change to monitor or CLI behavior.", + "No release and no registry publish." + ], + "complexity": "L", + "estimatedMinutes": 60, + "budgets": { + "maxImplementationFiles": 15, + "maxAffectedComponents": 5, + "maxPublicInterfaceChanges": 3, + "maxRuntimeDependencies": 3 + }, + "architecture": { + "status": "accepted", + "decision": "Adopt standard 0.20.32 using create_adoption_lock.py --upgrade, update pyproject.toml [tool.wellmanifest] pin to 0.20.32 and revision b6ba9c21, add repository root '.' to pythonpath in pytest options, harmonize coordination workstream ownership, and verify zero drift across all test suites and governance gates.", + "components": [ + { + "name": "governance-instance", + "paths": [ + ".governance/**", + "project/ticket-077/**", + "project/TICKETS.md" + ] + }, + { + "name": "packaging-pin", + "paths": [ + "pyproject.toml" + ] + }, + { + "name": "managed-guidance", + "paths": [ + "AGENTS.md", + "CLAUDE.md", + "GEMINI.md", + ".aider.conf.yml", + ".cursor/rules/**", + ".github/copilot-instructions.md", + "scripts/install-agent-hosts.sh", + "project/new-ticket.sh" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert commit to restore 0.20.31 governance files and packaging pin." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "python3 scripts/create_adoption_lock.py --target-root . --source-revision b6ba9c21a65a6a5648ecf904b64c3b75295e136f --upgrade", + "./project/governance-check.sh --base fb5259da3fb739cb0d138bd9008d422b59232381 --head HEAD", + "uv run --no-project --with pytest pytest -q" + ], + "evidence": "Pending exact-head publication." + } + ] + } +} diff --git a/pyproject.toml b/pyproject.toml index b84b06c..70241b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,13 +31,13 @@ monag = "monag.cli:main" where = ["src"] [tool.wellmanifest] -standard = "0.20.31" -revision = "2b016654cff1a1ccef2c0d6126a9c2550ae6b37a" +standard = "0.20.32" +revision = "b6ba9c21a65a6a5648ecf904b64c3b75295e136f" gate = "project/governance-check.sh" [tool.pytest.ini_options] addopts = "-p wellmanifest_governance" -pythonpath = ["src", "tests"] +pythonpath = [".", "src", "tests"] [tool.costs] # AI Cost tracking configuration diff --git a/scripts/install-agent-hosts.sh b/scripts/install-agent-hosts.sh index 80631d9..0108774 100755 --- a/scripts/install-agent-hosts.sh +++ b/scripts/install-agent-hosts.sh @@ -228,17 +228,43 @@ install_user_files() { local marker="wellmanifest/new-project host contract" for pointer in "$task_user_home/.gemini/GEMINI.md" "$task_user_home/.claude/CLAUDE.md"; do - if [[ ! -f "$pointer" ]] || ! grep -Fq "$marker" "$pointer"; then - cat >> "$pointer" <