From c3008a162aba6609c2fb23a12c709d54592f8874 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 23 May 2026 11:45:12 -0400 Subject: [PATCH 1/4] office: add TurtleTerm operator planning surface --- assets/sourceos/bin/sourceos-term | 178 +++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 1 deletion(-) diff --git a/assets/sourceos/bin/sourceos-term b/assets/sourceos/bin/sourceos-term index 117c07857e0..1b423459bb7 100644 --- a/assets/sourceos/bin/sourceos-term +++ b/assets/sourceos/bin/sourceos-term @@ -29,6 +29,15 @@ from typing import Any, Iterable SESSION_SCHEMA = "sourceos.terminal.session.v0" EVENT_SCHEMA = "sourceos.terminal.event.v0" RECEIPT_SCHEMA = "sourceos.terminal.receipt.v0" +OFFICE_OPERATOR_PLAN_SCHEMA = "sourceos.turtleterm.office.operator_plan.v0" +OFFICE_EVIDENCE_SUMMARY_SCHEMA = "sourceos.turtleterm.office.evidence_summary.v0" + +OFFICE_RUNTIME_CONTRACT_SCHEMAS = { + "officeDocumentRecord": "https://socioprophet.dev/schemas/office/office_document_record.schema.json", + "officeSessionRecord": "https://socioprophet.dev/schemas/office/office_session_record.schema.json", + "officeVersionRecord": "https://socioprophet.dev/schemas/office/office_version_record.schema.json", + "officeWritebackRecord": "https://socioprophet.dev/schemas/office/office_writeback_record.schema.json", +} def env(name: str, fallback: str = "") -> str: @@ -355,8 +364,152 @@ def parse_run_command(raw: list[str]) -> list[str]: return raw +def office_policy() -> dict[str, Any]: + return { + "dryRunDefault": True, + "mutatingExecutionRequires": ["--execute", "--policy-ok"], + "closedProviderRuntimeAuthorityAllowed": False, + "memoryOrSemanticMutationInHotPathAllowed": False, + "recommendedReceiptPath": "turtle-term run -- sourceosctl office ...", + } + + +def sourceosctl_office_plan_argv(args: argparse.Namespace) -> list[str]: + if args.office_action == "evidence-inspect": + return ["sourceosctl", "office", "evidence", "inspect", args.path] + + if args.office_action == "convert": + command = [ + "sourceosctl", + "office", + "convert", + args.input, + "--to", + args.to, + "--dry-run", + "--workroom-id", + args.workroom_id, + "--title", + args.title, + "--artifact-type", + args.artifact_type, + "--format", + args.format, + "--output-root", + args.output_root, + ] + elif args.office_action == "inspect": + command = ["sourceosctl", "office", "inspect", args.path] + else: + command = [ + "sourceosctl", + "office", + "generate", + "--dry-run", + "--workroom-id", + args.workroom_id, + "--title", + args.title, + "--artifact-type", + args.artifact_type, + "--format", + args.format, + "--output-root", + args.output_root, + ] + if args.template: + command.extend(["--template", args.template]) + if args.prompt_ref: + command.extend(["--prompt-ref", args.prompt_ref]) + if args.data_ref: + command.extend(["--data-ref", args.data_ref]) + + if getattr(args, "evidence_out", None): + command.extend(["--evidence-out", args.evidence_out]) + return command + + +def office_plan(args: argparse.Namespace) -> int: + command = sourceosctl_office_plan_argv(args) + payload = { + "schema": OFFICE_OPERATOR_PLAN_SCHEMA, + "kind": "TurtleTermOfficeOperatorPlan", + "created_at": utc_now(), + "workspace_id": env("SOURCEOS_WORKSPACE", "sourceos"), + "actor_id": env("SOURCEOS_ACTOR_ID", f"human:{env('USER', 'local-user')}"), + "frontend": env("SOURCEOS_TERMINAL_FRONTEND", product_name()), + "operation": args.office_action, + "command": shlex.join(command), + "command_argv": command, + "receipt_command": ["turtle-term", "run", "--", *command], + "runtime_contract_schemas": OFFICE_RUNTIME_CONTRACT_SCHEMAS, + "expected_runtime_contracts": [ + "officeDocumentRecord", + "officeSessionRecord", + "officeVersionRecord", + "officeWritebackRecord", + ] if args.office_action in {"generate", "convert"} else [], + "policy": office_policy(), + } + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +def office_evidence_summary(args: argparse.Namespace) -> int: + path = Path(args.path) + if not path.exists() or not path.is_file(): + print(f"{product_name()}: office evidence not found: {path}", file=sys.stderr) + return 1 + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + print(f"{product_name()}: invalid office evidence JSON: {exc}", file=sys.stderr) + return 1 + + runtime_contracts = payload.get("officeRuntimeContracts", {}) if isinstance(payload, dict) else {} + version = runtime_contracts.get("officeVersionRecord", {}) if isinstance(runtime_contracts, dict) else {} + writeback = runtime_contracts.get("officeWritebackRecord", {}) if isinstance(runtime_contracts, dict) else {} + document = runtime_contracts.get("officeDocumentRecord", {}) if isinstance(runtime_contracts, dict) else {} + + summary = { + "schema": OFFICE_EVIDENCE_SUMMARY_SCHEMA, + "kind": "TurtleTermOfficeEvidenceSummary", + "path": str(path), + "evidence_kind": payload.get("kind") if isinstance(payload, dict) else None, + "artifact_id": payload.get("artifactId") if isinstance(payload, dict) else None, + "workroom_id": payload.get("workroomId") if isinstance(payload, dict) else None, + "format": payload.get("format") if isinstance(payload, dict) else None, + "operation": payload.get("operation") if isinstance(payload, dict) else None, + "status": payload.get("status") if isinstance(payload, dict) else None, + "runtime_contract_kinds": sorted(runtime_contracts.keys()) if isinstance(runtime_contracts, dict) else [], + "office_document_id": document.get("document_id") if isinstance(document, dict) else None, + "office_version_id": version.get("version_id") if isinstance(version, dict) else None, + "office_writeback_id": writeback.get("writeback_id") if isinstance(writeback, dict) else None, + "content_hash": version.get("content_hash") if isinstance(version, dict) else None, + "policy": office_policy(), + } + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 + + +def add_office_common(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--workroom-id", default="workroom-local-default", help="Professional Workroom id") + parser.add_argument("--title", default="Untitled Office Artifact", help="Office artifact title") + parser.add_argument("--artifact-type", default="document", help="Office artifact type") + parser.add_argument("--format", default="md", help="Office artifact format") + parser.add_argument("--output-root", default="~/Documents/SourceOS/agent-output", help="Host Office output root") + parser.add_argument("--evidence-out", default=None, help="Optional OfficeArtifactEvidence output path") + + +def normalize_argv(argv: list[str]) -> list[str]: + if argv and argv[0] == "/office": + return ["office", *argv[1:]] + return argv + + def main(argv: list[str]) -> int: - if argv and argv[0] not in {"run", "paths", "-h", "--help"}: + argv = normalize_argv(argv) + if argv and argv[0] not in {"run", "paths", "office", "-h", "--help"}: return run_command(parse_run_command(argv)) parser = argparse.ArgumentParser(description="TurtleTerm command wrapper v0") @@ -367,6 +520,26 @@ def main(argv: list[str]) -> int: subparsers.add_parser("paths", help="print event and receipt paths") + office_parser = subparsers.add_parser("office", help="plan and inspect SourceOS Office operator flows") + office_sub = office_parser.add_subparsers(dest="office_command") + + office_plan_parser = office_sub.add_parser("plan", help="render a sourceosctl office operator plan") + office_plan_parser.add_argument("--office-action", default="generate", choices=["generate", "convert", "inspect", "evidence-inspect"], help="Office action to plan") + add_office_common(office_plan_parser) + office_plan_parser.add_argument("--template", default=None, help="Optional SourceOS office template reference") + office_plan_parser.add_argument("--prompt-ref", default=None, help="Optional prompt/context reference") + office_plan_parser.add_argument("--data-ref", default=None, help="Optional structured data reference") + office_plan_parser.add_argument("--input", default="./input.docx", help="Input path for convert action") + office_plan_parser.add_argument("--to", default="pdf", help="Target format for convert action") + office_plan_parser.add_argument("--path", default="./office-evidence.json", help="Path for inspect/evidence-inspect actions") + office_plan_parser.set_defaults(func=office_plan) + + evidence_parser = office_sub.add_parser("evidence", help="inspect SourceOS Office evidence") + evidence_sub = evidence_parser.add_subparsers(dest="office_evidence_command") + evidence_inspect = evidence_sub.add_parser("inspect", help="summarize OfficeArtifactEvidence runtime contract ids") + evidence_inspect.add_argument("path", help="Path to OfficeArtifactEvidence JSON") + evidence_inspect.set_defaults(func=office_evidence_summary) + args = parser.parse_args(argv) if args.command_name == "paths": @@ -375,6 +548,9 @@ def main(argv: list[str]) -> int: if args.command_name == "run": return run_command(parse_run_command(args.cmd)) + if args.command_name == "office" and hasattr(args, "func"): + return args.func(args) + parser.print_help(sys.stderr) return 2 From 2dc12eec49e539e2b780abcf0b3f56dce8c9c4af Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 23 May 2026 11:57:22 -0400 Subject: [PATCH 2/4] office: add TurtleTerm smoke coverage --- .../tests/test_sourceos_term_smoke.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/assets/sourceos/tests/test_sourceos_term_smoke.py b/assets/sourceos/tests/test_sourceos_term_smoke.py index c7cc3a41549..91bef582791 100644 --- a/assets/sourceos/tests/test_sourceos_term_smoke.py +++ b/assets/sourceos/tests/test_sourceos_term_smoke.py @@ -21,6 +21,29 @@ def read_ndjson(path: Path) -> list[dict]: return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] +def run_json(wrapper: Path, args: list[str]) -> dict: + env = dict(os.environ) + env.update( + { + "SOURCEOS_WORKSPACE": "office-smoke-workspace", + "SOURCEOS_ACTOR_ID": "test:office-smoke", + "SOURCEOS_POLICY_BUNDLE_ID": "policy:office-smoke", + "SOURCEOS_EXECUTION_DOMAIN": "host", + } + ) + result = subprocess.run( + [sys.executable, str(wrapper), *args], + cwd=str(REPO_ROOT), + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + assert result.returncode == 0, result.stderr + return json.loads(result.stdout) + + def run_wrapper(wrapper: Path, session_id: str, workspace: str, expected_text: str) -> tuple[list[dict], dict]: with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) @@ -85,6 +108,39 @@ def write_json(path: Path, data: dict) -> None: path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") +def sample_office_evidence(path: Path) -> None: + write_json( + path, + { + "kind": "OfficeArtifactEvidence", + "artifactId": "office-artifact-demo-report", + "workroomId": "workroom-demo", + "format": "md", + "operation": "generate", + "status": "requires-review", + "officeRuntimeContracts": { + "schemas": { + "officeDocumentRecord": "https://socioprophet.dev/schemas/office/office_document_record.schema.json", + "officeSessionRecord": "https://socioprophet.dev/schemas/office/office_session_record.schema.json", + "officeVersionRecord": "https://socioprophet.dev/schemas/office/office_version_record.schema.json", + "officeWritebackRecord": "https://socioprophet.dev/schemas/office/office_writeback_record.schema.json", + }, + "officeDocumentRecord": { + "document_id": "office-artifact-demo-report", + "version_head": "office-version-office-artifact-demo-report-0001", + }, + "officeVersionRecord": { + "version_id": "office-version-office-artifact-demo-report-0001", + "content_hash": "sha256:" + "a" * 64, + }, + "officeWritebackRecord": { + "writeback_id": "office-writeback-office-artifact-demo-report-0001", + }, + }, + }, + ) + + def run_agent_status(root: Path, expect_code: int) -> dict: result = subprocess.run( [sys.executable, str(AGENT_STATUS), "--root", str(root), "--json"], @@ -98,6 +154,68 @@ def run_agent_status(root: Path, expect_code: int) -> dict: return json.loads(result.stdout) +def test_office_operator_plan() -> None: + payload = run_json( + TURTLE_WRAPPER, + [ + "office", + "plan", + "--title", + "Demo Report", + "--artifact-type", + "document", + "--format", + "md", + "--workroom-id", + "workroom-demo", + ], + ) + assert payload["schema"] == "sourceos.turtleterm.office.operator_plan.v0" + assert payload["operation"] == "generate" + assert payload["command_argv"][:3] == ["sourceosctl", "office", "generate"] + assert "--dry-run" in payload["command_argv"] + assert payload["policy"]["closedProviderRuntimeAuthorityAllowed"] is False + assert "officeVersionRecord" in payload["expected_runtime_contracts"] + assert payload["receipt_command"][:3] == ["turtle-term", "run", "--"] + + +def test_slash_office_operator_alias() -> None: + payload = run_json( + SOURCEOS_WRAPPER, + [ + "/office", + "plan", + "--office-action", + "convert", + "--input", + "./demo.docx", + "--to", + "pdf", + "--title", + "Converted Report", + ], + ) + assert payload["operation"] == "convert" + assert payload["command_argv"][:4] == ["sourceosctl", "office", "convert", "./demo.docx"] + assert "--to" in payload["command_argv"] + assert "officeWritebackRecord" in payload["expected_runtime_contracts"] + + +def test_office_evidence_summary() -> None: + with tempfile.TemporaryDirectory() as tmp: + evidence = Path(tmp) / "office-evidence.json" + sample_office_evidence(evidence) + payload = run_json(TURTLE_WRAPPER, ["office", "evidence", "inspect", str(evidence)]) + + assert payload["schema"] == "sourceos.turtleterm.office.evidence_summary.v0" + assert payload["artifact_id"] == "office-artifact-demo-report" + assert payload["workroom_id"] == "workroom-demo" + assert payload["office_document_id"] == "office-artifact-demo-report" + assert payload["office_version_id"] == "office-version-office-artifact-demo-report-0001" + assert payload["office_writeback_id"] == "office-writeback-office-artifact-demo-report-0001" + assert payload["content_hash"] == "sha256:" + "a" * 64 + + def test_agent_status_no_artifacts() -> None: with tempfile.TemporaryDirectory() as tmp: summary = run_agent_status(Path(tmp), expect_code=0) @@ -165,6 +283,9 @@ def main() -> int: _, turtle_session = run_wrapper(TURTLE_WRAPPER, "turtle-term-test", "turtle-test", "turtle-smoke") assert turtle_session["frontend"] == "turtle-term" + test_office_operator_plan() + test_slash_office_operator_alias() + test_office_evidence_summary() test_agent_status_no_artifacts() test_agent_status_blocked_by_guardrail() test_agent_status_needs_review_from_governance_queue() From 87d1df67a2259f0c53b9ea17585c00432d41471e Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 23 May 2026 12:01:20 -0400 Subject: [PATCH 3/4] office: document TurtleTerm operator commands --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bf36fae646..b8374ad72d7 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,17 @@ turtleterm ```bash turtle-term paths turtle-term run -- echo hello +turtle-term office plan --title "Demo Report" --artifact-type document --format md +turtle-term /office plan --office-action convert --input ./demo.docx --to pdf +turtle-term office evidence inspect ./office-evidence.json turtle-agentctl --stdio ping turtle-tmux panes ``` `turtle-term` is the command wrapper. `turtleterm` is the graphical launcher. `sourceos-term` remains available for SourceOS contract compatibility. +The `office` / `/office` operator surface does not implement an office suite inside TurtleTerm. It produces SourceOS Office operator plans that point to `sourceosctl office`, records the receipt command to run through TurtleTerm, and summarizes `OfficeArtifactEvidence` runtime contract IDs when present. + ## Product surfaces - TurtleTerm graphical launcher @@ -65,10 +70,11 @@ turtle-tmux panes - TurtleTerm local agent gateway - TurtleTerm agent CLI - TurtleTerm tmux bridge +- TurtleTerm Office operator flow planning - TurtleTerm skill manifests - TurtleTerm turtle icon - TurtleTerm release artifacts, manifests, SBOMs, and attestations ## License and notices -TurtleTerm is MIT licensed. Required third-party notices are preserved in `LICENSE.md` and release artifacts. +TurtleTerm is MIT licensed. Required third-party notices are preserved in `LICENSE.md` and release artifacts. \ No newline at end of file From 30604e71c2c534f99ad444ef790cd7fd951d0670 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 23 May 2026 12:04:49 -0400 Subject: [PATCH 4/4] office: document install validation command --- docs/sourceos/INSTALL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sourceos/INSTALL.md b/docs/sourceos/INSTALL.md index a6ce76fcb9b..8e476a112d5 100644 --- a/docs/sourceos/INSTALL.md +++ b/docs/sourceos/INSTALL.md @@ -70,6 +70,7 @@ TURTLE_TERM_USE_BREW=never bash packaging/scripts/install-turtle-term.sh turtleterm --version || true turtle-term paths turtle-term run -- echo turtle-term-ok +turtle-term office plan --title "Demo Report" --artifact-type document --format md turtle-agentctl --stdio ping ```