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.
- arqux/__init__.py +28 -0
- arqux/__main__.py +6 -0
- arqux/cli.py +196 -0
- arqux/constants.py +185 -0
- arqux/cortex_out.py +124 -0
- arqux/formats.py +784 -0
- arqux/handlers/__init__.py +992 -0
- arqux/handlers/blueprint.py +1583 -0
- arqux/handlers/cortex.py +661 -0
- arqux/handlers/cycle.py +340 -0
- arqux/handlers/evidence.py +117 -0
- arqux/handlers/project.py +382 -0
- arqux/handlers/protocol.py +128 -0
- arqux/handlers/session.py +382 -0
- arqux/handlers/skill.py +513 -0
- arqux/handlers/task.py +382 -0
- arqux/handlers/workspace.py +178 -0
- arqux/identities/alfred.cortex +111 -0
- arqux/identities/auditor.cortex +47 -0
- arqux/identities/executor.cortex +49 -0
- arqux/identities/governor.cortex +53 -0
- arqux/identities/heimdall.cortex +77 -0
- arqux/identities/jarvis.cortex +82 -0
- arqux/identities/seshat.cortex +73 -0
- arqux/learning.py +408 -0
- arqux/permissions.py +151 -0
- arqux/plantuml.py +156 -0
- arqux/plantuml_server.py +243 -0
- arqux/pulse.py +178 -0
- arqux/server.py +153 -0
- arqux/sessions.py +124 -0
- arqux/skills/cortex.skill.md +252 -0
- arqux/skills/diagram.skill.md +138 -0
- arqux/skills/learning.skill.md +85 -0
- arqux/skills/mcp-handlers.skill.md +193 -0
- arqux/skills/protocol.skill.md +117 -0
- arqux/skills/workflows.skill.md +45 -0
- arqux/state.py +1045 -0
- arqux/sync.py +236 -0
- arqux/templates/AGENTS.md +112 -0
- arqux/templates/BLP_TEMPLATE.md +244 -0
- arqux/templates/CYCLE_MANIFEST_TEMPLATE.md +128 -0
- arqux/templates/brain.cortex +43 -0
- arqux/templates/brain.md +46 -0
- arqux/templates/learn-policies.cortex +36 -0
- arqux/templates/meta-brain.cortex +22 -0
- arqux/templates/meta-brain.md +24 -0
- arqux-0.3.0.dist-info/METADATA +209 -0
- arqux-0.3.0.dist-info/RECORD +53 -0
- arqux-0.3.0.dist-info/WHEEL +5 -0
- arqux-0.3.0.dist-info/entry_points.txt +2 -0
- arqux-0.3.0.dist-info/licenses/LICENSE +201 -0
- 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
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()
|