space-architect 9.2.0 → 9.3.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 +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +2 -4
- data/lib/space_architect/architect_project.rb +8 -21
- data/lib/space_architect/cli/architect.rb +10 -294
- data/lib/space_architect/harness.rb +2 -51
- data/lib/space_architect/version.rb +1 -1
- data/lib/space_architect.rb +0 -4
- metadata +2 -41
- data/lib/space_architect/conversations_client.rb +0 -81
- data/lib/space_architect/jobs_client.rb +0 -147
- data/lib/space_architect/run_creator.rb +0 -53
- data/lib/space_architect/session_sync/bin_path.rb +0 -28
- data/lib/space_architect/session_sync/cursor.rb +0 -43
- data/lib/space_architect/session_sync/plist.rb +0 -111
- data/lib/space_architect/session_sync/runner.rb +0 -64
- data/lib/space_architect/session_sync/session_id.rb +0 -20
- data/lib/space_architect/session_sync.rb +0 -56
- data/lib/space_architect/space_push.rb +0 -274
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fd0baee9b2db69fa1ef96c1b4993f75a8912b5b0898d012c13036a379e32a7f
|
|
4
|
+
data.tar.gz: 582172fa5c61e4a9419409cafdfa8701ca096a2621527141099944f3261c6592
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59dd000d01b8fd0b42729d87f7f843f93690b0fbccf927e127426f352fa79aada6078a885b54e3342d3c901500fde93338be3198b77e871df85f7b28ef816f9e
|
|
7
|
+
data.tar.gz: 29f88c1fdec961ed7b53d9d9e4a3a3c156cadac2da157786bea28fca4b20b09cb786ab195d5eedf0877e11c901a2d140d66347bbee3fc52570a4a68225b78284
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,62 @@ 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.3.0] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- **The gem cut (observation-deck entry 2).** Session capture moved to the
|
|
13
|
+
session-sink pi extension (jetpks/pi-extensions), so the gem's entire
|
|
14
|
+
push/streaming surface is gone: `SpacePush`/`Packer` and the `architect
|
|
15
|
+
push` command; dispatch `--push-url/--push-host/--push-token`, `RunCreator`,
|
|
16
|
+
and the harness tee; `ConversationsClient` + `session_sync/**` and the
|
|
17
|
+
sessions CLI that drove them; `JobsClient` (its only consumer was the
|
|
18
|
+
removed server jobs API). looplog, runlog, cost, routing, and the loop
|
|
19
|
+
lifecycle are untouched.
|
|
20
|
+
|
|
21
|
+
## [9.2.1] - 2026-10-15
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- dispatch copies the lane prompt into the build dir (`build/<id>-<lane>/`
|
|
26
|
+
`prompt.md`) at dispatch time, so the record is self-contained even if the
|
|
27
|
+
scratch source is removed.
|
|
28
|
+
|
|
29
|
+
## [9.2.0] - 2026-10-15
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **CLI observability:** `architect looplog` (follow every active loop run
|
|
34
|
+
live; `--list`/`--replay`/`--all`), `architect runlog` (pretty-print or
|
|
35
|
+
stream a run.jsonl), and full gate transcripts written to
|
|
36
|
+
`build/gate/<id>-<ts>.md`.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Docs re-centered on the coordinator path; role agents installed user-level
|
|
41
|
+
by `architect install-agents`.
|
|
42
|
+
|
|
43
|
+
## [9.1.1] - 2026-10-14
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `architect provision` fast-forward-syncs the integration branch from the
|
|
48
|
+
default branch (or cuts exactly `--base REF`), refusing loudly on
|
|
49
|
+
divergence instead of silently building on a stale base.
|
|
50
|
+
|
|
51
|
+
## [9.1.0] - 2026-10-13
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **The narrator role:** a sixth loop role (cheap model, one file per settled
|
|
56
|
+
event) writing `notes/narrative-*.md` — verbatim facts, never a gate.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- `architect cost` gains human formatting (significant figures, `--precise`,
|
|
61
|
+
color) and a latency turn/generation split.
|
|
62
|
+
- `architect new` flocks the scaffold critical section across processes.
|
|
63
|
+
|
|
8
64
|
## [9.0.0] - 2026-10-09
|
|
9
65
|
|
|
10
66
|
### 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
|
|
16
|
-
|
|
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,
|
|
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[:
|
|
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
|
|
@@ -1740,7 +1722,12 @@ module Space::Architect
|
|
|
1740
1722
|
if prompt
|
|
1741
1723
|
src = Pathname.new(prompt)
|
|
1742
1724
|
raise Space::Core::Error, "prompt file not found: #{src}" unless src.exist?
|
|
1743
|
-
|
|
1725
|
+
# src == dst: the file is already canonical — skip the copy; a truncate-then-
|
|
1726
|
+
# write onto the source itself would destroy the prompt before dispatch (I61
|
|
1727
|
+
# 20261009-pi-port: --prompt <canonical> emptied the kit to 0 bytes).
|
|
1728
|
+
unless src.expand_path == Pathname(prompt_path).expand_path
|
|
1729
|
+
File.binwrite(prompt_path, File.binread(src))
|
|
1730
|
+
end
|
|
1744
1731
|
end
|
|
1745
1732
|
|
|
1746
1733
|
raise Space::Core::Error, "prompt.md not found: #{prompt_path}" unless prompt_path.exist?
|
|
@@ -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
|
|
@@ -215,41 +211,6 @@ module Space::Architect
|
|
|
215
211
|
end
|
|
216
212
|
end
|
|
217
213
|
|
|
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
214
|
class Sync < BaseCommand
|
|
254
215
|
desc "Sync tracked repo clones with their remotes (fast-forward only, no rebase/reset)"
|
|
255
216
|
phase 53, "Project"
|
|
@@ -560,17 +521,14 @@ module Space::Architect
|
|
|
560
521
|
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
522
|
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
523
|
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
|
|
524
|
+
option :quiet, type: :boolean, default: false, desc: "Suppress thinking-translation and harness run-time warn lines (liveness) on $stderr for this dispatch"
|
|
564
525
|
option :detach, type: :boolean, default: false, desc: "Detach the builder process (returns immediately with PID; poll report for completion)"
|
|
565
526
|
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
527
|
|
|
570
528
|
def call(iteration:, lane:, space: nil, prompt: nil, model: nil,
|
|
571
529
|
max_turns: "200", harness: nil, effort: nil, thinking: nil, reasoning: nil,
|
|
572
530
|
force_effort: nil, force_thinking: nil, force_reasoning: nil, quiet: false, detach: false,
|
|
573
|
-
timeout: "14400",
|
|
531
|
+
timeout: "14400", **opts)
|
|
574
532
|
setup_terminal(**opts.slice(:color, :colors))
|
|
575
533
|
handle_errors do
|
|
576
534
|
level = resolve_thinking_alias(effort: effort, thinking: thinking, reasoning: reasoning)
|
|
@@ -588,9 +546,6 @@ module Space::Architect
|
|
|
588
546
|
kwargs[:force] = true if forced_level
|
|
589
547
|
kwargs[:quiet] = true if quiet
|
|
590
548
|
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
549
|
res = project.dispatch(iteration, lane, **kwargs)
|
|
595
550
|
terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
|
|
596
551
|
if detach
|
|
@@ -607,7 +562,6 @@ module Space::Architect
|
|
|
607
562
|
else
|
|
608
563
|
terminal.say "Run log: #{terminal.path(res[:run_log])}"
|
|
609
564
|
terminal.say "Report: #{terminal.path(res[:report])}"
|
|
610
|
-
terminal.say "Ingest URL: #{res[:push_url]}" if res[:push_url]
|
|
611
565
|
terminal.say "Builder exited with status #{res[:exit_code]}"
|
|
612
566
|
unless res[:report].exist? && !res[:report].read.strip.empty?
|
|
613
567
|
terminal.say "WARNING: no report at #{terminal.path(res[:report])} — the lane produced no deliverable."
|
|
@@ -953,7 +907,7 @@ module Space::Architect
|
|
|
953
907
|
# --help, which :boolean would render as `--[no-]json`.
|
|
954
908
|
option :json, type: :flag, default: false, desc: "Emit the frozen JSON contract instead of tables"
|
|
955
909
|
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
|
|
910
|
+
option :sessions_root, desc: "pi sessions root (default: ~/.pi/agent/sessions)"
|
|
957
911
|
option :since, desc: "Only artifacts starting at/after this ISO-8601 timestamp or YYYY-MM-DD date"
|
|
958
912
|
option :by_iteration, type: :flag, default: false, desc: "Split the cost table into per-iteration windows"
|
|
959
913
|
option :latency, type: :flag, default: false, desc: "Per-model latency: round wall, turn vs generation tok/s, tool wait share"
|
|
@@ -969,7 +923,7 @@ module Space::Architect
|
|
|
969
923
|
render(store.find(space)) do |sp|
|
|
970
924
|
report = LoopCost.report(
|
|
971
925
|
space: sp,
|
|
972
|
-
sessions_root: sessions_root ||
|
|
926
|
+
sessions_root: sessions_root || default_pi_sessions_root,
|
|
973
927
|
since: since,
|
|
974
928
|
by_iteration: by_iteration,
|
|
975
929
|
latency: latency
|
|
@@ -982,6 +936,12 @@ module Space::Architect
|
|
|
982
936
|
|
|
983
937
|
private
|
|
984
938
|
|
|
939
|
+
# The pi sessions root Cost reads by default — the same value the cut
|
|
940
|
+
# session-sync rail's default computed.
|
|
941
|
+
def default_pi_sessions_root(env: ENV)
|
|
942
|
+
File.join(Space::Core::XDG.home(env: env), ".pi", "agent", "sessions")
|
|
943
|
+
end
|
|
944
|
+
|
|
985
945
|
def render_cost_text(report, by_iteration, latency, precise)
|
|
986
946
|
if latency
|
|
987
947
|
latency_table(report["latency"], precise)
|
|
@@ -1340,235 +1300,6 @@ module Space::Architect
|
|
|
1340
1300
|
end
|
|
1341
1301
|
end
|
|
1342
1302
|
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
1303
|
end
|
|
1573
1304
|
end
|
|
1574
1305
|
end
|
|
@@ -1583,7 +1314,6 @@ Space::Architect::CLI::Registry.register "init", Space::Architect::CLI::Archit
|
|
|
1583
1314
|
Space::Architect::CLI::Registry.register "ground", Space::Architect::CLI::Architect::Ground
|
|
1584
1315
|
Space::Architect::CLI::Registry.register "new", Space::Architect::CLI::Architect::New
|
|
1585
1316
|
Space::Architect::CLI::Registry.register "status", Space::Architect::CLI::Architect::Status
|
|
1586
|
-
Space::Architect::CLI::Registry.register "push", Space::Architect::CLI::Architect::Push
|
|
1587
1317
|
Space::Architect::CLI::Registry.register "sync", Space::Architect::CLI::Architect::Sync
|
|
1588
1318
|
Space::Architect::CLI::Registry.register "rehearse", Space::Architect::CLI::Architect::Rehearse
|
|
1589
1319
|
Space::Architect::CLI::Registry.register "freeze", Space::Architect::CLI::Architect::Freeze
|
|
@@ -1610,20 +1340,6 @@ Space::Architect::CLI::Registry.register "worktree" do |wt|
|
|
|
1610
1340
|
wt.register "remove", Space::Architect::CLI::Architect::Worktree::Remove
|
|
1611
1341
|
wt.register "list", Space::Architect::CLI::Architect::Worktree::List
|
|
1612
1342
|
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
|
|
1626
|
-
end
|
|
1627
1343
|
Space::Architect::CLI::Registry.register "variant" do |v|
|
|
1628
1344
|
v.register "add", Space::Architect::CLI::Architect::Variant::Add
|
|
1629
1345
|
v.register "promote", Space::Architect::CLI::Architect::Variant::Promote
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "async/process"
|
|
4
|
-
require "async/http/client"
|
|
5
|
-
require "async/http/endpoint"
|
|
6
|
-
require "protocol/http/body/writable"
|
|
7
4
|
require "json"
|
|
8
5
|
require "pathname"
|
|
9
|
-
require "uri"
|
|
10
6
|
|
|
11
7
|
module Space::Architect
|
|
12
8
|
module Harness
|
|
@@ -106,8 +102,7 @@ module Space::Architect
|
|
|
106
102
|
@guard_path = guard_path
|
|
107
103
|
end
|
|
108
104
|
|
|
109
|
-
def run(prompt_path:, run_log_path:, chdir:,
|
|
110
|
-
push_client: nil, timeout: nil, liveness_delay: LIVENESS_DELAY_SECONDS, err: $stderr)
|
|
105
|
+
def run(prompt_path:, run_log_path:, chdir:, timeout: nil, liveness_delay: LIVENESS_DELAY_SECONDS, err: $stderr)
|
|
111
106
|
prompt_path = Pathname.new(prompt_path)
|
|
112
107
|
run_log_path = Pathname.new(run_log_path)
|
|
113
108
|
|
|
@@ -117,7 +112,7 @@ module Space::Architect
|
|
|
117
112
|
Sync do
|
|
118
113
|
child = Async::Process::Child.new(*argv, chdir: chdir.to_s, in: prompt_io, out: w, err: log)
|
|
119
114
|
w.close
|
|
120
|
-
tasks =
|
|
115
|
+
tasks = [Async { drain_pipe(r, log) }]
|
|
121
116
|
timed_out = false
|
|
122
117
|
timeout_task = nil
|
|
123
118
|
|
|
@@ -266,50 +261,6 @@ module Space::Architect
|
|
|
266
261
|
# Best-effort: an internal read/parse failure must never break the run.
|
|
267
262
|
end
|
|
268
263
|
|
|
269
|
-
def start_tee(r, log, push_url:, push_token:, push_client:, err: $stderr)
|
|
270
|
-
if push_url || push_client
|
|
271
|
-
body = Protocol::HTTP::Body::Writable.new(queue: Thread::SizedQueue.new(32))
|
|
272
|
-
push = Async { push_body(body, push_url: push_url, push_token: push_token, push_client: push_client, err: err) }
|
|
273
|
-
[Async { tee_pipe(r, log, body, err: err) }, push]
|
|
274
|
-
else
|
|
275
|
-
[Async { drain_pipe(r, log) }]
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def push_body(body, push_url:, push_token:, push_client:, err: $stderr)
|
|
280
|
-
path = push_url ? URI.parse(push_url).path : "/"
|
|
281
|
-
headers = [["content-type", "application/x-ndjson"]]
|
|
282
|
-
headers << ["authorization", "Bearer #{push_token}"] if push_token
|
|
283
|
-
if push_client
|
|
284
|
-
push_client.post(path, headers: headers, body: body).discard
|
|
285
|
-
else
|
|
286
|
-
Async::HTTP::Client.open(Async::HTTP::Endpoint.parse(push_url)) do |c|
|
|
287
|
-
c.post(path, headers: headers, body: body).discard
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
rescue StandardError => e
|
|
291
|
-
err.puts "push_body: transport error (best-effort, run log intact): #{e.class}: #{e.message}"
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def tee_pipe(r, log, body, err: $stderr)
|
|
295
|
-
pushing = true
|
|
296
|
-
while (chunk = r.gets)
|
|
297
|
-
log.write(chunk)
|
|
298
|
-
log.flush
|
|
299
|
-
if pushing
|
|
300
|
-
begin
|
|
301
|
-
body.write(chunk)
|
|
302
|
-
rescue StandardError => e
|
|
303
|
-
err.puts "tee_pipe: push write failed (best-effort, continuing log): #{e.class}: #{e.message}"
|
|
304
|
-
pushing = false
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
ensure
|
|
309
|
-
body.close_write
|
|
310
|
-
r.close
|
|
311
|
-
end
|
|
312
|
-
|
|
313
264
|
def drain_pipe(r, log)
|
|
314
265
|
while (chunk = r.gets)
|
|
315
266
|
log.write(chunk)
|
data/lib/space_architect.rb
CHANGED
|
@@ -5,14 +5,10 @@ require_relative "space_architect/version"
|
|
|
5
5
|
|
|
6
6
|
require_relative "space_architect/harness"
|
|
7
7
|
require_relative "space_architect/routing"
|
|
8
|
-
require_relative "space_architect/run_creator"
|
|
9
|
-
require_relative "space_architect/space_push"
|
|
10
8
|
require_relative "space_architect/gate_lint"
|
|
11
9
|
require_relative "space_architect/gate_evaluator"
|
|
12
10
|
require_relative "space_architect/architect_project"
|
|
13
11
|
require_relative "space_architect/skill_installer"
|
|
14
12
|
require_relative "space_architect/bug_report"
|
|
15
13
|
require_relative "space_architect/research"
|
|
16
|
-
require_relative "space_architect/conversations_client"
|
|
17
|
-
require_relative "space_architect/session_sync"
|
|
18
14
|
require_relative "space_architect/cli"
|