loki-mode 7.16.1 → 7.17.1

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.
package/autonomy/loki CHANGED
@@ -613,6 +613,7 @@ show_help() {
613
613
  echo " loki quick \"add dark mode\" # Single-task mode (3 iters max)"
614
614
  echo " loki demo # 60-second interactive demo"
615
615
  echo " loki init -t saas-starter # Scaffold from template"
616
+ echo " loki template install <src> # Install a community PRD template"
616
617
  echo ""
617
618
  echo " # Session ops + observability"
618
619
  echo " loki status [--json] # Current status"
@@ -621,6 +622,7 @@ show_help() {
621
622
  echo " loki doctor [--json] # System prereq + skill symlinks"
622
623
  echo " loki logs # Tail recent log output"
623
624
  echo " loki export json|markdown|csv|timeline # Export session"
625
+ echo " loki assets export team.tgz # Export shareable team assets (redacted)"
624
626
  echo " loki cleanup # Kill orphaned processes"
625
627
  echo ""
626
628
  echo " # Providers + model routing"
@@ -1218,6 +1220,13 @@ cmd_start() {
1218
1220
  esac
1219
1221
  done
1220
1222
 
1223
+ # Clear any stale raw-brief marker from a PRIOR run before we decide the
1224
+ # mode of THIS run. Only the brief branch (below) re-writes it. Without this,
1225
+ # a brief run leaves .loki/state/brief.txt behind and a later non-brief run
1226
+ # (PRD or codebase-analysis) would have its proof mislabel source="brief"
1227
+ # with the old one-liner, since proof-generator reads brief.txt first.
1228
+ rm -f "$LOKI_DIR/state/brief.txt" 2>/dev/null || true
1229
+
1221
1230
  # v6.84.0: Unified dispatch based on explicit flags or auto-detection
1222
1231
  # Precedence: --brief > --issue > --prd > positional auto-detect >
1223
1232
  # LOKI_PRD_FILE env
@@ -1290,6 +1299,15 @@ cmd_start() {
1290
1299
  synthesize_brief_prd "$brief_prd" "$brief_text"
1291
1300
  prd_file="$brief_prd"
1292
1301
 
1302
+ # Persist the raw one-liner so the proof-of-run can show the actual brief
1303
+ # the user typed ("Built and verified from: <brief>") instead of falling
1304
+ # back to "codebase-analysis / No brief recorded". The synthesized PRD is
1305
+ # kept distinct (brief-prd-$$.md); the raw brief is the stronger, more
1306
+ # honest shareable artifact. proof-generator.py::_collect_spec reads this
1307
+ # as a fallback when no PRD_PATH / generated-prd.md is present.
1308
+ mkdir -p "$LOKI_DIR/state" 2>/dev/null || true
1309
+ printf '%s' "$brief_text" > "$LOKI_DIR/state/brief.txt" 2>/dev/null || true
1310
+
1293
1311
  # Apply the shared lightweight profile and flag the TTFV first-run path.
1294
1312
  # The signal value ("brief") drives the end-of-run wording in run.sh so
1295
1313
  # the message matches what actually ran (lightweight, council off).
@@ -6298,6 +6316,187 @@ cmd_export() {
6298
6316
  esac
6299
6317
  }
6300
6318
 
6319
+ # R8: Shareable team assets. Bundles a team's invested, reusable assets
6320
+ # (cross-project + project memory/learnings, the agent registry, PRD
6321
+ # templates, council config, optionally the R5 wiki) into a portable,
6322
+ # REDACTED tarball, and re-imports them into another project or fresh clone.
6323
+ #
6324
+ # Scope vs cmd_export: cmd_export emits a per-run SESSION SNAPSHOT
6325
+ # (json/md/csv/timeline) and is NOT portable or redacted. cmd_assets emits a
6326
+ # portable, redacted TEAM-ASSET tarball -- different scope, different artifact.
6327
+ # Redaction REUSES the single proof_redact chokepoint via the
6328
+ # autonomy/lib/assets_bundle.py helper. No second redactor or parallel exporter.
6329
+ cmd_assets() {
6330
+ local subcommand="${1:-help}"
6331
+ shift 2>/dev/null || true
6332
+
6333
+ local helper="$_LOKI_SCRIPT_DIR/lib/assets_bundle.py"
6334
+ if [ ! -f "$helper" ]; then
6335
+ echo -e "${RED}Error: assets helper not found at $helper${NC}" >&2
6336
+ return 1
6337
+ fi
6338
+ if ! command -v python3 &>/dev/null; then
6339
+ echo -e "${RED}Error: python3 is required for 'loki assets'${NC}" >&2
6340
+ return 1
6341
+ fi
6342
+
6343
+ # Project dir holds .loki/ (memory, council, wiki). Default to cwd.
6344
+ local project_dir
6345
+ project_dir="$(pwd)"
6346
+
6347
+ # repo_root holds agents/types.json + templates/. The export and import
6348
+ # roots are DELIBERATELY ASYMMETRIC (not a bug):
6349
+ # - EXPORT reads from the loki install ($SKILL_DIR), where a team's
6350
+ # edited registry / templates actually live and are read at runtime
6351
+ # (autonomy/loki:9781, 8840 read templates from $SKILL_DIR; cmd_agent
6352
+ # reads agents/types.json from the install first).
6353
+ # - IMPORT writes to the current working directory (the root of the
6354
+ # target clone the user is setting up), NEVER back into the install.
6355
+ # Writing into $SKILL_DIR would clobber the live install's shipped
6356
+ # registry / templates with redacted copies.
6357
+ # Honest limitation: agents/templates restore relative to cwd, so run
6358
+ # `loki assets import` from the root of the target clone. A global-install
6359
+ # user must copy the restored agents/ + templates/ into their install for
6360
+ # loki to read them at runtime (memory/learnings/council/wiki are unaffected
6361
+ # -- those restore to $HOME/.loki and <project>/.loki and take effect
6362
+ # immediately).
6363
+ local export_repo_root="${SKILL_DIR:-$(cd "$_LOKI_SCRIPT_DIR/.." && pwd)}"
6364
+ local import_repo_root="$project_dir"
6365
+
6366
+ case "$subcommand" in
6367
+ --help|-h|help|"")
6368
+ echo -e "${BOLD}loki assets${NC} - Export/import shareable team assets (R8)"
6369
+ echo ""
6370
+ echo "Bundles a team's reusable assets into a portable, redacted"
6371
+ echo "tarball so setup compounds into shared, importable value."
6372
+ echo ""
6373
+ echo "Usage:"
6374
+ echo " loki assets export <bundle.tgz> [--wiki] [--categories a,b,c]"
6375
+ echo " loki assets import <bundle.tgz> [--no-merge] [--into-install]"
6376
+ echo " loki assets inspect <bundle.tgz>"
6377
+ echo ""
6378
+ echo "Assets bundled (default):"
6379
+ echo " learnings ~/.loki/learnings/*.jsonl (cross-project)"
6380
+ echo " memory <project>/.loki/memory/** (episodic/semantic/skills)"
6381
+ echo " agents agents/types.json (agent registry)"
6382
+ echo " templates templates/*.md (PRD templates)"
6383
+ echo " council <project>/.loki/council/*.json (council config/state)"
6384
+ echo " wiki <project>/.loki/wiki/** (opt-in via --wiki)"
6385
+ echo ""
6386
+ echo "All bundled content is redacted via the proof_redact chokepoint:"
6387
+ echo "secrets, keys, tokens, and absolute home/repo paths are stripped"
6388
+ echo "before the bundle is written. Originals on disk are never changed."
6389
+ echo ""
6390
+ echo "Restore locations on import:"
6391
+ echo " learnings -> \$HOME/.loki/learnings (effective immediately)"
6392
+ echo " memory -> <cwd>/.loki/memory (effective immediately)"
6393
+ echo " council -> <cwd>/.loki/council (effective immediately)"
6394
+ echo " wiki -> <cwd>/.loki/wiki (effective immediately)"
6395
+ echo " agents -> <cwd>/agents/types.json"
6396
+ echo " templates -> <cwd>/templates/*.md"
6397
+ echo "Run import from the ROOT of the target clone. Note: agents and"
6398
+ echo "templates are read by loki from its install dir at runtime. On a"
6399
+ echo "global install, pass --into-install so agents/templates restore"
6400
+ echo "into the install dir and are read at runtime (default is cwd,"
6401
+ echo "which suits a repo-clone where loki runs from the clone root)."
6402
+ echo ""
6403
+ echo "Examples:"
6404
+ echo " loki assets export team-assets.tgz"
6405
+ echo " loki assets export team-assets.tgz --wiki"
6406
+ echo " loki assets import team-assets.tgz # merge learnings, overwrite rest"
6407
+ echo " loki assets import team-assets.tgz --no-merge"
6408
+ echo " loki assets inspect team-assets.tgz # show manifest only"
6409
+ return 0
6410
+ ;;
6411
+ export)
6412
+ local out_path="${1:-}"
6413
+ if [ -z "$out_path" ]; then
6414
+ echo -e "${RED}Usage: loki assets export <bundle.tgz> [--wiki] [--categories a,b,c]${NC}" >&2
6415
+ return 1
6416
+ fi
6417
+ shift 2>/dev/null || true
6418
+ if ! _export_check_overwrite "$out_path"; then
6419
+ return 1
6420
+ fi
6421
+ echo "Bundling team assets (redacting secrets and paths)..."
6422
+ if LOKI_ASSETS_HOME="$HOME" LOKI_ASSETS_REPO="$export_repo_root" \
6423
+ LOKI_ASSETS_PROJECT="$project_dir" \
6424
+ python3 "$helper" export "$out_path" "$@"; then
6425
+ echo -e "${GREEN}Bundle written: $out_path${NC}"
6426
+ else
6427
+ echo -e "${RED}Export failed${NC}" >&2
6428
+ return 1
6429
+ fi
6430
+ ;;
6431
+ import)
6432
+ local bundle_path="${1:-}"
6433
+ if [ -z "$bundle_path" ]; then
6434
+ echo -e "${RED}Usage: loki assets import <bundle.tgz> [--no-merge] [--into-install]${NC}" >&2
6435
+ return 1
6436
+ fi
6437
+ shift 2>/dev/null || true
6438
+ if [ ! -f "$bundle_path" ]; then
6439
+ echo -e "${RED}Error: bundle not found: $bundle_path${NC}" >&2
6440
+ return 1
6441
+ fi
6442
+ # --into-install: restore agents/templates into the loki install
6443
+ # ($SKILL_DIR) so a global-install user has them read at runtime,
6444
+ # instead of into cwd. Strip the flag before passing the rest to
6445
+ # the helper (the helper does not understand it). The install dir
6446
+ # is the same source export reads from, so this is the symmetric
6447
+ # round-trip for a global install. Off by default (cwd is safe).
6448
+ local _import_args=()
6449
+ local arg
6450
+ for arg in "$@"; do
6451
+ if [ "$arg" = "--into-install" ]; then
6452
+ import_repo_root="$export_repo_root"
6453
+ else
6454
+ _import_args+=("$arg")
6455
+ fi
6456
+ done
6457
+ if [ "$import_repo_root" = "$export_repo_root" ]; then
6458
+ echo "Importing team assets (agents/templates -> install dir: $import_repo_root)..."
6459
+ else
6460
+ echo "Importing team assets into this project/clone (cwd)..."
6461
+ fi
6462
+ # Bash 3.2 (macOS) errors on "${arr[@]}" when the array is empty
6463
+ # and set -u is active; guard with the count.
6464
+ if [ "${#_import_args[@]}" -gt 0 ]; then
6465
+ LOKI_ASSETS_HOME="$HOME" LOKI_ASSETS_REPO="$import_repo_root" \
6466
+ LOKI_ASSETS_PROJECT="$project_dir" \
6467
+ python3 "$helper" import "$bundle_path" "${_import_args[@]}"
6468
+ else
6469
+ LOKI_ASSETS_HOME="$HOME" LOKI_ASSETS_REPO="$import_repo_root" \
6470
+ LOKI_ASSETS_PROJECT="$project_dir" \
6471
+ python3 "$helper" import "$bundle_path"
6472
+ fi
6473
+ if [ $? -eq 0 ]; then
6474
+ echo -e "${GREEN}Import complete${NC}"
6475
+ else
6476
+ echo -e "${RED}Import failed${NC}" >&2
6477
+ return 1
6478
+ fi
6479
+ ;;
6480
+ inspect)
6481
+ local bundle_path="${1:-}"
6482
+ if [ -z "$bundle_path" ]; then
6483
+ echo -e "${RED}Usage: loki assets inspect <bundle.tgz>${NC}" >&2
6484
+ return 1
6485
+ fi
6486
+ if [ ! -f "$bundle_path" ]; then
6487
+ echo -e "${RED}Error: bundle not found: $bundle_path${NC}" >&2
6488
+ return 1
6489
+ fi
6490
+ python3 "$helper" inspect "$bundle_path"
6491
+ ;;
6492
+ *)
6493
+ echo -e "${RED}Unknown subcommand: $subcommand${NC}" >&2
6494
+ echo "Run 'loki assets --help' for usage."
6495
+ return 1
6496
+ ;;
6497
+ esac
6498
+ }
6499
+
6301
6500
  # Guard: check if output file exists and warn before overwriting
6302
6501
  _export_check_overwrite() {
6303
6502
  local output="$1"
@@ -9633,6 +9832,22 @@ cmd_init() {
9633
9832
  echo -e "Selected: ${CYAN}$template_name${NC} ($(_get_template_label "$template_name"))"
9634
9833
  fi
9635
9834
 
9835
+ # R10: a hub-installed template (.loki/templates/<name>.md) is also valid.
9836
+ # Resolve it via agents/hub_install.py (validates the name as a safe id).
9837
+ local _installed_tpl=""
9838
+ local _hub_py_init="$SKILL_DIR/agents/hub_install.py"
9839
+ [ -f "$_hub_py_init" ] || _hub_py_init="agents/hub_install.py"
9840
+ if [[ -f "$_hub_py_init" ]]; then
9841
+ _installed_tpl=$(TPL_NAME="$template_name" HUB_PY="$_hub_py_init" python3 -c "
9842
+ import os
9843
+ import importlib.util as ilu
9844
+ sp = ilu.spec_from_file_location('loki_hub_install', os.environ.get('HUB_PY'))
9845
+ m = ilu.module_from_spec(sp); sp.loader.exec_module(m)
9846
+ p = m.installed_template_path(os.environ['TPL_NAME'])
9847
+ print(p or '')
9848
+ " 2>/dev/null)
9849
+ fi
9850
+
9636
9851
  # Validate template_name against known list before filesystem lookup
9637
9852
  local _tpl_valid=false
9638
9853
  for _tpl_check in "${TEMPLATE_NAMES[@]}"; do
@@ -9641,6 +9856,9 @@ cmd_init() {
9641
9856
  break
9642
9857
  fi
9643
9858
  done
9859
+ if [[ -n "$_installed_tpl" ]]; then
9860
+ _tpl_valid=true
9861
+ fi
9644
9862
  if ! $_tpl_valid; then
9645
9863
  echo -e "${RED}Unknown template: $template_name${NC}"
9646
9864
  echo ""
@@ -9649,18 +9867,23 @@ cmd_init() {
9649
9867
  echo " $tname"
9650
9868
  done
9651
9869
  echo ""
9652
- echo "Run 'loki init --list' for details."
9870
+ echo "Run 'loki init --list' for built-in details, 'loki template list' for installed."
9653
9871
  exit 1
9654
9872
  fi
9655
9873
 
9656
9874
  # Resolve template file
9657
9875
  local template_file=""
9658
- for dir in "$SKILL_DIR/templates" "$SKILL_DIR/examples"; do
9659
- if [[ -f "$dir/${template_name}.md" ]]; then
9660
- template_file="$dir/${template_name}.md"
9661
- break
9662
- fi
9663
- done
9876
+ if [[ -n "$_installed_tpl" && -f "$_installed_tpl" ]]; then
9877
+ template_file="$_installed_tpl"
9878
+ fi
9879
+ if [[ -z "$template_file" ]]; then
9880
+ for dir in "$SKILL_DIR/templates" "$SKILL_DIR/examples"; do
9881
+ if [[ -f "$dir/${template_name}.md" ]]; then
9882
+ template_file="$dir/${template_name}.md"
9883
+ break
9884
+ fi
9885
+ done
9886
+ fi
9664
9887
 
9665
9888
  if [[ -z "$template_file" ]]; then
9666
9889
  echo -e "${RED}Unknown template: $template_name${NC}"
@@ -13094,6 +13317,9 @@ main() {
13094
13317
  export)
13095
13318
  cmd_export "$@"
13096
13319
  ;;
13320
+ assets)
13321
+ cmd_assets "$@"
13322
+ ;;
13097
13323
  config)
13098
13324
  cmd_config "$@"
13099
13325
  ;;
@@ -13173,6 +13399,9 @@ main() {
13173
13399
  agent)
13174
13400
  cmd_agent "$@"
13175
13401
  ;;
13402
+ template)
13403
+ cmd_template "$@"
13404
+ ;;
13176
13405
  state)
13177
13406
  cmd_state "$@"
13178
13407
  ;;
@@ -19562,6 +19791,104 @@ cmd_completions() {
19562
19791
  esac
19563
19792
  }
19564
19793
 
19794
+ cmd_template() {
19795
+ local subcommand="${1:-list}"
19796
+ shift 2>/dev/null || true
19797
+
19798
+ local script_dir
19799
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19800
+ local hub_py="${script_dir}/../agents/hub_install.py"
19801
+ [ -f "$hub_py" ] || hub_py="agents/hub_install.py"
19802
+
19803
+ case "$subcommand" in
19804
+ install)
19805
+ local source="${1:-}"
19806
+ if [ -z "$source" ]; then
19807
+ echo -e "${RED}Usage: loki template install <source>${NC}"
19808
+ echo " source: local path, git repo URL, or raw manifest URL"
19809
+ return 1
19810
+ fi
19811
+ if [ ! -f "$hub_py" ]; then
19812
+ echo -e "${RED}Error: agents/hub_install.py not found${NC}"
19813
+ return 1
19814
+ fi
19815
+ local result
19816
+ if result=$(python3 "$hub_py" install-template "$source" 2>&1); then
19817
+ TPL_RESULT="$result" python3 - << 'PYEOF'
19818
+ import json, os
19819
+ r = json.loads(os.environ["TPL_RESULT"])
19820
+ print(f"Installed template: {r['name']} ({r.get('label','')})")
19821
+ desc = r.get("description") or ""
19822
+ if desc:
19823
+ print(f" {desc}")
19824
+ print(f" body: {r.get('path','')}")
19825
+ print(f" source: {r.get('source','')}")
19826
+ ig = r.get("_ignored_executable_fields") or []
19827
+ if ig:
19828
+ print(f" NOTE: ignored executable-looking fields (never run): {', '.join(ig)}")
19829
+ print(f"Use with: loki init --template {r['name']}")
19830
+ PYEOF
19831
+ else
19832
+ echo -e "${RED}Install failed:${NC} $result"
19833
+ return 1
19834
+ fi
19835
+ ;;
19836
+
19837
+ list|installed)
19838
+ echo -e "${BOLD}Hub-installed Templates${NC}"
19839
+ echo ""
19840
+ if [ ! -f "$hub_py" ]; then
19841
+ echo " (none)"
19842
+ else
19843
+ local _tpls_json
19844
+ _tpls_json="$(python3 "$hub_py" list-templates 2>/dev/null)"
19845
+ HUB_ITEMS="$_tpls_json" python3 - << 'PYEOF'
19846
+ import json, os
19847
+ try:
19848
+ items = json.loads(os.environ.get("HUB_ITEMS", "") or "[]")
19849
+ except Exception:
19850
+ items = []
19851
+ if not items:
19852
+ print(" (none)")
19853
+ else:
19854
+ for t in items:
19855
+ print(f" {t.get('name',''):24s} {t.get('label','')}")
19856
+ print(f"\n Total: {len(items)} installed template(s)")
19857
+ PYEOF
19858
+ fi
19859
+ ;;
19860
+
19861
+ --help|-h|help)
19862
+ echo -e "${BOLD}loki template${NC} - PRD template marketplace (R10)"
19863
+ echo ""
19864
+ echo "Usage: loki template <command> [args]"
19865
+ echo ""
19866
+ echo "Commands:"
19867
+ echo " install <source> Install a community template (local/git/url manifest)"
19868
+ echo " list List templates installed from a hub source"
19869
+ echo " help Show this help"
19870
+ echo ""
19871
+ echo "Examples:"
19872
+ echo " loki template install ./my-template/manifest.json"
19873
+ echo " loki template install https://github.com/owner/loki-template-rust.git"
19874
+ echo " loki template list"
19875
+ echo ""
19876
+ echo "Installed templates are usable via 'loki init --template <name>'."
19877
+ echo "Built-in templates ship in templates/ (see 'loki init --list')."
19878
+ echo ""
19879
+ echo "Note: install is install-from-source (git/local/url). A hosted"
19880
+ echo "central hub registry is future work. Manifests are validated as"
19881
+ echo "DATA only; no code from a manifest is ever executed."
19882
+ echo ""
19883
+ ;;
19884
+ *)
19885
+ echo -e "${RED}Unknown template command: $subcommand${NC}"
19886
+ echo "Run 'loki template help' for usage."
19887
+ return 1
19888
+ ;;
19889
+ esac
19890
+ }
19891
+
19565
19892
  # Agent type dispatch (v6.7.0)
19566
19893
  cmd_agent() {
19567
19894
  local subcommand="${1:-list}"
@@ -19592,6 +19919,20 @@ import json, sys, os
19592
19919
  with open(os.environ["TYPES_FILE"]) as f:
19593
19920
  agents = json.load(f)
19594
19921
 
19922
+ # R10: union built-in agents with hub-installed agents (.loki/agents/installed.json).
19923
+ try:
19924
+ import importlib.util as _ilu
19925
+ _hub_path = os.path.join(os.path.dirname(os.path.abspath(os.environ["TYPES_FILE"])), "hub_install.py")
19926
+ _spec = _ilu.spec_from_file_location("loki_hub_install", _hub_path)
19927
+ _hub = _ilu.module_from_spec(_spec)
19928
+ _spec.loader.exec_module(_hub)
19929
+ _seen = {a.get("type") for a in agents}
19930
+ for _inst in _hub.installed_agent_list():
19931
+ if _inst.get("type") not in _seen:
19932
+ agents.append(_inst)
19933
+ except Exception:
19934
+ pass
19935
+
19595
19936
  filter_swarm = os.environ.get("FILTER_SWARM", "")
19596
19937
  if filter_swarm and filter_swarm.startswith("--swarm"):
19597
19938
  # Handle --swarm=X or --swarm X
@@ -19634,6 +19975,20 @@ import json, sys, os
19634
19975
  with open(os.environ["TYPES_FILE"]) as f:
19635
19976
  agents = json.load(f)
19636
19977
 
19978
+ # R10: include hub-installed agents in the lookup.
19979
+ try:
19980
+ import importlib.util as _ilu
19981
+ _hub_path = os.path.join(os.path.dirname(os.path.abspath(os.environ["TYPES_FILE"])), "hub_install.py")
19982
+ _spec = _ilu.spec_from_file_location("loki_hub_install", _hub_path)
19983
+ _hub = _ilu.module_from_spec(_spec)
19984
+ _spec.loader.exec_module(_hub)
19985
+ _seen = {a.get("type") for a in agents}
19986
+ for _inst in _hub.installed_agent_list():
19987
+ if _inst.get("type") not in _seen:
19988
+ agents.append(_inst)
19989
+ except Exception:
19990
+ pass
19991
+
19637
19992
  target = os.environ["AGENT_TYPE"]
19638
19993
  found = None
19639
19994
  for a in agents:
@@ -19671,6 +20026,17 @@ PYEOF
19671
20026
  import json, os
19672
20027
  with open(os.environ['TYPES_FILE']) as f:
19673
20028
  agents = json.load(f)
20029
+ try:
20030
+ import importlib.util as _ilu
20031
+ _hp = os.path.join(os.path.dirname(os.path.abspath(os.environ['TYPES_FILE'])), 'hub_install.py')
20032
+ _sp = _ilu.spec_from_file_location('loki_hub_install', _hp)
20033
+ _hub = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_hub)
20034
+ _seen = {a.get('type') for a in agents}
20035
+ for _i in _hub.installed_agent_list():
20036
+ if _i.get('type') not in _seen:
20037
+ agents.append(_i)
20038
+ except Exception:
20039
+ pass
19674
20040
  for a in agents:
19675
20041
  if a['type'] == os.environ['AGENT_TYPE']:
19676
20042
  print(a['persona'])
@@ -19742,6 +20108,17 @@ USER TASK: ${prompt}"
19742
20108
  import json, os
19743
20109
  with open(os.environ['TYPES_FILE']) as f:
19744
20110
  agents = json.load(f)
20111
+ try:
20112
+ import importlib.util as _ilu
20113
+ _hp = os.path.join(os.path.dirname(os.path.abspath(os.environ['TYPES_FILE'])), 'hub_install.py')
20114
+ _sp = _ilu.spec_from_file_location('loki_hub_install', _hp)
20115
+ _hub = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_hub)
20116
+ _seen = {a.get('type') for a in agents}
20117
+ for _i in _hub.installed_agent_list():
20118
+ if _i.get('type') not in _seen:
20119
+ agents.append(_i)
20120
+ except Exception:
20121
+ pass
19745
20122
  for a in agents:
19746
20123
  if a['type'] == os.environ['AGENT_TYPE']:
19747
20124
  print(a['persona'])
@@ -19785,6 +20162,17 @@ for a in agents:
19785
20162
  import json, os
19786
20163
  with open(os.environ['TYPES_FILE']) as f:
19787
20164
  agents = json.load(f)
20165
+ try:
20166
+ import importlib.util as _ilu
20167
+ _hp = os.path.join(os.path.dirname(os.path.abspath(os.environ['TYPES_FILE'])), 'hub_install.py')
20168
+ _sp = _ilu.spec_from_file_location('loki_hub_install', _hp)
20169
+ _hub = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_hub)
20170
+ _seen = {a.get('type') for a in agents}
20171
+ for _i in _hub.installed_agent_list():
20172
+ if _i.get('type') not in _seen:
20173
+ agents.append(_i)
20174
+ except Exception:
20175
+ pass
19788
20176
  for a in agents:
19789
20177
  if a['type'] == os.environ['AGENT_TYPE']:
19790
20178
  print(a['persona'])
@@ -19822,17 +20210,81 @@ $diff"
19822
20210
  esac
19823
20211
  ;;
19824
20212
 
20213
+ install)
20214
+ # R10: install a community agent from a source (local path /
20215
+ # git repo / raw URL containing a manifest). DATA-ONLY install --
20216
+ # never executes code from the manifest. See agents/hub_install.py.
20217
+ local source="${1:-}"
20218
+ if [ -z "$source" ]; then
20219
+ echo -e "${RED}Usage: loki agent install <source>${NC}"
20220
+ echo " source: local path, git repo URL, or raw manifest URL"
20221
+ return 1
20222
+ fi
20223
+ local hub_py="${script_dir}/../agents/hub_install.py"
20224
+ [ -f "$hub_py" ] || hub_py="agents/hub_install.py"
20225
+ if [ ! -f "$hub_py" ]; then
20226
+ echo -e "${RED}Error: agents/hub_install.py not found${NC}"
20227
+ return 1
20228
+ fi
20229
+ local result
20230
+ if result=$(python3 "$hub_py" install-agent "$source" 2>&1); then
20231
+ AGENT_RESULT="$result" python3 - << 'PYEOF'
20232
+ import json, os
20233
+ r = json.loads(os.environ["AGENT_RESULT"])
20234
+ print(f"Installed agent: {r['type']} ({r.get('name','')})")
20235
+ print(f" swarm: {r.get('swarm','')}")
20236
+ print(f" source: {r.get('source','')}")
20237
+ ig = r.get("_ignored_executable_fields") or []
20238
+ if ig:
20239
+ print(f" NOTE: ignored executable-looking fields (never run): {', '.join(ig)}")
20240
+ print("Stored in .loki/agents/installed.json -- visible to 'loki agent list/info/run'.")
20241
+ PYEOF
20242
+ else
20243
+ echo -e "${RED}Install failed:${NC} $result"
20244
+ return 1
20245
+ fi
20246
+ ;;
20247
+
20248
+ installed)
20249
+ # R10: list agents installed from a hub source.
20250
+ local hub_py="${script_dir}/../agents/hub_install.py"
20251
+ [ -f "$hub_py" ] || hub_py="agents/hub_install.py"
20252
+ echo -e "${BOLD}Hub-installed Agents${NC}"
20253
+ echo ""
20254
+ if [ ! -f "$hub_py" ]; then
20255
+ echo " (none)"
20256
+ else
20257
+ local _agents_json
20258
+ _agents_json="$(python3 "$hub_py" list-agents 2>/dev/null)"
20259
+ HUB_ITEMS="$_agents_json" python3 - << 'PYEOF'
20260
+ import json, os
20261
+ try:
20262
+ items = json.loads(os.environ.get("HUB_ITEMS", "") or "[]")
20263
+ except Exception:
20264
+ items = []
20265
+ if not items:
20266
+ print(" (none)")
20267
+ else:
20268
+ for a in items:
20269
+ print(f" {a.get('type',''):24s} {a.get('name','')} [{a.get('swarm','')}]")
20270
+ print(f"\n Total: {len(items)} installed agent(s)")
20271
+ PYEOF
20272
+ fi
20273
+ ;;
20274
+
19825
20275
  --help|-h|help)
19826
20276
  echo -e "${BOLD}loki agent${NC} - Agent type dispatch (v6.7.0)"
19827
20277
  echo ""
19828
20278
  echo "Usage: loki agent <command> [args]"
19829
20279
  echo ""
19830
20280
  echo "Commands:"
19831
- echo " list [--swarm NAME] List all agent types (optionally filter by swarm)"
20281
+ echo " list [--swarm NAME] List all agent types (built-in + installed)"
19832
20282
  echo " info <type> Show agent type details"
19833
20283
  echo " run <type> \"<prompt>\" Single-shot: run prompt with agent persona"
19834
20284
  echo " start <type> <prd|prompt> Full autonomous loop with agent persona"
19835
20285
  echo " review [type] Code review with agent persona (default: ops-security)"
20286
+ echo " install <source> Install a community agent (local/git/url manifest)"
20287
+ echo " installed List agents installed from a hub source"
19836
20288
  echo " help Show this help"
19837
20289
  echo ""
19838
20290
  echo "Examples:"
@@ -19842,6 +20294,12 @@ $diff"
19842
20294
  echo " loki agent run eng-frontend \"Create a responsive navbar\""
19843
20295
  echo " loki agent start eng-backend ./api-prd.md"
19844
20296
  echo " loki agent review ops-security"
20297
+ echo " loki agent install ./my-agent/manifest.json"
20298
+ echo " loki agent install https://github.com/owner/loki-agent-rust.git"
20299
+ echo ""
20300
+ echo "Note: 'install' is install-from-source (git/local/url). A hosted"
20301
+ echo "central hub registry is future work. Manifests are validated as"
20302
+ echo "DATA only; no code from a manifest is ever executed."
19845
20303
  echo ""
19846
20304
  ;;
19847
20305
  *)
@@ -25313,19 +25771,38 @@ _loki_gist_upload() {
25313
25771
  local desc="$2"
25314
25772
  local visibility="$3"
25315
25773
 
25316
- local gist_url
25317
- gist_url=$(gh gist create "$file" --desc "$desc" $visibility 2>&1)
25318
- local gist_exit=$?
25774
+ # gh prints progress ("- Creating gist...", "Created public gist...") on
25775
+ # STDERR and the final gist URL on STDOUT. Capture them separately so the
25776
+ # URL we expose and print is clean: folding stderr into stdout pollutes the
25777
+ # "Shared:" line and the ready-to-post hook with multi-line gh chatter.
25778
+ local gist_out gist_err gist_exit
25779
+ local errfile
25780
+ # Fall back to /dev/null if mktemp fails (disk full / unwritable /tmp) so the
25781
+ # redirect target is always a real path -- never an empty string (2>"" is a
25782
+ # bash error). We lose the captured stderr text in that rare case, not the
25783
+ # clean URL parse.
25784
+ errfile=$(mktemp "/tmp/loki-gist-err-XXXXXX" 2>/dev/null) || errfile=/dev/null
25785
+ gist_out=$(gh gist create "$file" --desc "$desc" $visibility 2>"$errfile")
25786
+ gist_exit=$?
25787
+ gist_err=$(cat "$errfile" 2>/dev/null)
25788
+ [ "$errfile" != "/dev/null" ] && rm -f "$errfile"
25319
25789
 
25320
25790
  # Cleanup temp file
25321
25791
  rm -f "$file"
25322
25792
 
25323
25793
  if [ $gist_exit -ne 0 ]; then
25324
25794
  echo -e "${RED}Failed to create gist${NC}"
25325
- echo "$gist_url"
25795
+ [ -n "$gist_err" ] && echo "$gist_err"
25796
+ [ -n "$gist_out" ] && echo "$gist_out"
25326
25797
  exit 1
25327
25798
  fi
25328
25799
 
25800
+ # The URL is the gist permalink on stdout. Extract just the URL line in
25801
+ # case any non-URL text slips into stdout on some gh versions.
25802
+ local gist_url
25803
+ gist_url=$(printf '%s\n' "$gist_out" | grep -Eo 'https://gist\.github\.com/[^[:space:]]+' | head -1)
25804
+ [ -z "$gist_url" ] && gist_url="$gist_out"
25805
+
25329
25806
  # Expose the URL to callers (e.g. cmd_proof prints a ready-to-post hook
25330
25807
  # after this returns). The shared "Shared:" line stays unchanged.
25331
25808
  LOKI_LAST_GIST_URL="$gist_url"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.16.1"
10
+ __version__ = "7.17.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.16.1
5
+ **Version:** v7.17.1
6
6
 
7
7
  ---
8
8