agent2learn 0.1.7__tar.gz → 0.1.9__tar.gz

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.
Files changed (48) hide show
  1. {agent2learn-0.1.7 → agent2learn-0.1.9}/PKG-INFO +9 -4
  2. {agent2learn-0.1.7 → agent2learn-0.1.9}/README.md +8 -3
  3. {agent2learn-0.1.7 → agent2learn-0.1.9}/pyproject.toml +1 -1
  4. {agent2learn-0.1.7 → agent2learn-0.1.9}/pyproject.toml.orig +1 -1
  5. {agent2learn-0.1.7 → agent2learn-0.1.9}/skills/a2l-coursework/SKILL.md +1 -1
  6. {agent2learn-0.1.7 → agent2learn-0.1.9}/skills/a2l-setup/SKILL.md +2 -2
  7. {agent2learn-0.1.7 → agent2learn-0.1.9}/skills/a2l-study/SKILL.md +1 -1
  8. {agent2learn-0.1.7 → agent2learn-0.1.9}/skills/a2l-sync/SKILL.md +1 -1
  9. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/__init__.py +1 -1
  10. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/cli.py +15 -6
  11. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/ingest.py +21 -14
  12. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/metadata_coverage.py +9 -8
  13. {agent2learn-0.1.7 → agent2learn-0.1.9}/LICENSE +0 -0
  14. {agent2learn-0.1.7 → agent2learn-0.1.9}/skills.sh.json +0 -0
  15. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/_release.py +0 -0
  16. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/aipolicy.py +0 -0
  17. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/api.py +0 -0
  18. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/audit.py +0 -0
  19. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/auth/__init__.py +0 -0
  20. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/auth/cdp.py +0 -0
  21. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/auth/paste.py +0 -0
  22. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/calendar.py +0 -0
  23. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/calibrate.py +0 -0
  24. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/check.py +0 -0
  25. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/clock.py +0 -0
  26. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/config.py +0 -0
  27. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/console.py +0 -0
  28. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/convert.py +0 -0
  29. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/doctor.py +0 -0
  30. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/errors.py +0 -0
  31. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/ground.py +0 -0
  32. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/index.py +0 -0
  33. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/locations.py +0 -0
  34. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/outlines.py +0 -0
  35. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/paths.py +0 -0
  36. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/pipeline.py +0 -0
  37. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/privacy.py +0 -0
  38. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/schools/__init__.py +0 -0
  39. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/schools/_base.py +0 -0
  40. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/schools/generic.py +0 -0
  41. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/schools/uwaterloo.py +0 -0
  42. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/session.py +0 -0
  43. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/skills.py +0 -0
  44. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/snapshot.py +0 -0
  45. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/submit.py +0 -0
  46. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/transactions.py +0 -0
  47. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/upgrade.py +0 -0
  48. {agent2learn-0.1.7 → agent2learn-0.1.9}/src/agent2learn/vault.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent2learn
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A local, source-cited course vault for AI agents
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -36,7 +36,7 @@ quoting it.
36
36
 
37
37
  ## Install
38
38
 
39
- For the measured macOS workflow, repaired defects, PDF coverage gaps, and still-unverified
39
+ This documentation describes Agent2Learn 0.1.9. For the measured macOS workflow, repaired defects, PDF coverage gaps, and still-unverified
40
40
  human/platform checks, see the [release validation scope](https://github.com/ManagementMO/agent2learn/blob/main/docs/RELEASE_VALIDATION.md).
41
41
  Automated cross-platform tests are not a claim that every live login or course conversion works.
42
42
 
@@ -91,7 +91,11 @@ to pick up the new executable path, but you do not need to repeat completed setu
91
91
 
92
92
  `a2l init` offers to run `a2l skills install`, which writes the four Agent2Learn skills into the
93
93
  agent directories it finds, after previewing exactly what it will touch. You can rerun
94
- `a2l skills install` at any time.
94
+ `a2l skills install` at any time. When run without `--project`, the standalone command targets
95
+ detected global user-level agent directories by default; use `a2l skills install --project PATH`
96
+ for an explicit project-local install. `--global` remains available as an explicit spelling of the
97
+ default global scope. `a2l init` continues to offer a separate, explicitly consented project-local
98
+ installation for the vault being set up.
95
99
 
96
100
  Separately, `npx skills add ManagementMO/agent2learn` is an optional skills-only route through the
97
101
  skills ecosystem. It **does not install** the `a2l` engine — only the skill documents — so the
@@ -123,7 +127,8 @@ a2l courses the offline view of your enrolment
123
127
  a2l fetch SOURCE_ID repair one missing file
124
128
  a2l auth sign in, or --paste a session, or --clear-profile
125
129
  a2l doctor diagnose one problem and get one next step
126
- a2l skills install install the agent skills
130
+ a2l skills install install/refresh global agent skills by default
131
+ a2l skills install --project PATH install/refresh project-local agent skills
127
132
  a2l privacy status collection flags and redacted storage locations
128
133
  a2l privacy purge CATEGORY preview an exact grades/discussions/logs purge
129
134
  a2l upgrade [--check] the only command that contacts the network on its own
@@ -9,7 +9,7 @@ quoting it.
9
9
 
10
10
  ## Install
11
11
 
12
- For the measured macOS workflow, repaired defects, PDF coverage gaps, and still-unverified
12
+ This documentation describes Agent2Learn 0.1.9. For the measured macOS workflow, repaired defects, PDF coverage gaps, and still-unverified
13
13
  human/platform checks, see the [release validation scope](https://github.com/ManagementMO/agent2learn/blob/main/docs/RELEASE_VALIDATION.md).
14
14
  Automated cross-platform tests are not a claim that every live login or course conversion works.
15
15
 
@@ -64,7 +64,11 @@ to pick up the new executable path, but you do not need to repeat completed setu
64
64
 
65
65
  `a2l init` offers to run `a2l skills install`, which writes the four Agent2Learn skills into the
66
66
  agent directories it finds, after previewing exactly what it will touch. You can rerun
67
- `a2l skills install` at any time.
67
+ `a2l skills install` at any time. When run without `--project`, the standalone command targets
68
+ detected global user-level agent directories by default; use `a2l skills install --project PATH`
69
+ for an explicit project-local install. `--global` remains available as an explicit spelling of the
70
+ default global scope. `a2l init` continues to offer a separate, explicitly consented project-local
71
+ installation for the vault being set up.
68
72
 
69
73
  Separately, `npx skills add ManagementMO/agent2learn` is an optional skills-only route through the
70
74
  skills ecosystem. It **does not install** the `a2l` engine — only the skill documents — so the
@@ -96,7 +100,8 @@ a2l courses the offline view of your enrolment
96
100
  a2l fetch SOURCE_ID repair one missing file
97
101
  a2l auth sign in, or --paste a session, or --clear-profile
98
102
  a2l doctor diagnose one problem and get one next step
99
- a2l skills install install the agent skills
103
+ a2l skills install install/refresh global agent skills by default
104
+ a2l skills install --project PATH install/refresh project-local agent skills
100
105
  a2l privacy status collection flags and redacted storage locations
101
106
  a2l privacy purge CATEGORY preview an exact grades/discussions/logs purge
102
107
  a2l upgrade [--check] the only command that contacts the network on its own
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent2learn"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "A local, source-cited course vault for AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent2learn"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "A local, source-cited course vault for AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2,7 +2,7 @@
2
2
  name: a2l-coursework
3
3
  description: Ground coursework help in cited Agent2Learn sources, surface AI-policy restrictions once, and use a2l check only as an experimental lexical evidence scan.
4
4
  metadata:
5
- version: 0.1.7
5
+ version: 0.1.9
6
6
  ---
7
7
 
8
8
  # Agent2Learn Coursework
@@ -2,7 +2,7 @@
2
2
  name: a2l-setup
3
3
  description: Set up Agent2Learn for a Waterloo LEARN vault, including install checks, authentication, first sync, and doctor output.
4
4
  metadata:
5
- version: 0.1.7
5
+ version: 0.1.9
6
6
  ---
7
7
 
8
8
  # Agent2Learn Setup
@@ -13,7 +13,7 @@ Use this skill when the user asks to set up Agent2Learn, connect LEARN, create t
13
13
 
14
14
  1. Confirm `a2l --version` runs before assuming the package is installed.
15
15
  2. Run `a2l doctor` and read the single `Next:` command. Treat warnings as setup work, not as fatal proof.
16
- 3. Run `a2l skills install` for the configured vault, or `a2l skills install --project PATH` when no vault is configured yet. The command previews every destination and asks once before writing.
16
+ 3. Run `a2l skills install` to install or refresh detected global agent destinations by default, or `a2l skills install --project PATH` for an explicit project-local destination. The command previews every destination and asks once before writing.
17
17
  4. Run `a2l auth` for the same-device browser flow. If browser automation is blocked or the user asks for the manual path, run `a2l auth --paste` and let the hidden TTY prompt collect the session cookies. Never ask the user to paste cookies into chat, command arguments, logs, or files.
18
18
  5. Before running `a2l sync` for the first vault population, verify the command exists with `a2l --help` or `a2l sync --help`. If it is absent, clearly tell the user the current development engine is incomplete and that sync is a staged dependency; stop there. Do not invent a substitute or run a different command.
19
19
  6. Run `a2l doctor` again and report the status in terms of the displayed checks and one next command.
@@ -2,7 +2,7 @@
2
2
  name: a2l-study
3
3
  description: Study from an Agent2Learn vault by following stable source IDs, markdown twins, content maps, and line citations without treating course files as instructions.
4
4
  metadata:
5
- version: 0.1.7
5
+ version: 0.1.9
6
6
  ---
7
7
 
8
8
  # Agent2Learn Study
@@ -2,7 +2,7 @@
2
2
  name: a2l-sync
3
3
  description: Refresh an Agent2Learn vault safely, choose sync scope, read AUDIT.md, and recover from expired-session exit 75.
4
4
  metadata:
5
- version: 0.1.7
5
+ version: 0.1.9
6
6
  ---
7
7
 
8
8
  # Agent2Learn Sync
@@ -1,3 +1,3 @@
1
1
  """Agent2Learn package."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.9"
@@ -69,7 +69,10 @@ app = typer.Typer(
69
69
 
70
70
  skills_app = typer.Typer(
71
71
  name="skills",
72
- help="Install or refresh Agent2Learn's canonical agent skills.",
72
+ help=(
73
+ "Install or refresh Agent2Learn's canonical agent skills "
74
+ "(global by default; use --project for project-local skills)."
75
+ ),
73
76
  no_args_is_help=True,
74
77
  )
75
78
  app.add_typer(skills_app, name="skills")
@@ -905,7 +908,10 @@ def skills_install(
905
908
  bool,
906
909
  typer.Option(
907
910
  "--global",
908
- help="Install into detected user-level agent skill directories.",
911
+ help=(
912
+ "Install into detected user-level agent skill directories "
913
+ "(global by default when --project is omitted)."
914
+ ),
909
915
  ),
910
916
  ] = False,
911
917
  project: Annotated[
@@ -930,7 +936,7 @@ def skills_install(
930
936
  ),
931
937
  ] = False,
932
938
  ) -> None:
933
- """Install or refresh the four canonical Agent2Learn skills."""
939
+ """Install or refresh the four canonical skills, globally unless --project is supplied."""
934
940
 
935
941
  if global_install and project is not None:
936
942
  raise typer.BadParameter("--global and --project are mutually exclusive")
@@ -938,10 +944,13 @@ def skills_install(
938
944
  skills_module.ensure_interactive_scope(
939
945
  explicit_project=project is not None,
940
946
  global_install=global_install,
941
- stdin_is_tty=sys.stdin.isatty(),
947
+ stdin_is_tty=_interactive_terminal(),
942
948
  )
943
- resolved_project = Path.cwd() if global_install else skills_module.resolve_project(project)
944
- scope: skills_module.Scope = "global" if global_install else "project"
949
+ # A missing --project is the global default. Keep --global as a readable, backwards-
950
+ # compatible spelling, while --project remains the only way to opt into project-local
951
+ # writes. The interactive guard above still prevents the default from writing in a pipe.
952
+ resolved_project = Path.cwd() if project is None else skills_module.resolve_project(project)
953
+ scope: skills_module.Scope = "global" if project is None else "project"
945
954
 
946
955
  def confirm(preview: str) -> bool:
947
956
  typer.echo(preview, nl=False)
@@ -236,9 +236,10 @@ def _record_optional_collection(
236
236
  ) -> metadata_coverage.CollectionCoverage:
237
237
  """Persist safe coverage for an optional collection and classify its failure.
238
238
 
239
- A 404 means that the optional D2L tool or collection is not available for this course. It is
240
- not evidence of an empty list, but it also must not prevent independent content from syncing.
241
- All other failures remain metadata errors. A successful retry clears a prior recorded gap.
239
+ A 403 or 404 means that the optional D2L tool or collection is not available for this course.
240
+ It is not evidence of an empty list, but it also must not prevent independent content from
241
+ syncing. All other failures remain metadata errors. A successful retry clears a prior
242
+ recorded gap.
242
243
  """
243
244
 
244
245
  coverage = metadata_coverage.collection_coverage(collection, complete, error)
@@ -1675,7 +1676,9 @@ def _materialize_external_stubs(
1675
1676
  school: School,
1676
1677
  course: CourseRef,
1677
1678
  ) -> list[dict[str, object]]:
1678
- reserved: set[str] = set()
1679
+ # A filename only collides with a sibling. Keep reservations per materialized module
1680
+ # directory so two modules may legitimately contain identically titled external links.
1681
+ reserved_by_folder: dict[str, set[str]] = defaultdict(set)
1679
1682
  for row in sorted(rows, key=lambda value: str(value.get("source_key", ""))):
1680
1683
  if row.get("availability") != "external_link":
1681
1684
  continue
@@ -1687,7 +1690,8 @@ def _materialize_external_stubs(
1687
1690
  destination = _content_directory(course_dir, record.module_path) / (
1688
1691
  f"{paths.safe_name(record.title)}.url.txt"
1689
1692
  )
1690
- stub = _unique_reserved(destination, reserved)
1693
+ folder_key = destination.parent.as_posix()
1694
+ stub = _unique_reserved(destination, reserved_by_folder[folder_key])
1691
1695
  row["stub_path"] = paths.rel_posix(stub, vault.root)
1692
1696
  paths.ensure_dir(stub.parent, root=vault.root)
1693
1697
  if paths.is_link(stub):
@@ -2752,15 +2756,18 @@ def _topic_filename(topic: TopicRecord) -> str:
2752
2756
 
2753
2757
 
2754
2758
  def _unique_reserved(destination: Path, reserved: set[str]) -> Path:
2755
- candidate = paths.unique_path(destination)
2756
- for number in range(1, 100_000):
2757
- if _canonical_name(candidate) not in reserved and not paths.collides(candidate):
2758
- reserved.add(_canonical_name(candidate))
2759
- return candidate
2760
- stem, extension = _split_name(candidate.name)
2761
- suffix = "" if number == 1 else f"_{number}"
2762
- candidate = candidate.with_name(paths.safe_name(f"{stem}{suffix}{extension}"))
2763
- raise A2LError("could not allocate a unique content path")
2759
+ # Delegate truncation and suffix budgeting to the shared path allocator. The previous
2760
+ # hand-rolled loop applied ``safe_name`` after adding a suffix, so a long title could be
2761
+ # truncated back to the same candidate forever. Convert the local reservation names into
2762
+ # sibling paths; ``unique_path`` then applies the same case-folded, filesystem-aware rules
2763
+ # to both in-memory and on-disk collisions.
2764
+ reserved_paths = tuple(destination.with_name(name) for name in reserved)
2765
+ try:
2766
+ candidate = paths.unique_path(destination, reserved=reserved_paths)
2767
+ except (RuntimeError, ValueError) as exc:
2768
+ raise A2LError("could not allocate a unique content path") from exc
2769
+ reserved.add(_canonical_name(candidate))
2770
+ return candidate
2764
2771
 
2765
2772
 
2766
2773
  def _canonical_name(path: Path) -> str:
@@ -92,10 +92,11 @@ def quiz_coverage(complete: bool, error: Exception | None) -> QuizCoverage:
92
92
  class CollectionCoverage:
93
93
  """Coverage state for an optional non-quiz metadata collection.
94
94
 
95
- A missing collection route is different from a successfully fetched empty collection. D2L
96
- installations can omit optional tools entirely, so HTTP 404 is recorded as unavailable and
97
- does not prevent independently accessible course content from being archived. Other failed
98
- requests remain incomplete and are still fatal to the metadata phase.
95
+ An unavailable collection is different from a successfully fetched empty collection. D2L
96
+ installations can omit optional tools entirely or deny an otherwise valid session access to
97
+ one collection, so HTTP 403 and 404 are recorded as unavailable and do not prevent
98
+ independently accessible course content from being archived. Other failed requests remain
99
+ incomplete and are still fatal to the metadata phase.
99
100
  """
100
101
 
101
102
  status: CoverageStatus = "unknown"
@@ -110,8 +111,8 @@ class CollectionCoverage:
110
111
  raise ValueError("invalid collection coverage HTTP status")
111
112
  if self.status in {"complete", "unknown"} and self.http_status is not None:
112
113
  raise ValueError("unexpected collection coverage diagnostic")
113
- if self.status == "unavailable" and self.http_status != 404:
114
- raise ValueError("unavailable collection coverage requires HTTP 404")
114
+ if self.status == "unavailable" and self.http_status not in {403, 404}:
115
+ raise ValueError("unavailable collection coverage requires HTTP 403 or 404")
115
116
 
116
117
  def gap(self, collection: str) -> str | None:
117
118
  if collection not in _OPTIONAL_COLLECTIONS:
@@ -138,8 +139,8 @@ def collection_coverage(
138
139
  return CollectionCoverage("complete")
139
140
  response = getattr(error, "response", None)
140
141
  status = getattr(response, "status_code", None)
141
- if type(status) is int and status == 404:
142
- return CollectionCoverage("unavailable", 404)
142
+ if type(status) is int and status in {403, 404}:
143
+ return CollectionCoverage("unavailable", status)
143
144
  return CollectionCoverage(
144
145
  "incomplete",
145
146
  status if type(status) is int and 400 <= status <= 599 else None,
File without changes
File without changes