agent-apprenticeship 0.1.5 → 0.1.7

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 (34) hide show
  1. package/README.md +0 -197
  2. package/bin/agent-apprenticeship.js +85 -37
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/src/agent_apprenticeship_trace/__init__.py +1 -1
  6. package/src/agent_apprenticeship_trace/actual_outputs_normalizer.py +104 -28
  7. package/src/agent_apprenticeship_trace/apprentice_adapters.py +3 -1
  8. package/src/agent_apprenticeship_trace/artifact_resolver.py +6 -0
  9. package/src/agent_apprenticeship_trace/bundle_exporter.py +132 -2
  10. package/src/agent_apprenticeship_trace/cli.py +1229 -269
  11. package/src/agent_apprenticeship_trace/codex_runner.py +89 -6
  12. package/src/agent_apprenticeship_trace/config.py +251 -32
  13. package/src/agent_apprenticeship_trace/ecosystem_r2.py +515 -0
  14. package/src/agent_apprenticeship_trace/env.py +5 -5
  15. package/src/agent_apprenticeship_trace/evaluator.py +127 -11
  16. package/src/agent_apprenticeship_trace/grader.py +12 -11
  17. package/src/agent_apprenticeship_trace/learning.py +270 -1
  18. package/src/agent_apprenticeship_trace/llm_output_normalizer.py +8 -7
  19. package/src/agent_apprenticeship_trace/loop.py +103 -4
  20. package/src/agent_apprenticeship_trace/loop_review.py +686 -0
  21. package/src/agent_apprenticeship_trace/mentor_checkpoints.py +49 -21
  22. package/src/agent_apprenticeship_trace/openai_structured.py +84 -14
  23. package/src/agent_apprenticeship_trace/package_exporter.py +1 -1
  24. package/src/agent_apprenticeship_trace/progress.py +2 -1
  25. package/src/agent_apprenticeship_trace/public_run.py +369 -57
  26. package/src/agent_apprenticeship_trace/public_sanitizer.py +40 -3
  27. package/src/agent_apprenticeship_trace/rubric_generation.py +11 -10
  28. package/src/agent_apprenticeship_trace/schemas.py +378 -3
  29. package/src/agent_apprenticeship_trace/task_intake.py +43 -9
  30. package/src/agent_apprenticeship_trace/tdo.py +2304 -0
  31. package/src/agent_apprenticeship_trace/trace_normalizer.py +351 -17
  32. package/src/agent_apprenticeship_trace/trace_prompt.py +38 -11
  33. package/src/agent_apprenticeship_trace/training_signals.py +1 -1
  34. package/src/agent_apprenticeship_trace/verifier.py +12 -6
@@ -24,12 +24,21 @@ from .bundle_exporter import create_contribution_bundle
24
24
  from .release_exporter import create_release as create_release_impl
25
25
  from .validation import validate_release as validate_release_impl, format_counters
26
26
  from .config import (
27
+ APPRENTICESHIP_MODES,
27
28
  DATA_SHARING_LEVELS,
29
+ DEFAULT_ECOSYSTEM_STORAGE_BACKEND,
28
30
  DEFAULT_PUBLIC_ECOSYSTEM_REPO,
31
+ DEFAULT_R2_BUCKET,
32
+ DEFAULT_R2_CONTRIBUTION_PREFIX,
33
+ DEFAULT_R2_INDEX_PREFIX,
34
+ DEFAULT_R2_SEED_PREFIX,
29
35
  ECOSYSTEM_AUTO_SHARE_MODES,
30
36
  EVALUATION_MODES,
31
37
  MENTOR_MODES,
32
38
  SENSITIVE_INFO_MASKING_LEVELS,
39
+ TRAINING_CONTRIBUTION_MODES,
40
+ apprenticeship_mode_display,
41
+ apprenticeship_mode_to_mentor_mode,
33
42
  ecosystem_auto_share_display,
34
43
  get_settings,
35
44
  init_settings,
@@ -38,16 +47,36 @@ from .config import (
38
47
  apprentice_agent_readiness_status,
39
48
  mentor_model_provider_readiness,
40
49
  mentor_mode_display,
50
+ mentor_mode_to_evaluation_mode,
51
+ normalize_apprenticeship_mode,
41
52
  normalize_mentor_mode,
42
53
  normalize_ecosystem_auto_share,
54
+ normalize_ecosystem_storage,
43
55
  normalize_sensitive_info_masking,
56
+ normalize_training_contribution_mode,
57
+ training_contribution_mode_display,
44
58
  debug_settings,
45
59
  public_settings,
46
60
  public_settings_text,
47
61
  update_settings,
48
62
  )
63
+ from .ecosystem_r2 import (
64
+ append_successful_share,
65
+ download_r2_package,
66
+ ecosystem_item_from_bundle,
67
+ fetch_r2_index,
68
+ fetch_r2_item,
69
+ get_or_create_ecosystem_identity,
70
+ r2_api_url,
71
+ r2_status,
72
+ shared_history,
73
+ storage_ref_for_item,
74
+ upload_contribution_to_worker,
75
+ write_storage_ref,
76
+ )
49
77
  from .openai_structured import run_llm_smoke, format_smoke_counters
50
78
  from .env import contains_secret, redact_secrets
79
+ from .public_sanitizer import sanitize_public_obj, sanitize_public_text
51
80
  from .command_discovery import AGENT_COMMAND_CANDIDATES, resolve_command, resolve_agent_command
52
81
  from .integration_status import integrations_report
53
82
  from .certification import certification_exit_code, run_certification_matrix
@@ -55,6 +84,9 @@ from .learning import (
55
84
  active_packs,
56
85
  compare_replay,
57
86
  compile_experience_pack,
87
+ install_runtime_training_from_bundle,
88
+ installed_skill_run_refs,
89
+ list_installed_skills,
58
90
  list_packs,
59
91
  load_pack,
60
92
  pack_run_refs,
@@ -63,7 +95,10 @@ from .learning import (
63
95
  resolve_learning_source,
64
96
  resolve_packs_for_run,
65
97
  search_learning_sources,
98
+ select_runtime_training_for_run,
99
+ set_installed_skill_enabled,
66
100
  slugify,
101
+ uninstall_installed_skill,
67
102
  update_pack_status,
68
103
  write_before_after_result,
69
104
  )
@@ -72,11 +107,13 @@ from .progress import append_progress_event, format_progress_event, format_run_s
72
107
  from .recipes import MODEL_PROVIDER_RECIPES, WORKER_AGENT_RECIPES
73
108
  app=typer.Typer(add_completion=False)
74
109
  configure_app=typer.Typer(add_completion=False, help='Configure Apprentice Agents and Mentor Model Providers.')
75
- ecosystem_app=typer.Typer(add_completion=False, help='Explore and contribute Agent Apprenticeship ecosystem bundles.')
76
- bundle_app=typer.Typer(add_completion=False, help='Inspect local Contribution Bundles.')
110
+ ecosystem_app=typer.Typer(add_completion=False, help='Explore and share Agent Apprenticeship Experience Compilations.')
111
+ ecosystem_shared_app=typer.Typer(add_completion=False, help='Inspect local shared Experience Compilation history.')
112
+ bundle_app=typer.Typer(add_completion=False, help='Inspect local Experience Compilations.')
77
113
  learn_app=typer.Typer(add_completion=False, help='Create and apply reversible Experience Packs from ecosystem experience.')
78
114
  app.add_typer(configure_app, name='configure')
79
115
  app.add_typer(ecosystem_app, name='ecosystem')
116
+ ecosystem_app.add_typer(ecosystem_shared_app, name='shared')
80
117
  app.add_typer(bundle_app, name='bundle')
81
118
  app.add_typer(learn_app, name='learn')
82
119
 
@@ -101,9 +138,9 @@ def _split_asset_prompt(text: str) -> list[Path]:
101
138
  def _print_bundle_ready(bundle: Path | None) -> None:
102
139
  if not bundle:
103
140
  return
104
- typer.echo(f'Contribution Bundle path: {bundle}')
141
+ typer.echo(f'Experience Compilation path: {bundle}')
105
142
  typer.echo(f'Contribute to public ecosystem: apprentice ecosystem contribute {bundle}')
106
- typer.echo(f'Public ecosystem: {_ecosystem_repo_url()}')
143
+ typer.echo('Public ecosystem: Forsy ecosystem')
107
144
  typer.echo(f'View files: open {bundle}')
108
145
 
109
146
 
@@ -112,7 +149,7 @@ def _print_public_ecosystem_contribution_help(bundle: str | Path) -> None:
112
149
  typer.echo(f'apprentice ecosystem contribute {bundle}')
113
150
  typer.echo('')
114
151
  typer.echo('Public ecosystem:')
115
- typer.echo(_ecosystem_repo_url())
152
+ typer.echo('Forsy ecosystem')
116
153
 
117
154
 
118
155
  def _print_first_run_banner() -> None:
@@ -155,15 +192,14 @@ def _print_task_result(
155
192
  typer.echo(f'{label}:')
156
193
  typer.echo(str(artifacts))
157
194
  if bundle:
158
- typer.echo('Contribution Bundle:')
195
+ typer.echo('Experience Compilation:')
159
196
  typer.echo(str(bundle))
160
197
  if include_contribution_help:
161
198
  typer.echo('')
162
- mode = get_settings().ecosystem_auto_share
163
- if mode == "manual":
199
+ mode = get_settings().training_contribution_mode
200
+ typer.echo(f'Agent Training Contribution Mode: {training_contribution_mode_display(mode)}')
201
+ if mode == "private_internal_only":
164
202
  _print_public_ecosystem_contribution_help(bundle)
165
- else:
166
- typer.echo(f'Ecosystem Auto-Share: {ecosystem_auto_share_display(mode)}')
167
203
  typer.echo('')
168
204
  typer.echo('View files:')
169
205
  typer.echo(f'open {bundle}')
@@ -183,15 +219,14 @@ def _print_session_finished(status: dict, *, include_contribution_help: bool=Tru
183
219
  typer.echo("Artifacts:")
184
220
  typer.echo(str(status.get('artifacts_path')))
185
221
  if status.get('contribution_bundle_path'):
186
- typer.echo("Contribution Bundle:")
222
+ typer.echo("Experience Compilation:")
187
223
  typer.echo(str(status.get('contribution_bundle_path')))
188
224
  if include_contribution_help:
189
225
  typer.echo('')
190
- mode = get_settings().ecosystem_auto_share
191
- if mode == "manual":
226
+ mode = get_settings().training_contribution_mode
227
+ typer.echo(f'Agent Training Contribution Mode: {training_contribution_mode_display(mode)}')
228
+ if mode == "private_internal_only":
192
229
  _print_public_ecosystem_contribution_help(status.get('contribution_bundle_path'))
193
- else:
194
- typer.echo(f'Ecosystem Auto-Share: {ecosystem_auto_share_display(mode)}')
195
230
  typer.echo('')
196
231
  typer.echo('View files:')
197
232
  typer.echo(f"open {status.get('contribution_bundle_path')}")
@@ -326,6 +361,7 @@ def _configure_detected_model_provider(row: dict) -> None:
326
361
 
327
362
  def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
328
363
  detected_agents = _detect_apprentice_agents()
364
+ detected_providers = _detect_model_provider_keys()
329
365
  typer.echo("")
330
366
  if detected_agents:
331
367
  typer.echo("Detected Apprentice Agents:")
@@ -350,7 +386,6 @@ def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
350
386
  typer.echo("Or choose Custom to provide a command template.")
351
387
  typer.echo("Then rerun: apprentice init --setup")
352
388
 
353
- detected_providers = _detect_model_provider_keys()
354
389
  typer.echo("")
355
390
  if detected_providers:
356
391
  typer.echo("Detected Mentor Model Provider keys:")
@@ -369,6 +404,37 @@ def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
369
404
  typer.echo("No Mentor Model Provider API key was detected.")
370
405
  _print_key_storage_guidance()
371
406
 
407
+ typer.echo("")
408
+ default_mode = "autonomous" if detected_providers else "expert_led"
409
+ if defaults:
410
+ mode = default_mode
411
+ update_settings(apprenticeship_mode=mode, training_contribution_mode="public_ecosystem")
412
+ typer.echo(f"Configured Apprenticeship Mode: {apprenticeship_mode_display(mode)}")
413
+ typer.echo("Configured Agent Training Contribution Mode: Public Ecosystem")
414
+ elif interactive and sys.stdin.isatty():
415
+ typer.echo("Choose Apprenticeship Mode:")
416
+ typer.echo("1. Autonomous Apprenticeship")
417
+ typer.echo("2. Expert-Led Apprenticeship")
418
+ typer.echo("3. Organization Custom")
419
+ mode_choice = typer.prompt("Default", default=get_settings().apprenticeship_mode or default_mode).strip().lower()
420
+ mode = {"1": "autonomous", "2": "expert_led", "3": "org_custom"}.get(mode_choice, mode_choice)
421
+ mode = normalize_apprenticeship_mode(mode)
422
+ if mode == "org_custom":
423
+ typer.echo("Organization Custom is available through private setup. Visit https://agentapprenticeship.org/custom")
424
+ typer.echo("")
425
+ typer.echo("Choose Agent Training Contribution Mode:")
426
+ typer.echo("1. Public Ecosystem")
427
+ typer.echo("2. Private Internal Only")
428
+ contribution_choice = typer.prompt("Default", default=get_settings().training_contribution_mode).strip().lower()
429
+ contribution = {"1": "public_ecosystem", "2": "private_internal_only"}.get(contribution_choice, contribution_choice)
430
+ update_settings(apprenticeship_mode=mode, training_contribution_mode=contribution)
431
+ typer.echo(f"Configured Apprenticeship Mode: {apprenticeship_mode_display(mode)}")
432
+ typer.echo(f"Configured Agent Training Contribution Mode: {training_contribution_mode_display(contribution)}")
433
+ elif interactive:
434
+ update_settings(apprenticeship_mode=default_mode, training_contribution_mode="public_ecosystem")
435
+ typer.echo(f"Configured Apprenticeship Mode: {apprenticeship_mode_display(default_mode)}")
436
+ typer.echo("Configured Agent Training Contribution Mode: Public Ecosystem")
437
+
372
438
 
373
439
  def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bool=False):
374
440
  if quiet:
@@ -383,7 +449,7 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
383
449
  typer.echo('')
384
450
  typer.echo(f'Run: {status.get("run_id")}')
385
451
  typer.echo(f'Apprentice Agent: {status.get("apprentice_agent") or status.get("worker_agent")}')
386
- typer.echo(f'Mentor Mode: {mentor_mode_display(status.get("mentor_mode"))}')
452
+ typer.echo(f'Apprenticeship Mode: {apprenticeship_mode_display(status.get("apprenticeship_mode") or status.get("mentor_mode"))}')
387
453
  typer.echo(f'Maximum Improvement Loops: {status.get("maximum_improvement_loops")}')
388
454
  typer.echo(f'Task workspace: {status.get("task_workspace_path")}')
389
455
  typer.echo(f'Artifacts: {status.get("artifacts_path")}')
@@ -398,6 +464,16 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
398
464
  if event_type in {'followup_completed'}:
399
465
  _print_task_result(status, followup=True, record_only=bool((event.get('metadata_json') or {}).get('record_only')))
400
466
  return
467
+ if event_type == 'task_outputs_ready':
468
+ typer.echo('')
469
+ task_status = event.get('task_status') or status.get('task_status')
470
+ typer.echo('Task completed.' if task_status == 'completed' else 'Task outputs are available.')
471
+ if event.get('artifacts_path'):
472
+ typer.echo(f"Artifacts are available at: {event.get('artifacts_path')}")
473
+ return
474
+ if event_type in {'tdo_started','tdo_round_generated','tdo_round_judged','tdo_round_improved','tdo_completed','tdo_failed'}:
475
+ typer.echo(format_progress_event(event))
476
+ return
401
477
  if event_type in {'apprentice_attempt_started','apprentice_attempt_completed','worker_attempt_started','worker_attempt_completed','revision_started','revision_completed','mentor_review_started','mentor_review_completed','task_workspace_prepared','contribution_bundle_completed','operational_error'} or verbose:
402
478
  typer.echo(format_progress_event(event))
403
479
  return _emit
@@ -459,7 +535,7 @@ def _normalize_evaluation_mode(value: str | None) -> str | None:
459
535
  return None
460
536
  normalized=value.strip().lower().replace('_','-')
461
537
  if normalized not in EVALUATION_MODES:
462
- raise typer.BadParameter(f'mentor mode must be one of: {", ".join(MENTOR_MODES)}')
538
+ raise typer.BadParameter(f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}')
463
539
  return normalized
464
540
 
465
541
  def _normalize_mentor_mode(value: str | None) -> str | None:
@@ -468,7 +544,7 @@ def _normalize_mentor_mode(value: str | None) -> str | None:
468
544
  try:
469
545
  return normalize_mentor_mode(value)
470
546
  except ValueError as exc:
471
- raise typer.BadParameter(f'mentor mode must be one of: {", ".join(MENTOR_MODES)}') from exc
547
+ raise typer.BadParameter(f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}') from exc
472
548
 
473
549
  def _normalize_data_sharing(value: str | None) -> str | None:
474
550
  if value is None:
@@ -500,10 +576,33 @@ def _normalize_auto_share(value: str | None) -> str | None:
500
576
  return normalize_ecosystem_auto_share(value)
501
577
  except ValueError as exc:
502
578
  raise typer.BadParameter(
503
- f'ecosystem auto-share must be one of: {", ".join(ECOSYSTEM_AUTO_SHARE_MODES)}'
579
+ f'legacy sharing alias must be one of: {", ".join(ECOSYSTEM_AUTO_SHARE_MODES)}'
580
+ ) from exc
581
+
582
+
583
+ def _normalize_apprenticeship_mode(value: str | None) -> str | None:
584
+ if value is None:
585
+ return None
586
+ try:
587
+ return normalize_apprenticeship_mode(value)
588
+ except ValueError as exc:
589
+ raise typer.BadParameter(
590
+ f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}'
591
+ ) from exc
592
+
593
+
594
+ def _normalize_training_contribution(value: str | None) -> str | None:
595
+ if value is None:
596
+ return None
597
+ try:
598
+ return normalize_training_contribution_mode(value)
599
+ except ValueError as exc:
600
+ raise typer.BadParameter(
601
+ f'agent training contribution mode must be one of: {", ".join(TRAINING_CONTRIBUTION_MODES)}'
504
602
  ) from exc
505
603
 
506
604
  def _settings_for_run(
605
+ apprenticeship_mode: str | None=None,
507
606
  mentor_mode: str | None=None,
508
607
  sensitive_info_masking: str | None=None,
509
608
  max_loops: int | None=None,
@@ -512,12 +611,15 @@ def _settings_for_run(
512
611
  ):
513
612
  settings=get_settings()
514
613
  updates={}
614
+ legacy_mentor_mode = _normalize_mentor_mode(mentor_mode) if mentor_mode is not None else None
615
+ if apprenticeship_mode is not None:
616
+ updates['apprenticeship_mode']=_normalize_apprenticeship_mode(apprenticeship_mode)
515
617
  if mentor_mode is not None:
516
- updates['mentor_mode']=_normalize_mentor_mode(mentor_mode)
618
+ updates['apprenticeship_mode']=_normalize_apprenticeship_mode(mentor_mode)
517
619
  if evaluation_mode is not None:
518
620
  updates['evaluation_mode']=_normalize_evaluation_mode(evaluation_mode)
519
- if mentor_mode is None:
520
- updates['mentor_mode']=_normalize_mentor_mode(evaluation_mode)
621
+ if mentor_mode is None and apprenticeship_mode is None:
622
+ updates['apprenticeship_mode']=_normalize_apprenticeship_mode(evaluation_mode)
521
623
  if sensitive_info_masking is not None:
522
624
  updates['sensitive_info_masking']=_normalize_masking(sensitive_info_masking)
523
625
  if data_sharing_level is not None:
@@ -529,6 +631,12 @@ def _settings_for_run(
529
631
  raise typer.BadParameter('maximum improvement loops must be at least 1')
530
632
  updates['max_improvement_loops']=max_loops
531
633
  updates['max_iterations']=max_loops
634
+ if "apprenticeship_mode" in updates:
635
+ updates["mentor_mode"] = apprenticeship_mode_to_mentor_mode(updates["apprenticeship_mode"])
636
+ updates["evaluation_mode"] = mentor_mode_to_evaluation_mode(updates["mentor_mode"])
637
+ if legacy_mentor_mode == "hybrid" and apprenticeship_mode is None:
638
+ updates["mentor_mode"] = "hybrid"
639
+ updates["evaluation_mode"] = "hybrid"
532
640
  return settings.model_copy(update=updates)
533
641
 
534
642
  def _configure_model_impl(provider: str | None, model: str | None, api_key_env_var: str | None, test_connection: bool):
@@ -608,15 +716,15 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
608
716
  if settings.mentor_mode == 'expert_led' or model_provider_ready(settings):
609
717
  return settings
610
718
  if not interactive:
611
- raise typer.BadParameter('model-assisted/hybrid Mentor Mode requires a ready Mentor Model Provider; use configure model or --mentor-mode expert-led')
719
+ raise typer.BadParameter('Autonomous Apprenticeship requires a ready Mentor Model Provider; use configure model or --apprenticeship-mode expert-led')
612
720
  if not _stdin_allows_prompt():
613
721
  readiness = mentor_model_provider_readiness(settings)
614
722
  reason = readiness.get("reason") or "Mentor Model Provider is not ready."
615
723
  raise typer.BadParameter(
616
- f"model-assisted/hybrid Mentor Mode requires a ready Mentor Model Provider. {reason} "
617
- "Run `apprentice doctor --live`, `apprentice configure model`, or use `--mentor-mode expert-led`."
724
+ f"Autonomous Apprenticeship requires a ready Mentor Model Provider. {reason} "
725
+ "Run `apprentice doctor --live`, `apprentice configure model`, or use `--apprenticeship-mode expert-led`."
618
726
  )
619
- typer.echo('Model-assisted Mentor Mode needs a ready Mentor Model Provider.')
727
+ typer.echo('Autonomous Apprenticeship needs a ready Mentor Model Provider.')
620
728
  readiness=mentor_model_provider_readiness(settings)
621
729
  if readiness.get("provider"):
622
730
  typer.echo(_status_line('Mentor Model Provider Status', readiness.get("status"), readiness.get("reason")))
@@ -631,14 +739,14 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
631
739
  except typer.Abort:
632
740
  reason = readiness.get("reason") or "Mentor Model Provider is not ready."
633
741
  raise typer.BadParameter(
634
- f"model-assisted/hybrid Mentor Mode requires a ready Mentor Model Provider. {reason} "
635
- "Run `apprentice doctor --live`, `apprentice configure model`, or use `--mentor-mode expert-led`."
742
+ f"Autonomous Apprenticeship requires a ready Mentor Model Provider. {reason} "
743
+ "Run `apprentice doctor --live`, `apprentice configure model`, or use `--apprenticeship-mode expert-led`."
636
744
  ) from None
637
745
  if choice in {'1','setup','set up','configure','configure mentor model provider','set up model provider now'}:
638
746
  configured=_configure_model_impl(None, None, None, test_connection=False)
639
747
  return configured
640
748
  if choice in {'2','expert-led','expert','manual','run this session in expert-led mode'}:
641
- return settings.model_copy(update={'mentor_mode':'expert_led', 'evaluation_mode':'expert-led'})
749
+ return settings.model_copy(update={'apprenticeship_mode':'expert_led', 'mentor_mode':'expert_led', 'evaluation_mode':'expert-led'})
642
750
  raise typer.Exit(1)
643
751
 
644
752
 
@@ -726,6 +834,7 @@ def init(
726
834
  ):
727
835
  _print_first_run_banner()
728
836
  path=init_settings(overwrite=overwrite)
837
+ get_or_create_ecosystem_identity(get_settings())
729
838
  typer.echo(f'initialized Agent Apprenticeship settings at {path}')
730
839
  if defaults:
731
840
  _first_run_setup(interactive=False, defaults=True)
@@ -773,7 +882,7 @@ def doctor(
773
882
  typer.echo(f'Apprentice Agent command: {agent.get("command") or "Not configured"}')
774
883
  typer.echo(f'Apprentice Agent command found: {_yes_no(bool(agent.get("command_found")))}')
775
884
  typer.echo(_status_line('Apprentice Agent readiness', agent.get('status'), agent.get('reason')))
776
- typer.echo(f'Mentor Mode: {mentor_mode_display(settings.mentor_mode)}')
885
+ typer.echo(f'Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}')
777
886
  typer.echo(f'Mentor Model Provider: {provider.get("provider_display")}')
778
887
  typer.echo(f'Mentor model: {provider.get("model") or "Not configured"}')
779
888
  typer.echo(f'API key env var: {provider.get("api_key_env_var") or "Not configured"}')
@@ -1208,13 +1317,16 @@ def configure_agent(
1208
1317
 
1209
1318
  @configure_app.command('settings')
1210
1319
  def configure_settings(
1211
- mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='model-assisted, expert-led, or hybrid.'),
1320
+ apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
1321
+ mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
1212
1322
  sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
1213
1323
  max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops.'),
1214
1324
  ):
1215
1325
  updates={}
1326
+ if apprenticeship_mode is not None:
1327
+ updates['apprenticeship_mode']=_normalize_apprenticeship_mode(apprenticeship_mode)
1216
1328
  if mentor_mode is not None:
1217
- updates['mentor_mode']=_normalize_mentor_mode(mentor_mode)
1329
+ updates['apprenticeship_mode']=_normalize_apprenticeship_mode(mentor_mode)
1218
1330
  if sensitive_info_masking is not None:
1219
1331
  updates['sensitive_info_masking']=_normalize_masking(sensitive_info_masking)
1220
1332
  if max_loops is not None:
@@ -1227,6 +1339,46 @@ def configure_settings(
1227
1339
  update_settings(**updates)
1228
1340
  typer.echo(public_settings_text())
1229
1341
 
1342
+
1343
+ @configure_app.command('apprenticeship')
1344
+ def configure_apprenticeship(
1345
+ mode: str | None=typer.Argument(None, help='autonomous, expert-led, or org-custom.'),
1346
+ ):
1347
+ if mode is None:
1348
+ typer.echo("Apprenticeship Mode options:")
1349
+ typer.echo("- autonomous: Autonomous Apprenticeship")
1350
+ typer.echo("- expert-led: Expert-Led Apprenticeship")
1351
+ typer.echo("- org-custom: Organization Custom")
1352
+ mode = typer.prompt("Apprenticeship Mode")
1353
+ normalized = _normalize_apprenticeship_mode(mode)
1354
+ updated = update_settings(apprenticeship_mode=normalized)
1355
+ typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(updated.apprenticeship_mode)}")
1356
+ if updated.apprenticeship_mode == "org_custom":
1357
+ typer.echo("Organization Custom is available through private setup. Visit https://agentapprenticeship.org/custom")
1358
+
1359
+
1360
+ @configure_app.command('contribution')
1361
+ def configure_contribution(
1362
+ mode: str | None=typer.Argument(None, help='public-ecosystem or private-internal-only.'),
1363
+ sharing_behavior: str | None=typer.Option(None, "--sharing-behavior", help="Public Ecosystem behavior: manual or automatic."),
1364
+ ):
1365
+ if mode is None:
1366
+ typer.echo("Agent Training Contribution Mode options:")
1367
+ typer.echo("- public-ecosystem: share completed Experience Compilations to the Forsy ecosystem")
1368
+ typer.echo("- private-internal-only: keep completed packages local")
1369
+ mode = typer.prompt("Agent Training Contribution Mode")
1370
+ normalized = _normalize_training_contribution(mode)
1371
+ updates = {"training_contribution_mode": normalized}
1372
+ if sharing_behavior is not None:
1373
+ behavior = _normalize_auto_share(sharing_behavior)
1374
+ updates["ecosystem_auto_share"] = behavior
1375
+ if behavior != "disabled":
1376
+ updates["training_contribution_mode"] = "public_ecosystem"
1377
+ updated = update_settings(**updates)
1378
+ typer.echo(f"Agent Training Contribution Mode: {training_contribution_mode_display(updated.training_contribution_mode)}")
1379
+ if updated.training_contribution_mode == "public_ecosystem":
1380
+ typer.echo(f"Public Ecosystem sharing behavior: {ecosystem_auto_share_display(updated.ecosystem_auto_share)}")
1381
+
1230
1382
  @configure_app.command('model')
1231
1383
  def configure_model(
1232
1384
  provider: str | None=typer.Argument(None, help='Known Mentor Model Provider id.'),
@@ -1240,18 +1392,19 @@ def configure_model(
1240
1392
  def start(
1241
1393
  asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional task asset file or directory.'),
1242
1394
  runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
1243
- mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='model-assisted, expert-led, or hybrid.'),
1395
+ apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
1396
+ mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
1244
1397
  sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
1245
- evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --mentor-mode.'),
1246
- data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.'),
1398
+ evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
1399
+ data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.', hidden=True),
1247
1400
  max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops for this run.'),
1248
1401
  quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
1249
1402
  verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
1250
1403
  json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
1251
1404
  expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
1252
- hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Test/dev helper: auto-approve hybrid checkpoints.'),
1405
+ hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
1253
1406
  expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
1254
- hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Prompt for hybrid approval inputs even in non-TTY test harnesses.'),
1407
+ hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
1255
1408
  ):
1256
1409
  current_settings = get_settings()
1257
1410
  current_agent = apprentice_agent_readiness_status(current_settings)
@@ -1260,7 +1413,7 @@ def start(
1260
1413
  instruction=typer.prompt('What should your agent work on?')
1261
1414
  extra=typer.prompt('Add assets? optional', default='', show_default=False)
1262
1415
  assets=list(asset)+_split_asset_prompt(extra)
1263
- settings=_ensure_evaluation_ready(_settings_for_run(mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
1416
+ settings=_ensure_evaluation_ready(_settings_for_run(apprenticeship_mode, mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
1264
1417
  _ensure_apprentice_agent_ready(settings, runner, interactive=True)
1265
1418
  interactive_checkpoints = _interactive_checkpoint_requested(
1266
1419
  settings,
@@ -1294,10 +1447,11 @@ def run_prompt(
1294
1447
  asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional task asset file or directory.'),
1295
1448
  run_id: str | None=typer.Option(None, '--run-id', help='Optional run id/slug.'),
1296
1449
  runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
1297
- mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='model-assisted, expert-led, or hybrid.'),
1450
+ apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
1451
+ mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
1298
1452
  sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
1299
- evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --mentor-mode.'),
1300
- data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.'),
1453
+ evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
1454
+ data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.', hidden=True),
1301
1455
  max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops for this run.'),
1302
1456
  quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
1303
1457
  verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
@@ -1305,12 +1459,14 @@ def run_prompt(
1305
1459
  experience_pack: list[str]=typer.Option([], '--experience-pack', help='Experience Pack id/path to apply to this run.'),
1306
1460
  no_experience_packs: bool=typer.Option(False, '--no-experience-packs', help='Do not apply Experience Packs to this run.'),
1307
1461
  use_active_experience_packs: bool=typer.Option(False, '--use-active-experience-packs', help='Apply active Experience Packs to this run.'),
1462
+ use_installed_skills: bool=typer.Option(False, '--use-installed-skills', help='Force relevant installed runtime training into this run.'),
1463
+ no_installed_skills: bool=typer.Option(False, '--no-installed-skills', help='Do not apply installed runtime training to this run.'),
1308
1464
  expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
1309
- hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Test/dev helper: auto-approve hybrid checkpoints.'),
1465
+ hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
1310
1466
  expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
1311
- hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Prompt for hybrid approval inputs even in non-TTY test harnesses.'),
1467
+ hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
1312
1468
  ):
1313
- settings=_ensure_evaluation_ready(_settings_for_run(mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
1469
+ settings=_ensure_evaluation_ready(_settings_for_run(apprenticeship_mode, mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
1314
1470
  _ensure_apprentice_agent_ready(settings, runner, interactive=True)
1315
1471
  for pack_id in experience_pack:
1316
1472
  try:
@@ -1326,8 +1482,26 @@ def run_prompt(
1326
1482
  no_packs=no_experience_packs,
1327
1483
  settings=settings,
1328
1484
  )
1329
- effective_instruction = instruction.rstrip() + pack_guidance if pack_guidance else instruction
1485
+ installed_skills, runtime_guidance = ([], "")
1486
+ if not no_installed_skills:
1487
+ installed_skills, runtime_guidance = select_runtime_training_for_run(
1488
+ instruction,
1489
+ settings=settings,
1490
+ force=use_installed_skills,
1491
+ )
1492
+ if installed_skills and not quiet and not json_progress:
1493
+ typer.echo("Installed Runtime Training selected:")
1494
+ for skill in installed_skills:
1495
+ typer.echo(f"- {skill.get('installed_skill_id')}: {skill.get('title')}")
1496
+ effective_instruction = instruction.rstrip()
1497
+ if pack_guidance:
1498
+ effective_instruction += pack_guidance
1499
+ if runtime_guidance:
1500
+ effective_instruction += runtime_guidance
1501
+ if effective_instruction == instruction.rstrip():
1502
+ effective_instruction = instruction
1330
1503
  experience_refs = pack_run_refs(selected_packs)
1504
+ runtime_refs = installed_skill_run_refs(installed_skills)
1331
1505
  interactive_checkpoints = _interactive_checkpoint_requested(
1332
1506
  settings,
1333
1507
  expert_auto_approve=expert_auto_approve,
@@ -1349,6 +1523,7 @@ def run_prompt(
1349
1523
  runner=runner,
1350
1524
  progress_callback=_progress_callback(quiet, verbose, json_progress),
1351
1525
  experience_pack_refs=experience_refs,
1526
+ runtime_training_refs=runtime_refs,
1352
1527
  )
1353
1528
  except RunInterrupted as exc:
1354
1529
  if not quiet and not json_progress:
@@ -1368,15 +1543,15 @@ def continue_run(
1368
1543
  followup_instruction: str | None=typer.Argument(None, help='Optional direct follow-up instruction.'),
1369
1544
  asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional follow-up asset file or directory.'),
1370
1545
  run_loop: bool=typer.Option(False, '--run-loop', help='Run another improvement loop after recording the follow-up.'),
1371
- record_only: bool=typer.Option(False, '--record-only', '--no-run-loop', help='Only record the follow-up and update the Contribution Bundle.'),
1546
+ record_only: bool=typer.Option(False, '--record-only', '--no-run-loop', help='Only record the follow-up and update the Experience Compilation.'),
1372
1547
  runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
1373
1548
  quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
1374
1549
  verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
1375
1550
  json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
1376
1551
  expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
1377
- hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Prompt for hybrid approval inputs even in non-TTY test harnesses.'),
1552
+ hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
1378
1553
  expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
1379
- hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Test/dev helper: auto-approve hybrid checkpoints.'),
1554
+ hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
1380
1555
  ):
1381
1556
  settings=get_settings()
1382
1557
  run_root=run_root_for(run_id, settings)
@@ -1428,7 +1603,7 @@ def continue_run(
1428
1603
  typer.echo("")
1429
1604
  typer.echo("Follow-up interrupted.")
1430
1605
  if previous_status.get("contribution_bundle_path"):
1431
- typer.echo("Previous Contribution Bundle:")
1606
+ typer.echo("Previous Experience Compilation:")
1432
1607
  typer.echo(str(previous_status.get("contribution_bundle_path")))
1433
1608
  raise typer.Exit(130)
1434
1609
  status = read_run_status(run_root)
@@ -1442,9 +1617,9 @@ def finish(
1442
1617
  quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
1443
1618
  json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
1444
1619
  expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led final checkpoint inputs even in non-TTY test harnesses.'),
1445
- hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Prompt for hybrid final checkpoint inputs even in non-TTY test harnesses.'),
1620
+ hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
1446
1621
  expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
1447
- hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Test/dev helper: auto-approve hybrid checkpoints.'),
1622
+ hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
1448
1623
  ):
1449
1624
  settings = get_settings()
1450
1625
  interactive_checkpoints = _interactive_checkpoint_requested(
@@ -1459,7 +1634,7 @@ def finish(
1459
1634
  hybrid_auto_approve=hybrid_auto_approve,
1460
1635
  mentor_interactive_checkpoints=interactive_checkpoints,
1461
1636
  ):
1462
- run_root,bundle=finish_session(run_id, settings=settings)
1637
+ run_root,bundle=finish_session(run_id, settings=settings, progress_callback=None if json_progress else _progress_callback(quiet, False, False))
1463
1638
  status=read_run_status(run_root)
1464
1639
  if json_progress:
1465
1640
  for event in read_jsonl(run_root/'progress_events.jsonl'):
@@ -1532,6 +1707,8 @@ def _ecosystem_status() -> dict:
1532
1707
  repo_path = _configured_ecosystem_repo_path()
1533
1708
  gh_installed = bool(_gh_path())
1534
1709
  gh_authed = _gh_authenticated() if gh_installed else False
1710
+ settings = get_settings()
1711
+ r2 = r2_status(settings)
1535
1712
  return {
1536
1713
  "repo": repo,
1537
1714
  "repo_url": _ecosystem_repo_url(repo),
@@ -1540,10 +1717,22 @@ def _ecosystem_status() -> dict:
1540
1717
  "gh_installed": gh_installed,
1541
1718
  "gh_authenticated": gh_authed,
1542
1719
  "automatic_contribution_ready": bool(repo and gh_installed and gh_authed),
1720
+ **r2,
1543
1721
  }
1544
1722
 
1545
1723
 
1546
1724
  def _ecosystem_auto_share_readiness(repo: str | None = None) -> dict:
1725
+ settings = get_settings()
1726
+ if settings.ecosystem_storage_backend == "forsy-r2":
1727
+ api_url = r2_api_url(settings)
1728
+ return {
1729
+ "ready": bool(api_url),
1730
+ "reason": None if api_url else "R2 ingestion API is not configured",
1731
+ "next_action": "apprentice ecosystem configure --r2-api-url <url>" if not api_url else None,
1732
+ "repo": _configured_ecosystem_repo(),
1733
+ "repo_url": _ecosystem_repo_url(),
1734
+ "storage_backend": "forsy-r2",
1735
+ }
1547
1736
  configured_repo = repo or _configured_ecosystem_repo() or DEFAULT_PUBLIC_ECOSYSTEM_REPO
1548
1737
  gh_installed = bool(_gh_path())
1549
1738
  gh_authed = _gh_authenticated() if gh_installed else False
@@ -1591,13 +1780,15 @@ def _ecosystem_auto_share_readiness(repo: str | None = None) -> dict:
1591
1780
  def _ecosystem_auto_share_status_line(settings=None) -> str:
1592
1781
  s = settings or get_settings()
1593
1782
  readiness = _ecosystem_auto_share_readiness()
1594
- label = ecosystem_auto_share_display(s.ecosystem_auto_share)
1595
- if s.ecosystem_auto_share == "manual":
1596
- return f"Ecosystem Auto-Share: {label}"
1783
+ label = training_contribution_mode_display(s.training_contribution_mode)
1784
+ if s.training_contribution_mode == "private_internal_only":
1785
+ return f"Agent Training Contribution Mode: {label}"
1786
+ behavior = ecosystem_auto_share_display(s.ecosystem_auto_share)
1787
+ behavior_label = "automatic" if behavior == "automatic" else "manual"
1597
1788
  if readiness["ready"]:
1598
- return f"Ecosystem Auto-Share: {label}\nEcosystem Auto-Share Status: Ready"
1789
+ return f"Agent Training Contribution Mode: {label}\nPublic Ecosystem sharing behavior: {behavior_label}\nPublic Ecosystem upload status: Ready"
1599
1790
  reason = readiness.get("reason") or "not ready"
1600
- return f"Ecosystem Auto-Share: {label}\nEcosystem Auto-Share Status: Not ready - {reason}"
1791
+ return f"Agent Training Contribution Mode: {label}\nPublic Ecosystem sharing behavior: {behavior_label}\nPublic Ecosystem upload status: Not ready - {reason}"
1601
1792
 
1602
1793
 
1603
1794
  def _public_settings_text_with_ecosystem() -> str:
@@ -1617,7 +1808,7 @@ def _public_settings_text_with_ecosystem() -> str:
1617
1808
  out.append(f"GitHub URL: {_ecosystem_repo_url(settings.ecosystem_repo)}")
1618
1809
  out.append(git_line)
1619
1810
  inserted = True
1620
- if line.startswith("Ecosystem Auto-Share:"):
1811
+ if line.startswith("Agent Training Contribution Mode:"):
1621
1812
  out.pop()
1622
1813
  out.extend(auto_status)
1623
1814
  if not inserted:
@@ -1900,8 +2091,16 @@ def _load_registry_file(path: Path) -> list[dict]:
1900
2091
  return [row for row in rows if isinstance(row, dict)]
1901
2092
 
1902
2093
 
1903
- def _load_registry(registry: Path | None=None) -> list[dict]:
2094
+ def _load_registry(registry: Path | None=None, storage: str | None = None) -> list[dict]:
1904
2095
  if registry is None:
2096
+ backend = normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend)
2097
+ if backend == "forsy-r2":
2098
+ try:
2099
+ return fetch_r2_index(get_settings())
2100
+ except FileNotFoundError as exc:
2101
+ raise FileNotFoundError(f"{exc} Fallback: apprentice ecosystem list --storage github") from exc
2102
+ except Exception as exc:
2103
+ raise FileNotFoundError(f"R2 ecosystem index could not be read. {redact_secrets(str(exc))} Fallback: apprentice ecosystem list --storage github") from exc
1905
2104
  repo_path = _configured_ecosystem_repo_path()
1906
2105
  if repo_path:
1907
2106
  return _index_from_public_repo_path(repo_path)
@@ -1942,20 +2141,67 @@ def _load_registry(registry: Path | None=None) -> list[dict]:
1942
2141
  return _load_registry_file(path)
1943
2142
 
1944
2143
 
2144
+ def _row_id(row: dict) -> str:
2145
+ return str(row.get("id") or row.get("bundle_id") or row.get("package_id") or row.get("seed_task_id") or row.get("task_id") or "")
2146
+
2147
+
2148
+ def _row_values(row: dict, *keys: str) -> list[str]:
2149
+ values: list[str] = []
2150
+ for key in keys:
2151
+ value = row.get(key)
2152
+ if isinstance(value, (list, tuple, set)):
2153
+ values.extend(str(item) for item in value if item not in (None, ""))
2154
+ elif isinstance(value, dict):
2155
+ values.extend(str(item) for item in value.values() if item not in (None, ""))
2156
+ elif value not in (None, ""):
2157
+ values.append(str(value))
2158
+ return values
2159
+
2160
+
2161
+ def _row_contains(row: dict, expected: str, *keys: str) -> bool:
2162
+ needle = str(expected or "").strip().lower()
2163
+ return bool(needle) and any(needle == value.lower() or needle in value.lower() for value in _row_values(row, *keys))
2164
+
2165
+
1945
2166
  def _matches_registry_row(row: dict, query: str | None, filters: dict) -> bool:
2167
+ if filters.get("mine"):
2168
+ identity = get_or_create_ecosystem_identity(get_settings())
2169
+ contributor = row.get("contributor") or {}
2170
+ if str(contributor.get("ecosystem_user_id") or "") != str(identity.get("ecosystem_user_id")):
2171
+ return False
1946
2172
  if query:
1947
- haystack=' '.join(str(row.get(k) or '') for k in ['bundle_id','title','agent_apprentice_role','expected_economic_value'])
1948
- haystack+=' '+' '.join(str(v) for v in row.get('domains') or [])
1949
- haystack+=' '+' '.join(str(v) for v in row.get('subdomains') or [])
2173
+ haystack=' '.join(_row_values(
2174
+ row,
2175
+ 'id', 'item_id', 'bundle_id', 'package_id', 'experience_compilation_id',
2176
+ 'title', 'task_title', 'summary', 'agent_apprentice_role',
2177
+ 'expected_economic_value', 'economic_value_summary',
2178
+ 'domain', 'domains', 'subdomain', 'subdomains', 'tags', 'surfaces', 'tools',
2179
+ ))
1950
2180
  if query.lower() not in haystack.lower():
1951
2181
  return False
1952
- for key in ['title','mentor_mode','task_status','run_status']:
2182
+ if filters.get("apprenticeship_mode"):
2183
+ expected = _normalize_apprenticeship_mode(str(filters["apprenticeship_mode"]))
2184
+ actual = row.get("apprenticeship_mode")
2185
+ if not actual and row.get("mentor_mode"):
2186
+ actual = normalize_apprenticeship_mode(str(row.get("mentor_mode")))
2187
+ if not actual:
2188
+ return False
2189
+ if normalize_apprenticeship_mode(str(actual or "")) != expected:
2190
+ return False
2191
+ for key in ['title','task_status','run_status']:
1953
2192
  value=filters.get(key)
1954
2193
  if value and str(row.get(key) or '').lower() != str(value).lower():
1955
2194
  return False
1956
- for key, field in [('domain','domains'), ('subdomain','subdomains')]:
2195
+ if filters.get("mentor_mode"):
2196
+ expected = _normalize_apprenticeship_mode(str(filters["mentor_mode"]))
2197
+ actual = row.get("apprenticeship_mode") or row.get("mentor_mode")
2198
+ if not actual:
2199
+ return False
2200
+ if normalize_apprenticeship_mode(str(actual or "")) != expected:
2201
+ return False
2202
+ for key, fields in [('domain', ('domain', 'domains')), ('subdomain', ('subdomain', 'subdomains')), ('tag', ('tags',))]:
1957
2203
  value=filters.get(key)
1958
- if value and value.lower() not in [str(v).lower() for v in row.get(field) or []]:
2204
+ if value and not _row_contains(row, str(value), *fields):
1959
2205
  return False
1960
2206
  if filters.get('min_traced_steps') is not None and int(row.get('traced_steps') or 0) < filters['min_traced_steps']:
1961
2207
  return False
@@ -1986,12 +2232,51 @@ def _matches_registry_row(row: dict, query: str | None, filters: dict) -> bool:
1986
2232
  return True
1987
2233
 
1988
2234
 
1989
- def _print_registry_rows(rows: list[dict]) -> None:
2235
+ def _select_registry_row(rows: list[dict]) -> None:
2236
+ if not sys.stdin.isatty():
2237
+ typer.echo("Selection requires an interactive terminal. Use `--json` to select an item non-interactively.")
2238
+ return
2239
+ choice = typer.prompt("Choose Experience Compilation", default="1").strip()
2240
+ try:
2241
+ index = int(choice)
2242
+ except ValueError:
2243
+ typer.echo("Selection cancelled.")
2244
+ return
2245
+ if index < 1 or index > len(rows):
2246
+ typer.echo("Selection cancelled.")
2247
+ return
2248
+ row = rows[index - 1]
2249
+ item_id = _row_id(row)
2250
+ typer.echo("")
2251
+ typer.echo("Choose action:")
2252
+ typer.echo("1. Export Full Experience Compilation")
2253
+ typer.echo("2. Install Runtime Training")
2254
+ action = typer.prompt("Action", default="1").strip()
2255
+ if action == "1":
2256
+ output = typer.prompt("Output path", default=str(get_settings().app_home / "ecosystem" / "exports" / item_id)).strip()
2257
+ package_root = _package_for_ecosystem_item(item_id)
2258
+ exported = _copy_or_zip_package(package_root, Path(output), item_id=item_id, source="ecosystem_select")
2259
+ typer.echo(f"Export Full Experience Compilation complete: {exported}")
2260
+ elif action == "2":
2261
+ package_root = _package_for_ecosystem_item(item_id)
2262
+ manifest = install_runtime_training_from_bundle(item_id, package_root, settings=get_settings())
2263
+ typer.echo("Install Runtime Training complete.")
2264
+ typer.echo(f"installed_skill_id: {manifest.get('installed_skill_id')}")
2265
+ else:
2266
+ typer.echo("Selection cancelled.")
2267
+
2268
+
2269
+ def _print_registry_rows(rows: list[dict], *, json_output: bool = False, select: bool = False) -> None:
2270
+ if json_output:
2271
+ typer.echo(json.dumps({"items": sanitize_public_obj(rows)}, indent=2, sort_keys=True))
2272
+ return
1990
2273
  if not rows:
1991
- typer.echo('No ecosystem bundles matched.')
2274
+ typer.echo('No Experience Compilations matched.')
1992
2275
  return
1993
- for row in rows:
1994
- typer.echo(_format_registry_row(row))
2276
+ for index, row in enumerate(rows, 1):
2277
+ typer.echo(_format_registry_row(row, index=index))
2278
+ if select:
2279
+ _select_registry_row(rows)
1995
2280
 
1996
2281
 
1997
2282
  def _is_seed_registry_row(row: dict) -> bool:
@@ -2002,7 +2287,7 @@ def _format_count(value) -> str:
2002
2287
  return str(int(value)) if isinstance(value, (int, float)) or str(value).isdigit() else "0"
2003
2288
 
2004
2289
 
2005
- def _format_seed_registry_row(row: dict) -> str:
2290
+ def _format_seed_registry_row(row: dict, *, index: int | None = None) -> str:
2006
2291
  bundle_id = row.get("bundle_id") or row.get("seed_task_id") or row.get("task_id")
2007
2292
  status = row.get("task_status") or "ready_to_run"
2008
2293
  run_status = row.get("run_status") or "not_run"
@@ -2024,34 +2309,46 @@ def _format_seed_registry_row(row: dict) -> str:
2024
2309
  if row.get("revision_preference_pairs") is not None:
2025
2310
  details.append(f"preferences: {_format_count(row.get('revision_preference_pairs'))}")
2026
2311
  details.append(learning_label)
2027
- return f"{bundle_id}: {row.get('title')} [{' · '.join(details)}]"
2312
+ prefix = f"{index}. " if index is not None else ""
2313
+ return f"{prefix}{bundle_id}: {row.get('title')} [{' · '.join(details)}]"
2028
2314
 
2029
2315
 
2030
- def _format_registry_row(row: dict) -> str:
2316
+ def _format_registry_row(row: dict, *, index: int | None = None) -> str:
2031
2317
  if _is_seed_registry_row(row):
2032
- return _format_seed_registry_row(row)
2318
+ return _format_seed_registry_row(row, index=index)
2033
2319
  details = [
2034
2320
  str(row.get("task_status") or row.get("run_status") or "status_unknown"),
2035
2321
  ]
2036
- if row.get("mentor_mode"):
2037
- details.append(f"mentor_mode={mentor_mode_display(row.get('mentor_mode'))}")
2322
+ mode = row.get("apprenticeship_mode") or row.get("mentor_mode")
2323
+ if mode:
2324
+ details.append(f"apprenticeship_mode={apprenticeship_mode_display(normalize_apprenticeship_mode(str(mode)))}")
2038
2325
  traces = row.get("traced_steps")
2039
2326
  if traces is not None:
2040
2327
  details.append(f"traces: {_format_count(traces)}")
2041
2328
  artifacts = row.get("artifact_count")
2042
2329
  if artifacts is not None:
2043
2330
  details.append(f"artifacts: {_format_count(artifacts)}")
2044
- return (
2045
- f"{row.get('bundle_id')}: {row.get('title')} "
2046
- f"[{' · '.join(details)}]"
2047
- )
2331
+ summary = str(row.get("summary") or "").strip()
2332
+ if len(summary) > 180:
2333
+ summary = summary[:177].rstrip() + "..."
2334
+ prefix = f"{index}. " if index is not None else ""
2335
+ return "\n".join([
2336
+ f"{prefix}{row.get('title') or _row_id(row)}",
2337
+ f" item_id: {_row_id(row)}",
2338
+ f" domain: {row.get('domain') or 'unknown'}",
2339
+ f" subdomain: {row.get('subdomain') or 'unknown'}",
2340
+ f" expected economic value: {row.get('expected_economic_value') or 'unknown'}",
2341
+ f" summary: {summary or 'No summary provided.'}",
2342
+ f" details: {' · '.join(details)}",
2343
+ " actions: 1. Export Full Experience Compilation 2. Install Runtime Training",
2344
+ ])
2048
2345
 
2049
2346
 
2050
2347
  def _safe_bundle_path(path: Path) -> Path:
2051
2348
  bundle = path.expanduser()
2052
2349
  manifest = bundle / "contribution_manifest.json"
2053
2350
  if not manifest.exists():
2054
- raise FileNotFoundError(f"Contribution Bundle manifest not found: {manifest}")
2351
+ raise FileNotFoundError(f"Experience Compilation manifest not found: {manifest}")
2055
2352
  return bundle
2056
2353
 
2057
2354
 
@@ -2102,6 +2399,7 @@ def _bundle_submission_metadata(bundle: Path, package_name: str | None = None, p
2102
2399
  "task_status": manifest.get("task_status"),
2103
2400
  "run_status": manifest.get("run_status"),
2104
2401
  "apprentice_agent": session.get("apprentice_agent"),
2402
+ "apprenticeship_mode": manifest.get("apprenticeship_mode") or session.get("apprenticeship_mode"),
2105
2403
  "mentor_mode": manifest.get("mentor_mode"),
2106
2404
  "mentor_model_provider": session.get("model_provider"),
2107
2405
  "domains": manifest.get("domains") or [],
@@ -2149,7 +2447,7 @@ def _submission_summary(metadata: dict, bundle: Path, package_zip: Path | None =
2149
2447
  f"Task Status: {metadata.get('task_status')}",
2150
2448
  f"Run Status: {metadata.get('run_status')}",
2151
2449
  f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}",
2152
- f"Mentor Mode: {mentor_mode_display(metadata.get('mentor_mode'))}",
2450
+ f"Apprenticeship Mode: {apprenticeship_mode_display(metadata.get('apprenticeship_mode') or metadata.get('mentor_mode'))}",
2153
2451
  f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}",
2154
2452
  f"Attempts: {metadata.get('attempts')}",
2155
2453
  f"Traced Steps: {metadata.get('traced_steps')}",
@@ -2188,12 +2486,61 @@ def _zip_dir(src: Path, dst: Path) -> str:
2188
2486
  return f"sha256:{digest}"
2189
2487
 
2190
2488
 
2191
- def _create_ecosystem_submission(bundle: Path) -> tuple[Path, Path, dict]:
2489
+ def _sanitize_submission_copy(root: Path) -> None:
2490
+ text_suffixes = {
2491
+ ".json",
2492
+ ".jsonl",
2493
+ ".md",
2494
+ ".txt",
2495
+ ".yaml",
2496
+ ".yml",
2497
+ ".csv",
2498
+ ".tsv",
2499
+ ".log",
2500
+ }
2501
+ for path in root.rglob("*"):
2502
+ if not path.is_file() or path.stat().st_size > 5_000_000:
2503
+ continue
2504
+ if path.suffix.lower() not in text_suffixes:
2505
+ continue
2506
+ raw = path.read_text(errors="ignore")
2507
+ if path.suffix.lower() == ".json":
2508
+ try:
2509
+ data = json.loads(raw or "{}")
2510
+ except Exception:
2511
+ path.write_text(sanitize_public_text(raw) or "")
2512
+ else:
2513
+ if isinstance(data, dict):
2514
+ write_json(path, sanitize_public_obj(data))
2515
+ elif isinstance(data, list):
2516
+ path.write_text(json.dumps(sanitize_public_obj({"items": data})["items"], indent=2, sort_keys=True) + "\n")
2517
+ else:
2518
+ path.write_text(sanitize_public_text(raw) or "")
2519
+ elif path.suffix.lower() == ".jsonl":
2520
+ rows = []
2521
+ for line in raw.splitlines():
2522
+ if not line.strip():
2523
+ continue
2524
+ try:
2525
+ data = json.loads(line)
2526
+ except Exception:
2527
+ rows.append(json.dumps(sanitize_public_text(line) or "", sort_keys=True))
2528
+ else:
2529
+ if isinstance(data, dict):
2530
+ rows.append(json.dumps(sanitize_public_obj(data), sort_keys=True))
2531
+ else:
2532
+ rows.append(json.dumps(sanitize_public_obj({"item": data})["item"], sort_keys=True))
2533
+ path.write_text("\n".join(rows) + ("\n" if rows else ""))
2534
+ else:
2535
+ path.write_text(sanitize_public_text(raw) or "")
2536
+
2537
+
2538
+ def _create_ecosystem_submission(bundle: Path, *, block_secret_like_values: bool = False) -> tuple[Path, Path, dict]:
2192
2539
  bundle = _safe_bundle_path(bundle)
2193
2540
  secret_hits = _bundle_secret_hits(bundle)
2194
- if secret_hits:
2541
+ if block_secret_like_values and secret_hits:
2195
2542
  raise RuntimeError(
2196
- "Obvious secret-like values were found in the Contribution Bundle. "
2543
+ "Obvious secret-like values were found in the Experience Compilation. "
2197
2544
  "Remove or regenerate the affected files before public contribution: "
2198
2545
  + ", ".join(secret_hits[:10])
2199
2546
  )
@@ -2206,6 +2553,7 @@ def _create_ecosystem_submission(bundle: Path) -> tuple[Path, Path, dict]:
2206
2553
  submission_dir / "contribution_bundle",
2207
2554
  ignore=shutil.ignore_patterns(".env", ".env.local", "__pycache__", "*.pyc"),
2208
2555
  )
2556
+ _sanitize_submission_copy(submission_dir / "contribution_bundle")
2209
2557
  package_zip = bundle.parent / f"{bundle.name}_ecosystem_submission.zip"
2210
2558
  metadata = _bundle_submission_metadata(bundle, package_name=package_zip.name)
2211
2559
  write_json(submission_dir / "ecosystem_submission.json", metadata)
@@ -2229,6 +2577,7 @@ def _ecosystem_contribution_record(
2229
2577
  return {
2230
2578
  "bundle_id": metadata.get("bundle_id"),
2231
2579
  "auto_share_mode": auto_share_mode,
2580
+ "training_contribution_mode": auto_share_mode,
2232
2581
  "contribution_created": contribution_created,
2233
2582
  "contribution_url": contribution_url,
2234
2583
  "contribution_method": contribution_method,
@@ -2250,10 +2599,15 @@ def _ecosystem_contribute_impl(
2250
2599
  repo: str | None = None,
2251
2600
  dry_run: bool = False,
2252
2601
  auto_share_mode: str = "manual",
2602
+ storage: str | None = None,
2603
+ qa: bool = False,
2253
2604
  ) -> dict:
2254
2605
  bundle = _safe_bundle_path(bundle_path)
2606
+ qa = qa or str(os.environ.get("AA_R2_QA_UPLOAD") or "").strip().lower() in {"1", "true", "yes", "on"}
2255
2607
  submission_dir, package_zip, metadata = _create_ecosystem_submission(bundle)
2256
2608
  target_repo = repo or _configured_ecosystem_repo()
2609
+ settings = get_settings()
2610
+ storage_backend = normalize_ecosystem_storage(storage or settings.ecosystem_storage_backend or DEFAULT_ECOSYSTEM_STORAGE_BACKEND)
2257
2611
  issue_body = _gh_issue_body(metadata, submission_dir, package_zip)
2258
2612
  result = {
2259
2613
  "bundle": bundle,
@@ -2262,14 +2616,84 @@ def _ecosystem_contribute_impl(
2262
2616
  "metadata": metadata,
2263
2617
  "issue_body": issue_body,
2264
2618
  "target_repo": target_repo,
2619
+ "storage_backend": storage_backend,
2620
+ "storage_ref": None,
2621
+ "ecosystem_item": None,
2622
+ "upload_response": None,
2265
2623
  "contribution_created": False,
2266
2624
  "contribution_url": None,
2267
2625
  "contribution_method": "manual_package",
2268
2626
  "skipped_reason": None,
2269
2627
  "gh_error": None,
2270
2628
  }
2629
+
2630
+ if storage_backend == "forsy-r2":
2631
+ identity = get_or_create_ecosystem_identity(settings)
2632
+ item = ecosystem_item_from_bundle(bundle, metadata, settings=settings, identity=identity, source="qa" if qa else "contribution")
2633
+ storage_ref = storage_ref_for_item(item, settings=settings, dry_run=dry_run)
2634
+ write_storage_ref(bundle, submission_dir, storage_ref)
2635
+ _zip_dir(submission_dir, package_zip)
2636
+ result["ecosystem_item"] = item
2637
+ result["storage_ref"] = storage_ref
2638
+ result["contribution_method"] = "r2_worker_ingestion" if not dry_run else "r2_worker_dry_run"
2639
+ if dry_run:
2640
+ result["skipped_reason"] = "dry run"
2641
+ else:
2642
+ api_url = r2_api_url(settings)
2643
+ if not api_url:
2644
+ result["skipped_reason"] = "R2 ingestion API is not configured"
2645
+ result["contribution_method"] = "skipped"
2646
+ else:
2647
+ response = upload_contribution_to_worker(
2648
+ api_url=api_url,
2649
+ package_zip=package_zip,
2650
+ ecosystem_item=item,
2651
+ manifest=read_json(bundle / "contribution_manifest.json"),
2652
+ storage_ref=storage_ref,
2653
+ qa=qa,
2654
+ )
2655
+ result["upload_response"] = response
2656
+ result["contribution_created"] = response.get("status") == "ok"
2657
+ result["contribution_url"] = (
2658
+ (response.get("storage") or {}).get("url")
2659
+ or storage_ref.get("url")
2660
+ or (api_url.rstrip("/") + f"/v1/ecosystem/items/{item.get('id')}")
2661
+ )
2662
+ response_storage = response.get("storage") if isinstance(response.get("storage"), dict) else {}
2663
+ if response_storage:
2664
+ item["storage"] = {**(item.get("storage") or {}), **response_storage}
2665
+ uploaded_ref = storage_ref_for_item(
2666
+ item,
2667
+ settings=settings,
2668
+ dry_run=False,
2669
+ uploaded_at=datetime.now(timezone.utc).isoformat(),
2670
+ url=result["contribution_url"],
2671
+ )
2672
+ write_storage_ref(bundle, submission_dir, uploaded_ref)
2673
+ _zip_dir(submission_dir, package_zip)
2674
+ result["storage_ref"] = uploaded_ref
2675
+ append_successful_share(
2676
+ item=item,
2677
+ bundle=bundle,
2678
+ storage_backend="forsy-r2",
2679
+ url=result["contribution_url"],
2680
+ sharing_mode=auto_share_mode,
2681
+ settings=settings,
2682
+ )
2683
+ record = _ecosystem_contribution_record(
2684
+ metadata=metadata,
2685
+ auto_share_mode=auto_share_mode,
2686
+ contribution_created=bool(result["contribution_created"]),
2687
+ contribution_url=result.get("contribution_url"),
2688
+ contribution_method=str(result["contribution_method"]),
2689
+ skipped_reason=result.get("skipped_reason"),
2690
+ )
2691
+ result["record"] = record
2692
+ _write_ecosystem_contribution_record(bundle, record)
2693
+ return result
2694
+
2271
2695
  if target_repo and _gh_path() and _gh_authenticated() and not dry_run:
2272
- title = f"Contribution Bundle: {metadata.get('bundle_id')}"
2696
+ title = f"Experience Compilation: {metadata.get('bundle_id')}"
2273
2697
  cp = _run_gh(["issue", "create", "--repo", target_repo, "--title", title, "--body-file", str(issue_body)], timeout=60)
2274
2698
  if cp.returncode == 0:
2275
2699
  result["contribution_created"] = True
@@ -2305,12 +2729,12 @@ def _bundle_from_status(status: dict) -> Path | None:
2305
2729
 
2306
2730
 
2307
2731
  def _print_auto_share_summary(metadata: dict) -> None:
2308
- typer.echo("Contribution Bundle summary")
2732
+ typer.echo("Experience Compilation summary")
2309
2733
  typer.echo(f"bundle_id: {metadata.get('bundle_id')}")
2310
2734
  typer.echo(f"title: {metadata.get('title')}")
2311
2735
  typer.echo(f"task_status: {metadata.get('task_status')}")
2312
2736
  typer.echo(f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}")
2313
- typer.echo(f"Mentor Mode: {mentor_mode_display(metadata.get('mentor_mode'))}")
2737
+ typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(metadata.get('apprenticeship_mode') or metadata.get('mentor_mode'))}")
2314
2738
  typer.echo(f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}")
2315
2739
  typer.echo(f"artifact_count: {metadata.get('artifact_count')}")
2316
2740
 
@@ -2330,28 +2754,32 @@ def _record_auto_share_skip(bundle: Path, *, mode: str, reason: str) -> None:
2330
2754
 
2331
2755
  def _handle_ecosystem_auto_share(status: dict, *, quiet: bool = False, json_progress: bool = False) -> None:
2332
2756
  settings = get_settings()
2333
- mode = settings.ecosystem_auto_share
2334
- if mode == "manual":
2757
+ mode = settings.training_contribution_mode
2758
+ if mode == "private_internal_only":
2335
2759
  return
2336
2760
  bundle = _bundle_from_status(status)
2337
2761
  if bundle is None:
2338
2762
  return
2339
2763
  emit = not quiet and not json_progress
2340
- if mode == "automatic":
2341
- readiness = _ecosystem_auto_share_readiness()
2342
- if not readiness["ready"]:
2343
- reason = str(readiness.get("reason") or "not ready")
2344
- _record_auto_share_skip(bundle, mode=mode, reason=reason)
2345
- if emit:
2346
- typer.echo("")
2347
- typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
2348
- typer.echo("Ecosystem Auto-Share is configured for:")
2349
- typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url()))
2350
- if readiness.get("next_action"):
2764
+ if mode == "public_ecosystem":
2765
+ behavior = normalize_ecosystem_auto_share(settings.ecosystem_auto_share)
2766
+ if behavior == "manual":
2767
+ if not emit or not sys.stdin.isatty():
2768
+ _record_auto_share_skip(bundle, mode=mode, reason="manual sharing behavior; upload not confirmed")
2769
+ if emit:
2351
2770
  typer.echo("")
2352
- typer.echo("Run:")
2353
- typer.echo(str(readiness["next_action"]))
2354
- return
2771
+ typer.echo("Public Ecosystem upload: manual; no upload was made.")
2772
+ typer.echo(f"Experience Compilation: {bundle}")
2773
+ typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
2774
+ return
2775
+ typer.echo("")
2776
+ typer.echo("Share this Experience Compilation to the Forsy ecosystem? [y/N]")
2777
+ choice = typer.prompt("Share", default="N", show_default=False).strip().lower()
2778
+ if choice not in {"y", "yes"}:
2779
+ _record_auto_share_skip(bundle, mode=mode, reason="user chose not to share")
2780
+ typer.echo("Kept local package only.")
2781
+ typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
2782
+ return
2355
2783
  try:
2356
2784
  result = _ecosystem_contribute_impl(bundle, auto_share_mode=mode)
2357
2785
  except Exception as exc:
@@ -2359,77 +2787,23 @@ def _handle_ecosystem_auto_share(status: dict, *, quiet: bool = False, json_prog
2359
2787
  _record_auto_share_skip(bundle, mode=mode, reason=reason)
2360
2788
  if emit:
2361
2789
  typer.echo("")
2362
- typer.echo("Ecosystem Auto-Share skipped - safety check failed")
2790
+ typer.echo("Public Ecosystem upload skipped - package could not be prepared")
2363
2791
  typer.echo(f"Reason: {reason}")
2364
- typer.echo(f"Bundle: {bundle}")
2792
+ typer.echo(f"Experience Compilation: {bundle}")
2793
+ typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
2365
2794
  return
2366
2795
  if emit:
2367
2796
  typer.echo("")
2368
2797
  if result["contribution_created"]:
2369
- typer.echo(f"Public ecosystem contribution created: {result['contribution_url']}")
2798
+ typer.echo("Agent Training Contribution Mode: Public Ecosystem")
2799
+ typer.echo("Uploading Experience Compilation to Forsy ecosystem...")
2800
+ typer.echo(f"Uploaded: {result['contribution_url']}")
2370
2801
  else:
2371
2802
  reason = result.get("skipped_reason") or "public contribution was not created"
2372
- typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
2373
- typer.echo(f"Bundle: {bundle}")
2803
+ typer.echo(f"Public Ecosystem upload skipped - {reason}")
2804
+ typer.echo(f"Experience Compilation: {bundle}")
2805
+ typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
2374
2806
  return
2375
- if mode == "ask":
2376
- try:
2377
- submission_dir, package_zip, metadata = _create_ecosystem_submission(bundle)
2378
- _gh_issue_body(metadata, submission_dir, package_zip)
2379
- except Exception as exc:
2380
- reason = redact_secrets(str(exc))
2381
- _record_auto_share_skip(bundle, mode=mode, reason=reason)
2382
- if emit:
2383
- typer.echo("")
2384
- typer.echo("Ecosystem Auto-Share skipped - safety check failed")
2385
- typer.echo(f"Reason: {reason}")
2386
- typer.echo(f"Bundle: {bundle}")
2387
- return
2388
- if not emit:
2389
- _record_auto_share_skip(bundle, mode=mode, reason="ask mode requires interactive output")
2390
- return
2391
- while True:
2392
- typer.echo("")
2393
- typer.echo("Share this Contribution Bundle to the public ecosystem now?")
2394
- typer.echo("")
2395
- typer.echo("1. Share now")
2396
- typer.echo("2. Skip")
2397
- typer.echo("3. Show submission summary")
2398
- choice = typer.prompt("Default", default="skip").strip().lower()
2399
- if choice in {"1", "share", "share now", "yes", "y"}:
2400
- readiness = _ecosystem_auto_share_readiness()
2401
- if not readiness["ready"]:
2402
- reason = str(readiness.get("reason") or "not ready")
2403
- _record_auto_share_skip(bundle, mode=mode, reason=reason)
2404
- typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
2405
- typer.echo("Ecosystem Auto-Share is configured for:")
2406
- typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url()))
2407
- if readiness.get("next_action"):
2408
- typer.echo("")
2409
- typer.echo("Run:")
2410
- typer.echo(str(readiness["next_action"]))
2411
- return
2412
- try:
2413
- result = _ecosystem_contribute_impl(bundle, auto_share_mode=mode)
2414
- except Exception as exc:
2415
- reason = redact_secrets(str(exc))
2416
- _record_auto_share_skip(bundle, mode=mode, reason=reason)
2417
- typer.echo("Ecosystem Auto-Share skipped - safety check failed")
2418
- typer.echo(f"Reason: {reason}")
2419
- typer.echo(f"Bundle: {bundle}")
2420
- return
2421
- if result["contribution_created"]:
2422
- typer.echo(f"Public ecosystem contribution created: {result['contribution_url']}")
2423
- else:
2424
- typer.echo(f"Ecosystem Auto-Share skipped - {result.get('skipped_reason') or 'public contribution was not created'}")
2425
- typer.echo(f"Share later: apprentice ecosystem contribute {bundle}")
2426
- return
2427
- if choice in {"3", "summary", "show", "show submission summary"}:
2428
- _print_auto_share_summary(metadata)
2429
- continue
2430
- _record_auto_share_skip(bundle, mode=mode, reason="user skipped")
2431
- typer.echo(f"Share later: apprentice ecosystem contribute {bundle}")
2432
- return
2433
2807
 
2434
2808
 
2435
2809
  def _gh_issue_body(metadata: dict, submission_dir: Path, package_zip: Path) -> Path:
@@ -2468,11 +2842,12 @@ def learn_search(query: str | None = typer.Argument(None, help="Search ecosystem
2468
2842
 
2469
2843
  @learn_app.command("create")
2470
2844
  def learn_create(
2471
- sources: list[str] = typer.Argument(..., help="Ecosystem ids or local Contribution Bundle paths."),
2845
+ sources: list[str] = typer.Argument(..., help="Ecosystem ids or local Experience Compilation paths."),
2472
2846
  title: str | None = typer.Option(None, "--title", help="Optional Experience Pack title."),
2473
2847
  replay: bool = typer.Option(False, "--replay", help="Run before/after replay after creating the pack."),
2474
2848
  runner: str | None = typer.Option(None, "--runner", help="Advanced compatibility runner override for replay."),
2475
- mentor_mode: str = typer.Option("expert-led", "--mentor-mode", help="Mentor Mode for replay, default expert-led."),
2849
+ apprenticeship_mode: str = typer.Option("expert-led", "--apprenticeship-mode", help="Apprenticeship Mode for replay."),
2850
+ mentor_mode: str | None = typer.Option(None, "--mentor-mode", help="Backward-compatible alias for --apprenticeship-mode.", hidden=True),
2476
2851
  ):
2477
2852
  settings = get_settings()
2478
2853
  resolved = [resolve_learning_source(source, settings) for source in sources]
@@ -2486,7 +2861,7 @@ def learn_create(
2486
2861
  typer.echo(f"- apprentice learn preview {pack.get('pack_id')}")
2487
2862
  typer.echo(f"- apprentice learn replay {pack.get('pack_id')}")
2488
2863
  if replay:
2489
- _learn_replay_impl(str(pack.get("pack_id")), runner=runner, mentor_mode=mentor_mode)
2864
+ _learn_replay_impl(str(pack.get("pack_id")), runner=runner, apprenticeship_mode=mentor_mode or apprenticeship_mode)
2490
2865
 
2491
2866
 
2492
2867
  @learn_app.command("preview")
@@ -2514,8 +2889,8 @@ def learn_preview(pack_id: str = typer.Argument(..., help="Experience Pack id or
2514
2889
  typer.echo(f"- {requirement}")
2515
2890
 
2516
2891
 
2517
- def _learn_replay_impl(pack_id: str, *, runner: str | None, mentor_mode: str) -> dict:
2518
- settings = _settings_for_run(mentor_mode, None, 1, None, None)
2892
+ def _learn_replay_impl(pack_id: str, *, runner: str | None, apprenticeship_mode: str) -> dict:
2893
+ settings = _settings_for_run(apprenticeship_mode=apprenticeship_mode, max_loops=1)
2519
2894
  _ensure_apprentice_agent_ready(settings, runner, interactive=False)
2520
2895
  pack_path, pack = load_pack(pack_id, settings)
2521
2896
  instruction = replay_instruction_for_pack(pack, settings)
@@ -2570,9 +2945,10 @@ def _learn_replay_impl(pack_id: str, *, runner: str | None, mentor_mode: str) ->
2570
2945
  def learn_replay(
2571
2946
  pack_id: str = typer.Argument(..., help="Experience Pack id or path."),
2572
2947
  runner: str | None = typer.Option(None, "--runner", help="Advanced compatibility runner override."),
2573
- mentor_mode: str = typer.Option("expert-led", "--mentor-mode", help="Mentor Mode for replay, default expert-led."),
2948
+ apprenticeship_mode: str = typer.Option("expert-led", "--apprenticeship-mode", help="Apprenticeship Mode for replay."),
2949
+ mentor_mode: str | None = typer.Option(None, "--mentor-mode", help="Backward-compatible alias for --apprenticeship-mode.", hidden=True),
2574
2950
  ):
2575
- _learn_replay_impl(pack_id, runner=runner, mentor_mode=mentor_mode)
2951
+ _learn_replay_impl(pack_id, runner=runner, apprenticeship_mode=mentor_mode or apprenticeship_mode)
2576
2952
 
2577
2953
 
2578
2954
  @learn_app.command("keep")
@@ -2619,25 +2995,104 @@ def learn_status():
2619
2995
  typer.echo(f"- {pack.get('pack_id')}: {pack.get('title')}")
2620
2996
 
2621
2997
 
2998
+ @learn_app.command("install")
2999
+ def learn_install(
3000
+ item_id: str=typer.Argument(..., help="Ecosystem item id or local Experience Compilation path."),
3001
+ dry_run: bool=typer.Option(False, "--dry-run", help="Show what would be installed without writing files."),
3002
+ agent_target: str=typer.Option("current", "--agent-target", help="current, codex, claude, cursor, opencode, openclaw, hermes, or custom."),
3003
+ registry: Path | None=typer.Option(None, "--registry", help="Public ecosystem index JSON path for offline/test use."),
3004
+ storage: str | None=typer.Option(None, "--storage", help="Storage backend: forsy-r2 or github."),
3005
+ ):
3006
+ source_path = Path(item_id).expanduser()
3007
+ try:
3008
+ if source_path.exists():
3009
+ package = source_path
3010
+ source = str(source_path.name)
3011
+ else:
3012
+ package = _package_for_ecosystem_item(item_id, registry=registry, storage=storage)
3013
+ source = item_id
3014
+ result = install_runtime_training_from_bundle(source, package, settings=get_settings(), agent_target=agent_target, dry_run=dry_run)
3015
+ except Exception as exc:
3016
+ typer.echo(f"Could not install runtime training: {redact_secrets(str(exc))}")
3017
+ raise typer.Exit(1)
3018
+ if dry_run:
3019
+ typer.echo("Install Runtime Training dry run complete.")
3020
+ typer.echo(f"installed_skill_id: {result.get('installed_skill_id')}")
3021
+ typer.echo(f"target: {result.get('target')}")
3022
+ typer.echo("No files were installed.")
3023
+ return
3024
+ typer.echo("Install Runtime Training complete.")
3025
+ typer.echo(f"installed_skill_id: {result.get('installed_skill_id')}")
3026
+ typer.echo(f"enabled: {str(result.get('enabled')).lower()}")
3027
+ typer.echo("registry: local Agent Apprenticeship runtime")
3028
+
3029
+
3030
+ @learn_app.command("installed")
3031
+ def learn_installed(json_output: bool=typer.Option(False, "--json", help="Print installed runtime training as JSON.")):
3032
+ rows = list_installed_skills(get_settings())
3033
+ if json_output:
3034
+ typer.echo(json.dumps(rows, indent=2, sort_keys=True))
3035
+ return
3036
+ if not rows:
3037
+ typer.echo("Installed Runtime Training: none")
3038
+ return
3039
+ typer.echo("Installed Runtime Training")
3040
+ for row in rows:
3041
+ status = "enabled" if row.get("enabled", True) else "disabled"
3042
+ typer.echo(f"- {row.get('installed_skill_id')}: {row.get('title')} [{status}]")
3043
+
3044
+
3045
+ @learn_app.command("enable")
3046
+ def learn_enable(installed_skill_id: str=typer.Argument(..., help="Installed runtime training id.")):
3047
+ try:
3048
+ row = set_installed_skill_enabled(installed_skill_id, True, get_settings())
3049
+ except FileNotFoundError as exc:
3050
+ typer.echo(str(exc))
3051
+ raise typer.Exit(1)
3052
+ typer.echo("Runtime training enabled.")
3053
+ typer.echo(f"installed_skill_id: {row.get('installed_skill_id')}")
3054
+
3055
+
3056
+ @learn_app.command("disable")
3057
+ def learn_disable(installed_skill_id: str=typer.Argument(..., help="Installed runtime training id.")):
3058
+ try:
3059
+ row = set_installed_skill_enabled(installed_skill_id, False, get_settings())
3060
+ except FileNotFoundError as exc:
3061
+ typer.echo(str(exc))
3062
+ raise typer.Exit(1)
3063
+ typer.echo("Runtime training disabled.")
3064
+ typer.echo(f"installed_skill_id: {row.get('installed_skill_id')}")
3065
+
3066
+
3067
+ @learn_app.command("uninstall")
3068
+ def learn_uninstall(installed_skill_id: str=typer.Argument(..., help="Installed runtime training id.")):
3069
+ try:
3070
+ uninstall_installed_skill(installed_skill_id, get_settings())
3071
+ except FileNotFoundError as exc:
3072
+ typer.echo(str(exc))
3073
+ raise typer.Exit(1)
3074
+ typer.echo("Runtime training uninstalled.")
3075
+ typer.echo("registry: local Agent Apprenticeship runtime")
3076
+
3077
+
2622
3078
  @ecosystem_app.command('guide')
2623
3079
  def ecosystem_guide():
2624
3080
  typer.echo('Agent Apprenticeship Ecosystem')
2625
3081
  typer.echo('')
2626
- typer.echo('The public ecosystem is the open-source Agent Apprenticeship ecosystem repo.')
2627
- typer.echo('Run a task, then contribute the generated Contribution Bundle:')
3082
+ typer.echo('The public ecosystem stores shared Experience Compilations in Forsy Cloudflare R2.')
3083
+ typer.echo('Run a task, then share the generated Experience Compilation:')
2628
3084
  typer.echo(' apprentice ecosystem contribute <bundle_path>')
2629
3085
  typer.echo('')
2630
- typer.echo('Configure the public ecosystem repo:')
2631
- typer.echo(f' default: {DEFAULT_PUBLIC_ECOSYSTEM_REPO}')
2632
- typer.echo(' apprentice ecosystem configure --repo <owner>/<repo>')
2633
- typer.echo(' apprentice ecosystem configure --repo-path <public-ecosystem-repo-checkout>')
3086
+ typer.echo('Configure ecosystem storage:')
3087
+ typer.echo(' apprentice ecosystem configure --storage forsy-r2')
3088
+ typer.echo(' apprentice ecosystem configure --r2-api-url <worker-url>')
2634
3089
  typer.echo('')
2635
- typer.echo('Ecosystem Auto-Share is Manual by default.')
2636
- typer.echo(' apprentice ecosystem configure --auto-share ask')
2637
- typer.echo(' apprentice ecosystem configure --auto-share automatic')
3090
+ typer.echo('Agent Training Contribution Mode is Public Ecosystem by default.')
3091
+ typer.echo(' apprentice ecosystem configure --training-contribution public-ecosystem')
3092
+ typer.echo(' apprentice ecosystem configure --training-contribution private-internal-only')
2638
3093
  typer.echo('')
2639
- typer.echo('If GitHub CLI automation is unavailable, the contribute command prepares a local submission package and copy-paste-ready public issue text.')
2640
- typer.echo('Remote ecosystem sync is not included in v0. Public repo discovery works through the configured repo when `gh` can read its index; `--registry` can point at an exported public index JSON for tests or offline use.')
3094
+ typer.echo('Legacy GitHub issue sharing remains available only with:')
3095
+ typer.echo(' apprentice ecosystem contribute <bundle_path> --storage github')
2641
3096
  typer.echo(f'Community Slack: {SLACK_LINK}')
2642
3097
 
2643
3098
 
@@ -2645,28 +3100,40 @@ def ecosystem_guide():
2645
3100
  def ecosystem_configure(
2646
3101
  repo: str | None=typer.Option(None, '--repo', help='Public ecosystem repo in owner/name form.'),
2647
3102
  repo_path: Path | None=typer.Option(None, '--repo-path', help='Public ecosystem repo path for a local checkout/export.'),
2648
- auto_share: str | None=typer.Option(None, '--auto-share', help='Ecosystem Auto-Share mode: manual, ask, or automatic.'),
3103
+ apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
3104
+ training_contribution: str | None=typer.Option(None, '--training-contribution', help='public-ecosystem or private-internal-only.'),
3105
+ sharing_behavior: str | None=typer.Option(None, '--sharing-behavior', help='Public Ecosystem behavior: manual or automatic.'),
3106
+ auto_share: str | None=typer.Option(None, '--auto-share', help='Deprecated alias for --training-contribution.', hidden=True),
3107
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3108
+ r2_api_url: str | None=typer.Option(None, '--r2-api-url', help='Forsy R2 Worker ingestion API URL.'),
3109
+ r2_bucket: str | None=typer.Option(None, '--r2-bucket', help='R2 bucket name.'),
3110
+ r2_index_prefix: str | None=typer.Option(None, '--r2-index-prefix', help='R2 index prefix.'),
3111
+ r2_contribution_prefix: str | None=typer.Option(None, '--r2-contribution-prefix', help='R2 Experience Compilation storage prefix.'),
3112
+ r2_seed_prefix: str | None=typer.Option(None, '--r2-seed-prefix', help='R2 seed dataset prefix.'),
2649
3113
  ):
2650
3114
  settings = get_settings()
2651
- if repo is None and repo_path is None and auto_share is None:
3115
+ if all(v is None for v in [repo, repo_path, apprenticeship_mode, training_contribution, sharing_behavior, auto_share, storage, r2_api_url, r2_bucket, r2_index_prefix, r2_contribution_prefix, r2_seed_prefix]):
2652
3116
  status = _ecosystem_status()
2653
3117
  typer.echo("Public Ecosystem Configuration")
3118
+ typer.echo(f"Storage backend: {settings.ecosystem_storage_backend}")
3119
+ typer.echo(f"R2 ingestion API: {settings.r2_api_url or 'not configured'}")
3120
+ typer.echo(f"R2 bucket: {settings.r2_bucket}")
2654
3121
  typer.echo(f"Current repo: {settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO}")
2655
3122
  typer.echo(f"GitHub URL: {_ecosystem_repo_url(settings.ecosystem_repo)}")
2656
3123
  typer.echo(f"Current public ecosystem repo path: {settings.ecosystem_repo_path or 'Not configured'}")
2657
3124
  typer.echo(f"GitHub CLI installed: {_yes_no(status['gh_installed'])}")
2658
3125
  typer.echo(f"GitHub CLI authenticated: {_yes_no(status['gh_authenticated'])}")
2659
- typer.echo(f"Current Ecosystem Auto-Share: {ecosystem_auto_share_display(settings.ecosystem_auto_share)}")
3126
+ typer.echo(f"Current Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}")
3127
+ typer.echo(f"Current Agent Training Contribution Mode: {training_contribution_mode_display(settings.training_contribution_mode)}")
2660
3128
  entered_repo = typer.prompt("Public ecosystem repo, owner/name, optional", default=settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO, show_default=False).strip()
2661
3129
  if entered_repo:
2662
3130
  repo = entered_repo
2663
3131
  typer.echo("")
2664
- typer.echo("Choose Ecosystem Auto-Share mode:")
2665
- typer.echo("1. Manual")
2666
- typer.echo("2. Ask before sharing")
2667
- typer.echo("3. Share automatically")
2668
- choice = typer.prompt("Default", default=settings.ecosystem_auto_share).strip().lower()
2669
- auto_share = {"1": "manual", "2": "ask", "3": "automatic"}.get(choice, choice)
3132
+ typer.echo("Choose Agent Training Contribution Mode:")
3133
+ typer.echo("1. Public Ecosystem")
3134
+ typer.echo("2. Private Internal Only")
3135
+ choice = typer.prompt("Default", default=settings.training_contribution_mode).strip().lower()
3136
+ training_contribution = {"1": "public_ecosystem", "2": "private_internal_only"}.get(choice, choice)
2670
3137
  updates = {}
2671
3138
  candidate_repo = repo or settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO
2672
3139
  if repo is not None:
@@ -2683,23 +3150,31 @@ def ecosystem_configure(
2683
3150
  typer.echo(f"Public ecosystem repo path does not look like an Agent Apprenticeship ecosystem repo: {expanded}")
2684
3151
  raise typer.Exit(1)
2685
3152
  updates["ecosystem_repo_path"] = expanded
3153
+ if apprenticeship_mode is not None:
3154
+ updates["apprenticeship_mode"] = _normalize_apprenticeship_mode(apprenticeship_mode)
3155
+ if training_contribution is not None:
3156
+ updates["training_contribution_mode"] = _normalize_training_contribution(training_contribution)
3157
+ if sharing_behavior is not None:
3158
+ updates["ecosystem_auto_share"] = _normalize_auto_share(sharing_behavior)
3159
+ if _normalize_auto_share(sharing_behavior) != "disabled":
3160
+ updates["training_contribution_mode"] = "public_ecosystem"
3161
+ else:
3162
+ updates["training_contribution_mode"] = "private_internal_only"
2686
3163
  if auto_share is not None:
2687
- mode = _normalize_auto_share(auto_share)
2688
- if mode in {"ask", "automatic"}:
2689
- readiness = _ecosystem_auto_share_readiness(candidate_repo)
2690
- if not readiness["ready"]:
2691
- typer.echo("Ecosystem Auto-Share cannot be enabled yet.")
2692
- typer.echo("")
2693
- typer.echo("Ecosystem Auto-Share is configured for:")
2694
- typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url(candidate_repo)))
2695
- typer.echo("")
2696
- typer.echo("Required:")
2697
- next_action = str(readiness.get("next_action") or "gh auth login")
2698
- typer.echo(f"- {next_action}")
2699
- if readiness.get("reason"):
2700
- typer.echo(f"Reason: {readiness['reason']}")
2701
- raise typer.Exit(1)
2702
- updates["ecosystem_auto_share"] = mode
3164
+ updates["ecosystem_auto_share"] = _normalize_auto_share(auto_share)
3165
+ updates["training_contribution_mode"] = "public_ecosystem" if _normalize_auto_share(auto_share) != "disabled" else "private_internal_only"
3166
+ if storage is not None:
3167
+ updates["ecosystem_storage_backend"] = normalize_ecosystem_storage(storage)
3168
+ if r2_api_url is not None:
3169
+ updates["r2_api_url"] = r2_api_url.rstrip("/")
3170
+ if r2_bucket is not None:
3171
+ updates["r2_bucket"] = r2_bucket
3172
+ if r2_index_prefix is not None:
3173
+ updates["r2_index_prefix"] = r2_index_prefix.strip("/")
3174
+ if r2_contribution_prefix is not None:
3175
+ updates["r2_contribution_prefix"] = r2_contribution_prefix.strip("/")
3176
+ if r2_seed_prefix is not None:
3177
+ updates["r2_seed_prefix"] = r2_seed_prefix.strip("/")
2703
3178
  if not updates:
2704
3179
  typer.echo("No ecosystem settings changed.")
2705
3180
  return
@@ -2708,8 +3183,21 @@ def ecosystem_configure(
2708
3183
  typer.echo(f"Public ecosystem repo configured: {repo}")
2709
3184
  if repo_path is not None:
2710
3185
  typer.echo(f"Public ecosystem repo path configured: {Path(repo_path).expanduser()}")
2711
- if auto_share is not None:
2712
- typer.echo(f"Ecosystem Auto-Share: {ecosystem_auto_share_display(updated.ecosystem_auto_share)}")
3186
+ if apprenticeship_mode is not None:
3187
+ typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(updated.apprenticeship_mode)}")
3188
+ if training_contribution is not None or sharing_behavior is not None or auto_share is not None:
3189
+ typer.echo(f"Agent Training Contribution Mode: {training_contribution_mode_display(updated.training_contribution_mode)}")
3190
+ if updated.training_contribution_mode == "public_ecosystem":
3191
+ typer.echo(f"Public Ecosystem sharing behavior: {ecosystem_auto_share_display(updated.ecosystem_auto_share)}")
3192
+ if storage is not None:
3193
+ typer.echo(f"Storage backend: {updated.ecosystem_storage_backend}")
3194
+ if r2_api_url is not None:
3195
+ typer.echo(f"R2 ingestion API: {updated.r2_api_url or 'not configured'}")
3196
+ if any(v is not None for v in [r2_bucket, r2_index_prefix, r2_contribution_prefix, r2_seed_prefix]):
3197
+ typer.echo(f"R2 bucket: {updated.r2_bucket}")
3198
+ typer.echo(f"R2 index prefix: {updated.r2_index_prefix}")
3199
+ typer.echo(f"R2 Experience Compilation prefix: {updated.r2_contribution_prefix}")
3200
+ typer.echo(f"R2 seed prefix: {updated.r2_seed_prefix}")
2713
3201
  typer.echo("Next: apprentice ecosystem status")
2714
3202
 
2715
3203
 
@@ -2718,32 +3206,116 @@ def ecosystem_status_command():
2718
3206
  status = _ecosystem_status()
2719
3207
  settings = get_settings()
2720
3208
  typer.echo("Public Ecosystem Status")
2721
- typer.echo(f"Public Ecosystem Repo: {status['repo'] or DEFAULT_PUBLIC_ECOSYSTEM_REPO}")
3209
+ typer.echo(f"Storage backend: {status['storage_backend']}")
3210
+ typer.echo(f"R2 ingestion API: {status.get('r2_api_url') or 'not configured'}")
3211
+ typer.echo(f"R2 bucket: {status['r2_bucket']}")
3212
+ typer.echo(f"R2 index prefix: {status['r2_index_prefix']}")
3213
+ typer.echo(f"R2 Experience Compilation prefix: {status['r2_contribution_prefix']}")
3214
+ typer.echo(f"R2 seed prefix: {status['r2_seed_prefix']}")
3215
+ typer.echo(f"GitHub fallback repo: {status['repo'] or DEFAULT_PUBLIC_ECOSYSTEM_REPO}")
2722
3216
  typer.echo(f"GitHub URL: {status['repo_url']}")
2723
3217
  typer.echo(f"Public ecosystem repo path: {status.get('repo_path') or 'Not configured'}")
2724
3218
  if status.get("repo_path"):
2725
3219
  typer.echo(f"Public ecosystem repo path exists: {_yes_no(status.get('repo_path_exists'))}")
2726
3220
  typer.echo(f"GitHub CLI installed: {_yes_no(status['gh_installed'])}")
2727
3221
  typer.echo(f"GitHub CLI authenticated: {_yes_no(status['gh_authenticated'])}")
2728
- typer.echo(f"Automatic public contribution: {_yes_no(status['automatic_contribution_ready'])}")
2729
- typer.echo(f"Ecosystem Auto-Share: {ecosystem_auto_share_display(settings.ecosystem_auto_share)}")
2730
- if settings.ecosystem_auto_share != "manual":
3222
+ typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}")
3223
+ typer.echo(f"Agent Training Contribution Mode: {training_contribution_mode_display(settings.training_contribution_mode)}")
3224
+ typer.echo(f"Local ecosystem user: {status.get('ecosystem_user_id')}")
3225
+ typer.echo(f"Shared contributions: {status.get('shared_count', 0)}")
3226
+ if settings.training_contribution_mode == "public_ecosystem":
2731
3227
  readiness = _ecosystem_auto_share_readiness()
2732
- typer.echo(f"Ecosystem Auto-Share Status: {'Ready' if readiness['ready'] else 'Not ready - ' + str(readiness.get('reason') or 'not ready')}")
2733
- if not status["gh_installed"]:
2734
- typer.echo("Next: install GitHub CLI (`gh`) or use `apprentice ecosystem contribute <bundle>` for manual submission files.")
2735
- elif not status["gh_authenticated"]:
2736
- typer.echo("Next: run `gh auth login`, or use the manual submission package generated by `apprentice ecosystem contribute <bundle>`.")
3228
+ typer.echo(f"Public Ecosystem upload status: {'Ready' if readiness['ready'] else 'Not ready - ' + str(readiness.get('reason') or 'not ready')}")
3229
+
3230
+
3231
+ @ecosystem_app.command('identity')
3232
+ def ecosystem_identity():
3233
+ settings = get_settings()
3234
+ identity = get_or_create_ecosystem_identity(settings)
3235
+ typer.echo("Local ecosystem identity")
3236
+ typer.echo(f"ecosystem_user_id: {identity.get('ecosystem_user_id')}")
3237
+ typer.echo(f"installation_id: {identity.get('installation_id')}")
3238
+ typer.echo(f"created_at: {identity.get('created_at')}")
3239
+ typer.echo(f"shared_contributions: {len(shared_history(settings))}")
3240
+
3241
+
3242
+ def _print_shared_rows(rows: list[dict[str, Any]]) -> None:
3243
+ if not rows:
3244
+ typer.echo("No shared Experience Compilations recorded yet.")
3245
+ return
3246
+ for row in rows:
3247
+ typer.echo(f"{row.get('item_id')}: {row.get('title')} [{row.get('storage_backend')} · {row.get('uploaded_at')}]")
3248
+
3249
+
3250
+ @ecosystem_shared_app.callback(invoke_without_command=True)
3251
+ def ecosystem_shared(
3252
+ ctx: typer.Context,
3253
+ json_output: bool = typer.Option(False, "--json", help="Print local shared Experience Compilation history as JSON."),
3254
+ ):
3255
+ if ctx.invoked_subcommand is not None:
3256
+ return
3257
+ rows = shared_history(get_settings())
3258
+ if json_output:
3259
+ typer.echo(json.dumps(rows, indent=2, sort_keys=True))
3260
+ return
3261
+ _print_shared_rows(rows)
3262
+
3263
+
3264
+ def _local_shared_record(item_id: str) -> dict[str, Any]:
3265
+ for row in shared_history(get_settings()):
3266
+ if item_id in {str(row.get("item_id") or ""), str(row.get("package_id") or "")}:
3267
+ return row
3268
+ raise FileNotFoundError(f"Shared contribution not found locally: {item_id}")
3269
+
3270
+
3271
+ @ecosystem_shared_app.command("inspect")
3272
+ def ecosystem_shared_inspect(item_id: str = typer.Argument(...)):
3273
+ try:
3274
+ row = _local_shared_record(item_id)
3275
+ except FileNotFoundError as exc:
3276
+ typer.echo(str(exc))
3277
+ raise typer.Exit(1)
3278
+ typer.echo(f"item_id: {row.get('item_id')}")
3279
+ typer.echo(f"package_id: {row.get('package_id')}")
3280
+ typer.echo(f"title: {row.get('title')}")
3281
+ typer.echo(f"storage_backend: {row.get('storage_backend')}")
3282
+ typer.echo(f"url: {row.get('url')}")
3283
+ typer.echo(f"local_bundle_path: {row.get('local_bundle_path')}")
3284
+
3285
+
3286
+ @ecosystem_shared_app.command("pull")
3287
+ def ecosystem_shared_pull(item_id: str = typer.Argument(...)):
3288
+ try:
3289
+ row = _local_shared_record(item_id)
3290
+ except FileNotFoundError as exc:
3291
+ typer.echo(str(exc))
3292
+ raise typer.Exit(1)
3293
+ local = Path(str(row.get("local_bundle_path") or "")).expanduser()
3294
+ dest = _prepare_ecosystem_pull_destination(str(row.get("item_id") or item_id))
3295
+ if local.exists():
3296
+ if dest.exists():
3297
+ shutil.rmtree(dest)
3298
+ shutil.copytree(local, dest)
3299
+ typer.echo(f"pulled_bundle={dest}")
3300
+ return
3301
+ try:
3302
+ download_r2_package(str(row.get("item_id") or item_id), dest, get_settings())
3303
+ except Exception as exc:
3304
+ typer.echo(f"Could not pull shared Experience Compilation: {redact_secrets(str(exc))}")
3305
+ raise typer.Exit(1)
3306
+ typer.echo(f"pulled_bundle={dest}")
2737
3307
 
2738
3308
 
2739
3309
  @ecosystem_app.command('contribute')
2740
3310
  def ecosystem_contribute(
2741
- bundle_path: Path=typer.Argument(..., help='Contribution Bundle folder.'),
3311
+ bundle_path: Path=typer.Argument(..., help='Experience Compilation folder.'),
2742
3312
  repo: str | None=typer.Option(None, '--repo', help='Public ecosystem repo in owner/name form.'),
2743
- dry_run: bool=typer.Option(False, '--dry-run', help='Prepare files and show the GitHub command without creating an issue.'),
3313
+ dry_run: bool=typer.Option(False, '--dry-run', help='Prepare upload package without uploading.'),
3314
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3315
+ qa: bool=typer.Option(False, '--qa', help='Use QA storage path when supported.'),
2744
3316
  ):
2745
3317
  try:
2746
- result = _ecosystem_contribute_impl(bundle_path, repo=repo, dry_run=dry_run, auto_share_mode="manual")
3318
+ result = _ecosystem_contribute_impl(bundle_path, repo=repo, dry_run=dry_run, auto_share_mode="manual", storage=storage, qa=qa)
2747
3319
  except Exception as exc:
2748
3320
  typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
2749
3321
  raise typer.Exit(1)
@@ -2753,13 +3325,58 @@ def ecosystem_contribute(
2753
3325
  metadata = result["metadata"]
2754
3326
  target_repo = result["target_repo"]
2755
3327
  issue_body = result["issue_body"]
2756
- typer.echo("Public ecosystem contribution prepared.")
3328
+ if result.get("storage_backend") == "forsy-r2":
3329
+ item = result.get("ecosystem_item") or {}
3330
+ storage_ref = result.get("storage_ref") or {}
3331
+ if dry_run:
3332
+ typer.echo("Forsy ecosystem upload dry run complete.")
3333
+ typer.echo("No files were uploaded.")
3334
+ typer.echo("")
3335
+ typer.echo("Storage backend:")
3336
+ typer.echo("Cloudflare R2")
3337
+ typer.echo("")
3338
+ typer.echo("Ingestion API:")
3339
+ typer.echo(str(storage_ref.get("ingestion_api_url") or "not configured"))
3340
+ typer.echo("")
3341
+ typer.echo("Local ecosystem user:")
3342
+ typer.echo(str(storage_ref.get("ecosystem_user_id")))
3343
+ typer.echo("")
3344
+ typer.echo("Prepared package:")
3345
+ typer.echo(str(package_zip))
3346
+ typer.echo("")
3347
+ typer.echo("Storage reference:")
3348
+ typer.echo("ecosystem_submission/storage_ref.json")
3349
+ return
3350
+ if result["contribution_created"]:
3351
+ typer.echo("Experience Compilation uploaded to the Forsy ecosystem.")
3352
+ typer.echo("")
3353
+ typer.echo("Item:")
3354
+ typer.echo(str(item.get("id")))
3355
+ typer.echo("")
3356
+ typer.echo("Local ecosystem user:")
3357
+ typer.echo(str(storage_ref.get("ecosystem_user_id")))
3358
+ typer.echo("")
3359
+ typer.echo("Storage:")
3360
+ typer.echo(f"Cloudflare R2 / {(item.get('storage') or {}).get('path_prefix')}")
3361
+ typer.echo("")
3362
+ typer.echo("URL:")
3363
+ typer.echo(str(result["contribution_url"]))
3364
+ return
3365
+ typer.echo("Forsy ecosystem upload package prepared.")
3366
+ typer.echo(f"Reason upload did not run: {result.get('skipped_reason') or 'not uploaded'}")
3367
+ typer.echo(f"Prepared package: {package_zip}")
3368
+ typer.echo("Storage reference: ecosystem_submission/storage_ref.json")
3369
+ typer.echo("Retry after configuring the Worker URL:")
3370
+ typer.echo("apprentice ecosystem configure --r2-api-url <url>")
3371
+ return
3372
+
3373
+ typer.echo("Legacy GitHub ecosystem contribution prepared.")
2757
3374
  typer.echo("")
2758
3375
  typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
2759
3376
  typer.echo(f"Title: {metadata.get('title')}")
2760
- typer.echo(f"Bundle: {bundle}")
2761
- typer.echo(f"Submission package: {package_zip}")
2762
- typer.echo(f"Submission metadata: {submission_dir / 'ecosystem_submission.json'}")
3377
+ typer.echo(f"Bundle folder: {bundle.name}")
3378
+ typer.echo(f"Submission package: {package_zip.name}")
3379
+ typer.echo("Submission metadata: ecosystem_submission/ecosystem_submission.json")
2763
3380
  if result["contribution_created"]:
2764
3381
  typer.echo(f"Public contribution URL: {result['contribution_url']}")
2765
3382
  typer.echo("No bundle files were uploaded automatically. Attach or submit the generated package according to the repo instructions.")
@@ -2769,9 +3386,9 @@ def ecosystem_contribute(
2769
3386
  typer.echo(str(result.get("gh_error") or ""))
2770
3387
  elif target_repo and _gh_path() and _gh_authenticated() and dry_run:
2771
3388
  typer.echo("Dry run: no GitHub issue was created.")
2772
- typer.echo(f"Would run: gh issue create --repo {target_repo} --title \"Contribution Bundle: {metadata.get('bundle_id')}\" --body-file {issue_body}")
3389
+ typer.echo(f"Would run: gh issue create --repo {target_repo} --title \"Experience Compilation: {metadata.get('bundle_id')}\" --body-file GITHUB_ISSUE_BODY.md")
2773
3390
  else:
2774
- typer.echo("No upload was performed in v0.")
3391
+ typer.echo("No GitHub issue was created.")
2775
3392
  typer.echo(f"Public ecosystem: {_ecosystem_repo_url(target_repo)}")
2776
3393
  if not _gh_path():
2777
3394
  typer.echo("GitHub CLI `gh` is not installed.")
@@ -2780,19 +3397,31 @@ def ecosystem_contribute(
2780
3397
  typer.echo("")
2781
3398
  typer.echo("Manual public submission steps:")
2782
3399
  typer.echo(f"1. Open {_ecosystem_repo_url(target_repo)} in GitHub.")
2783
- typer.echo(f"2. Create a new issue using: {issue_body}")
2784
- typer.echo(f"3. Attach or share the submission package: {package_zip}")
3400
+ typer.echo("2. Create a new issue using: ecosystem_submission/GITHUB_ISSUE_BODY.md")
3401
+ typer.echo(f"3. Attach or share the submission package: {package_zip.name}")
2785
3402
  typer.echo(f"Community Slack: {SLACK_LINK}")
2786
3403
 
2787
3404
 
2788
3405
  @ecosystem_app.command('list')
2789
- def ecosystem_list(registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.')):
3406
+ def ecosystem_list(
3407
+ registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
3408
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3409
+ mine: bool=typer.Option(False, '--mine', help='Show items shared by this local ecosystem user.'),
3410
+ domain: str | None=typer.Option(None, '--domain', help='Filter by domain.'),
3411
+ subdomain: str | None=typer.Option(None, '--subdomain', help='Filter by subdomain.'),
3412
+ tag: str | None=typer.Option(None, '--tag', help='Filter by tag.'),
3413
+ json_output: bool=typer.Option(False, '--json', help='Print matching Experience Compilations as JSON.'),
3414
+ ):
2790
3415
  try:
2791
- rows=_load_registry(registry)
3416
+ rows=_load_registry(registry, storage=storage)
2792
3417
  except FileNotFoundError as exc:
2793
3418
  typer.echo(str(exc))
2794
3419
  raise typer.Exit(1)
2795
- _print_registry_rows(rows)
3420
+ if mine:
3421
+ rows = [row for row in rows if _matches_registry_row(row, None, {"mine": True})]
3422
+ filters = {"domain": domain, "subdomain": subdomain, "tag": tag}
3423
+ rows = [row for row in rows if _matches_registry_row(row, None, filters)]
3424
+ _print_registry_rows(rows, json_output=json_output)
2796
3425
 
2797
3426
 
2798
3427
  @ecosystem_app.command('search')
@@ -2800,21 +3429,27 @@ def ecosystem_search(
2800
3429
  query_arg: str | None=typer.Argument(None, help='Free-text query.'),
2801
3430
  query: str | None=typer.Option(None, '--query', help='Free-text query.'),
2802
3431
  registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
3432
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3433
+ mine: bool=typer.Option(False, '--mine', help='Search items shared by this local ecosystem user.'),
2803
3434
  title: str | None=typer.Option(None, '--title'),
2804
3435
  domain: str | None=typer.Option(None, '--domain'),
2805
3436
  subdomain: str | None=typer.Option(None, '--subdomain'),
2806
- mentor_mode: str | None=typer.Option(None, '--mentor-mode'),
3437
+ apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
3438
+ mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
2807
3439
  task_status: str | None=typer.Option(None, '--task-status'),
2808
3440
  run_status: str | None=typer.Option(None, '--run-status'),
2809
3441
  min_traced_steps: int | None=typer.Option(None, '--min-traced-steps'),
2810
3442
  max_traced_steps: int | None=typer.Option(None, '--max-traced-steps'),
2811
3443
  expected_economic_value: str | None=typer.Option(None, '--expected-economic-value'),
3444
+ tag: str | None=typer.Option(None, '--tag'),
2812
3445
  artifact_type: str | None=typer.Option(None, '--artifact-type'),
2813
3446
  apprentice_agent: str | None=typer.Option(None, '--apprentice-agent'),
2814
3447
  mentor_model_provider: str | None=typer.Option(None, '--mentor-model-provider'),
3448
+ json_output: bool=typer.Option(False, '--json', help='Print matching Experience Compilations as JSON.'),
3449
+ select: bool=typer.Option(False, '--select', help='Interactively choose a matching Experience Compilation and action.'),
2815
3450
  ):
2816
3451
  try:
2817
- rows=_load_registry(registry)
3452
+ rows=_load_registry(registry, storage=storage)
2818
3453
  except FileNotFoundError as exc:
2819
3454
  typer.echo(str(exc))
2820
3455
  raise typer.Exit(1)
@@ -2823,14 +3458,25 @@ def ecosystem_search(
2823
3458
  filters.pop('query_arg', None)
2824
3459
  filters.pop('query', None)
2825
3460
  filters.pop('registry', None)
3461
+ filters.pop('storage', None)
2826
3462
  filters.pop('effective_query', None)
2827
- _print_registry_rows([row for row in rows if _matches_registry_row(row, effective_query, filters)])
3463
+ filters["mine"] = mine
3464
+ if mentor_mode and not apprenticeship_mode:
3465
+ filters["apprenticeship_mode"] = mentor_mode
3466
+ _print_registry_rows([row for row in rows if _matches_registry_row(row, effective_query, filters)], json_output=json_output, select=select)
2828
3467
 
2829
3468
 
2830
- def _find_registry_row(bundle_id: str, registry: Path | None=None) -> dict:
2831
- rows=_load_registry(registry)
3469
+ def _find_registry_row(bundle_id: str, registry: Path | None=None, storage: str | None = None) -> dict:
3470
+ rows=_load_registry(registry, storage=storage)
2832
3471
  for row in rows:
2833
- if row.get('bundle_id') == bundle_id:
3472
+ ids = {
3473
+ str(row.get("id") or ""),
3474
+ str(row.get("bundle_id") or ""),
3475
+ str(row.get("package_id") or ""),
3476
+ str(row.get("seed_task_id") or ""),
3477
+ str(row.get("task_id") or ""),
3478
+ }
3479
+ if bundle_id in ids:
2834
3480
  return row
2835
3481
  raise FileNotFoundError(f'Bundle id not found in registry: {bundle_id}')
2836
3482
 
@@ -2850,16 +3496,113 @@ def _prepare_ecosystem_pull_destination(bundle_id: str) -> Path:
2850
3496
  return dest
2851
3497
 
2852
3498
 
3499
+ def _locate_package_root(path: Path) -> Path:
3500
+ path = path.expanduser()
3501
+ if path.is_file():
3502
+ path = path.parent
3503
+ candidates = [path, path / "contribution_bundle", path / "package"]
3504
+ for candidate in candidates:
3505
+ if (candidate / "contribution_manifest.json").exists() or (candidate / "experience_compiler").exists():
3506
+ return candidate
3507
+ for manifest in path.rglob("contribution_manifest.json"):
3508
+ return manifest.parent
3509
+ return path
3510
+
3511
+
3512
+ def _copy_or_zip_package(src: Path, output: Path, *, item_id: str, source: str) -> Path:
3513
+ src = _locate_package_root(src)
3514
+ output = output.expanduser()
3515
+ manifest = {
3516
+ "export_type": "full_experience_compilation",
3517
+ "item_id": item_id,
3518
+ "source": source,
3519
+ "exported_at": datetime.now(timezone.utc).isoformat(),
3520
+ "package_root": "package",
3521
+ "includes": [
3522
+ "traces",
3523
+ "attempts",
3524
+ "artifacts",
3525
+ "outputs",
3526
+ "evaluation",
3527
+ "learning_signals",
3528
+ "experience_compiler",
3529
+ "skill_pack",
3530
+ "runtime_learning_package",
3531
+ "training_time_package",
3532
+ "environment",
3533
+ "manifests",
3534
+ ],
3535
+ }
3536
+ if output.suffix == ".zip":
3537
+ staging = output.parent / f".{output.stem}-export"
3538
+ if staging.exists():
3539
+ shutil.rmtree(staging)
3540
+ (staging / "package").mkdir(parents=True)
3541
+ shutil.copytree(src, staging / "package", dirs_exist_ok=True)
3542
+ write_json(staging / "export_manifest.json", manifest)
3543
+ _zip_dir(staging, output)
3544
+ shutil.rmtree(staging)
3545
+ return output
3546
+ if output.exists():
3547
+ shutil.rmtree(output)
3548
+ output.mkdir(parents=True)
3549
+ shutil.copytree(src, output / "package", dirs_exist_ok=True)
3550
+ write_json(output / "export_manifest.json", manifest)
3551
+ return output
3552
+
3553
+
3554
+ def _package_for_ecosystem_item(item_id: str, *, registry: Path | None = None, storage: str | None = None) -> Path:
3555
+ direct = Path(item_id).expanduser()
3556
+ if direct.exists():
3557
+ return _locate_package_root(direct)
3558
+ local = get_settings().app_home / "ecosystem" / "bundles" / item_id
3559
+ if local.exists():
3560
+ return _locate_package_root(local)
3561
+ backend = normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend)
3562
+ if backend == "forsy-r2" and registry is None:
3563
+ dest = _prepare_ecosystem_pull_destination(item_id)
3564
+ download_r2_package(item_id, dest, get_settings())
3565
+ return _locate_package_root(dest)
3566
+ row = _find_registry_row(item_id, registry, storage=storage)
3567
+ src = row.get("local_bundle_path") or row.get("path") or row.get("bundle_path") or row.get("bundle_path_or_url")
3568
+ if src and Path(str(src)).expanduser().exists():
3569
+ return _locate_package_root(Path(str(src)).expanduser())
3570
+ raise FileNotFoundError(f"No full package is available for ecosystem item: {item_id}")
3571
+
3572
+
2853
3573
  @ecosystem_app.command('pull')
2854
3574
  def ecosystem_pull(
2855
3575
  bundle_id: str=typer.Argument(...),
2856
3576
  registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
3577
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3578
+ full: bool=typer.Option(False, '--full', help='Export the Full Experience Compilation after pulling.'),
3579
+ runtime: bool=typer.Option(False, '--runtime', help='Pull runtime training subset for install.'),
3580
+ install: bool=typer.Option(False, '--install', help='Install runtime training after pulling.'),
3581
+ mode: str | None=typer.Option(None, '--mode', help='full or runtime.'),
2857
3582
  ):
2858
3583
  try:
2859
- row=_find_registry_row(bundle_id, registry)
3584
+ row=_find_registry_row(bundle_id, registry, storage=storage)
2860
3585
  except FileNotFoundError as exc:
2861
3586
  typer.echo(str(exc))
2862
3587
  raise typer.Exit(1)
3588
+ backend = normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend)
3589
+ if backend == "forsy-r2" and not registry:
3590
+ dest=_prepare_ecosystem_pull_destination(_row_id(row) or bundle_id)
3591
+ try:
3592
+ download_r2_package(str(row.get("id") or bundle_id), dest, get_settings())
3593
+ except Exception as exc:
3594
+ typer.echo(f"Could not pull ecosystem item from R2: {redact_secrets(str(exc))}")
3595
+ typer.echo("Fallback: apprentice ecosystem pull " + bundle_id + " --storage github")
3596
+ raise typer.Exit(1)
3597
+ typer.echo(f'pulled_bundle={dest}')
3598
+ if full or mode == "full":
3599
+ export_path = _copy_or_zip_package(dest, get_settings().app_home / "ecosystem" / "exports" / (_row_id(row) or bundle_id), item_id=_row_id(row) or bundle_id, source="r2_pull")
3600
+ typer.echo(f"full_experience_compilation={export_path}")
3601
+ if install or runtime or mode == "runtime":
3602
+ manifest = install_runtime_training_from_bundle(_row_id(row) or bundle_id, dest, settings=get_settings())
3603
+ typer.echo("Runtime training installed.")
3604
+ typer.echo(f"installed_skill_id: {manifest.get('installed_skill_id')}")
3605
+ return
2863
3606
  src=row.get('local_bundle_path') or row.get('path') or row.get('bundle_path') or row.get('bundle_path_or_url')
2864
3607
  if not src:
2865
3608
  task_path = row.get("task_path") or row.get("task_packet_path")
@@ -2927,6 +3670,39 @@ def ecosystem_pull(
2927
3670
  typer.echo('Next action: set AA_HOME to a writable directory and try again.')
2928
3671
  raise typer.Exit(1)
2929
3672
  typer.echo(f'pulled_bundle={dest}')
3673
+ if full or mode == "full":
3674
+ export_path = _copy_or_zip_package(dest, get_settings().app_home / "ecosystem" / "exports" / bundle_id, item_id=bundle_id, source="local_pull")
3675
+ typer.echo(f"full_experience_compilation={export_path}")
3676
+ if install or runtime or mode == "runtime":
3677
+ try:
3678
+ manifest = install_runtime_training_from_bundle(bundle_id, dest, settings=get_settings())
3679
+ except Exception as exc:
3680
+ typer.echo(f"Could not install runtime training: {redact_secrets(str(exc))}")
3681
+ raise typer.Exit(1)
3682
+ typer.echo("Runtime training installed.")
3683
+ typer.echo(f"installed_skill_id: {manifest.get('installed_skill_id')}")
3684
+
3685
+
3686
+ @ecosystem_app.command('export')
3687
+ def ecosystem_export(
3688
+ item_id: str=typer.Argument(..., help='Ecosystem item id.'),
3689
+ output: Path=typer.Option(..., '--output', '-o', help='Output folder or .zip path.'),
3690
+ full: bool=typer.Option(False, '--full', help='Export the Full Experience Compilation.'),
3691
+ mode: str | None=typer.Option(None, '--mode', help='Export mode: full.'),
3692
+ registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
3693
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3694
+ ):
3695
+ if not full and mode not in {None, "full"}:
3696
+ raise typer.BadParameter("Only Full Experience Compilation export is supported for this command.")
3697
+ try:
3698
+ package = _package_for_ecosystem_item(item_id, registry=registry, storage=storage)
3699
+ exported = _copy_or_zip_package(package, output, item_id=item_id, source="ecosystem_export")
3700
+ except Exception as exc:
3701
+ typer.echo(f"Could not export Full Experience Compilation: {redact_secrets(str(exc))}")
3702
+ raise typer.Exit(1)
3703
+ typer.echo("Export Full Experience Compilation complete.")
3704
+ typer.echo(f"item_id: {item_id}")
3705
+ typer.echo(f"output: {exported}")
2930
3706
 
2931
3707
 
2932
3708
  def _summarize_bundle_manifest(manifest: dict) -> str:
@@ -2934,14 +3710,16 @@ def _summarize_bundle_manifest(manifest: dict) -> str:
2934
3710
  'bundle_id','title','attempts','traced_steps','process_supervision_rows',
2935
3711
  'reward_modeling_rows','revision_preference_pairs','domains','subdomains',
2936
3712
  'agent_apprentice_role','expected_economic_value',
2937
- 'expected_economic_value_for_agent_apprentice','mentor_mode','task_status','run_status'
3713
+ 'expected_economic_value_for_agent_apprentice','task_status','run_status'
2938
3714
  ]
2939
3715
  lines = []
2940
3716
  for key in keys:
2941
3717
  if key not in manifest:
2942
3718
  continue
2943
- value = mentor_mode_display(manifest.get(key)) if key == "mentor_mode" else manifest.get(key)
2944
- lines.append(f"{key}: {value}")
3719
+ lines.append(f"{key}: {manifest.get(key)}")
3720
+ mode = manifest.get("apprenticeship_mode") or manifest.get("mentor_mode")
3721
+ if mode:
3722
+ lines.append(f"apprenticeship_mode: {apprenticeship_mode_display(normalize_apprenticeship_mode(str(mode)))}")
2945
3723
  return "\n".join(lines)
2946
3724
 
2947
3725
 
@@ -2966,21 +3744,23 @@ def _summarize_seed_registry_row(row: dict) -> str:
2966
3744
  lines.append(f"domains: {row.get('domains')}")
2967
3745
  if row.get("subdomains"):
2968
3746
  lines.append(f"subdomains: {row.get('subdomains')}")
2969
- lines.append("note: this is a seed task, not a completed Contribution Bundle")
3747
+ lines.append("note: this is a seed task, not a completed Experience Compilation")
2970
3748
  return "\n".join(lines)
2971
3749
 
2972
3750
 
2973
3751
  def _summarize_contribution_registry_row(row: dict) -> str:
3752
+ item_id = _row_id(row)
3753
+ compiler = row.get("experience_compiler") or {}
3754
+ available = row.get("available_outputs") or {}
2974
3755
  lines = [
2975
- "kind: contribution_bundle",
2976
- f"bundle_id: {row.get('bundle_id')}",
3756
+ f"kind: {row.get('kind') or 'agent_experience_package'}",
3757
+ f"item_id: {item_id}",
2977
3758
  f"title: {row.get('title')}",
2978
3759
  f"task_status: {row.get('task_status') or 'unknown'}",
2979
3760
  f"run_status: {row.get('run_status') or 'unknown'}",
2980
3761
  ]
2981
3762
  for key in [
2982
3763
  "apprentice_agent",
2983
- "mentor_mode",
2984
3764
  "mentor_model_provider",
2985
3765
  "attempts",
2986
3766
  "traced_steps",
@@ -2992,26 +3772,119 @@ def _summarize_contribution_registry_row(row: dict) -> str:
2992
3772
  "expected_economic_value_for_agent_apprentice",
2993
3773
  ]:
2994
3774
  if row.get(key) is not None:
2995
- value = mentor_mode_display(row.get(key)) if key == "mentor_mode" else row.get(key)
2996
- lines.append(f"{key}: {value}")
3775
+ lines.append(f"{key}: {row.get(key)}")
3776
+ mode = row.get("apprenticeship_mode") or row.get("mentor_mode")
3777
+ if mode:
3778
+ lines.append(f"apprenticeship_mode: {apprenticeship_mode_display(normalize_apprenticeship_mode(str(mode)))}")
2997
3779
  if row.get("domains"):
2998
3780
  lines.append(f"domains: {row.get('domains')}")
2999
3781
  if row.get("subdomains"):
3000
3782
  lines.append(f"subdomains: {row.get('subdomains')}")
3783
+ lines.extend(
3784
+ [
3785
+ f"has Full Experience Compilation: {'yes' if available.get('full_experience_compilation', available.get('full_training_package', row.get('kind') == 'agent_experience_package')) else 'no'}",
3786
+ f"has runtime training: {'yes' if available.get('runtime_training', row.get('kind') == 'agent_experience_package') else 'no'}",
3787
+ f"skill title: {compiler.get('skill_title') or row.get('title') or 'unknown'}",
3788
+ f"skill summary: {compiler.get('skill_summary') or compiler.get('runtime_training_summary') or 'available when Experience Compiler outputs are present'}",
3789
+ f"environment domain: {compiler.get('environment_domain') or row.get('environment_domain') or row.get('domain') or 'unknown'}",
3790
+ f"Experience Compiler status: {compiler.get('status') or row.get('experience_compiler_status') or row.get('tdo_status') or 'unknown'}",
3791
+ ]
3792
+ )
3793
+ installed = [
3794
+ skill for skill in list_installed_skills(get_settings())
3795
+ if item_id in {str(skill.get("source_item_id") or ""), str(skill.get("source_package_id") or "")}
3796
+ ]
3797
+ lines.append(f"local installed status: {'installed' if installed else 'not installed'}")
3798
+ lines.extend(
3799
+ [
3800
+ "",
3801
+ "Available actions:",
3802
+ "",
3803
+ "1. Export Full Experience Compilation",
3804
+ " Use this when you want the complete traces, artifacts, training data, evaluation data, runtime learning, and generated data packages for research/training/evaluation pipelines.",
3805
+ " Command:",
3806
+ f" apprentice ecosystem export {item_id} --full --output <path>",
3807
+ "",
3808
+ "2. Install Runtime Training",
3809
+ " Use this when you want your Apprentice Agent to use the compiled skill/runtime learning in future runs.",
3810
+ " Command:",
3811
+ f" apprentice learn install {item_id}",
3812
+ ]
3813
+ )
3001
3814
  return "\n".join(lines)
3002
3815
 
3003
3816
 
3817
+ def _local_package_registry_row(path: Path) -> dict:
3818
+ package = _locate_package_root(path)
3819
+ manifest = {}
3820
+ for candidate in [
3821
+ package / "contribution_manifest.json",
3822
+ package / "task_manifest.json",
3823
+ package / "package_manifest.json",
3824
+ package / "manifest.json",
3825
+ ]:
3826
+ if candidate.exists():
3827
+ try:
3828
+ manifest = read_json(candidate)
3829
+ break
3830
+ except Exception:
3831
+ manifest = {}
3832
+ compiler = package / "experience_compiler"
3833
+ canonical = compiler / "skill_pack" / "canonical_skill.json"
3834
+ skill = read_json(canonical) if canonical.exists() else {}
3835
+ compiler_report = read_json(compiler / "compiler_report.json") if (compiler / "compiler_report.json").exists() else {}
3836
+ item_id = str(manifest.get("item_id") or manifest.get("package_id") or package.name)
3837
+ return {
3838
+ "id": item_id,
3839
+ "kind": "agent_experience_package",
3840
+ "title": skill.get("title") or manifest.get("title") or package.name,
3841
+ "summary": skill.get("summary") or compiler_report.get("description"),
3842
+ "domain": manifest.get("domain"),
3843
+ "subdomain": manifest.get("subdomain"),
3844
+ "package_id": manifest.get("package_id") or item_id,
3845
+ "task_id": manifest.get("task_id"),
3846
+ "local_bundle_path": str(package),
3847
+ "experience_compiler_status": compiler_report.get("compiler_status") or compiler_report.get("tdo_status") or ("completed" if compiler.exists() else "unknown"),
3848
+ "available_outputs": {
3849
+ "full_experience_compilation": True,
3850
+ "full_training_package": True,
3851
+ "runtime_training": bool((compiler / "skill_pack" / "skill.md").exists()),
3852
+ "skill_pack": (compiler / "skill_pack").exists(),
3853
+ "training_time_package": (compiler / "training_time_package").exists(),
3854
+ "eval_package": (compiler / "eval_package").exists(),
3855
+ "environment_package": (compiler / "environment").exists(),
3856
+ },
3857
+ "experience_compiler": {
3858
+ "enabled": compiler.exists(),
3859
+ "status": compiler_report.get("compiler_status") or compiler_report.get("tdo_status") or ("completed" if compiler.exists() else "unknown"),
3860
+ "skill_title": skill.get("title") or manifest.get("title") or package.name,
3861
+ "skill_summary": skill.get("summary") or compiler_report.get("description"),
3862
+ "runtime_training_summary": skill.get("summary") or "Runtime training available from local Experience Compiler outputs.",
3863
+ "training_data_counts": compiler_report.get("rows_accepted_by_type") or {},
3864
+ "environment_domain": manifest.get("environment_domain") or manifest.get("domain"),
3865
+ },
3866
+ }
3867
+
3868
+
3004
3869
  @ecosystem_app.command('inspect')
3005
3870
  def ecosystem_inspect(
3006
3871
  bundle_id: str=typer.Argument(...),
3007
3872
  registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
3873
+ storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
3008
3874
  ):
3875
+ local_arg = Path(bundle_id).expanduser()
3876
+ if local_arg.exists():
3877
+ typer.echo(_summarize_contribution_registry_row(_local_package_registry_row(local_arg)))
3878
+ return
3009
3879
  bundle_path=get_settings().app_home / 'ecosystem' / 'bundles' / bundle_id
3010
3880
  if bundle_path.exists() and (bundle_path/'contribution_manifest.json').exists():
3011
3881
  typer.echo(_summarize_bundle_manifest(read_json(bundle_path/'contribution_manifest.json')))
3012
3882
  return
3013
3883
  try:
3014
- row=_find_registry_row(bundle_id, registry)
3884
+ if normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend) == "forsy-r2" and registry is None:
3885
+ row=fetch_r2_item(bundle_id, get_settings())
3886
+ else:
3887
+ row=_find_registry_row(bundle_id, registry, storage=storage)
3015
3888
  except FileNotFoundError as exc:
3016
3889
  typer.echo(str(exc))
3017
3890
  raise typer.Exit(1)
@@ -3022,10 +3895,12 @@ def ecosystem_inspect(
3022
3895
 
3023
3896
 
3024
3897
  @bundle_app.command('inspect')
3025
- def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle folder.')):
3898
+ def bundle_inspect(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
3899
+ from .loop_review import summarize_loop_reviews
3900
+
3026
3901
  manifest=path/'contribution_manifest.json'
3027
3902
  if not manifest.exists():
3028
- typer.echo(f'Contribution Bundle manifest not found: {manifest}')
3903
+ typer.echo(f'Experience Compilation manifest not found: {manifest}')
3029
3904
  raise typer.Exit(1)
3030
3905
  manifest_data=read_json(manifest)
3031
3906
  artifacts=_artifact_index(path)
@@ -3041,6 +3916,15 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
3041
3916
  typer.echo(f"follow_up_count: {len(followups)}")
3042
3917
  if checkpoints:
3043
3918
  typer.echo(f"mentor_checkpoint_count: {len(checkpoints)}")
3919
+ loop_summary = summarize_loop_reviews(path)
3920
+ if loop_summary.get("has_loop_review_packets"):
3921
+ typer.echo(f"loop_review_packets: {loop_summary.get('loop_iteration_count')}")
3922
+ typer.echo(f"loop_final_verdict: {loop_summary.get('final_loop_verdict') or 'unknown'}")
3923
+ reviewers = ", ".join(loop_summary.get("reviewer_types") or []) or "unknown"
3924
+ typer.echo(f"loop_reviewer_types: {reviewers}")
3925
+ refs = loop_summary.get("loop_review_refs") or []
3926
+ if refs:
3927
+ typer.echo(f"latest_review_packet: {refs[-1]}")
3044
3928
  top=[row.get("package_relative_path") or row.get("artifact_ref") for row in artifacts[:5]]
3045
3929
  if top:
3046
3930
  typer.echo("top_artifacts:")
@@ -3051,14 +3935,16 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
3051
3935
 
3052
3936
 
3053
3937
  @bundle_app.command('check')
3054
- def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder.')):
3938
+ def bundle_check(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
3939
+ from .artifact_resolver import artifact_ref_candidates, artifact_ref_resolves, normalize_artifact_ref
3940
+
3055
3941
  issues: list[str] = []
3056
3942
  bundle = Path(path).expanduser()
3057
3943
  manifest_path = bundle / "contribution_manifest.json"
3058
3944
  if not bundle.exists():
3059
3945
  issues.append(f"Bundle path does not exist: {bundle}")
3060
3946
  elif not manifest_path.exists():
3061
- issues.append(f"Contribution Bundle manifest not found: {manifest_path}")
3947
+ issues.append(f"Experience Compilation manifest not found: {manifest_path}")
3062
3948
  manifest_data = {}
3063
3949
  if manifest_path.exists():
3064
3950
  try:
@@ -3078,8 +3964,16 @@ def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder
3078
3964
  issues.append(f"outputs/artifacts_index.json could not be parsed: {exc}")
3079
3965
  for row in artifacts:
3080
3966
  ref = row.get("package_relative_path") or row.get("artifact_ref")
3081
- if ref and not (bundle / str(ref)).exists():
3082
- issues.append(f"Referenced artifact is missing: {ref}")
3967
+ if ref:
3968
+ normalized = normalize_artifact_ref(ref)
3969
+ existing_refs = {
3970
+ str(p.relative_to(bundle)).replace("\\", "/")
3971
+ for p in bundle.rglob("*")
3972
+ if p.is_file()
3973
+ }
3974
+ candidates = artifact_ref_candidates(normalized) | {normalized}
3975
+ if not any((bundle / candidate).exists() for candidate in candidates) and not artifact_ref_resolves(normalized, existing_refs):
3976
+ issues.append(f"Referenced artifact is missing: {ref}")
3083
3977
  for optional in ["session_events.jsonl", "progress_events.jsonl"]:
3084
3978
  file_path = bundle / optional
3085
3979
  if file_path.exists():
@@ -3087,44 +3981,110 @@ def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder
3087
3981
  read_jsonl(file_path)
3088
3982
  except Exception as exc:
3089
3983
  issues.append(f"{optional} could not be parsed: {exc}")
3984
+ for ref in manifest_data.get("experience_compiler_output_refs") or []:
3985
+ normalized = normalize_artifact_ref(ref)
3986
+ if not (bundle / normalized).exists():
3987
+ issues.append(f"Referenced Experience Compiler output is missing: {ref}")
3988
+ for ref in manifest_data.get("tdo_output_refs") or []:
3989
+ normalized = normalize_artifact_ref(ref)
3990
+ if not (bundle / normalized).exists():
3991
+ issues.append(f"Referenced legacy learning output is missing: {ref}")
3992
+ for ref in manifest_data.get("loop_review_refs") or []:
3993
+ normalized = normalize_artifact_ref(ref)
3994
+ if not (bundle / normalized).exists():
3995
+ issues.append(f"Referenced loop review packet is missing: {ref}")
3996
+ tdo_dir = bundle / "tdo"
3997
+ if tdo_dir.exists():
3998
+ for file_path in tdo_dir.rglob("*.json"):
3999
+ try:
4000
+ read_json(file_path)
4001
+ except Exception as exc:
4002
+ issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
4003
+ for file_path in tdo_dir.rglob("*.jsonl"):
4004
+ try:
4005
+ read_jsonl(file_path)
4006
+ except Exception as exc:
4007
+ issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
4008
+ compiler_dir = bundle / "experience_compiler"
4009
+ if compiler_dir.exists():
4010
+ for file_path in compiler_dir.rglob("*.json"):
4011
+ try:
4012
+ read_json(file_path)
4013
+ except Exception as exc:
4014
+ issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
4015
+ loops_dir = bundle / "loops"
4016
+ if loops_dir.exists():
4017
+ for file_path in loops_dir.rglob("*.json"):
4018
+ try:
4019
+ read_json(file_path)
4020
+ except Exception as exc:
4021
+ issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
4022
+ for file_path in loops_dir.rglob("*.jsonl"):
4023
+ try:
4024
+ read_jsonl(file_path)
4025
+ except Exception as exc:
4026
+ issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
4027
+ for file_path in compiler_dir.rglob("*.jsonl"):
4028
+ try:
4029
+ read_jsonl(file_path)
4030
+ except Exception as exc:
4031
+ issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
4032
+ surface_warnings: list[str] = []
4033
+ for trace_path in [bundle / "traces" / "agent_traces.jsonl", bundle / "traces" / "raw_agent_traces.jsonl"]:
4034
+ if not trace_path.exists():
4035
+ continue
4036
+ try:
4037
+ for trace in read_jsonl(trace_path):
4038
+ for step in trace.get("steps") or []:
4039
+ missing = step.get("missing_surface_fields") or []
4040
+ status = step.get("surface_capture_status")
4041
+ surface = step.get("interaction_surface")
4042
+ if surface and status in {"partial", "unavailable"} and missing:
4043
+ surface_warnings.append(
4044
+ f"{trace_path.relative_to(bundle)} step {step.get('step')}: {surface} capture {status}; missing {', '.join(str(x) for x in missing[:6])}"
4045
+ )
4046
+ except Exception:
4047
+ pass
3090
4048
  secret_hits = _bundle_secret_hits(bundle) if bundle.exists() else []
3091
4049
  if secret_hits:
3092
4050
  issues.append("Obvious secret-like values were found: " + ", ".join(secret_hits[:10]))
3093
4051
  if issues:
3094
- typer.echo("Bundle check: needs attention")
3095
- typer.echo(f"Bundle: {bundle}")
4052
+ typer.echo("Experience Compilation check: needs attention")
4053
+ typer.echo(f"Experience Compilation: {bundle}")
3096
4054
  for issue in issues:
3097
4055
  typer.echo(f"Reason: {redact_secrets(issue)}")
3098
4056
  typer.echo("Next action: fix the packaging issue, then rerun `apprentice bundle check`.")
3099
4057
  raise typer.Exit(1)
3100
4058
  status = manifest_data.get("task_status") or manifest_data.get("run_status") or "unknown"
3101
- typer.echo("Bundle check: passed")
3102
- typer.echo(f"Bundle: {bundle}")
4059
+ typer.echo("Experience Compilation check: passed")
4060
+ typer.echo(f"Experience Compilation: {bundle}")
3103
4061
  typer.echo(f"Task status: {status}")
3104
- typer.echo("This bundle is ready to add to the public ecosystem.")
4062
+ for warning in surface_warnings[:10]:
4063
+ typer.echo(f"Surface completeness warning: {redact_secrets(warning)}")
4064
+ typer.echo("This Experience Compilation is ready to add to the public ecosystem.")
3105
4065
 
3106
4066
 
3107
4067
  @bundle_app.command('contribute')
3108
- def bundle_contribute(path: Path=typer.Argument(..., help='Contribution Bundle folder.')):
4068
+ def bundle_contribute(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
3109
4069
  manifest=path/'contribution_manifest.json'
3110
4070
  if not manifest.exists():
3111
- typer.echo(f'Contribution Bundle manifest not found: {manifest}')
4071
+ typer.echo(f'Experience Compilation manifest not found: {manifest}')
3112
4072
  raise typer.Exit(1)
3113
4073
  try:
3114
4074
  submission_dir, package_zip, metadata = _create_ecosystem_submission(path)
3115
4075
  except Exception as exc:
3116
4076
  typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
3117
4077
  raise typer.Exit(1)
3118
- typer.echo('Contribution Bundle ready.')
4078
+ typer.echo('Experience Compilation ready.')
3119
4079
  typer.echo('')
3120
4080
  typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
3121
4081
  typer.echo(f"Title: {metadata.get('title')}")
3122
- typer.echo('Bundle:')
4082
+ typer.echo('Experience Compilation:')
3123
4083
  typer.echo(str(path))
3124
4084
  typer.echo('')
3125
4085
  typer.echo('No upload was performed by this command.')
3126
- typer.echo(f"Submission package: {package_zip}")
3127
- typer.echo(f"Submission metadata: {submission_dir / 'ecosystem_submission.json'}")
4086
+ typer.echo(f"Submission package: {package_zip.name}")
4087
+ typer.echo("Submission metadata: ecosystem_submission/ecosystem_submission.json")
3128
4088
  typer.echo('')
3129
4089
  typer.echo('Contribute to public ecosystem:')
3130
4090
  typer.echo(f'apprentice ecosystem contribute {path}')