Skip to content

Commit 45ddedb

Browse files
[ticket-001] adopt published native governance 0.20.129
1 parent 4225a40 commit 45ddedb

44 files changed

Lines changed: 2568 additions & 381 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.githooks/pre-commit‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ fi
2121
if [[ "$branch" =~ ^ticket[-/][0-9]{3,} ]]; then
2222
writer_runner="$root/.governance/change_lease_check.py"
2323
[[ -f "$writer_runner" ]] || writer_runner="$root/scripts/change_lease_check.py"
24-
if [[ -f "$writer_runner" ]]; then
25-
python3 -B "$writer_runner" writer-check --root "$root" --operation commit --ref "refs/heads/$branch"
24+
if [[ ! -f "$writer_runner" ]]; then
25+
echo "GOV-CHANGE-LEASE-001: writer-lease checker is missing; restore the managed installation." >&2
26+
exit 1
2627
fi
28+
python3 -B "$writer_runner" writer-check --root "$root" --operation commit --ref "refs/heads/$branch"
2729
fi
2830

2931
run_worktree_guard() {

‎.githooks/pre-push‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ while read -r local_ref local_sha remote_ref remote_sha; do
3838
if [[ "$ref" =~ ^refs/heads/ticket[-/][0-9]{3,} ]]; then
3939
writer_runner="$root/.governance/change_lease_check.py"
4040
[[ -f "$writer_runner" ]] || writer_runner="$root/scripts/change_lease_check.py"
41-
if [[ -f "$writer_runner" ]]; then
42-
if ! python3 -B "$writer_runner" writer-check --root "$root" --operation push --ref "$ref"; then
43-
refused=1
44-
fi
41+
if [[ ! -f "$writer_runner" ]]; then
42+
echo "GOV-CHANGE-LEASE-001: writer-lease checker is missing; restore the managed installation." >&2
43+
refused=1
44+
elif ! python3 -B "$writer_runner" writer-check --root "$root" --operation push --ref "$ref"; then
45+
refused=1
4546
fi
4647
fi
4748
done
Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
name: new-project-branch-hygiene
22

3-
# `delete_branch_on_merge` covers the merge exit. This workflow covers the
4-
# other one: a branch whose pull request was closed without merging. Nothing
5-
# in the toolchain removes those, and `GOV-BRANCH-LIFECYCLE-002` validates the
6-
# repository as a whole rather than the diff under review, so a single left
7-
# behind branch fails `governance / remote lifecycle` on *every* later pull
8-
# request, indefinitely and through no fault of its own.
9-
#
10-
# Closing a pull request without merging is the explicit owner decision to
11-
# discard the branch that the check's own remediation asks for, and GitHub
12-
# keeps the branch restorable from the pull request page afterwards.
13-
3+
# GitHub's delete_branch_on_merge setting handles successfully merged branches.
4+
# Closing an unmerged pull request does not authorize discarding its work.
5+
# Explicit discard requires a separate owner decision and intent reconciliation.
146
on:
157
pull_request:
168
types: [closed]
@@ -19,54 +11,17 @@ permissions:
1911
contents: read
2012

2113
jobs:
22-
discard-unmerged-head:
23-
# Forks cannot be written to, and the default branch is never a head to
24-
# discard. Both guards are cheap and keep the job from ever running where
25-
# deleting would be wrong.
26-
if: >-
27-
github.event.pull_request.merged == false &&
28-
github.event.pull_request.head.repo.full_name == github.repository &&
29-
github.event.pull_request.head.ref != github.event.repository.default_branch
14+
preserve-unmerged-head:
15+
if: github.event.pull_request.merged == false
3016
runs-on: ${{ vars.NEW_PROJECT_RUNNER_LABEL || 'ubuntu-latest' }}
31-
permissions:
32-
contents: write
3317
steps:
34-
- name: Delete the closed pull request's head branch
18+
- name: Report preserved pull request branch
3519
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
3620
with:
3721
script: |
3822
const ref = context.payload.pull_request.head.ref;
39-
40-
// Several pull requests may share one head branch. Deleting it
41-
// while another is still open would break that one instead.
42-
const owners = await github.paginate(github.rest.pulls.list, {
43-
owner: context.repo.owner,
44-
repo: context.repo.repo,
45-
state: 'open',
46-
head: `${context.repo.owner}:${ref}`,
47-
per_page: 100,
48-
});
49-
if (owners.length > 0) {
50-
const numbers = owners.map(pull => `#${pull.number}`).join(', ');
51-
core.info(`Kept ${ref}: still owned by ${numbers}.`);
52-
return;
53-
}
54-
55-
try {
56-
await github.rest.git.deleteRef({
57-
owner: context.repo.owner,
58-
repo: context.repo.repo,
59-
ref: `heads/${ref}`,
60-
});
61-
core.info(
62-
`Deleted ${ref}. GitHub keeps it restorable from the pull request page.`,
63-
);
64-
} catch (error) {
65-
// The branch is commonly gone already, because the person
66-
// closing the pull request ticked "delete branch".
67-
if (error.status === 404 || error.status === 422) {
68-
core.info(`${ref} was already deleted.`);
69-
return;
70-
}
71-
throw error;
72-
}
23+
core.notice(
24+
`Preserved ${JSON.stringify(ref)}. Closing a pull request does not ` +
25+
'authorize branch deletion. Reconcile remaining intent and obtain ' +
26+
'an explicit owner discard decision before a separate deletion.',
27+
);

‎.github/workflows/new-project-governance.yml‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,41 @@ jobs:
176176
runtime_venv="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/wellman-governance.XXXXXX")"
177177
python3 -I -m venv "$runtime_venv"
178178
"$runtime_venv/bin/python" -I -m pip install --disable-pip-version-check "git+https://github.com/wellmanifest/new-project.git@${runtime_ref}#subdirectory=packages/wellman"
179+
# Accept only a recognized console entrypoint from this isolated runtime.
180+
runtime_command="$("$runtime_venv/bin/python" -I - <<'PY_RUNTIME_COMMAND'
181+
import os
182+
import sys
183+
from importlib.metadata import distributions
184+
from pathlib import Path
185+
186+
if sys.prefix == sys.base_prefix:
187+
raise SystemExit("Governance runtime requires its isolated environment")
188+
found = []
189+
for name, command, module in (
190+
("wellmanifest-governance", "wellman-governance", "wellmanifest_governance_runtime"),
191+
("wellman", "wellman", "wellman"),
192+
):
193+
matches = list(distributions(name=name))
194+
if not matches:
195+
continue
196+
if len(matches) != 1:
197+
raise SystemExit("Ambiguous governance runtime distribution: " + name)
198+
installed = matches[0]
199+
entries = [entry.value for entry in installed.entry_points
200+
if entry.group == "console_scripts" and entry.name == command]
201+
launcher = Path(sys.executable).parent / command
202+
if (entries != [module + ".check:main"]
203+
or not Path(installed.locate_file("")).resolve().is_relative_to(Path(sys.prefix).resolve())
204+
or launcher.is_symlink() or not launcher.is_file() or not os.access(launcher, os.X_OK)):
205+
raise SystemExit("Unsupported governance runtime identity: " + name)
206+
found.append(command)
207+
if len(found) != 1:
208+
raise SystemExit("Expected exactly one supported governance runtime")
209+
print(found[0])
210+
PY_RUNTIME_COMMAND
211+
)"
212+
"$runtime_venv/bin/$runtime_command" --version
179213
printf '%s\n' "$runtime_venv/bin" >> "$GITHUB_PATH"
180-
"$runtime_venv/bin/wellman" --version
181214
- name: Run the managed governance gate
182215
shell: bash
183216
env:

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ __pycache__/
55
dist/
66
build/
77
.pytest_cache/
8+
/.venv/
89

910
# Repository-local worktrees and operational state
1011
/.worktrees/

‎.governance/AGENT_DECISIONS.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ both HEADs is inherited context. Two actual dirty writers remain contested;
4848
unresolved ancestry retains conservative checking. Intent ownership remains a
4949
separate check even when Git can merge file contents.
5050

51+
For a non-primary detached checkout with no commits ahead of the observed
52+
target, a staged path already identical to that target is not an outstanding
53+
contribution. The query verifies exact target blob and mode, stage-zero index
54+
entry, and raw filesystem blob and mode. Different or unknown evidence remains
55+
pending. Named branches and active intent retain their reservations. All dirty
56+
paths remain visible, and the observation digest binds index entries as well
57+
as filesystem content. This does not classify the checkout as disposable or
58+
authorize its cleanup, lease transfer, source writes or publication.
59+
5160
`wellmanifest/git-lifecycle` still owns cleanup effects; `ticket-lifecycle`
5261
owns reservations, and protected merge/publication controllers own external
5362
effects. Fix an incorrect checker at its HOME with a failing regression and

‎.governance/agent_host_check.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,8 @@ def python_declaration(marker: Path) -> tuple[dict[str, Any] | None, dict[str, A
342342
document = tomllib.loads(marker.read_text(encoding="utf-8"))
343343
except (OSError, tomllib.TOMLDecodeError):
344344
return None, {}
345-
declaration = document.get("tool", {}).get("wellmanifest")
345+
tool = document.get("tool")
346+
declaration = tool.get("wellmanifest") if isinstance(tool, dict) else None
346347
return (declaration if isinstance(declaration, dict) else None), document
347348

348349

@@ -362,7 +363,9 @@ def lifecycle_value(kind: str, document: dict[str, Any]) -> str:
362363
scripts = document.get("scripts")
363364
value = scripts.get("prepare") if isinstance(scripts, dict) else None
364365
else:
365-
options = document.get("tool", {}).get("pytest", {}).get("ini_options", {})
366+
tool = document.get("tool")
367+
pytest = tool.get("pytest") if isinstance(tool, dict) else None
368+
options = pytest.get("ini_options") if isinstance(pytest, dict) else None
366369
value = options.get("addopts") if isinstance(options, dict) else None
367370
if isinstance(value, list):
368371
return " ".join(str(item) for item in value)

‎.governance/change_lease_check.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
PHASES = {"claimed", "editing", "validating", "publication_frozen", "dispatching", "approved", "merged", "closed", "cancelled", "expired", "released"}
2525
FROZEN_PHASES = {"publication_frozen", "dispatching", "approved"}
2626
TERMINAL_REPLACEMENT_PHASES = {"merged", "closed", "cancelled", "released"}
27+
# wellmanifest/worktrees v5 layout: the single source for every lease and worktree reader.
28+
WORKTREES_ROOT = ".worktrees"
29+
LEASE_ROOT = ".subactor/leases"
30+
MAX_LEASE_BYTES = 65536
2731
TRANSITIONS = {
2832
"heartbeat": {phase: phase for phase in PHASES - {"released"}},
2933
"begin-edit": {"claimed": "editing"},
@@ -358,7 +362,7 @@ def read_writer_lease(path: Path) -> dict[str, Any]:
358362
if not path.is_file():
359363
raise ValueError("Lease must be a regular file")
360364
raw = path.read_bytes()
361-
if len(raw) > 65536:
365+
if len(raw) > MAX_LEASE_BYTES:
362366
raise ValueError("Lease exceeds bounded size")
363367
value = json.loads(raw.decode("utf-8"))
364368
if not isinstance(value, dict):
@@ -375,8 +379,8 @@ def read_writer_lease(path: Path) -> dict[str, Any]:
375379
with os.fdopen(leaf, "rb") as stream:
376380
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
377381
raise ValueError("Lease must be a regular file")
378-
raw = stream.read(65537)
379-
if len(raw) > 65536:
382+
raw = stream.read(MAX_LEASE_BYTES + 1)
383+
if len(raw) > MAX_LEASE_BYTES:
380384
raise ValueError("Lease exceeds bounded size")
381385
value = json.loads(raw)
382386
if not isinstance(value, dict):
@@ -418,7 +422,7 @@ def git(*args: str) -> str:
418422
ticket = "ticket-" + match[1]
419423
slug = ref.split(match[1] + "-", 1)[1]
420424
worktree_id = ticket + "--" + slug
421-
expected = primary / ".worktrees" / worktree_id
425+
expected = primary / WORKTREES_ROOT / worktree_id
422426
owner_worktree = Path(owners[0]["worktree"])
423427
if os.name == "nt":
424428
if owner_worktree.resolve() != expected.resolve() or expected.is_symlink():
@@ -430,7 +434,7 @@ def git(*args: str) -> str:
430434
raise ValueError("Ticket worktree is not canonical")
431435
if operation == "commit" and root.resolve() != expected:
432436
raise ValueError("Commit must run in the owned worktree")
433-
lease = read_writer_lease(primary / ".subactor/leases" / (worktree_id + ".json"))
437+
lease = read_writer_lease(primary / LEASE_ROOT / (worktree_id + ".json"))
434438
errors = validate_lease(lease)
435439
if errors:
436440
return errors

‎.governance/decision_record.py‎

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def classify_action(action: str) -> dict[str, Any]:
5959
)
6060
REJECTED_LINE = re.compile(r"^REJECTED\s+(\S+)\s+BECAUSE\s+(.+)$")
6161
ADVISORY_LINE = re.compile(
62-
r'^ADVISORY\s+llm_verdict\s*=\s*"([^"]*)"\s+MODEL\s+"([^"]*)"\s*$'
62+
r'^ADVISORY\s+llm_verdict\s*=\s*("(?:[^"\\]|\\.)*")\s+'
63+
r'MODEL\s+("(?:[^"\\]|\\.)*")\s*$'
6364
)
6465
ASSERT_LINE = re.compile(r"^ASSERT\s+(.+)$")
6566

@@ -126,10 +127,14 @@ def apply_decision_line(record: dict[str, Any], line: str) -> bool:
126127
return True
127128
match = ADVISORY_LINE.match(line)
128129
if match:
129-
record["advisory"] = {
130-
"llmVerdict": match.group(1),
131-
"model": match.group(2),
132-
}
130+
try:
131+
advisory = {
132+
"llmVerdict": json.loads(match.group(1)),
133+
"model": json.loads(match.group(2)),
134+
}
135+
except json.JSONDecodeError:
136+
return False
137+
record["advisory"] = advisory
133138
return True
134139
match = ASSERT_LINE.match(line)
135140
if match:
@@ -199,8 +204,8 @@ def to_dsl(record: dict[str, Any]) -> str:
199204
adv = record.get("advisory")
200205
if adv:
201206
lines.append(
202-
f'ADVISORY llm_verdict = "{adv.get("llmVerdict", "")}" '
203-
f'MODEL "{adv.get("model", "")}"'
207+
f'ADVISORY llm_verdict = {format_value(adv.get("llmVerdict", ""))} '
208+
f'MODEL {format_value(adv.get("model", ""))}'
204209
)
205210
for assertion in record.get("assertions") or []:
206211
lines.append(f"ASSERT {assertion}")
@@ -313,21 +318,24 @@ def validate_record(record: dict[str, Any]) -> list[str]:
313318

314319

315320
def split_decision_blocks(markdown: str) -> list[str]:
316-
"""Extract fenced ```dsl DECISION ... blocks or bare DECISION sequences."""
321+
"""Extract bare and fenced DSL decisions once, in document order."""
322+
def decisions(text: str) -> list[str]:
323+
parts = re.split(r"(?=^DECISION\s+D-)", text.strip(), flags=re.MULTILINE)
324+
return [part.strip() for part in parts if part.strip().startswith("DECISION ")]
325+
317326
blocks: list[str] = []
318-
fence = re.findall(r"```dsl\n(.*?)```", markdown, flags=re.DOTALL)
319-
for body in fence:
320-
if "DECISION " in body:
321-
# may contain multiple DECISION records
322-
parts = re.split(r"(?=^DECISION\s+D-)", body.strip(), flags=re.MULTILINE)
323-
for part in parts:
324-
part = part.strip()
325-
if part.startswith("DECISION "):
326-
blocks.append(part)
327-
if blocks:
328-
return blocks
329-
parts = re.split(r"(?=^DECISION\s+D-)", markdown.strip(), flags=re.MULTILINE)
330-
return [p.strip() for p in parts if p.strip().startswith("DECISION ")]
327+
cursor = 0
328+
fences = re.finditer(
329+
r"^[ \t]{0,3}```([^\n]*)\n(.*?)^[ \t]{0,3}```[ \t]*(?:\n|$)",
330+
markdown, flags=re.MULTILINE | re.DOTALL,
331+
)
332+
for fence in fences:
333+
blocks.extend(decisions(markdown[cursor:fence.start()]))
334+
if fence.group(1).strip() == "dsl":
335+
blocks.extend(decisions(fence.group(2)))
336+
cursor = fence.end()
337+
blocks.extend(decisions(markdown[cursor:]))
338+
return blocks
331339

332340

333341
def check_append_only(previous_markdown: str, current_markdown: str) -> list[str]:

‎.governance/diagnostics.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@
106106
"remediation": "Reduce the slice or obtain fresh authorization for a larger bounded intent.",
107107
"documentation": null
108108
},
109+
"GOV-IDLE-TAKEOVER-001": {
110+
"message": "Idle takeover was refused: the ticket is active, frozen, integrated, noncanonical, disabled by policy or its lease changed concurrently.",
111+
"remediation": "Read the reported state on .subactor/cache/autonomy-board.md; continue only an IDLE_TAKEOVER_ELIGIBLE ticket, otherwise assist, reconcile or request an accepted handoff.",
112+
"documentation": "error/GOV-IDLE-TAKEOVER.md"
113+
},
114+
"GOV-IDLE-TAKEOVER-002": {
115+
"message": "Idle takeover could not observe the repository or write its snapshot, receipt, lease or board.",
116+
"remediation": "Preserve the worktree, fix the reported read or write failure and re-run observe before any takeover.",
117+
"documentation": "error/GOV-IDLE-TAKEOVER.md"
118+
},
109119
"GOV-CHANGE-LEASE-001": {
110120
"message": "A change lease, transition request or receipt is malformed or not closed.",
111121
"remediation": "Rebuild the document from the managed change-lease schema without undeclared fields.",

0 commit comments

Comments
 (0)