arqux 1.0.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.
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,102 @@
1
+ """CLI entry point.
2
+
3
+ Exposes three 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 --version
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from typing import Sequence
14
+
15
+ import click
16
+
17
+ from . import __version__
18
+ from .constants import PRODUCT_NAME, PRODUCT_NAME_TITLE
19
+
20
+
21
+ @click.group(
22
+ help=f"{PRODUCT_NAME_TITLE} — minimum-viable governance framework for AI agent teams."
23
+ )
24
+ @click.version_option(version=__version__, prog_name=PRODUCT_NAME)
25
+ def cli() -> None:
26
+ """Top-level command group."""
27
+
28
+
29
+ @cli.command(help="Start the MCP server on stdio.")
30
+ @click.option(
31
+ "--verbose",
32
+ is_flag=True,
33
+ default=False,
34
+ help="Log handler invocations to stderr (for debugging).",
35
+ )
36
+ def serve(verbose: bool) -> None:
37
+ """Start the MCP server."""
38
+ # Import lazily so `--version` and `--help` do not require MCP deps.
39
+ from .server import run_server
40
+
41
+ run_server(verbose=verbose)
42
+
43
+
44
+ @cli.command(help="Initialize .arqux/ in the current directory.")
45
+ @click.option(
46
+ "--workspace",
47
+ is_flag=True,
48
+ default=False,
49
+ help="Initialize as a workspace root (creates meta-brain + projects index).",
50
+ )
51
+ @click.option(
52
+ "--project",
53
+ "project_name",
54
+ default=None,
55
+ help="Register the current directory as a project with this name.",
56
+ )
57
+ def init(workspace: bool, project_name: str | None) -> None:
58
+ """Initialize governance in the current directory."""
59
+ from .handlers.workspace import init_workspace
60
+ from .handlers.project import init_project
61
+
62
+ if not workspace and project_name is None:
63
+ # Default: initialize as both workspace and a project named "default".
64
+ workspace = True
65
+ project_name = "default"
66
+
67
+ if workspace:
68
+ result = init_workspace(path=".", verbose=True)
69
+ click.echo(result.to_text())
70
+
71
+ if project_name is not None:
72
+ result = init_project(name=project_name, path=".", verbose=True)
73
+ click.echo(result.to_text())
74
+
75
+
76
+ @cli.command(help="Print workspace/project/cycle status.")
77
+ @click.option(
78
+ "--profile",
79
+ type=click.Choice(["OUT-MIN", "OUT-WORK", "OUT-AUDIT", "OUT-FULL"]),
80
+ default="OUT-WORK",
81
+ help="CORTEX-OUT profile to use for the output.",
82
+ )
83
+ def status(profile: str) -> None:
84
+ """Print current status."""
85
+ from .cortex_out import format_status
86
+ from .state import find_workspace_root
87
+
88
+ root = find_workspace_root()
89
+ if root is None:
90
+ click.echo(f"ERROR code=NOT_FOUND reason=no_workspace_init")
91
+ sys.exit(1)
92
+
93
+ click.echo(format_status(root, profile=profile))
94
+
95
+
96
+ def main(argv: Sequence[str] | None = None) -> None:
97
+ """Console-script entry point."""
98
+ cli(args=list(argv) if argv is not None else None)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
arqux/constants.py ADDED
@@ -0,0 +1,184 @@
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
+
69
+ # --- Brain sections (the project brain is the single shared mind) -----------
70
+ #
71
+ # The brain.cortex is the shared project mind. All handoffs, pulses,
72
+ # sessions, lessons, focus, and active context live HERE — not in separate
73
+ # files. This guarantees that every agent bound to the project shares the
74
+ # same mental state.
75
+
76
+ BRAIN_SECTION_FOCUS: str = "FOCUS"
77
+ BRAIN_SECTION_OBJECTIVES: str = "OBJECTIVES"
78
+ BRAIN_SECTION_SESSIONS: str = "SESSIONS"
79
+ BRAIN_SECTION_HANDOFFS: str = "HANDOFFS"
80
+ BRAIN_SECTION_PULSE: str = "PULSE"
81
+ BRAIN_SECTION_LESSONS: str = "LESSONS"
82
+ BRAIN_SECTION_ACTIVE_CONTEXT: str = "ACTIVE_CONTEXT"
83
+ BRAIN_SECTION_RISKS: str = "RISKS"
84
+ BRAIN_SECTION_CONCURRENCY: str = "CONCURRENCY"
85
+
86
+ #: All canonical brain sections (used for validation).
87
+ ALL_BRAIN_SECTIONS: tuple[str, ...] = (
88
+ BRAIN_SECTION_FOCUS,
89
+ BRAIN_SECTION_OBJECTIVES,
90
+ BRAIN_SECTION_SESSIONS,
91
+ BRAIN_SECTION_HANDOFFS,
92
+ BRAIN_SECTION_PULSE,
93
+ BRAIN_SECTION_LESSONS,
94
+ BRAIN_SECTION_ACTIVE_CONTEXT,
95
+ BRAIN_SECTION_RISKS,
96
+ BRAIN_SECTION_CONCURRENCY,
97
+ )
98
+
99
+ # --- Roles and permissions -------------------------------------------------
100
+
101
+ ROLE_GOVERNOR: str = "governor"
102
+ ROLE_EXECUTOR: str = "executor"
103
+ ROLE_AUDITOR: str = "auditor"
104
+
105
+ ALL_ROLES: tuple[str, ...] = (ROLE_GOVERNOR, ROLE_EXECUTOR, ROLE_AUDITOR)
106
+
107
+ # --- Task state machine ----------------------------------------------------
108
+
109
+ TASK_DRAFT: str = "draft"
110
+ TASK_OPEN: str = "open"
111
+ TASK_IN_PROGRESS: str = "in_progress"
112
+ TASK_REVIEW: str = "review"
113
+ TASK_DONE: str = "done"
114
+ TASK_BLOCKED: str = "blocked"
115
+ TASK_CANCELLED: str = "cancelled"
116
+
117
+ TASK_ACTIVE_STATES: tuple[str, ...] = (
118
+ TASK_DRAFT,
119
+ TASK_OPEN,
120
+ TASK_IN_PROGRESS,
121
+ TASK_REVIEW,
122
+ TASK_BLOCKED,
123
+ )
124
+
125
+ TASK_TERMINAL_STATES: tuple[str, ...] = (TASK_DONE, TASK_CANCELLED)
126
+
127
+ TASK_TRANSITIONS: dict[str, tuple[str, ...]] = {
128
+ TASK_DRAFT: (TASK_OPEN, TASK_CANCELLED),
129
+ TASK_OPEN: (TASK_IN_PROGRESS, TASK_CANCELLED),
130
+ TASK_IN_PROGRESS: (TASK_REVIEW, TASK_BLOCKED, TASK_DONE, TASK_CANCELLED),
131
+ TASK_REVIEW: (TASK_DONE, TASK_IN_PROGRESS, TASK_BLOCKED, TASK_CANCELLED),
132
+ TASK_BLOCKED: (TASK_IN_PROGRESS, TASK_CANCELLED),
133
+ TASK_DONE: (),
134
+ TASK_CANCELLED: (),
135
+ }
136
+
137
+ # --- Cycle state machine ---------------------------------------------------
138
+
139
+ CYCLE_OPEN: str = "open"
140
+ CYCLE_CLOSED: str = "closed"
141
+
142
+ CYCLE_TRANSITIONS: dict[str, tuple[str, ...]] = {
143
+ CYCLE_OPEN: (CYCLE_CLOSED,),
144
+ CYCLE_CLOSED: (),
145
+ }
146
+
147
+ # --- CORTEX-OUT profiles ---------------------------------------------------
148
+
149
+ OUT_MIN: str = "OUT-MIN"
150
+ OUT_WORK: str = "OUT-WORK"
151
+ OUT_AUDIT: str = "OUT-AUDIT"
152
+ OUT_FULL: str = "OUT-FULL"
153
+ OUT_ERROR: str = "OUT-ERROR"
154
+
155
+ ALL_OUT_PROFILES: tuple[str, ...] = (
156
+ OUT_MIN,
157
+ OUT_WORK,
158
+ OUT_AUDIT,
159
+ OUT_FULL,
160
+ OUT_ERROR,
161
+ )
162
+
163
+ DEFAULT_OUT_PROFILE: str = OUT_WORK
164
+
165
+ # --- Error codes -----------------------------------------------------------
166
+
167
+ PERMISSION_DENIED: str = "PERMISSION_DENIED"
168
+ NOT_FOUND: str = "NOT_FOUND"
169
+ INVALID_STATE: str = "INVALID_STATE"
170
+ INVALID_ARGUMENT: str = "INVALID_ARGUMENT"
171
+ ALREADY_EXISTS: str = "ALREADY_EXISTS"
172
+ INTERNAL_ERROR: str = "INTERNAL_ERROR"
173
+
174
+ # --- Paths -----------------------------------------------------------------
175
+
176
+ PACKAGE_ROOT: Path = Path(__file__).resolve().parent
177
+ TEMPLATES_DIR: Path = PACKAGE_ROOT / "templates"
178
+ IDENTITIES_DIR: Path = PACKAGE_ROOT / "identities"
179
+
180
+ # --- Environment overrides -------------------------------------------------
181
+
182
+ def env(var: str, default: str | None = None) -> str | None:
183
+ """Read an environment variable prefixed with the product name."""
184
+ 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()