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 +28 -0
- arqux/__main__.py +6 -0
- arqux/cli.py +102 -0
- arqux/constants.py +184 -0
- arqux/cortex_out.py +124 -0
- arqux/handlers/__init__.py +336 -0
- arqux/handlers/cycle.py +147 -0
- arqux/handlers/evidence.py +118 -0
- arqux/handlers/project.py +178 -0
- arqux/handlers/protocol.py +127 -0
- arqux/handlers/task.py +382 -0
- arqux/handlers/workspace.py +133 -0
- arqux/identities/auditor.cortex +41 -0
- arqux/identities/auditor.md +19 -0
- arqux/identities/executor.cortex +49 -0
- arqux/identities/executor.md +20 -0
- arqux/identities/governor.cortex +39 -0
- arqux/identities/governor.md +21 -0
- arqux/permissions.py +161 -0
- arqux/server.py +133 -0
- arqux/state.py +672 -0
- arqux/templates/brain.cortex +43 -0
- arqux/templates/brain.md +46 -0
- arqux/templates/meta-brain.cortex +22 -0
- arqux/templates/meta-brain.md +24 -0
- arqux/templates/task.cortex +31 -0
- arqux/templates/task.md +30 -0
- arqux-1.0.0.dist-info/METADATA +235 -0
- arqux-1.0.0.dist-info/RECORD +33 -0
- arqux-1.0.0.dist-info/WHEEL +5 -0
- arqux-1.0.0.dist-info/entry_points.txt +2 -0
- arqux-1.0.0.dist-info/licenses/LICENSE +201 -0
- arqux-1.0.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,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()
|