sia-package 0.2.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.
sia/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """SIA's host-neutral orchestration runtime."""
2
+
3
+ __version__ = "0.2.0"
sia/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
sia/adapters.py ADDED
@@ -0,0 +1,136 @@
1
+ """Opt-in host adapter templates.
2
+
3
+ Adapters only expose SIA's shared state packet. They intentionally contain no
4
+ host-specific copy of the workflow and never modify a project's root agent
5
+ instructions.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ MARKER = "<!-- managed-by-sia-adapter-v1 -->"
12
+
13
+ ADAPTERS: dict[str, tuple[Path, str]] = {
14
+ "claude": (
15
+ Path(".claude/skills/sia/SKILL.md"),
16
+ """---
17
+ name: sia
18
+ description: Continue the SIA workflow when the user explicitly invokes /sia.
19
+ disable-model-invocation: true
20
+ ---
21
+
22
+ # SIA
23
+
24
+ <!-- managed-by-sia-adapter-v1 -->
25
+ Run `sia next --json` in the project root. Read `sia/AGENT.md` when SIA is
26
+ vendored, otherwise use the installed package's workflow contract. Follow the
27
+ returned stage and mode. Keep every task's native subagent run ID in SIA with
28
+ `sia task dispatch`; do not implement a dispatched task in the controller.
29
+ Other skills and plugins remain available and keep ownership of their files.
30
+ """,
31
+ ),
32
+ "codex": (
33
+ Path(".agents/skills/sia/SKILL.md"),
34
+ """---
35
+ name: sia
36
+ description: Continue the SIA workflow only when the user explicitly asks for SIA or invokes this skill.
37
+ ---
38
+
39
+ # SIA
40
+
41
+ <!-- managed-by-sia-adapter-v1 -->
42
+ Run `sia next --json` in the project root and follow the returned stage and
43
+ mode. Read `sia/AGENT.md` if it is vendored. Use Codex native subagents for
44
+ independent implementation/review work and record their run IDs with
45
+ `sia task dispatch`. Do not replace `AGENTS.md` or interfere with other skills.
46
+ """,
47
+ ),
48
+ "kiro": (
49
+ Path(".kiro/steering/sia.md"),
50
+ """---
51
+ inclusion: manual
52
+ ---
53
+
54
+ # SIA
55
+
56
+ <!-- managed-by-sia-adapter-v1 -->
57
+ Run `sia next --json` in the project root and follow the returned stage and
58
+ mode. Read `sia/AGENT.md` if it is vendored. In orchestrator mode, delegate
59
+ prepared tasks to Kiro subagents and persist each returned run/agent ID with
60
+ `sia task dispatch`. Keep BMAD, Superpowers, steering, skills, and hooks active;
61
+ SIA owns only `.sia/` and explicitly generated SIA artifacts.
62
+ """,
63
+ ),
64
+ "antigravity": (
65
+ Path(".agents/workflows/sia.md"),
66
+ """---
67
+ description: Continue the persistent SIA workflow for this project
68
+ ---
69
+
70
+ # SIA workflow
71
+
72
+ <!-- managed-by-sia-adapter-v1 -->
73
+ When the user invokes `/sia`, run `sia next --json` in the project root and
74
+ follow the returned stage and mode. Read `sia/AGENT.md` if vendored. Use
75
+ Antigravity's native agents for prepared implementation and review tasks, then
76
+ record their run IDs with `sia task dispatch`. Do not alter other `.agents/`
77
+ workflows, skills, or agent definitions.
78
+ """,
79
+ ),
80
+ }
81
+
82
+ ALIASES = {"claude-code": "claude", "openai-codex": "codex", "google-antigravity": "antigravity"}
83
+
84
+
85
+ def normalize_host(host: str) -> str:
86
+ value = ALIASES.get(host.lower(), host.lower())
87
+ if value not in ADAPTERS:
88
+ raise ValueError(f"unsupported host {host!r}; choose: {', '.join(ADAPTERS)}")
89
+ return value
90
+
91
+
92
+ def _safe_target(root: Path, relative: Path, create_parent: bool) -> Path:
93
+ resolved_root = root.resolve()
94
+ candidate = root / relative
95
+ current = resolved_root
96
+ for part in relative.parent.parts:
97
+ current = current / part
98
+ if current.exists():
99
+ try:
100
+ current.resolve().relative_to(resolved_root)
101
+ except ValueError as exc:
102
+ raise PermissionError(f"adapter path escapes project through redirected directory: {current}") from exc
103
+ if create_parent:
104
+ candidate.parent.mkdir(parents=True, exist_ok=True)
105
+ if candidate.parent.exists():
106
+ try:
107
+ candidate.parent.resolve().relative_to(resolved_root)
108
+ candidate.resolve(strict=False).relative_to(resolved_root)
109
+ except ValueError as exc:
110
+ raise PermissionError(f"adapter target escapes project root: {candidate}") from exc
111
+ return candidate
112
+
113
+
114
+ def install_adapter(root: Path, host: str) -> tuple[str, Path]:
115
+ host = normalize_host(host)
116
+ relative, content = ADAPTERS[host]
117
+ target = _safe_target(root, relative, create_parent=True)
118
+ if target.exists():
119
+ existing = target.read_text(encoding="utf-8")
120
+ if existing == content:
121
+ return "unchanged", target
122
+ raise FileExistsError(f"refusing to overwrite existing adapter: {target}")
123
+ target.write_text(content, encoding="utf-8")
124
+ return "installed", target
125
+
126
+
127
+ def remove_adapter(root: Path, host: str) -> tuple[str, Path]:
128
+ host = normalize_host(host)
129
+ relative, content = ADAPTERS[host]
130
+ target = _safe_target(root, relative, create_parent=False)
131
+ if not target.exists():
132
+ return "absent", target
133
+ if target.read_text(encoding="utf-8") != content:
134
+ raise PermissionError(f"refusing to remove modified or unmanaged file: {target}")
135
+ target.unlink()
136
+ return "removed", target
sia/cli.py ADDED
@@ -0,0 +1,166 @@
1
+ """Command-line entry point for SIA."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from . import __version__
11
+ from .adapters import ADAPTERS, install_adapter, remove_adapter
12
+ from .core import MODES, Project, SiaError
13
+ from .policy import render_policy
14
+
15
+
16
+ def emit(value: Any, as_json: bool = False) -> None:
17
+ if as_json or isinstance(value, (dict, list)):
18
+ print(json.dumps(value, indent=2, sort_keys=True))
19
+ else:
20
+ text = str(value)
21
+ encoding = sys.stdout.encoding or "utf-8"
22
+ print(text.encode(encoding, errors="replace").decode(encoding))
23
+
24
+
25
+ def parser() -> argparse.ArgumentParser:
26
+ root = argparse.ArgumentParser(prog="sia", description="Persistent, host-neutral SIA orchestration")
27
+ root.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
28
+ root.add_argument("--root", default=".", help="project root (default: current directory)")
29
+ root.add_argument("--json", action="store_true", help="emit machine-readable JSON")
30
+ commands = root.add_subparsers(dest="command", required=True)
31
+
32
+ init = commands.add_parser("init", help="initialize durable SIA state")
33
+ init.add_argument("--mode", choices=MODES, default="orchestrator")
34
+ init.add_argument("--force", action="store_true", help="reset workflow state but keep existing evidence files")
35
+
36
+ commands.add_parser("status", help="show persisted workflow position")
37
+ next_command = commands.add_parser("next", help="emit the next host-neutral action packet")
38
+ next_command.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help="emit machine-readable JSON")
39
+ commands.add_parser("guide", help="print the installed workflow contract")
40
+ commands.add_parser("doctor", help="diagnose package and project integration")
41
+
42
+ owner = commands.add_parser("owner", help="assign a workflow stage to SIA or another framework")
43
+ owner.add_argument("--stage", required=True)
44
+ owner.add_argument("--to", required=True, dest="stage_owner")
45
+
46
+ advance = commands.add_parser("advance", help="complete the current stage after validating evidence")
47
+ advance.add_argument("--evidence", action="append", default=[], help="project-local artifact path; repeatable")
48
+
49
+ record = commands.add_parser("record", help="record a PASS or DEVIATION proposal outcome")
50
+ record.add_argument("--outcome", required=True, choices=("pass", "deviation"))
51
+ _event_arguments(record, deviation=False)
52
+
53
+ capture = commands.add_parser("capture", help="capture a DEVIATION using the feedback interface")
54
+ _event_arguments(capture, deviation=True)
55
+
56
+ commands.add_parser("convergence", help="compute persisted deviation rates")
57
+
58
+ preflight = commands.add_parser("preflight", help="load applicable active rules before work")
59
+ preflight.add_argument("--scope", action="append", required=True, help="file or glob; repeatable")
60
+ preflight.add_argument("--acknowledge", action="append", default=[], help="approved medium/high rule ID")
61
+
62
+ rule = commands.add_parser("rule", help="manage provenance-bearing feedback rules").add_subparsers(dest="rule_command", required=True)
63
+ add_rule = rule.add_parser("add", help="add an approved active rule")
64
+ add_rule.add_argument("--text", required=True)
65
+ add_rule.add_argument("--scope", required=True)
66
+ add_rule.add_argument("--severity", required=True, choices=("low", "medium", "high"))
67
+ add_rule.add_argument("--error-class", required=True)
68
+ add_rule.add_argument("--source-event", required=True)
69
+ retire = rule.add_parser("retire", help="retire, but do not erase, a rule")
70
+ retire.add_argument("--id", required=True)
71
+ retire.add_argument("--reason", required=True)
72
+
73
+ adapter = commands.add_parser("adapter", help="manage explicit host launchers").add_subparsers(dest="adapter_command", required=True)
74
+ for action in ("install", "remove"):
75
+ item = adapter.add_parser(action)
76
+ item.add_argument("--host", required=True, choices=tuple(ADAPTERS))
77
+
78
+ task = commands.add_parser("task", help="record native subagent execution evidence").add_subparsers(dest="task_command", required=True)
79
+ prepare = task.add_parser("prepare")
80
+ prepare.add_argument("--task", required=True)
81
+ prepare.add_argument("--brief", required=True, type=Path)
82
+ prepare.add_argument("--files", action="append", required=True)
83
+ dispatch = task.add_parser("dispatch")
84
+ dispatch.add_argument("--task", required=True)
85
+ dispatch.add_argument("--host", required=True)
86
+ dispatch.add_argument("--agent-id", required=True)
87
+ dispatch.add_argument("--native-run-id", required=True)
88
+ finish = task.add_parser("finish")
89
+ finish.add_argument("--task", required=True)
90
+ finish.add_argument("--report", required=True, type=Path)
91
+ finish.add_argument("--review", required=True, type=Path)
92
+ finish.add_argument("--reviewer-agent-id", required=True)
93
+ finish.add_argument("--review-native-run-id", required=True)
94
+
95
+ integration = commands.add_parser("integration", help="record final combined validation and review")
96
+ integration.add_argument("--evidence", required=True, type=Path)
97
+ return root
98
+
99
+
100
+ def _event_arguments(command: argparse.ArgumentParser, deviation: bool) -> None:
101
+ command.add_argument("--signal", required=True)
102
+ command.add_argument("--context", required=True)
103
+ command.add_argument("--severity", required=True, choices=("low", "medium", "high"))
104
+ command.add_argument("--error-class", required=deviation)
105
+
106
+
107
+ def main(argv: list[str] | None = None) -> int:
108
+ args = parser().parse_args(argv)
109
+ project = Project(args.root)
110
+ try:
111
+ result: Any
112
+ if args.command == "init":
113
+ result = project.initialize(args.mode, args.force)
114
+ elif args.command == "status":
115
+ result = project.status()
116
+ elif args.command == "next":
117
+ result = project.next_packet()
118
+ elif args.command == "guide":
119
+ emit(render_policy(), False)
120
+ return 0
121
+ elif args.command == "doctor":
122
+ result = project.doctor()
123
+ emit(result, args.json)
124
+ return 0 if result["ok"] else 1
125
+ elif args.command == "advance":
126
+ result = project.advance(args.evidence)
127
+ elif args.command == "owner":
128
+ result = project.set_stage_owner(args.stage, args.stage_owner)
129
+ elif args.command in {"record", "capture"}:
130
+ outcome = args.outcome if args.command == "record" else "deviation"
131
+ result = project.record_event(outcome, args.signal, args.context, args.severity, args.error_class)
132
+ elif args.command == "convergence":
133
+ result = project.convergence()
134
+ elif args.command == "preflight":
135
+ result, clear = project.preflight(args.scope, set(args.acknowledge))
136
+ emit(result, args.json)
137
+ return 0 if clear else 2
138
+ elif args.command == "rule":
139
+ if args.rule_command == "add":
140
+ result = project.add_rule(args.text, args.scope, args.severity, args.error_class, args.source_event)
141
+ else:
142
+ result = project.retire_rule(args.id, args.reason)
143
+ elif args.command == "adapter":
144
+ action = install_adapter if args.adapter_command == "install" else remove_adapter
145
+ status, path = action(project.root, args.host)
146
+ result = {"status": status, "host": args.host, "path": str(path.relative_to(project.root))}
147
+ elif args.command == "task":
148
+ if args.task_command == "prepare":
149
+ result = project.prepare_task(args.task, args.brief, args.files)
150
+ elif args.task_command == "dispatch":
151
+ result = project.dispatch_task(args.task, args.host, args.agent_id, args.native_run_id)
152
+ else:
153
+ result = project.finish_task(args.task, args.report, args.review, args.reviewer_agent_id, args.review_native_run_id)
154
+ elif args.command == "integration":
155
+ result = project.record_integration(args.evidence)
156
+ else: # pragma: no cover - argparse prevents this
157
+ raise SiaError(f"unknown command: {args.command}")
158
+ emit(result, args.json)
159
+ return 0
160
+ except (SiaError, FileExistsError, PermissionError, ValueError) as exc:
161
+ print(f"sia: {exc}", file=sys.stderr)
162
+ return 2
163
+
164
+
165
+ if __name__ == "__main__":
166
+ raise SystemExit(main())