arqux 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. arqux/__init__.py +28 -0
  2. arqux/__main__.py +6 -0
  3. arqux/cli.py +196 -0
  4. arqux/constants.py +185 -0
  5. arqux/cortex_out.py +124 -0
  6. arqux/formats.py +784 -0
  7. arqux/handlers/__init__.py +992 -0
  8. arqux/handlers/blueprint.py +1583 -0
  9. arqux/handlers/cortex.py +661 -0
  10. arqux/handlers/cycle.py +340 -0
  11. arqux/handlers/evidence.py +117 -0
  12. arqux/handlers/project.py +382 -0
  13. arqux/handlers/protocol.py +128 -0
  14. arqux/handlers/session.py +382 -0
  15. arqux/handlers/skill.py +513 -0
  16. arqux/handlers/task.py +382 -0
  17. arqux/handlers/workspace.py +178 -0
  18. arqux/identities/alfred.cortex +111 -0
  19. arqux/identities/auditor.cortex +47 -0
  20. arqux/identities/executor.cortex +49 -0
  21. arqux/identities/governor.cortex +53 -0
  22. arqux/identities/heimdall.cortex +77 -0
  23. arqux/identities/jarvis.cortex +82 -0
  24. arqux/identities/seshat.cortex +73 -0
  25. arqux/learning.py +408 -0
  26. arqux/permissions.py +151 -0
  27. arqux/plantuml.py +156 -0
  28. arqux/plantuml_server.py +243 -0
  29. arqux/pulse.py +178 -0
  30. arqux/server.py +153 -0
  31. arqux/sessions.py +124 -0
  32. arqux/skills/cortex.skill.md +252 -0
  33. arqux/skills/diagram.skill.md +138 -0
  34. arqux/skills/learning.skill.md +85 -0
  35. arqux/skills/mcp-handlers.skill.md +193 -0
  36. arqux/skills/protocol.skill.md +117 -0
  37. arqux/skills/workflows.skill.md +45 -0
  38. arqux/state.py +1045 -0
  39. arqux/sync.py +236 -0
  40. arqux/templates/AGENTS.md +112 -0
  41. arqux/templates/BLP_TEMPLATE.md +244 -0
  42. arqux/templates/CYCLE_MANIFEST_TEMPLATE.md +128 -0
  43. arqux/templates/brain.cortex +43 -0
  44. arqux/templates/brain.md +46 -0
  45. arqux/templates/learn-policies.cortex +36 -0
  46. arqux/templates/meta-brain.cortex +22 -0
  47. arqux/templates/meta-brain.md +24 -0
  48. arqux-0.3.0.dist-info/METADATA +209 -0
  49. arqux-0.3.0.dist-info/RECORD +53 -0
  50. arqux-0.3.0.dist-info/WHEEL +5 -0
  51. arqux-0.3.0.dist-info/entry_points.txt +2 -0
  52. arqux-0.3.0.dist-info/licenses/LICENSE +201 -0
  53. arqux-0.3.0.dist-info/top_level.txt +1 -0
arqux/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """
2
+ Arqux — Minimum-viable governance framework for AI agent teams.
3
+
4
+ Public API surface:
5
+ - `cli.main()` — entry point for the `arqux` console script.
6
+ - `server.serve()` — start the MCP server.
7
+ - `__version__` — package version string.
8
+
9
+ The framework exposes 24 MCP handlers grouped in 6 modules:
10
+ workspace, project, cycle, task, evidence, protocol.
11
+
12
+ See `AGENTS.md` for the single-entry-point documentation.
13
+ """
14
+
15
+ from .constants import (
16
+ ARQUX_DIR,
17
+ ARQUX_VERSION,
18
+ PRODUCT_NAME,
19
+ )
20
+
21
+ __version__ = ARQUX_VERSION
22
+
23
+ __all__ = [
24
+ "PRODUCT_NAME",
25
+ "ARQUX_DIR",
26
+ "ARQUX_VERSION",
27
+ "__version__",
28
+ ]
arqux/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m arqux`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
arqux/cli.py ADDED
@@ -0,0 +1,196 @@
1
+ """CLI entry point.
2
+
3
+ Commands:
4
+ arqux serve — start the MCP server on stdio.
5
+ arqux init — initialize .arqux/ in the current directory.
6
+ arqux status — print workspace/project/cycle status.
7
+ arqux call <handler> — call any handler directly (no MCP required).
8
+ arqux setup-plantuml — download plantuml.jar.
9
+ arqux serve-plantuml — start PlantUML render server.
10
+ arqux --version
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any, Sequence
19
+
20
+ import click
21
+
22
+ from . import __version__
23
+ from .constants import PRODUCT_NAME, PRODUCT_NAME_TITLE
24
+
25
+
26
+
27
+
28
+ def _call_handler(name: str, raw_args: list[str]) -> str:
29
+ """Dispatch a handler by name with key=value arguments.
30
+
31
+ Returns the handler's CORTEX-OUT message as a string.
32
+ """
33
+ from .handlers import REGISTRY
34
+ from .permissions import PermissionContext
35
+
36
+ if name not in REGISTRY:
37
+ # Try MCP-safe name (underscores → dots)
38
+ dotted = name.replace("_", ".")
39
+ if dotted in REGISTRY:
40
+ name = dotted
41
+ else:
42
+ available = "\n".join(f" {n}" for n in sorted(REGISTRY.keys()))
43
+ return f"ERROR: unknown handler '{name}'. Available:\n{available}"
44
+
45
+ spec = REGISTRY[name]
46
+ ctx = PermissionContext.from_env()
47
+
48
+ # Parse key=value arguments
49
+ kwargs: dict[str, Any] = {}
50
+ for arg in raw_args:
51
+ if "=" in arg:
52
+ key, val = arg.split("=", 1)
53
+ # Try JSON parsing for complex values
54
+ try:
55
+ kwargs[key] = json.loads(val)
56
+ except (json.JSONDecodeError, ValueError):
57
+ kwargs[key] = val
58
+ else:
59
+ # Positional: use as first required param's value
60
+ req = spec.input_schema.get("required", [])
61
+ if req:
62
+ kwargs[req[0]] = arg
63
+
64
+ # Add path if not provided
65
+ if "path" not in kwargs:
66
+ kwargs["path"] = str(Path.cwd())
67
+
68
+ # Call handler
69
+ try:
70
+ result = spec.fn(**kwargs, ctx=ctx)
71
+ except TypeError as e:
72
+ return f"ERROR: {e}. Expected params: {list(spec.input_schema.get('properties', {}).keys())}"
73
+
74
+ if hasattr(result, "to_text"):
75
+ return result.to_text()
76
+ if hasattr(result, "message"):
77
+ return str(result.message)
78
+ return str(result)
79
+
80
+
81
+ @click.group()
82
+ @click.version_option(version=__version__, prog_name=PRODUCT_NAME, message="%(prog)s %(version)s")
83
+ def main():
84
+ """Arqux — governance framework for AI agent teams."""
85
+ pass
86
+
87
+ @main.command("init")
88
+ @click.option("--path", default=None, help="Path to initialize.")
89
+ @click.option("--verbose", is_flag=True, help="Use OUT-AUDIT profile.")
90
+ def cmd_init(path: str | None, verbose: bool):
91
+ """Initialize .arqux/ in the current directory."""
92
+ from .handlers.workspace import init_workspace
93
+
94
+ result = init_workspace(path=path, verbose=verbose)
95
+ click.echo(result.to_text())
96
+
97
+ @main.command("status")
98
+ @click.option("--path", default=None, help="Path to check.")
99
+ @click.option("--verbose", is_flag=True, help="Verbose output.")
100
+ def cmd_status(path: str | None, verbose: bool):
101
+ """Print workspace + project + cycle status."""
102
+ from .handlers.workspace import status as ws_status
103
+ from .handlers.project import status as pr_status
104
+ from .handlers.cycle import current_cycle
105
+
106
+ ws = ws_status(verbose=verbose, path=path)
107
+ click.echo(ws.to_text())
108
+
109
+ try:
110
+ pr = pr_status(verbose=verbose, path=path)
111
+ click.echo(pr.to_text())
112
+ except Exception:
113
+ pass
114
+
115
+ try:
116
+ cy = current_cycle(path=path)
117
+ click.echo(cy.to_text())
118
+ except Exception:
119
+ pass
120
+
121
+
122
+ @main.command("serve")
123
+ @click.option("--verbose", is_flag=True, help="Verbose startup.")
124
+ def cmd_serve(verbose: bool):
125
+ """Start the MCP server on stdio."""
126
+ from .server import run_server
127
+ run_server(verbose=verbose)
128
+
129
+
130
+ @main.command("call")
131
+ @click.argument("handler")
132
+ @click.argument("args", nargs=-1)
133
+ def cmd_call(handler: str, args: tuple[str, ...]):
134
+ """Call any Arqux handler directly (no MCP required).
135
+
136
+ Examples:
137
+ arqux call workspace.status
138
+ arqux call blueprint.create obj="OAuth2 endpoint" cycle=CYCLE-01
139
+ arqux call identity.record lesson="Always verify P0" kind=behavioral
140
+ arqux call blueprint.list status=done cycle=CYCLE-01
141
+ """
142
+ result = _call_handler(handler, list(args))
143
+ click.echo(result)
144
+
145
+
146
+ @main.command("setup-plantuml")
147
+ @click.option("--force", is_flag=True, help="Force re-download.")
148
+ def cmd_setup_plantuml(force: bool):
149
+ """Install plantuml.jar to ~/.arqux/bin/."""
150
+ from .plantuml import setup_plantuml as sp
151
+
152
+ ok, msg = sp(force=force)
153
+ click.echo(msg)
154
+ sys.exit(0 if ok else 1)
155
+
156
+
157
+ @main.command("serve-plantuml")
158
+ @click.option("--port", type=int, default=9876, help="Port to listen on.")
159
+ def cmd_serve_plantuml(port: int):
160
+ """Start PlantUML rendering server."""
161
+ from .plantuml_server import start_server
162
+
163
+ srv = start_server(port=port)
164
+ import time
165
+
166
+ try:
167
+ while True:
168
+ time.sleep(1)
169
+ except KeyboardInterrupt:
170
+ srv.shutdown()
171
+
172
+
173
+ @main.command("render-diagram")
174
+ @click.argument("puml_file")
175
+ @click.option("--format", "fmt", default="svg", type=click.Choice(["svg", "png"]))
176
+ @click.option("--output", "-o", default=None, help="Output directory.")
177
+ def cmd_render_diagram(puml_file: str, fmt: str, output: str | None):
178
+ """Render a PUML file to SVG/PNG."""
179
+ from pathlib import Path
180
+
181
+ from .plantuml import render_puml
182
+
183
+ source = Path(puml_file).read_text(encoding="utf-8")
184
+ out_dir = Path(output) if output else None
185
+ ok, result = render_puml(source, format=fmt, output_dir=out_dir)
186
+ click.echo(result)
187
+ sys.exit(0 if ok else 1)
188
+
189
+
190
+ @main.command("handlers")
191
+ def cmd_handlers():
192
+ """List all available handlers."""
193
+ from .handlers import list_handlers
194
+
195
+ for name in sorted(list_handlers()):
196
+ click.echo(name)
arqux/constants.py ADDED
@@ -0,0 +1,185 @@
1
+ """Module-level constants for Arqux.
2
+
3
+ All placeholder-derived identifiers live here so the rename script has a single
4
+ canonical surface to swap. After running `scripts/rename-product.py <name>`:
5
+
6
+ arqux -> <name> (lowercase, package/cli/paths)
7
+ ARQUX -> <NAME> (uppercase, constants/markers)
8
+ Arqux -> <Name> (title case, display names)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ # --- Identity --------------------------------------------------------------
17
+
18
+ #: Lowercase product name. Used as the package name, CLI command, and the
19
+ #: governance directory name (e.g. `.<product>/`).
20
+ PRODUCT_NAME: str = "arqux"
21
+
22
+ #: Uppercase product name. Used for environment variables and constants.
23
+ PRODUCT_NAME_UPPER: str = "ARQUX"
24
+
25
+ #: Title-case product name. Used in human-readable documentation.
26
+ PRODUCT_NAME_TITLE: str = "Arqux"
27
+
28
+ #: Version string — single source of truth.
29
+ ARQUX_VERSION: str = "1.0.0"
30
+
31
+ # --- Filesystem layout -----------------------------------------------------
32
+
33
+ #: Name of the governance directory created inside a workspace or project.
34
+ ARQUX_DIR: str = f".{PRODUCT_NAME}"
35
+
36
+ #: Environment variable prefix for runtime configuration.
37
+ ARQUX_ENV_PREFIX: str = f"{PRODUCT_NAME_UPPER}_"
38
+
39
+ #: Default workspace manifest file (machine-readable).
40
+ MANIFEST_CORTEX: str = "manifest.cortex"
41
+
42
+ #: Default workspace manifest file (human-readable).
43
+ MANIFEST_HCORTEX: str = "manifest.md"
44
+
45
+ #: Workspace-level projects index (machine-readable).
46
+ PROJECTS_CORTEX: str = "projects.cortex"
47
+
48
+ #: Workspace-level projects index (human-readable).
49
+ PROJECTS_HCORTEX: str = "projects.md"
50
+
51
+ #: Project-level brain (machine-readable).
52
+ BRAIN_CORTEX: str = "brain.cortex"
53
+
54
+ #: Project-level brain (human-readable).
55
+ BRAIN_HCORTEX: str = "brain.md"
56
+
57
+ #: Workspace-level meta-brain (machine-readable).
58
+ META_BRAIN_CORTEX: str = "meta-brain.cortex"
59
+
60
+ #: Workspace-level meta-brain (human-readable).
61
+ META_BRAIN_HCORTEX: str = "meta-brain.md"
62
+
63
+ #: Per-cycle tasks directory.
64
+ TASKS_DIR: str = "tasks"
65
+
66
+ #: Per-project cycles directory.
67
+ CYCLES_DIR: str = "cycles"
68
+ BLUEPRINTS_DIR: str = "blueprints"
69
+
70
+ # --- Brain sections (the project brain is the single shared mind) -----------
71
+ #
72
+ # The brain.cortex is the shared project mind. All handoffs, pulses,
73
+ # sessions, lessons, focus, and active context live HERE — not in separate
74
+ # files. This guarantees that every agent bound to the project shares the
75
+ # same mental state.
76
+
77
+ BRAIN_SECTION_FOCUS: str = "FOCUS"
78
+ BRAIN_SECTION_OBJECTIVES: str = "OBJECTIVES"
79
+ BRAIN_SECTION_SESSIONS: str = "SESSIONS"
80
+ BRAIN_SECTION_HANDOFFS: str = "HANDOFFS"
81
+ BRAIN_SECTION_PULSE: str = "PULSE"
82
+ BRAIN_SECTION_LESSONS: str = "LESSONS"
83
+ BRAIN_SECTION_ACTIVE_CONTEXT: str = "ACTIVE_CONTEXT"
84
+ BRAIN_SECTION_RISKS: str = "RISKS"
85
+ BRAIN_SECTION_CONCURRENCY: str = "CONCURRENCY"
86
+
87
+ #: All canonical brain sections (used for validation).
88
+ ALL_BRAIN_SECTIONS: tuple[str, ...] = (
89
+ BRAIN_SECTION_FOCUS,
90
+ BRAIN_SECTION_OBJECTIVES,
91
+ BRAIN_SECTION_SESSIONS,
92
+ BRAIN_SECTION_HANDOFFS,
93
+ BRAIN_SECTION_PULSE,
94
+ BRAIN_SECTION_LESSONS,
95
+ BRAIN_SECTION_ACTIVE_CONTEXT,
96
+ BRAIN_SECTION_RISKS,
97
+ BRAIN_SECTION_CONCURRENCY,
98
+ )
99
+
100
+ # --- Roles and permissions -------------------------------------------------
101
+
102
+ ROLE_GOVERNOR: str = "governor"
103
+ ROLE_EXECUTOR: str = "executor"
104
+ ROLE_AUDITOR: str = "auditor"
105
+
106
+ ALL_ROLES: tuple[str, ...] = (ROLE_GOVERNOR, ROLE_EXECUTOR, ROLE_AUDITOR)
107
+
108
+ # --- Task state machine ----------------------------------------------------
109
+
110
+ TASK_DRAFT: str = "draft"
111
+ TASK_OPEN: str = "open"
112
+ TASK_IN_PROGRESS: str = "in_progress"
113
+ TASK_REVIEW: str = "review"
114
+ TASK_DONE: str = "done"
115
+ TASK_BLOCKED: str = "blocked"
116
+ TASK_CANCELLED: str = "cancelled"
117
+
118
+ TASK_ACTIVE_STATES: tuple[str, ...] = (
119
+ TASK_DRAFT,
120
+ TASK_OPEN,
121
+ TASK_IN_PROGRESS,
122
+ TASK_REVIEW,
123
+ TASK_BLOCKED,
124
+ )
125
+
126
+ TASK_TERMINAL_STATES: tuple[str, ...] = (TASK_DONE, TASK_CANCELLED)
127
+
128
+ TASK_TRANSITIONS: dict[str, tuple[str, ...]] = {
129
+ TASK_DRAFT: (TASK_OPEN, TASK_CANCELLED),
130
+ TASK_OPEN: (TASK_IN_PROGRESS, TASK_CANCELLED),
131
+ TASK_IN_PROGRESS: (TASK_REVIEW, TASK_BLOCKED, TASK_DONE, TASK_CANCELLED),
132
+ TASK_REVIEW: (TASK_DONE, TASK_IN_PROGRESS, TASK_BLOCKED, TASK_CANCELLED),
133
+ TASK_BLOCKED: (TASK_IN_PROGRESS, TASK_CANCELLED),
134
+ TASK_DONE: (),
135
+ TASK_CANCELLED: (),
136
+ }
137
+
138
+ # --- Cycle state machine ---------------------------------------------------
139
+
140
+ CYCLE_OPEN: str = "open"
141
+ CYCLE_CLOSED: str = "closed"
142
+
143
+ CYCLE_TRANSITIONS: dict[str, tuple[str, ...]] = {
144
+ CYCLE_OPEN: (CYCLE_CLOSED,),
145
+ CYCLE_CLOSED: (),
146
+ }
147
+
148
+ # --- CORTEX-OUT profiles ---------------------------------------------------
149
+
150
+ OUT_MIN: str = "OUT-MIN"
151
+ OUT_WORK: str = "OUT-WORK"
152
+ OUT_AUDIT: str = "OUT-AUDIT"
153
+ OUT_FULL: str = "OUT-FULL"
154
+ OUT_ERROR: str = "OUT-ERROR"
155
+
156
+ ALL_OUT_PROFILES: tuple[str, ...] = (
157
+ OUT_MIN,
158
+ OUT_WORK,
159
+ OUT_AUDIT,
160
+ OUT_FULL,
161
+ OUT_ERROR,
162
+ )
163
+
164
+ DEFAULT_OUT_PROFILE: str = OUT_WORK
165
+
166
+ # --- Error codes -----------------------------------------------------------
167
+
168
+ PERMISSION_DENIED: str = "PERMISSION_DENIED"
169
+ NOT_FOUND: str = "NOT_FOUND"
170
+ INVALID_STATE: str = "INVALID_STATE"
171
+ INVALID_ARGUMENT: str = "INVALID_ARGUMENT"
172
+ ALREADY_EXISTS: str = "ALREADY_EXISTS"
173
+ INTERNAL_ERROR: str = "INTERNAL_ERROR"
174
+
175
+ # --- Paths -----------------------------------------------------------------
176
+
177
+ PACKAGE_ROOT: Path = Path(__file__).resolve().parent
178
+ TEMPLATES_DIR: Path = PACKAGE_ROOT / "templates"
179
+ IDENTITIES_DIR: Path = PACKAGE_ROOT / "identities"
180
+
181
+ # --- Environment overrides -------------------------------------------------
182
+
183
+ def env(var: str, default: str | None = None) -> str | None:
184
+ """Read an environment variable prefixed with the product name."""
185
+ return os.environ.get(f"{ARQUX_ENV_PREFIX}{var}", default)
arqux/cortex_out.py ADDED
@@ -0,0 +1,124 @@
1
+ """CORTEX-OUT output protocol.
2
+
3
+ Five profiles, picked by context:
4
+
5
+ OUT-MIN — quick acks, no detail.
6
+ OUT-WORK — work updates, deliverables, evidence of progress.
7
+ OUT-AUDIT — architecture reviews, decision logs.
8
+ OUT-FULL — full prose, no compression (for humans).
9
+ OUT-ERROR — failures, blockers, permission denials.
10
+
11
+ Format: <PROFILE> <key=value>... [message]
12
+
13
+ Examples:
14
+ OUT-MIN ok T-001 in_progress
15
+ OUT-WORK done T-001 evidence=E-007 coverage=87%
16
+ OUT-AUDIT review cycle=CYCLE-01 risk=low rationale="..."
17
+ OUT-ERROR code=PERMISSION_DENIED handler=task.create reason=executor_role
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from typing import Any
24
+
25
+ from .constants import (
26
+ DEFAULT_OUT_PROFILE,
27
+ OUT_AUDIT,
28
+ OUT_ERROR,
29
+ OUT_FULL,
30
+ OUT_MIN,
31
+ OUT_WORK,
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class CortexOUT:
37
+ """A structured CORTEX-OUT response."""
38
+
39
+ profile: str
40
+ fields: dict[str, Any] = field(default_factory=dict)
41
+ message: str = ""
42
+
43
+ @classmethod
44
+ def profile(cls, profile: str, message: str = "", **fields: Any) -> "CortexOUT":
45
+ """Build a CORTEX-OUT response with the given profile."""
46
+ return cls(profile=profile, fields=dict(fields), message=message)
47
+
48
+ @classmethod
49
+ def min(cls, message: str = "", **fields: Any) -> "CortexOUT":
50
+ return cls(profile=OUT_MIN, fields=dict(fields), message=message)
51
+
52
+ @classmethod
53
+ def work(cls, message: str = "", **fields: Any) -> "CortexOUT":
54
+ return cls(profile=OUT_WORK, fields=dict(fields), message=message)
55
+
56
+ @classmethod
57
+ def audit(cls, message: str = "", **fields: Any) -> "CortexOUT":
58
+ return cls(profile=OUT_AUDIT, fields=dict(fields), message=message)
59
+
60
+ @classmethod
61
+ def full(cls, message: str = "", **fields: Any) -> "CortexOUT":
62
+ return cls(profile=OUT_FULL, fields=dict(fields), message=message)
63
+
64
+ @classmethod
65
+ def error(cls, message: str = "", **fields: Any) -> "CortexOUT":
66
+ return cls(profile=OUT_ERROR, fields=dict(fields), message=message)
67
+
68
+ def to_text(self) -> str:
69
+ """Render as a single text line (or multi-line for OUT-FULL)."""
70
+ if self.profile == OUT_FULL:
71
+ # OUT-FULL: just the message, no key=value compression.
72
+ return self.message
73
+
74
+ parts: list[str] = [self.profile]
75
+ for key, value in self.fields.items():
76
+ parts.append(f"{key}={self._format_value(value)}")
77
+ if self.message:
78
+ parts.append(self.message)
79
+ return " ".join(parts)
80
+
81
+ @staticmethod
82
+ def _format_value(value: Any) -> str:
83
+ if isinstance(value, bool):
84
+ return "true" if value else "false"
85
+ if isinstance(value, (list, tuple)):
86
+ return ",".join(str(v) for v in value)
87
+ if isinstance(value, dict):
88
+ return ";".join(f"{k}:{v}" for k, v in value.items())
89
+ return str(value)
90
+
91
+
92
+ def format_status(workspace_root: Any, profile: str = DEFAULT_OUT_PROFILE) -> str:
93
+ """Format a workspace status report using the requested CORTEX-OUT profile."""
94
+ from pathlib import Path
95
+
96
+ root: Path = workspace_root
97
+
98
+ if profile == OUT_MIN:
99
+ return f"OUT-MIN workspace={root.name} projects=? cycles=? tasks=?"
100
+
101
+ if profile == OUT_WORK:
102
+ return (
103
+ f"OUT-WORK workspace={root.name} "
104
+ f"manifest={'yes' if (root / 'manifest.cortex').exists() else 'no'} "
105
+ f"projects={'yes' if (root / 'projects.cortex').exists() else 'no'}"
106
+ )
107
+
108
+ if profile == OUT_AUDIT:
109
+ lines = [f"OUT-AUDIT workspace={root.name}"]
110
+ for child in sorted(root.iterdir()):
111
+ if child.is_dir():
112
+ lines.append(f" dir={child.name}")
113
+ elif child.is_file():
114
+ lines.append(f" file={child.name}")
115
+ return "\n".join(lines)
116
+
117
+ if profile == OUT_FULL:
118
+ return (
119
+ f"Workspace at {root}\n\n"
120
+ f"This workspace is governed by the framework. The manifest "
121
+ f"and projects index live in this directory."
122
+ )
123
+
124
+ return CortexOUT.error(message="unknown profile").to_text()