agent-migration-toolkit 0.1.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.
@@ -0,0 +1,5 @@
1
+ """Shared hub toolkit for the agent-migration program."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("agent-migration-toolkit")
@@ -0,0 +1,122 @@
1
+ """Command line entry points: thin wrappers over the pure core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import math
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, NoReturn
10
+
11
+ import tomli_w
12
+
13
+ from agent_migration_toolkit.gap import compute_gaps
14
+ from agent_migration_toolkit.schema import ValidationError, load_table
15
+ from agent_migration_toolkit.sources import (
16
+ DEFAULT_TIMEOUT,
17
+ Fetcher,
18
+ load_all,
19
+ parse_sources,
20
+ urllib_fetcher,
21
+ )
22
+ from agent_migration_toolkit.timeline import build_timeline
23
+
24
+ USER_ERRORS = (ValidationError, OSError, UnicodeDecodeError)
25
+ EXIT_ERROR = 1
26
+ EXIT_PARTIAL = 2
27
+
28
+
29
+ class _Parser(argparse.ArgumentParser):
30
+ """argparse exits with 2 on bad arguments; use 1 so 2 only means a partial timeline."""
31
+
32
+ def error(self, message: str) -> NoReturn:
33
+ self.print_usage(sys.stderr)
34
+ self.exit(EXIT_ERROR, f"error: {message}\n")
35
+
36
+
37
+ def gap_main(argv: list[str] | None = None) -> int:
38
+ """gap-analysis-creator: process table -> gap table (R-GAP-004)."""
39
+ parser = _Parser(
40
+ prog="gap-analysis-creator", description="Create a gap table from a process table"
41
+ )
42
+ parser.add_argument("process_table", type=Path, help="path to process-table.toml")
43
+ _add_output(parser)
44
+ args = parser.parse_args(argv)
45
+ try:
46
+ rows = load_table(_read_text(args.process_table))
47
+ _write({"gap": compute_gaps(rows)}, args.output)
48
+ except USER_ERRORS as exc:
49
+ return _fail(exc)
50
+ return 0
51
+
52
+
53
+ def timeline_main(argv: list[str] | None = None, fetcher: Fetcher = urllib_fetcher) -> int:
54
+ """critical-path-timeline: source list -> program timeline (R-TIME-009)."""
55
+ parser = _Parser(
56
+ prog="critical-path-timeline",
57
+ description="Build the critical path timeline across department process tables",
58
+ )
59
+ parser.add_argument("sources", type=Path, help="path to the department source list (TOML)")
60
+ _add_output(parser)
61
+ parser.add_argument(
62
+ "--timeout",
63
+ type=_positive_seconds,
64
+ default=DEFAULT_TIMEOUT,
65
+ help=f"seconds per fetch (default: {DEFAULT_TIMEOUT})",
66
+ )
67
+ args = parser.parse_args(argv)
68
+ try:
69
+ sources = parse_sources(_read_text(args.sources))
70
+ loaded = load_all(
71
+ sources, fetcher=fetcher, base_dir=args.sources.parent, timeout=args.timeout
72
+ )
73
+ timeline = build_timeline(loaded)
74
+ if not timeline["department"]:
75
+ raise ValidationError(f"no department could be processed: {_reasons(timeline)}")
76
+ _write(timeline, args.output)
77
+ except USER_ERRORS as exc:
78
+ return _fail(exc)
79
+ for failure in timeline["failure"]:
80
+ sys.stderr.write(f"warning: {failure['department']}: {failure['reason']}\n")
81
+ return EXIT_PARTIAL if timeline["failure"] else 0
82
+
83
+
84
+ def _positive_seconds(text: str) -> float:
85
+ value = float(text)
86
+ if not math.isfinite(value) or value <= 0:
87
+ raise argparse.ArgumentTypeError(f"must be a positive number of seconds, got {text!r}")
88
+ return value
89
+
90
+
91
+ def _reasons(timeline: dict[str, Any]) -> str:
92
+ return "; ".join(f"{f['department']}: {f['reason']}" for f in timeline["failure"]) or "none"
93
+
94
+
95
+ def _read_text(path: Path) -> str:
96
+ try:
97
+ return path.read_text(encoding="utf-8")
98
+ except UnicodeDecodeError as exc:
99
+ raise ValidationError(f"{path}: not UTF-8 text") from exc
100
+
101
+
102
+ def _fail(exc: Exception) -> int:
103
+ """Print a one-line error to stderr and return exit code 1 (NFR-03)."""
104
+ message = " ".join(str(exc).split())
105
+ sys.stderr.write(f"error: {message}\n")
106
+ return EXIT_ERROR
107
+
108
+
109
+ def _add_output(parser: argparse.ArgumentParser) -> None:
110
+ parser.add_argument(
111
+ "-o", "--output", type=Path, default=None, help="output path (default: stdout)"
112
+ )
113
+
114
+
115
+ def _write(data: dict[str, Any], output: Path | None) -> None:
116
+ text = tomli_w.dumps(data)
117
+ if output is None:
118
+ sys.stdout.flush()
119
+ sys.stdout.buffer.write(text.encode("utf-8"))
120
+ sys.stdout.buffer.flush()
121
+ else:
122
+ output.write_text(text, encoding="utf-8")
@@ -0,0 +1,29 @@
1
+ """Gap analysis: sub-processes whose interface is not yet an OpenAPI contract (R-GAP)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ GAP_DESCRIPTIONS = {
8
+ "none": "no artifact defined yet - needs artifact definition first",
9
+ "manual": "manual handoff - needs schema and an OpenAPI endpoint",
10
+ "file": "file-based handoff - needs an OpenAPI wrap or bridge",
11
+ }
12
+ CARRIED_FIELDS = ("effort_days", "depends_on")
13
+
14
+
15
+ def compute_gaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
16
+ """Return one gap row per validated row whose interface_type is not openapi."""
17
+ return [_gap_row(row) for row in rows if row["interface_type"] != "openapi"]
18
+
19
+
20
+ def _gap_row(row: dict[str, Any]) -> dict[str, Any]:
21
+ gap = {
22
+ "department": row["department"],
23
+ "process": row["process"],
24
+ "sub_process": row["sub_process"],
25
+ "current_interface": row["interface_type"],
26
+ "gap": GAP_DESCRIPTIONS[row["interface_type"]],
27
+ }
28
+ gap.update({field: row[field] for field in CARRIED_FIELDS if field in row})
29
+ return gap
@@ -0,0 +1,115 @@
1
+ """Process-table data contract: parsing and validation (R-SCHEMA)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import tomllib
7
+ from typing import Any
8
+
9
+
10
+ class ValidationError(ValueError):
11
+ """Raised when a process table or source list breaks the contract."""
12
+
13
+
14
+ def parse_toml(text: str) -> dict[str, Any]:
15
+ """Parse TOML text, turning syntax errors into ValidationError."""
16
+ try:
17
+ return tomllib.loads(text)
18
+ except tomllib.TOMLDecodeError as exc:
19
+ raise ValidationError(f"not valid TOML: {exc}") from exc
20
+
21
+
22
+ def parse_table(text: str) -> list[dict[str, Any]]:
23
+ """Parse TOML text into the list of [[sub_process]] rows (R-SCHEMA-001)."""
24
+ rows = parse_toml(text).get("sub_process")
25
+ if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows):
26
+ raise ValidationError("process table must contain a [[sub_process]] array of tables")
27
+ return rows
28
+
29
+
30
+ REQUIRED_FIELDS = (
31
+ "department",
32
+ "process",
33
+ "sub_process",
34
+ "automation_grade",
35
+ "output_artifact",
36
+ "interface_type",
37
+ )
38
+
39
+ VALID_GRADES = ("full", "partial", "manual")
40
+ VALID_INTERFACE_TYPES = ("openapi", "file", "manual", "none")
41
+ ENUM_FIELDS = {"automation_grade": VALID_GRADES, "interface_type": VALID_INTERFACE_TYPES}
42
+
43
+
44
+ def _label(index: int, row: dict[str, Any]) -> str:
45
+ return f"row {index + 1} ({row.get('department', '?')}/{row.get('sub_process', '?')})"
46
+
47
+
48
+ def validate_table(rows: list[dict[str, Any]]) -> list[str]:
49
+ """Return every contract violation in the table, one message per problem."""
50
+ names_by_department: dict[str, set[str]] = {}
51
+ for row in rows:
52
+ names_by_department.setdefault(_text(row, "department"), set()).add(
53
+ _text(row, "sub_process")
54
+ )
55
+ errors: list[str] = []
56
+ seen: set[tuple[str, str]] = set()
57
+ for index, row in enumerate(rows):
58
+ key = (_text(row, "department"), _text(row, "sub_process"))
59
+ problems = ["duplicate department and sub_process"] if key in seen else []
60
+ seen.add(key)
61
+ problems += _row_problems(row, names_by_department[key[0]])
62
+ errors.extend(f"{_label(index, row)}: {problem}" for problem in problems)
63
+ return errors
64
+
65
+
66
+ def _text(row: dict[str, Any], field: str) -> str:
67
+ """Hashable text form of a field, used for lookups before types are checked."""
68
+ value = row.get(field)
69
+ return value if isinstance(value, str) else repr(value)
70
+
71
+
72
+ def _row_problems(row: dict[str, Any], known: set[str]) -> list[str]:
73
+ """Every problem of one row; checks run on whatever fields are present (F-11)."""
74
+ problems: list[str] = []
75
+ missing = [field for field in REQUIRED_FIELDS if field not in row]
76
+ if missing:
77
+ problems.append(f"missing fields {missing}")
78
+ not_text = [f for f in REQUIRED_FIELDS if f in row and not isinstance(row[f], str)]
79
+ if not_text:
80
+ problems.append(f"fields must be strings: {not_text}")
81
+ problems += [
82
+ f"invalid {field} {row[field]!r}"
83
+ for field, allowed in ENUM_FIELDS.items()
84
+ if isinstance(row.get(field), str) and row[field] not in allowed
85
+ ]
86
+ if "effort_days" in row and not _is_non_negative_number(row["effort_days"]):
87
+ problems.append(f"effort_days must be a finite non-negative number, got {row['effort_days']!r}")
88
+ if "depends_on" in row:
89
+ problems += _depends_on_problems(row["depends_on"], known)
90
+ return problems
91
+
92
+
93
+ def _depends_on_problems(depends_on: Any, known: set[str]) -> list[str]:
94
+ if not isinstance(depends_on, list) or not all(isinstance(n, str) for n in depends_on):
95
+ return [f"depends_on must be a list of sub_process names, got {depends_on!r}"]
96
+ return [
97
+ f"depends_on {name!r} is not a sub_process of the same department"
98
+ for name in depends_on
99
+ if name not in known
100
+ ]
101
+
102
+
103
+ def _is_non_negative_number(value: Any) -> bool:
104
+ if not isinstance(value, int | float) or isinstance(value, bool):
105
+ return False
106
+ return math.isfinite(value) and value >= 0
107
+
108
+
109
+ def load_table(text: str) -> list[dict[str, Any]]:
110
+ """Parse and validate a process table; raise ValidationError listing every problem."""
111
+ rows = parse_table(text)
112
+ errors = validate_table(rows)
113
+ if errors:
114
+ raise ValidationError("; ".join(errors))
115
+ return rows
@@ -0,0 +1,111 @@
1
+ """Department source list and fetching of process tables (R-SRC)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.request
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from urllib.error import URLError
11
+ from urllib.parse import urlsplit
12
+
13
+ from agent_migration_toolkit.schema import ValidationError, load_table, parse_toml
14
+
15
+ ALLOWED_HOST = "raw.githubusercontent.com"
16
+ DEFAULT_TIMEOUT = 10.0
17
+
18
+ Fetcher = Callable[[str, float], str]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Source:
23
+ department: str
24
+ url: str | None = None
25
+ path: str | None = None
26
+
27
+
28
+ def parse_sources(text: str) -> list[Source]:
29
+ """Parse the TOML source list into one Source per [[department]] entry (R-SRC-001)."""
30
+ entries = parse_toml(text).get("department")
31
+ if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
32
+ raise ValidationError("source list must contain a [[department]] array of tables")
33
+ return [_source(index, entry) for index, entry in enumerate(entries)]
34
+
35
+
36
+ def _source(index: int, entry: dict[str, Any]) -> Source:
37
+ name = entry.get("name")
38
+ if not isinstance(name, str) or not name:
39
+ raise ValidationError(f"source entry {index + 1}: missing name")
40
+ url, path = entry.get("url"), entry.get("path")
41
+ if (url is None) == (path is None):
42
+ raise ValidationError(f"source {name!r}: needs exactly one of url or path")
43
+ if url is not None and not _is_allowed_url(url):
44
+ raise ValidationError(f"source {name!r}: url must be https://{ALLOWED_HOST}/...")
45
+ return Source(name, url=url, path=path)
46
+
47
+
48
+ def _is_allowed_url(url: Any) -> bool:
49
+ if not isinstance(url, str):
50
+ return False
51
+ parts = urlsplit(url)
52
+ return parts.scheme == "https" and parts.hostname == ALLOWED_HOST
53
+
54
+
55
+ class _SameHostRedirect(urllib.request.HTTPRedirectHandler):
56
+ """Follow redirects only when they stay on the allowed https host (R-SRC-003)."""
57
+
58
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
59
+ if not _is_allowed_url(newurl):
60
+ raise URLError(f"refused redirect to {newurl}")
61
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
62
+
63
+
64
+ _OPENER = urllib.request.build_opener(_SameHostRedirect)
65
+
66
+
67
+ def urllib_fetcher(url: str, timeout: float) -> str:
68
+ """Fetch a URL as UTF-8 text. Callers pass only urls checked by _is_allowed_url."""
69
+ with _OPENER.open(url, timeout=timeout) as response: # nosec B310
70
+ return response.read().decode("utf-8")
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Loaded:
75
+ department: str
76
+ rows: list[dict[str, Any]] | None = None
77
+ error: str | None = None
78
+
79
+
80
+ def load_all(
81
+ sources: list[Source],
82
+ fetcher: Fetcher,
83
+ base_dir: Path,
84
+ timeout: float = DEFAULT_TIMEOUT,
85
+ ) -> list[Loaded]:
86
+ """Load and validate every department's process table."""
87
+ return [_load_one(source, fetcher, base_dir, timeout) for source in sources]
88
+
89
+
90
+ def _load_one(source: Source, fetcher: Fetcher, base_dir: Path, timeout: float) -> Loaded:
91
+ try:
92
+ rows = load_table(_read(source, fetcher, base_dir, timeout))
93
+ except (TimeoutError, URLError) as exc:
94
+ if isinstance(exc, TimeoutError) or isinstance(exc.reason, TimeoutError):
95
+ return Loaded(source.department, error=f"timeout after {timeout} s")
96
+ return Loaded(source.department, error=f"fetch failed: {exc.reason}")
97
+ except (OSError, UnicodeDecodeError, ValidationError) as exc:
98
+ return Loaded(source.department, error=str(exc))
99
+ foreign = sorted({row["department"] for row in rows} - {source.department})
100
+ if foreign:
101
+ return Loaded(
102
+ source.department,
103
+ error=f"table contains department(s) {foreign}, expected {source.department!r}",
104
+ )
105
+ return Loaded(source.department, rows=rows)
106
+
107
+
108
+ def _read(source: Source, fetcher: Fetcher, base_dir: Path, timeout: float) -> str:
109
+ if source.url is not None:
110
+ return fetcher(source.url, timeout)
111
+ return (base_dir / str(source.path)).read_text(encoding="utf-8")
@@ -0,0 +1,119 @@
1
+ """Critical path timeline per department and across the program (R-TIME)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from graphlib import CycleError, TopologicalSorter
6
+ from typing import Any
7
+
8
+ from agent_migration_toolkit.gap import compute_gaps
9
+ from agent_migration_toolkit.schema import ValidationError
10
+ from agent_migration_toolkit.sources import Loaded
11
+
12
+ GAP_FIELDS = ("process", "sub_process", "current_interface")
13
+ TOLERANCE = 1e-9
14
+ QUICK_RAMP_SHARE = 0.2
15
+
16
+
17
+ def find_cycle(rows: list[dict[str, Any]]) -> list[str] | None:
18
+ """Return the sub_process names of one depends_on cycle, or None (R-TIME-008)."""
19
+ graph = {row["sub_process"]: row.get("depends_on", []) for row in rows}
20
+ try:
21
+ TopologicalSorter(graph).prepare()
22
+ except CycleError as exc:
23
+ return exc.args[1]
24
+ return None
25
+
26
+
27
+ def schedule_department(department: str, rows: list[dict[str, Any]]) -> dict[str, Any]:
28
+ """Schedule the gap rows of one department's validated table."""
29
+ cycle = find_cycle(rows)
30
+ if cycle:
31
+ raise ValidationError(f"depends_on cycle: {' -> '.join(cycle)}")
32
+ gaps = compute_gaps(rows)
33
+ estimated = {gap["sub_process"]: gap for gap in gaps if "effort_days" in gap}
34
+ unestimated = [gap for gap in gaps if "effort_days" not in gap]
35
+ tasks, length = _schedule(estimated, {gap["sub_process"] for gap in unestimated})
36
+ critical = sorted((t for t in tasks if t["critical"]), key=lambda t: t["earliest_start"])
37
+ return {
38
+ "name": department,
39
+ "critical_path_days": length,
40
+ "critical_path": [t["sub_process"] for t in critical],
41
+ "task": tasks,
42
+ "unestimated": [_pick(gap) for gap in unestimated],
43
+ }
44
+
45
+
46
+ def _pick(gap: dict[str, Any]) -> dict[str, Any]:
47
+ return {key: gap[key] for key in GAP_FIELDS}
48
+
49
+
50
+ def _schedule(
51
+ gaps: dict[str, dict[str, Any]], unestimated: set[str]
52
+ ) -> tuple[list[dict[str, Any]], float]:
53
+ """Critical path method over tasks; predecessors that are not tasks are ignored."""
54
+ preds = {
55
+ name: [p for p in gap.get("depends_on", []) if p in gaps] for name, gap in gaps.items()
56
+ }
57
+ order = list(TopologicalSorter(preds).static_order())
58
+
59
+ start: dict[str, float] = {}
60
+ finish: dict[str, float] = {}
61
+ for name in order:
62
+ start[name] = max((finish[p] for p in preds[name]), default=0)
63
+ finish[name] = start[name] + gaps[name]["effort_days"]
64
+ length = max(finish.values(), default=0)
65
+
66
+ succs: dict[str, list[str]] = {name: [] for name in gaps}
67
+ for name, names in preds.items():
68
+ for pred in names:
69
+ succs[pred].append(name)
70
+ latest_start: dict[str, float] = {}
71
+ for name in reversed(order):
72
+ latest_finish = min((latest_start[s] for s in succs[name]), default=length)
73
+ latest_start[name] = latest_finish - gaps[name]["effort_days"]
74
+
75
+ tasks = []
76
+ for name, gap in gaps.items():
77
+ slack = latest_start[name] - start[name]
78
+ critical = abs(slack) < TOLERANCE
79
+ tasks.append(
80
+ {
81
+ **_pick(gap),
82
+ "effort_days": gap["effort_days"],
83
+ "earliest_start": start[name],
84
+ "earliest_finish": finish[name],
85
+ "slack_days": 0 if critical else slack,
86
+ "critical": critical,
87
+ "block": _block(start[name], length),
88
+ "waits_on_unestimated": [
89
+ p for p in gap.get("depends_on", []) if p in unestimated
90
+ ],
91
+ }
92
+ )
93
+ return tasks, length
94
+
95
+
96
+ def _block(earliest_start: float, length: float) -> str:
97
+ if length == 0 or earliest_start < QUICK_RAMP_SHARE * length - TOLERANCE:
98
+ return "quick_ramp"
99
+ return "hardening"
100
+
101
+
102
+ def build_timeline(loaded: list[Loaded]) -> dict[str, Any]:
103
+ """Schedule every loaded department and summarise the program (R-TIME-001/007)."""
104
+ departments: list[dict[str, Any]] = []
105
+ failures: list[dict[str, str]] = []
106
+ for item in loaded:
107
+ if item.rows is None:
108
+ failures.append({"department": item.department, "reason": str(item.error)})
109
+ continue
110
+ try:
111
+ departments.append(schedule_department(item.department, item.rows))
112
+ except ValidationError as exc:
113
+ failures.append({"department": item.department, "reason": str(exc)})
114
+ longest = max(departments, key=lambda d: d["critical_path_days"], default=None)
115
+ program = {
116
+ "length_days": longest["critical_path_days"] if longest else 0,
117
+ "critical_department": longest["name"] if longest else "",
118
+ }
119
+ return {"program": program, "department": departments, "failure": failures}
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-migration-toolkit
3
+ Version: 0.1.0
4
+ Summary: Shared hub toolkit for the agent-migration program: process tables, gap analysis, critical path timeline
5
+ Keywords: agent-migration,process-table,gap-analysis,critical-path,toml
6
+ Author: roebi
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Dist: tomli-w>=1.0
18
+ Requires-Python: >=3.11
19
+ Project-URL: Homepage, https://github.com/roebi/agent-migration-toolkit
20
+ Project-URL: Repository, https://github.com/roebi/agent-migration-toolkit
21
+ Project-URL: Changelog, https://github.com/roebi/agent-migration-toolkit/blob/main/CHANGELOG.md
22
+ Project-URL: Issues, https://github.com/roebi/agent-migration-toolkit/issues
23
+ Description-Content-Type: text/markdown
24
+
25
+ # agent-migration-toolkit
26
+
27
+ Shared hub toolkit for the agent-migration program. Departments describe
28
+ their processes in a TOML process table; the toolkit shows which
29
+ handoffs are not yet clean OpenAPI contracts (gaps) and builds a
30
+ critical path timeline across all departments.
31
+
32
+ - **`gap-analysis-creator`** - one department: process table -> gap table,
33
+ with the next step for each gap.
34
+ - **`critical-path-timeline`** - all departments: reads each department's
35
+ process table (local file or `raw.githubusercontent.com`) and writes
36
+ the critical path (red thread) per department, the quick ramp /
37
+ hardening split and the program length.
38
+ - **Schema and templates** - the process-table contract and the
39
+ collection meeting templates used in every department.
40
+
41
+ The toolkit only reads process tables and writes local TOML files; it
42
+ never changes anything in a department repo.
43
+
44
+ ## Installation
45
+
46
+ Python 3.11 or newer.
47
+
48
+ ```console
49
+ $ pip install agent-migration-toolkit
50
+ ```
51
+
52
+ or in a uv project:
53
+
54
+ ```console
55
+ $ uv add agent-migration-toolkit
56
+ ```
57
+
58
+ ## Quickstart
59
+
60
+ The example files are in
61
+ [`docs/examples/`](https://github.com/roebi/agent-migration-toolkit/tree/main/docs/examples).
62
+
63
+ Gap table for one department:
64
+
65
+ ```console
66
+ $ gap-analysis-creator docs/examples/finance-process-table.toml
67
+ ```
68
+
69
+ Timeline across departments, listed in a source list:
70
+
71
+ ```console
72
+ $ critical-path-timeline docs/examples/sources.toml -o timeline.toml
73
+ ```
74
+
75
+ A source list names each department and where its table lives:
76
+
77
+ ```toml
78
+ [[department]]
79
+ name = "Finance/Billing"
80
+ path = "finance-process-table.toml"
81
+
82
+ [[department]]
83
+ name = "Legal/Compliance"
84
+ url = "https://raw.githubusercontent.com/<owner>/<repo>/main/process-table.toml"
85
+ ```
86
+
87
+ Exit codes: 0 success, 1 invalid input, 2 timeline written but some
88
+ departments failed.
89
+
90
+ ## Documentation
91
+
92
+ - [Usage guide](https://github.com/roebi/agent-migration-toolkit/blob/main/docs/usage.md) -
93
+ process-table fields, source list, output format, exit codes
94
+ - [Process-table schema](https://github.com/roebi/agent-migration-toolkit/blob/main/schema/process-table.schema.toml) -
95
+ copy it as the start of a department's `process-table.toml`
96
+ - [Collection templates](https://github.com/roebi/agent-migration-toolkit/tree/main/templates)
97
+ - [CHANGELOG.md](https://github.com/roebi/agent-migration-toolkit/blob/main/CHANGELOG.md)
98
+
99
+ ## Development
100
+
101
+ ```console
102
+ $ uv sync
103
+ $ uv run pytest --cov=src --cov-report=term-missing
104
+ $ uv run ruff check src/ tests/
105
+ ```
106
+
107
+ ## License
108
+
109
+ [MIT](https://github.com/roebi/agent-migration-toolkit/blob/main/LICENSE)
@@ -0,0 +1,11 @@
1
+ agent_migration_toolkit/__init__.py,sha256=bZZM36LLVnEGkrQiM3kZppiHkSlhCvFNjwPclxx_3QQ,148
2
+ agent_migration_toolkit/cli.py,sha256=WaYA9q4rkIte13RB4T_RPeWwyvhv7rURSAx_E8ZlPNc,4060
3
+ agent_migration_toolkit/gap.py,sha256=OW175CtIiWvXMtMMFCcWipX9R_22wLX2EsAMgdMY-NI,1065
4
+ agent_migration_toolkit/schema.py,sha256=VcWTnjwDeC1KtKfOTVXJS4AaPA7n6xm9ojTL2DUVfTI,4281
5
+ agent_migration_toolkit/sources.py,sha256=wjf2xIZmxXDe5eu1Ya1HxtEMP64_wTv4WUeEbuIZL4o,4067
6
+ agent_migration_toolkit/timeline.py,sha256=PK3n2PzH-St2WUfJ2Y3CTmCtF3tHczNAd5MSWrXv9Y8,4627
7
+ agent_migration_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=de8OV1f9KfA6SMJEHwW2YCtIFT-JBfOYBASV_QUEWXs,1062
8
+ agent_migration_toolkit-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
9
+ agent_migration_toolkit-0.1.0.dist-info/entry_points.txt,sha256=4LWFFxY_1mFN_VkCV3I1IY1WCO9Tki-d52t3SATlBrY,146
10
+ agent_migration_toolkit-0.1.0.dist-info/METADATA,sha256=2-dlYP4cNChEenxxJwRdon3ZBdWxnFcDMSfGtIQOX1g,3643
11
+ agent_migration_toolkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ critical-path-timeline = agent_migration_toolkit.cli:timeline_main
3
+ gap-analysis-creator = agent_migration_toolkit.cli:gap_main
4
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 roebi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.