space-architect 9.2.1 → 9.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 173cd44e1e27f8fc09d46bb0d18a5c7f26138731d4315bc6db1e7cd2c704e0a4
4
- data.tar.gz: cebbf1f5602efa92a9f3982f31fcc79c202549489531262cfaab3ecc1c25ba13
3
+ metadata.gz: e768903f0762630e4c4c510a83621cf4c82be30ea7d95026d794f90d70205f44
4
+ data.tar.gz: 77c7b1faae07696200558929456ab481aa5f8f061f69a0ff923fac53a4957f2c
5
5
  SHA512:
6
- metadata.gz: 44430c9d2fb759035df014e4023084424b66edd6dfea696f4e0e0f555ab7d0e3d418410ae8fa117936456fd3b4ea365cf9bf217ffadbb0be184e9ccb6835526a
7
- data.tar.gz: 5abb3a70daaca512e957ed2a577a45904e709be3cd05c6b57ae49f7e58ce9d612b64c33addb6ba191f49777826258e5b481e27c440bacdfd4f4e8d7f4d997dd0
6
+ metadata.gz: 27e9c5abd1ae5a60b42aee5bed88640b3dd49352214b2af420384333b8d93c558369e14220f4f996729d1ccdcf4155552e05dbd59c0ffaf05ca81fe0ea4f9489
7
+ data.tar.gz: 5e86aa794e2ec154f8e797040cdadd6a59c43489bf98ffdb070f8e7d90a82572e73f19ba5cadedfebdd90fea29062fd0fa873e9bba08cf06f1277483a60e7ee0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,95 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [9.4.0] - 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - **The skill family.** `/architect` is now the inline working mode — work
13
+ outside the loop at a conversational cadence (UI, ops, small fixes), with
14
+ the space as the workspace (notes kept, `ARCHITECT.md` updated, commits
15
+ when the work calls for them) and the vocabulary glossary folded in.
16
+ **`/architect-discuss`** carries the judgment seat: exploration,
17
+ arbitration, judging finished runs, landing, iteration surgery.
18
+ **`/architect-status`** reports state read-only from CLI output and the
19
+ newest notes. The coordinator launch is agent-owned end to end:
20
+ **`/architect-action`** (renamed from `/architect-plan`) launches via the
21
+ new `architect_loop` pi-extension tool (jetpks/pi-extensions) — the
22
+ agent-side equivalent of `/loop`, sharing one spawn path so the two cannot
23
+ drift, awaiting the run id, then self-compacting the launching session
24
+ into a small on-call escalation seat.
25
+
26
+ ### Changed
27
+
28
+ - The `install-skills` help text, `docs/DESIGN.md`, and
29
+ `docs/reference/project.md` describe the five-skill family; the
30
+ `/architect-discuss` routing section now describes the real dual
31
+ agent-file mechanism (per-space `.pi/agents/` generation with resolved
32
+ pins, plus user-level `install-agents` playbooks as the headless/
33
+ untrusted fallback) instead of claiming nothing generates into the space.
34
+
35
+ ### Removed
36
+
37
+ - **`/architect-plan`** (renamed to `/architect-action`) and
38
+ **`/architect-vocabulary`** (folded into `/architect`); the `dispatch.md`
39
+ and `research.md` companion references moved to the discuss skill.
40
+
41
+ ## [9.3.0] - 2026-10-17
42
+
43
+ ### Removed
44
+
45
+ - **The gem cut (observation-deck entry 2).** Session capture moved to the
46
+ session-sink pi extension (jetpks/pi-extensions), so the gem's entire
47
+ push/streaming surface is gone: `SpacePush`/`Packer` and the `architect
48
+ push` command; dispatch `--push-url/--push-host/--push-token`, `RunCreator`,
49
+ and the harness tee; `ConversationsClient` + `session_sync/**` and the
50
+ sessions CLI that drove them; `JobsClient` (its only consumer was the
51
+ removed server jobs API). looplog, runlog, cost, routing, and the loop
52
+ lifecycle are untouched.
53
+
54
+ ## [9.2.1] - 2026-10-15
55
+
56
+ ### Fixed
57
+
58
+ - dispatch copies the lane prompt into the build dir (`build/<id>-<lane>/`
59
+ `prompt.md`) at dispatch time, so the record is self-contained even if the
60
+ scratch source is removed.
61
+
62
+ ## [9.2.0] - 2026-10-15
63
+
64
+ ### Added
65
+
66
+ - **CLI observability:** `architect looplog` (follow every active loop run
67
+ live; `--list`/`--replay`/`--all`), `architect runlog` (pretty-print or
68
+ stream a run.jsonl), and full gate transcripts written to
69
+ `build/gate/<id>-<ts>.md`.
70
+
71
+ ### Changed
72
+
73
+ - Docs re-centered on the coordinator path; role agents installed user-level
74
+ by `architect install-agents`.
75
+
76
+ ## [9.1.1] - 2026-10-14
77
+
78
+ ### Fixed
79
+
80
+ - `architect provision` fast-forward-syncs the integration branch from the
81
+ default branch (or cuts exactly `--base REF`), refusing loudly on
82
+ divergence instead of silently building on a stale base.
83
+
84
+ ## [9.1.0] - 2026-10-13
85
+
86
+ ### Added
87
+
88
+ - **The narrator role:** a sixth loop role (cheap model, one file per settled
89
+ event) writing `notes/narrative-*.md` — verbatim facts, never a gate.
90
+
91
+ ### Changed
92
+
93
+ - `architect cost` gains human formatting (significant figures, `--precise`,
94
+ color) and a latency turn/generation split.
95
+ - `architect new` flocks the scaffold critical section across processes.
96
+
8
97
  ## [9.0.0] - 2026-10-09
9
98
 
10
99
  ### Changed
data/README.md CHANGED
@@ -12,19 +12,17 @@ task-scoped **spaces**: directories that hold repos, notes, and artifacts under
12
12
  one obvious filesystem root. Nothing lives only in a chat window — every
13
13
  decision, spec, and verdict is committed to the space repo.
14
14
 
15
- It installs **one executable**, `architect`. The spaces substrate and the
16
- evergreen-checkout engine are two sibling gems:
15
+ It installs **one executable**, `architect`. The spaces substrate is a sibling
16
+ gem:
17
17
 
18
18
  | Gem | Dependency | What it provides |
19
19
  |-----|------------|------------------|
20
20
  | **space-cadet** | hard | The spaces substrate (`Space::Core`): create, manage & containerize task-scoped workspaces — the `space` binary — plus the copy-on-write evergreen-checkout substrate repos provision from |
21
- | **repo-tender** | soft | Evergreen clone tending — the `repo-tender` binary (with the deprecated `src` shim) — and the launchd agent the `architect sessions agent` commands drive. Install with `gem install repo-tender` when you want the session-sync rail — everything else works without it |
22
21
 
23
22
  ## Installation 📦
24
23
 
25
24
  ```bash
26
25
  gem install space-architect
27
- gem install repo-tender # optional: enables the sessions launchd agent
28
26
  ```
29
27
 
30
28
  The gem installs one executable: `architect`. 🎀 (Or add
@@ -956,13 +956,7 @@ module Space::Architect
956
956
 
957
957
  def dispatch(iteration, lane, model: nil, max_turns: 200, bin: nil, harness: nil, effort: nil,
958
958
  force: false, quiet: false,
959
- detach: false, push_url: nil, push_token: nil, push_host: nil, run_creator: nil,
960
- push_client: nil, timeout: nil, prompt: nil, now: Time.now)
961
- raise Space::Core::Error, "Specify --push-host or --push-url, not both" if push_host && push_url
962
- raise Space::Core::Error, "--push-host requires --push-token" if push_host && !push_token
963
- raise Space::Core::Error, "--detach cannot be combined with --push-url or --push-host" \
964
- if detach && (push_url || push_host)
965
-
959
+ detach: false, timeout: nil, prompt: nil, now: Time.now)
966
960
  err = quiet ? File.open(File::NULL, "w") : $stderr
967
961
 
968
962
  entry = slice_entry(iteration)
@@ -1026,26 +1020,14 @@ module Space::Architect
1026
1020
  result[:prompt_copied] = prompt_path if prompt
1027
1021
  result
1028
1022
  else
1029
- created_run_id = nil
1030
- if push_host
1031
- creator = run_creator || RunCreator.new(push_host, push_token)
1032
- created_run_id = creator.create
1033
- push_url = "#{push_host.chomp('/')}/runs/#{created_run_id}/ingest"
1034
- end
1035
-
1036
1023
  run_kwargs = { prompt_path: prompt_path, run_log_path: run_log_path, chdir: wt_path }
1037
1024
  run_kwargs[:timeout] = timeout if timeout
1038
- run_kwargs[:push_url] = push_url if push_url
1039
- run_kwargs[:push_token] = push_token if push_token
1040
- run_kwargs[:push_client] = push_client if push_client
1041
- run_kwargs[:err] = err if quiet
1025
+ run_kwargs[:err] = err if quiet
1042
1026
  exit_code = harness_obj.run(**run_kwargs)
1043
1027
 
1044
1028
  result = { exit_code: exit_code, run_log: run_log_path, report: report_path, worktree: wt_path }
1045
1029
  result[:prompt_copied] = prompt_path if prompt
1046
1030
  result[:timed_out] = true if exit_code == Harness::PiHarness::TIMEOUT_EXIT_CODE
1047
- result[:created_run_id] = created_run_id if created_run_id
1048
- result[:push_url] = push_url if push_url
1049
1031
  result
1050
1032
  end
1051
1033
  end
@@ -1731,7 +1713,7 @@ module Space::Architect
1731
1713
  # two paths looked at, and the default in use.
1732
1714
  def default_model_inform
1733
1715
  "model: no architect.yaml found (looked at #{Routing.space_path(space.path)} + #{Routing.global_path}) — " \
1734
- "using default '#{Harness::DEFAULT_MODEL}'"
1716
+ "using default '#{Harness::DEFAULT_MODEL}' #{Routing::INIT_HINT}"
1735
1717
  end
1736
1718
 
1737
1719
  # --prompt: the caller authors the lane prompt anywhere (a fresh scratch file) and
@@ -2,14 +2,10 @@
2
2
 
3
3
  require "json"
4
4
  require "fileutils"
5
- require_relative "../jobs_client"
6
- require_relative "../conversations_client"
7
5
  require_relative "../loop_cost"
8
- require_relative "../session_sync"
9
6
  require_relative "../runlog"
10
7
  require_relative "../looplog"
11
8
  require_relative "../extension_installer"
12
- require_relative "../space_push"
13
9
 
14
10
  module Space::Architect
15
11
  module CLI
@@ -173,7 +169,9 @@ module Space::Architect
173
169
  # Legacy lanes store no model — resolve the config layer once so the
174
170
  # display shows what they would actually route, and mark the bare
175
171
  # constant as the last resort it is, not ordinary behavior.
176
- config_model = Routing.for_space(sp.path)&.dig("builder", "model")
172
+ # Fully qualified: the Architect::Routing subcommand namespace
173
+ # (I09) lexically shadows Space::Architect::Routing inside this module.
174
+ config_model = Space::Architect::Routing.for_space(sp.path)&.dig("builder", "model")
177
175
 
178
176
  terminal.say "Project status: #{block['status'] || '(none)'}"
179
177
  terminal.say "Current iteration: #{block['current_iteration'] || '(none)'}"
@@ -215,41 +213,6 @@ module Space::Architect
215
213
  end
216
214
  end
217
215
 
218
- class Push < BaseCommand
219
- desc "Pack the current space and push it to a space-server for import"
220
- phase 53, "Project"
221
- argument :space, required: false, desc: "Space identifier (default: $PWD)"
222
- option :host, required: true, desc: "Base URL of the space-server"
223
- option :token, required: true, desc: "Bearer token for authorization"
224
-
225
- def call(space: nil, host: nil, token: nil, **opts)
226
- setup_terminal(**opts.slice(:color, :colors))
227
- handle_errors do
228
- raise Space::Core::Error, "--host is required" unless host
229
- raise Space::Core::Error, "--token is required" unless token
230
-
231
- render(store.find(space)) do |sp|
232
- bundle = SpacePush::Packer.new(sp.path).pack
233
- begin
234
- terminal.say "Packed #{sp.id}: #{format_bytes(File.size(bundle))}"
235
- summary = SpacePush.new(host, token).push(bundle)
236
- terminal.say "Imported: #{summary['slug']} (id #{summary['id']}) — " \
237
- "#{summary['iterations']} iterations, #{summary['artifacts']} artifacts, #{summary['runs']} runs"
238
- CLI.record_outcome(Outcome.new(exit_code: 0))
239
- ensure
240
- File.delete(bundle)
241
- end
242
- end
243
- end
244
- end
245
-
246
- private
247
-
248
- def format_bytes(size)
249
- format("%.1f MiB", size.to_f / (1024 * 1024))
250
- end
251
- end
252
-
253
216
  class Sync < BaseCommand
254
217
  desc "Sync tracked repo clones with their remotes (fast-forward only, no rebase/reset)"
255
218
  phase 53, "Project"
@@ -560,17 +523,14 @@ module Space::Architect
560
523
  option :force_effort, default: nil, desc: "Force the literal level onto the --thinking flag, skipping architect's validation — alias for --force-thinking/--force-reasoning (dispatch only; the binary's rejection is final)"
561
524
  option :force_thinking, default: nil, desc: "Force the literal level onto the --thinking flag, skipping architect's validation — alias for --force-effort/--force-reasoning (dispatch only; the binary's rejection is final)"
562
525
  option :force_reasoning, default: nil, desc: "Force the literal level onto the --thinking flag, skipping architect's validation — alias for --force-effort/--force-thinking (dispatch only; the binary's rejection is final)"
563
- option :quiet, type: :boolean, default: false, desc: "Suppress thinking-translation and harness run-time warn lines (liveness/push) on $stderr for this dispatch"
526
+ option :quiet, type: :boolean, default: false, desc: "Suppress thinking-translation and harness run-time warn lines (liveness) on $stderr for this dispatch"
564
527
  option :detach, type: :boolean, default: false, desc: "Detach the builder process (returns immediately with PID; poll report for completion)"
565
528
  option :timeout, default: "14400", desc: "Wall-clock timeout in seconds (0 disables; default 4h); foreground only"
566
- option :push_url, default: nil, desc: "HTTP endpoint for streaming push (POST body to this URL)"
567
- option :push_token, default: nil, desc: "Bearer token for push endpoint authorization"
568
- option :push_host, default: nil, desc: "Base URL of the ingest server; the CLI creates a run via POST <host>/runs and streams to /runs/<id>/ingest (requires --push-token)"
569
529
 
570
530
  def call(iteration:, lane:, space: nil, prompt: nil, model: nil,
571
531
  max_turns: "200", harness: nil, effort: nil, thinking: nil, reasoning: nil,
572
532
  force_effort: nil, force_thinking: nil, force_reasoning: nil, quiet: false, detach: false,
573
- timeout: "14400", push_url: nil, push_token: nil, push_host: nil, **opts)
533
+ timeout: "14400", **opts)
574
534
  setup_terminal(**opts.slice(:color, :colors))
575
535
  handle_errors do
576
536
  level = resolve_thinking_alias(effort: effort, thinking: thinking, reasoning: reasoning)
@@ -588,9 +548,6 @@ module Space::Architect
588
548
  kwargs[:force] = true if forced_level
589
549
  kwargs[:quiet] = true if quiet
590
550
  kwargs[:timeout] = timeout.to_i unless detach
591
- kwargs[:push_url] = push_url if push_url
592
- kwargs[:push_token] = push_token if push_token
593
- kwargs[:push_host] = push_host if push_host
594
551
  res = project.dispatch(iteration, lane, **kwargs)
595
552
  terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
596
553
  if detach
@@ -607,7 +564,6 @@ module Space::Architect
607
564
  else
608
565
  terminal.say "Run log: #{terminal.path(res[:run_log])}"
609
566
  terminal.say "Report: #{terminal.path(res[:report])}"
610
- terminal.say "Ingest URL: #{res[:push_url]}" if res[:push_url]
611
567
  terminal.say "Builder exited with status #{res[:exit_code]}"
612
568
  unless res[:report].exist? && !res[:report].read.strip.empty?
613
569
  terminal.say "WARNING: no report at #{terminal.path(res[:report])} — the lane produced no deliverable."
@@ -953,7 +909,7 @@ module Space::Architect
953
909
  # --help, which :boolean would render as `--[no-]json`.
954
910
  option :json, type: :flag, default: false, desc: "Emit the frozen JSON contract instead of tables"
955
911
  option :space, desc: "Space identifier or path (default: nearest ancestor with space.yaml)"
956
- option :sessions_root, desc: "pi sessions root (default: ~/.pi/agent/sessions via SessionSync)"
912
+ option :sessions_root, desc: "pi sessions root (default: ~/.pi/agent/sessions)"
957
913
  option :since, desc: "Only artifacts starting at/after this ISO-8601 timestamp or YYYY-MM-DD date"
958
914
  option :by_iteration, type: :flag, default: false, desc: "Split the cost table into per-iteration windows"
959
915
  option :latency, type: :flag, default: false, desc: "Per-model latency: round wall, turn vs generation tok/s, tool wait share"
@@ -969,7 +925,7 @@ module Space::Architect
969
925
  render(store.find(space)) do |sp|
970
926
  report = LoopCost.report(
971
927
  space: sp,
972
- sessions_root: sessions_root || SessionSync.default_pi_root,
928
+ sessions_root: sessions_root || default_pi_sessions_root,
973
929
  since: since,
974
930
  by_iteration: by_iteration,
975
931
  latency: latency
@@ -982,6 +938,12 @@ module Space::Architect
982
938
 
983
939
  private
984
940
 
941
+ # The pi sessions root Cost reads by default — the same value the cut
942
+ # session-sync rail's default computed.
943
+ def default_pi_sessions_root(env: ENV)
944
+ File.join(Space::Core::XDG.home(env: env), ".pi", "agent", "sessions")
945
+ end
946
+
985
947
  def render_cost_text(report, by_iteration, latency, precise)
986
948
  if latency
987
949
  latency_table(report["latency"], precise)
@@ -1100,7 +1062,7 @@ module Space::Architect
1100
1062
  end
1101
1063
 
1102
1064
  class InstallSkills < BaseCommand
1103
- desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
1065
+ desc "Install bundled skills (architect, architect-discuss, architect-action, architect-status, architect-research) for a harness"
1104
1066
  phase 53, "Project"
1105
1067
  option :provider, default: "claude", desc: "Skill install target (default: claude; validated by the installer)"
1106
1068
  option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
@@ -1140,6 +1102,56 @@ module Space::Architect
1140
1102
  end
1141
1103
  end
1142
1104
 
1105
+ module Routing
1106
+ # I09: `architect routing init` — the self-service cure for the
1107
+ # missing-config wall (default_model_inform's hint names this command).
1108
+ # Writes Routing::TEMPLATE verbatim and round-trips the write through
1109
+ # Routing.parse before claiming success; the routing machinery itself
1110
+ # is untouched — the command only writes a file it already accepts.
1111
+ class Init < BaseCommand
1112
+ desc "Write a commented starter architect.yaml (space root by default; --global for the machine-wide config)"
1113
+ phase 53, "Project"
1114
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
1115
+ option :global, type: :boolean, default: false,
1116
+ desc: "Write ~/.config/space-architect/architect.yaml (the machine-wide config; SPACE_ARCHITECT_GLOBAL_CONFIG overrides the path) instead of the space root, creating parent directories as needed"
1117
+
1118
+ def call(space: nil, global: false, **opts)
1119
+ setup_terminal(**opts.slice(:color, :colors))
1120
+ handle_errors do
1121
+ target =
1122
+ if global
1123
+ raise Space::Core::Error, "--global and an explicit space argument are mutually exclusive" if space
1124
+ # Routing.global_path is the env-overridable canonical path — never re-derive it here.
1125
+ path = Space::Architect::Routing.global_path
1126
+ FileUtils.mkdir_p(path.dirname)
1127
+ path
1128
+ else
1129
+ # The house walk-up: empty identifier looks for space.yaml from $PWD upward.
1130
+ found = store.find(space).value_or(nil)
1131
+ unless found
1132
+ raise Space::Core::Error,
1133
+ "no space.yaml found upward from #{Dir.pwd} — run `architect routing init --global` to scaffold the machine-wide config instead"
1134
+ end
1135
+ Space::Architect::Routing.space_path(found.path)
1136
+ end
1137
+
1138
+ raise Space::Core::Error, "architect.yaml already exists: #{target} — routing init never overwrites" if target.exist?
1139
+
1140
+ File.write(target, Space::Architect::Routing::TEMPLATE)
1141
+ # A scaffold that fails its own parser (or misses a required role)
1142
+ # is a bug — validate the round-trip, let the error escape loud.
1143
+ parsed = Space::Architect::Routing.parse(File.read(target), target)
1144
+ missing = Space::Architect::Routing::REQUIRED_ROLES - parsed.keys
1145
+ raise Space::Core::Error, "scaffolded #{target} is missing required roles: #{missing.join(', ')}" if missing.any?
1146
+
1147
+ terminal.say "Wrote starter routing config: #{terminal.path(target.to_s)}"
1148
+ terminal.say "Edit the example/REPLACE_ME models; the next command or /loop picks the file up."
1149
+ CLI.record_outcome(Outcome.new(exit_code: 0))
1150
+ end
1151
+ end
1152
+ end
1153
+ end
1154
+
1143
1155
  module Worktree
1144
1156
  class Add < BaseCommand
1145
1157
  desc "Create a worktree for a lane"
@@ -1340,235 +1352,6 @@ module Space::Architect
1340
1352
  end
1341
1353
  end
1342
1354
  end
1343
-
1344
- module Jobs
1345
- # dry-cli 1.4.1 only enforces `required: true` on argument, not on
1346
- # option (Dry::CLI::Parser#parse_required_params only ever consults
1347
- # command.required_arguments) — a missing --host/--token would
1348
- # otherwise surface as a raw ArgumentError from the **opts keyword
1349
- # splat, uncaught by handle_errors. Every subcommand below validates
1350
- # explicitly so the failure is a clean Space::Core::Error instead.
1351
- def self.require_credentials!(host, token)
1352
- raise Space::Core::Error, "--host is required" unless host
1353
- raise Space::Core::Error, "--token is required" unless token
1354
- end
1355
-
1356
- class List < BaseCommand
1357
- desc "List jobs for the authenticated user (owner-scoped, newest-first)"
1358
- option :host, required: true, desc: "Base URL of the space-server"
1359
- option :token, required: true, desc: "Bearer token for authorization"
1360
-
1361
- def call(host: nil, token: nil, **opts)
1362
- setup_terminal(**opts.slice(:color, :colors))
1363
- handle_errors do
1364
- Jobs.require_credentials!(host, token)
1365
- jobs = JobsClient.new(host, token).list
1366
- if jobs.empty?
1367
- terminal.say "No jobs"
1368
- else
1369
- rows = jobs.map do |j|
1370
- [j["id"], j["status"], j["run_id"], j["created_at"],
1371
- j["harness"], j["model"], j.dig("provenance", "lane")]
1372
- end
1373
- terminal.say terminal.table(%w[ID Status RunID CreatedAt Harness Model Lane], rows)
1374
- end
1375
- CLI.record_outcome(Outcome.new(exit_code: 0))
1376
- end
1377
- end
1378
- end
1379
-
1380
- class Show < BaseCommand
1381
- desc "Show a job's full JSON"
1382
- argument :id, required: true, desc: "Job id"
1383
- option :host, required: true, desc: "Base URL of the space-server"
1384
- option :token, required: true, desc: "Bearer token for authorization"
1385
-
1386
- def call(id:, host: nil, token: nil, **opts)
1387
- setup_terminal(**opts.slice(:color, :colors))
1388
- handle_errors do
1389
- Jobs.require_credentials!(host, token)
1390
- job = JobsClient.new(host, token).show(id)
1391
- terminal.say JSON.pretty_generate(job)
1392
- CLI.record_outcome(Outcome.new(exit_code: 0))
1393
- end
1394
- end
1395
- end
1396
-
1397
- class Cancel < BaseCommand
1398
- desc "Cancel a job"
1399
- argument :id, required: true, desc: "Job id"
1400
- option :host, required: true, desc: "Base URL of the space-server"
1401
- option :token, required: true, desc: "Bearer token for authorization"
1402
-
1403
- def call(id:, host: nil, token: nil, **opts)
1404
- setup_terminal(**opts.slice(:color, :colors))
1405
- handle_errors do
1406
- Jobs.require_credentials!(host, token)
1407
- result = JobsClient.new(host, token).cancel(id)
1408
- terminal.say JSON.pretty_generate(result)
1409
- CLI.record_outcome(Outcome.new(exit_code: 0))
1410
- end
1411
- end
1412
- end
1413
-
1414
- class Watch < BaseCommand
1415
- desc "Resolve a job's run and stream its live events (SSE) until run_complete"
1416
- argument :id, required: true, desc: "Job id"
1417
- option :host, required: true, desc: "Base URL of the space-server"
1418
- option :token, required: true, desc: "Bearer token for authorization"
1419
-
1420
- def call(id:, host: nil, token: nil, **opts)
1421
- setup_terminal(**opts.slice(:color, :colors))
1422
- handle_errors do
1423
- Jobs.require_credentials!(host, token)
1424
- client = JobsClient.new(host, token)
1425
- run_id = client.wait_for_run_id(id)
1426
- client.stream(run_id) { |data| terminal.say data }
1427
- CLI.record_outcome(Outcome.new(exit_code: 0))
1428
- end
1429
- end
1430
- end
1431
- end
1432
-
1433
- module Sessions
1434
- # See Jobs.require_credentials! above for why this is validated
1435
- # explicitly rather than relying on dry-cli's `required:` option flag.
1436
- def self.require_credentials!(host, token)
1437
- raise Space::Core::Error, "--host is required" unless host
1438
- raise Space::Core::Error, "--token or #{SessionSync::TOKEN_ENV} is required" unless token
1439
- end
1440
-
1441
- class Sync < BaseCommand
1442
- desc "Scan pi/claude session files and upload new/grown conversations to a space-server"
1443
- option :host, required: true, desc: "Base URL of the space-server"
1444
- option :token, default: nil, desc: "Bearer token for authorization (defaults to $#{SessionSync::TOKEN_ENV}; an explicit --token wins; an op:// ref is resolved once via `op read`)"
1445
- option :state_file, default: nil, desc: "Cursor YAML path (default: $XDG_STATE_HOME/space-architect/session-sync.yaml)"
1446
- option :pi_root, default: nil, desc: "Override the pi sessions root (default: ~/.pi/agent/sessions)"
1447
- option :claude_root, default: nil, desc: "Override the claude projects root (default: ~/.claude/projects)"
1448
- option :dry_run, type: :boolean, default: false, desc: "Report what would upload without uploading or recording the cursor"
1449
-
1450
- def call(host: nil, token: nil, state_file: nil, pi_root: nil, claude_root: nil, dry_run: false, **opts)
1451
- setup_terminal(**opts.slice(:color, :colors))
1452
- handle_errors do
1453
- resolved_token = token || ENV[SessionSync::TOKEN_ENV]
1454
- Sessions.require_credentials!(host, resolved_token)
1455
- runner = SessionSync::Runner.new(
1456
- client: ConversationsClient.new(host, resolved_token),
1457
- state_path: state_file || SessionSync.default_state_file,
1458
- pi_root: pi_root || SessionSync.default_pi_root,
1459
- claude_root: claude_root || SessionSync.default_claude_root,
1460
- dry_run: dry_run
1461
- )
1462
- results = runner.call
1463
- results.each do |r|
1464
- terminal.say "#{r[:action]}: #{r[:path]}#{r[:action] == :failed ? " (#{r[:status]}: #{r[:errors]})" : ""}"
1465
- end
1466
- failed = results.count { |r| r[:action] == :failed }
1467
- terminal.say "#{results.size} file(s): #{results.size - failed} ok, #{failed} failed"
1468
- CLI.record_outcome(Outcome.new(exit_code: failed.zero? ? 0 : 1))
1469
- end
1470
- end
1471
- end
1472
-
1473
- module Agent
1474
- module Helpers
1475
- def plist_path
1476
- File.join(SessionSync.launch_agents_dir, "#{SessionSync::LABEL}.plist")
1477
- end
1478
-
1479
- def format_failure(f) = f.is_a?(Hash) ? f.inspect : f.to_s
1480
-
1481
- # The launchd-agent seam is repo-tender's — a soft dependency, never a
1482
- # gemspec runtime dep. Required lazily so `sessions agent …` degrades
1483
- # with an actionable hint instead of failing at boot when the gem is
1484
- # absent; handle_errors turns the raise into a red message + exit 1.
1485
- def agent
1486
- require "repo_tender"
1487
- RepoTender::Launchd::Agent
1488
- rescue LoadError
1489
- raise Space::Core::Error, "sessions agent needs the repo-tender gem — install it with: gem install repo-tender"
1490
- end
1491
- end
1492
-
1493
- class Install < BaseCommand
1494
- include Helpers
1495
- desc "Install the launchd agent that periodically runs `architect sessions sync`"
1496
- option :host, required: true, desc: "Base URL of the space-server"
1497
- option :token, required: true, desc: "Bearer token for authorization (an op:// ref is resolved once at install time; the raw value is stored in the plist's EnvironmentVariables, not argv)"
1498
- option :interval, default: "900", desc: "StartInterval in seconds (default 900)"
1499
-
1500
- def call(host: nil, token: nil, interval: "900", **opts)
1501
- setup_terminal(**opts.slice(:color, :colors))
1502
- handle_errors do
1503
- Sessions.require_credentials!(host, token)
1504
- resolved_token = token.start_with?("op://") ? SessionSync.resolve_token(token) : token
1505
- pp = plist_path
1506
- xml = SessionSync::Plist.call(
1507
- label: SessionSync::LABEL,
1508
- refresh_interval: interval.to_i,
1509
- log_dir: SessionSync.log_dir,
1510
- bin_path: SessionSync::BinPath.detect,
1511
- ruby_bin: RbConfig.ruby,
1512
- host: host,
1513
- env: {SessionSync::TOKEN_ENV => resolved_token}
1514
- )
1515
- FileUtils.mkdir_p(File.dirname(pp))
1516
- File.write(pp, xml)
1517
- File.chmod(0o600, pp)
1518
-
1519
- result = agent.new(label: SessionSync::LABEL).install(pp)
1520
- raise Space::Core::Error, "bootstrap failed: #{format_failure(result.failure)}" if result.failure?
1521
-
1522
- terminal.say "Installed: #{terminal.path(pp)}"
1523
- CLI.record_outcome(Outcome.new(exit_code: 0))
1524
- end
1525
- end
1526
- end
1527
-
1528
- class Uninstall < BaseCommand
1529
- include Helpers
1530
- desc "Uninstall the session-sync launchd agent (bootout + remove the plist)"
1531
-
1532
- def call(**opts)
1533
- setup_terminal(**opts.slice(:color, :colors))
1534
- handle_errors do
1535
- pp = plist_path
1536
- result = agent.new(label: SessionSync::LABEL).uninstall
1537
- terminal.say "bootout reported: #{format_failure(result.failure)}" if result.failure?
1538
-
1539
- if File.exist?(pp)
1540
- File.delete(pp)
1541
- terminal.say "Removed plist: #{terminal.path(pp)}"
1542
- else
1543
- terminal.say "Plist not present: #{terminal.path(pp)}"
1544
- end
1545
- CLI.record_outcome(Outcome.new(exit_code: 0))
1546
- end
1547
- end
1548
- end
1549
-
1550
- class Status < BaseCommand
1551
- include Helpers
1552
- desc "Report the session-sync launchd agent's loaded/running/last-exit state"
1553
-
1554
- def call(**opts)
1555
- setup_terminal(**opts.slice(:color, :colors))
1556
- handle_errors do
1557
- result = agent.new(label: SessionSync::LABEL).status
1558
- raise Space::Core::Error, "status failed: #{format_failure(result.failure)}" if result.failure?
1559
-
1560
- s = result.success
1561
- terminal.say "label: #{SessionSync::LABEL}"
1562
- terminal.say "loaded: #{s[:loaded]}"
1563
- terminal.say "running: #{s[:running]}"
1564
- terminal.say "pid: #{s[:pid].inspect}"
1565
- terminal.say "last_exit: #{s[:last_exit].inspect}"
1566
- CLI.record_outcome(Outcome.new(exit_code: 0))
1567
- end
1568
- end
1569
- end
1570
- end
1571
- end
1572
1355
  end
1573
1356
  end
1574
1357
  end
@@ -1583,7 +1366,6 @@ Space::Architect::CLI::Registry.register "init", Space::Architect::CLI::Archit
1583
1366
  Space::Architect::CLI::Registry.register "ground", Space::Architect::CLI::Architect::Ground
1584
1367
  Space::Architect::CLI::Registry.register "new", Space::Architect::CLI::Architect::New
1585
1368
  Space::Architect::CLI::Registry.register "status", Space::Architect::CLI::Architect::Status
1586
- Space::Architect::CLI::Registry.register "push", Space::Architect::CLI::Architect::Push
1587
1369
  Space::Architect::CLI::Registry.register "sync", Space::Architect::CLI::Architect::Sync
1588
1370
  Space::Architect::CLI::Registry.register "rehearse", Space::Architect::CLI::Architect::Rehearse
1589
1371
  Space::Architect::CLI::Registry.register "freeze", Space::Architect::CLI::Architect::Freeze
@@ -1610,19 +1392,8 @@ Space::Architect::CLI::Registry.register "worktree" do |wt|
1610
1392
  wt.register "remove", Space::Architect::CLI::Architect::Worktree::Remove
1611
1393
  wt.register "list", Space::Architect::CLI::Architect::Worktree::List
1612
1394
  end
1613
- Space::Architect::CLI::Registry.register "jobs" do |j|
1614
- j.register "list", Space::Architect::CLI::Architect::Jobs::List
1615
- j.register "show", Space::Architect::CLI::Architect::Jobs::Show
1616
- j.register "watch", Space::Architect::CLI::Architect::Jobs::Watch
1617
- j.register "cancel", Space::Architect::CLI::Architect::Jobs::Cancel
1618
- end
1619
- Space::Architect::CLI::Registry.register "sessions" do |s|
1620
- s.register "sync", Space::Architect::CLI::Architect::Sessions::Sync
1621
- s.register "agent" do |a|
1622
- a.register "install", Space::Architect::CLI::Architect::Sessions::Agent::Install
1623
- a.register "uninstall", Space::Architect::CLI::Architect::Sessions::Agent::Uninstall
1624
- a.register "status", Space::Architect::CLI::Architect::Sessions::Agent::Status
1625
- end
1395
+ Space::Architect::CLI::Registry.register "routing" do |r|
1396
+ r.register "init", Space::Architect::CLI::Architect::Routing::Init
1626
1397
  end
1627
1398
  Space::Architect::CLI::Registry.register "variant" do |v|
1628
1399
  v.register "add", Space::Architect::CLI::Architect::Variant::Add