4"""Repository workflow client for plans and canonical agent workspaces.
6The only creating path delegates to ``scripts/dev/agent_workspace.sh``. This
7module owns no worktree lock, metadata store, branch creator, cleanup path, or
8gate implementation. It reads the shared monitor's cached verdict without
9polling. GitHub mutations are emitted for human review and never executed here.
12from __future__
import annotations
22from dataclasses
import dataclass
23from pathlib
import Path
25sys.path.insert(0, str(Path(__file__).resolve().parent))
26sys.path.insert(0, str(Path(__file__).resolve().parents[4] /
"scripts/dev"))
28from work_emit
import render_commands
29from work_gh
import STATE_DEGRADED, STATE_UNAVAILABLE, Probe, probe_auth, probe_version
38 git_child_environment,
43 reject_executable_attributes,
48from work_plan
import PlanError, load_plan, plan_to_json, render_summary
49from work_workspace
import (
74WS_ROOT_ENV =
"RA8_WS_ROOT"
78@dataclass(frozen=True)
80 """One doctor result."""
87@dataclass(frozen=True)
89 """One dry-run description passed to the canonical workspace creator."""
97 base_commit: str |
None
98 refusals: tuple[str, ...]
101def _print(text: str) ->
None:
102 """Print one sanitized logical line."""
103 print(printable(text))
106def _fail(text: str) ->
None:
107 """Print one sanitized logical error line."""
108 print(printable(text), file=sys.stderr)
111def _notice(text: str) ->
None:
112 """Print one sanitized non-error diagnostic without polluting stdout."""
113 print(printable(text), file=sys.stderr)
116def _print_block(text: str, *, error: bool =
False) ->
None:
117 """Print captured multiline output one sanitized line at a time."""
118 writer = _fail
if error
else _print
119 for line
in text.splitlines():
123def _ws_root(value: str |
None =
None) -> Path:
124 """Return the absolute lexical canonical workspace root."""
125 selected = value
or os.environ.get(WS_ROOT_ENV)
or str(Path.home() /
"ra8-ws")
126 if not selected.isascii()
or not selected.isprintable():
127 message =
"workspace root must contain only printable single-line ASCII"
128 raise WorkError(message)
132 os.fspath(Path(selected).expanduser())
137def _workspace_script(paths: RepoPaths) -> Path:
138 """Return the canonical workspace lifecycle implementation."""
139 return paths.toplevel /
"scripts/dev/agent_workspace.sh"
142def _writability(path: Path) -> tuple[bool, Path]:
143 """Return whether the nearest existing ancestor is writable."""
145 while not probe.exists()
and probe.parent != probe:
147 return os.access(probe, os.W_OK), probe
150def _probe_to_check(probe: Probe) -> Check:
151 """Adapt one GitHub probe."""
152 return Check(probe.name, probe.state, probe.detail)
155def _doctor_checks(paths: RepoPaths, cwd: Path) -> list[Check]:
156 """Build read-only readiness checks, including emitted-script dependencies."""
157 version =
".".join(str(part)
for part
in sys.version_info[:3])
158 python_state = CHECK_OK
if sys.version_info[:2] >= MIN_PYTHON
else CHECK_FAIL
159 git_version = run_process([git_executable(),
"--version"], cwd=cwd).stdout.strip()
161 writable, ancestor = _writability(root)
162 script = _workspace_script(paths)
163 jq = shutil.which(
"jq")
164 jq_state = CHECK_OK
if jq
is not None else CHECK_FAIL
165 jq_detail = run_process([jq,
"--version"], cwd=cwd).stdout.strip()
if jq
else "missing"
166 kind =
"linked worktree" if paths.is_linked_worktree
else "main worktree"
168 Check(
"python", python_state, f
"{version} (minimum {MIN_PYTHON[0]}.{MIN_PYTHON[1]})"),
169 Check(
"git", CHECK_OK, git_version),
170 Check(
"jq", jq_state, jq_detail),
171 Check(
"repository", CHECK_OK, f
"{paths.toplevel} ({kind})"),
173 "canonical workspace tool",
174 CHECK_OK
if script.is_file()
else CHECK_FAIL,
179 CHECK_OK
if writable
else CHECK_FAIL,
180 f
"{root} (nearest ancestor {ancestor} is {'writable' if writable else 'NOT writable'})",
182 _probe_to_check(probe_version()),
183 _probe_to_check(probe_auth()),
187def cmd_doctor(_options: argparse.Namespace) -> int:
188 """Report local readiness; ``gh auth status`` may perform a read-only API probe."""
190 paths = discover_repo(cwd)
191 checks = _doctor_checks(paths, cwd)
192 width = max(len(check.name)
for check
in checks)
194 _print(f
" {check.state:<12} {check.name:<{width}} {check.detail}")
196 if check.state
in (STATE_DEGRADED, STATE_UNAVAILABLE):
197 _print(f
"notice: {check.name} is {check.state.lower()} -- {check.detail}")
198 failed = [check
for check
in checks
if check.state == CHECK_FAIL]
200 _fail(f
"work doctor: {len(failed)} check(s) FAILED")
205def _reject_nonregular_destination(path: Path) ->
None:
206 """Reject an existing destination unless ``lstat`` proves it is regular."""
208 mode = path.lstat().st_mode
209 except FileNotFoundError:
211 if not stat.S_ISREG(mode):
212 message = f
"refusing to replace destination that is not a regular file: {path}"
213 raise WorkError(message)
216def _write_text_atomic(path: Path, text: str) ->
None:
217 """Write derived output atomically without opening a nonregular destination."""
218 _reject_nonregular_destination(path)
219 path.parent.mkdir(parents=
True, exist_ok=
True)
220 descriptor, temporary = tempfile.mkstemp(prefix=f
".{path.name}.", dir=path.parent)
222 with os.fdopen(descriptor,
"w", encoding=
"ascii")
as handle:
225 os.fsync(handle.fileno())
226 _reject_nonregular_destination(path)
227 Path(temporary).replace(path)
229 with contextlib.suppress(FileNotFoundError):
230 Path(temporary).unlink()
233def _plan_output_problem(options: argparse.Namespace) -> str:
234 """Return why the requested stdout artifacts are ambiguous, if they are."""
238 options.emit_commands,
243 return "--json -, --emit-commands, and --summary are mutually exclusive stdout modes"
247def cmd_plan(options: argparse.Namespace) -> int:
248 """Validate notes and emit deterministic artifacts."""
249 if problem := _plan_output_problem(options):
250 _fail(f
"work plan: {problem}")
252 notes = Path(options.notes).expanduser()
254 plan = load_plan(notes)
255 except PlanError
as exc:
256 for problem
in exc.problems:
257 _fail(f
"{notes}: {problem}")
258 _fail(f
"{notes}: {len(exc.problems)} problem(s); nothing was emitted")
261 if options.json
is not None:
262 text = plan_to_json(plan)
263 if options.json ==
"-":
264 sys.stdout.write(text)
266 _write_text_atomic(Path(options.json), text)
267 _notice(f
"wrote {options.json}")
269 if options.emit_commands:
270 sys.stdout.write(render_commands(plan))
272 if options.summary
or not emitted:
273 sys.stdout.write(render_summary(plan))
277def _build_start_plan(paths: RepoPaths, cwd: Path, options: argparse.Namespace) -> StartPlan:
278 """Build a read-only preview; canonical creation repeats checks under its lock."""
279 root = _ws_root(options.ws_root)
280 identifier = options.identifier
281 name = workspace_name(identifier)
282 branch = branch_name(identifier)
284 base = resolve_commit(options.ref, cwd=cwd)
286 reject_executable_attributes(cwd, base)
287 refusals: list[str] = []
289 refusals.append(f
"start ref does not resolve locally: {options.ref}")
290 if target.exists()
or target.is_symlink():
291 refusals.append(f
"workspace path already exists: {target}")
292 claim_path = metadata_path(root, identifier)
293 if claim_path.exists()
or claim_path.is_symlink():
294 refusals.append(f
"canonical workspace metadata already exists for {name}")
295 if branch_exists(branch, cwd=cwd):
296 refusals.append(f
"branch already exists: {branch}")
297 if target.parent != root:
298 refusals.append(
"derived workspace is not a direct child of the canonical root")
299 if not _workspace_script(paths).is_file():
300 refusals.append(
"canonical workspace script is absent")
301 return StartPlan(identifier, name, branch, target, root, options.ref, base, tuple(refusals))
304def _describe_start(paths: RepoPaths, start: StartPlan) ->
None:
305 """Print one mutation-free canonical-workspace preview."""
306 _print(
"work start: DRY RUN -- nothing has been created.")
307 _print(f
" identifier {start.identifier}")
308 _print(f
" canonical {start.name}")
309 _print(f
" branch {start.branch}")
310 _print(f
" worktree {start.target}")
311 _print(f
" start ref {start.ref} -> {start.base_commit or 'unresolved'}")
312 _print(f
" metadata {metadata_path(start.ws_root, start.identifier)}")
313 _print(f
" creator {_workspace_script(paths)}")
314 _print(
"Re-run with --execute to ask the canonical workspace lifecycle to create it.")
317def _execute_start(paths: RepoPaths, start: StartPlan) -> int:
318 """Delegate creation to the sole workspace lock/state authority."""
319 if start.base_commit
is None:
324 str(_workspace_script(paths)),
333 environment = git_child_environment()
334 environment[WS_ROOT_ENV] = str(start.ws_root)
335 environment[
"RA8_WS_UPSTREAM"] = str(paths.toplevel)
336 done = run_process(argv, cwd=paths.toplevel, timeout=300, env=environment)
337 _print_block(done.stdout)
339 _print_block(done.stderr, error=
True)
344def cmd_start(options: argparse.Namespace) -> int:
345 """Preview or canonically create one identifier-owned workspace."""
346 if not is_identifier(options.identifier):
347 _fail(f
"work start: refusing invalid identifier {options.identifier!r}")
350 paths = discover_repo(cwd)
351 start = _build_start_plan(paths, cwd, options)
352 _describe_start(paths, start)
354 for refusal
in start.refusals:
355 _fail(f
"REFUSE: {refusal}")
357 return _execute_start(paths, start)
if options.execute
else EXIT_OK
360def _claim_for(identifier: str, paths: RepoPaths, root: Path) -> Claim:
361 """Load one canonical claim and require exact branch-to-worktree binding."""
362 claim = load_claim(metadata_path(root, identifier), root)
363 verdict = classify(claim, paths.toplevel)
365 message = f
"canonical claim is {verdict}"
366 raise ClaimError(message)
370def cmd_status(options: argparse.Namespace) -> int:
371 """Report only work-owned records from canonical workspace metadata."""
372 paths = discover_repo(Path.cwd())
373 root = _ws_root(options.ws_root)
374 records = list_claims(root)
376 _print(f
"work status: no work-owned canonical metadata under {metadata_dir(root)}")
379 for path, value
in records:
380 if isinstance(value, ClaimError):
381 _print(f
" {FORGED:<9} {path.name:<24} {value}")
384 verdict = classify(value, paths.toplevel)
385 _print(f
" {verdict:<9} {value.identifier:<24} {value.branch:<28} {value.worktree}")
386 forged = forged
or verdict == FORGED
387 if verdict
in (STALE, FOREIGN):
388 command = recovery_command(value, paths.toplevel, stale=verdict == STALE)
389 _print(f
" after human review: {command}")
390 return EXIT_FAIL
if forged
else EXIT_OK
393def _remote_ci(paths: RepoPaths, workspace: Path, head: str) -> tuple[str, str]:
394 """Read the shared monitor's cached verdict without polling GitHub."""
395 monitor = paths.toplevel /
"scripts/ci/monitor.sh"
396 if not monitor.is_file():
397 return "UNKNOWN",
"shared CI monitor script is absent"
398 result = run_process(
399 [
"/bin/bash",
"-p", str(monitor),
"status",
"--sha", head],
403 state = {0:
"PASS", 1:
"FAIL", 3:
"UNKNOWN"}.get(result.returncode,
"UNKNOWN")
405 (line
for line
in result.stdout.splitlines()
if line.strip()),
"no monitor detail"
410def _workspace_report(claim: Claim, phase: str) -> tuple[str |
None, list[str]]:
411 """Print shared workspace facts and return its HEAD plus dirty paths."""
412 dirty = porcelain_status(claim.worktree)
413 head = resolve_commit(
"HEAD", cwd=claim.worktree)
415 _fail(f
"work {phase}: workspace HEAD does not resolve")
417 _print(f
"work {phase}: {claim.identifier}")
418 _print(f
" branch {claim.branch}")
419 _print(f
" worktree {claim.worktree}")
420 _print(f
" base {claim.base_ref} ({claim.base_commit})")
421 _print(f
" claimed {claim.created} by {claim.creator}")
422 _print(f
" HEAD {head}")
423 _print(f
" working set {len(dirty)} changed path(s)")
424 _print(
" diffstat vs base:")
425 for line
in diff_stat(claim.worktree, claim.base_commit).splitlines()
or [
"(empty)"]:
430def _clean_claim(options: argparse.Namespace, phase: str) -> tuple[RepoPaths, Claim] |
None:
431 """Load one exact claim and require a clean committed working tree."""
432 if not is_identifier(options.identifier):
433 _fail(f
"work {phase}: refusing invalid identifier {options.identifier!r}")
435 paths = discover_repo(Path.cwd())
436 root = _ws_root(options.ws_root)
438 claim = _claim_for(options.identifier, paths, root)
439 except ClaimError
as exc:
440 _fail(f
"work {phase}: {exc}")
442 _head, dirty = _workspace_report(claim, phase)
444 _fail(f
"work {phase}: REFUSE -- commit or intentionally discard the working set first")
445 _fail(
"just ci validates committed HEAD, not these uncommitted bytes")
450def cmd_ready(options: argparse.Namespace) -> int:
451 """Run exact local CI for a clean committed work claim before the sole push."""
452 loaded = _clean_claim(options,
"ready")
455 _paths, claim = loaded
456 if not options.run_ci:
457 _fail(
"work ready: REFUSE -- pass --run-ci to record exact local gate evidence")
459 just = shutil.which(
"just")
461 _fail(
"work ready: just is not installed")
463 _print(
" local CI RUNNING: just ci")
464 result = run_process([just,
"ci"], cwd=claim.worktree, timeout=7200)
465 _print_block(result.stdout)
467 _print_block(result.stderr, error=
True)
468 _fail(f
"work ready: local CI did not PASS (exit {result.returncode})")
470 head = resolve_commit(
"HEAD", cwd=claim.worktree)
471 _print(f
" local CI PASS: just ci at {head}")
472 _print(
"work ready: review/squash this tree, then perform the one authorized push")
476def cmd_landed(options: argparse.Namespace) -> int:
477 """Require content-equivalent pushed dev and cached PASS after the push."""
478 loaded = _clean_claim(options,
"landed")
481 paths, claim = loaded
482 claim_head = resolve_commit(
"HEAD", cwd=claim.worktree)
483 dev_head = resolve_commit(
"origin/dev", cwd=claim.worktree)
484 claim_tree = resolve_tree(claim_head, cwd=claim.worktree)
if claim_head
else None
485 dev_tree = resolve_tree(dev_head, cwd=claim.worktree)
if dev_head
else None
486 if dev_head
is None or claim_tree
is None or claim_tree != dev_tree:
487 _fail(
"work landed: origin/dev is absent or not content-equivalent to this work claim")
489 ci_state, ci_detail = _remote_ci(paths, claim.worktree, dev_head)
490 _print(f
" remote CI {ci_state}: {ci_detail}")
491 if ci_state !=
"PASS":
492 _fail(
"work landed: remote CI is not PASS; Landed would be false")
494 _print(f
"work landed: origin/dev {dev_head} is content-equivalent and remotely green")
496 " now set the board card to Landed, close citing the squash SHA, "
497 "then review branch cleanup"
503 "doctor": cmd_doctor,
506 "status": cmd_status,
508 "landed": cmd_landed,
512def build_parser() -> argparse.ArgumentParser:
513 """Build the command-line parser."""
514 parser = argparse.ArgumentParser(prog=
"work", description=__doc__.splitlines()[0])
515 parser.add_argument(
"--selftest", action=
"store_true", help=
"run the offline tests")
516 sub = parser.add_subparsers(dest=
"command", metavar=
"<command>")
517 sub.add_parser(
"doctor", help=
"report local and emitted-script readiness")
518 plan = sub.add_parser(
"plan", help=
"validate notes and emit an ordered plan")
519 plan.add_argument(
"notes")
520 plan.add_argument(
"--json", metavar=
"PATH")
521 stdout = plan.add_mutually_exclusive_group()
522 stdout.add_argument(
"--emit-commands", action=
"store_true")
523 stdout.add_argument(
"--summary", action=
"store_true")
524 start = sub.add_parser(
"start", help=
"preview or canonically create one workspace")
525 start.add_argument(
"identifier")
526 start.add_argument(
"--ref", default=
"HEAD")
527 start.add_argument(
"--execute", action=
"store_true")
528 start.add_argument(
"--ws-root", metavar=
"DIR")
529 status = sub.add_parser(
"status", help=
"list canonical work-owned workspaces")
530 status.add_argument(
"--ws-root", metavar=
"DIR")
531 ready = sub.add_parser(
"ready", help=
"run exact local CI before the sole push")
532 ready.add_argument(
"identifier")
533 ready.add_argument(
"--ws-root", metavar=
"DIR")
534 ready.add_argument(
"--run-ci", action=
"store_true")
535 landed = sub.add_parser(
"landed", help=
"require content-equivalent green origin/dev")
536 landed.add_argument(
"identifier")
537 landed.add_argument(
"--ws-root", metavar=
"DIR")
541def run_selftest() -> int:
542 """Run tests and fail if discovery silently collapses below its floor."""
543 tests = Path(__file__).resolve().parents[1] /
"tests"
544 suite = unittest.TestLoader().discover(str(tests), top_level_dir=str(tests))
545 result = unittest.TextTestRunner(verbosity=2).run(suite)
546 if result.testsRun < SELFTEST_MINIMUM:
547 _fail(f
"work selftest: discovered {result.testsRun} tests, below floor {SELFTEST_MINIMUM}")
549 return EXIT_OK
if result.wasSuccessful()
else EXIT_FAIL
552def main(argv: list[str]) -> int:
553 """Dispatch one command with redacted failures."""
554 parser = build_parser()
555 options = parser.parse_args(argv)
557 return run_selftest()
558 if options.command
is None:
562 return HANDLERS[options.command](options)
563 except (ToolMissingError, GitCommandError, ClaimError, WorkError, OSError)
as exc:
564 _fail(f
"work: {redact(str(exc))}")
568if __name__ ==
"__main__":
569 raise SystemExit(
main(sys.argv[1:]))
void main(void)
The application entry point Reset_Handler hands control to.