space-architect 5.3.0 → 5.5.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: de2f5d7d6f80acc7b6a7bd16c9ec3dbaf23ef9c2d425c8b1e532f5c24822533a
4
- data.tar.gz: b39f07d1d24b1288d54e37a08624322ad0fc4f5a664459d6bdcbe12acf4d0438
3
+ metadata.gz: e170b3f69d5b2a6bdeb9422aedc341df20148e7085ea38b120b43508a9aaf3d0
4
+ data.tar.gz: 8ed8737fdeacf88a4ef1917fcf2a695e3d7f6f679eb1c6745094179bf57a8166
5
5
  SHA512:
6
- metadata.gz: 5086b34800652f32d243dce10163543a6ee182096b2a42e6938c303de99cf3fa9212591cadba7494491f04f505e16a2c569388c2434c2bbca59585ece77671a9
7
- data.tar.gz: 5824a84007e85b84a19ce50c0af0ed6318315b3060caceb5ab4e560416da098d980155bbe74833560a1f7b35330bafb2e73a25e3154f28dc08cf6f53c2ed14e8
6
+ metadata.gz: '059f2419d80fc974f33c66367f9bc3a3c58492ed095d155c7e9aaa07c0c127ef374e236a973dd21c635e0c2eea97f62fafe83e66c03f8c3a6f5109f4745c20c3'
7
+ data.tar.gz: 2ee62770684237d0f60eedc5e752695846ff9dfd95836876b555e94e68683bc1ec4367a2006ea35fb350313b568a1d81d6a13822dea4747364f4710688984002
data/CHANGELOG.md CHANGED
@@ -5,6 +5,104 @@ 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
+ ## [5.5.0] - 2026-07-22
9
+
10
+ ### Added
11
+
12
+ - **`architect jobs list` provenance columns** — the table gains Harness /
13
+ Model / Lane (sourced from the list JSON's existing fields), blank when an
14
+ older row lacks them, so parallel lanes are tellable apart at a glance.
15
+ - **Server: pagination on the jobs, runs, and conversations indexes** — 50
16
+ rows per page with a `?page=N` param and Prev/Next controls; runs and
17
+ conversations were previously unbounded fetches. Conversations order
18
+ newest-updated first, and the index still never loads messages.
19
+ - **Server: job re-run prefill** — `GET /jobs/new?from=<job id>` (owner-gated)
20
+ prefills the new-job form field-for-field from that job's spec via the same
21
+ mapping profile application uses; Jobs/Show gains a "Run again" link.
22
+ - **Server: embedded live run view** — the run stream (SSE + rendering) is
23
+ extracted into a shared `RunStream` component and embedded on Jobs/Show
24
+ whenever the job has a run, so watching a job live no longer requires
25
+ leaving the job page.
26
+ - **Server: browser cancel + honest canceled lifecycle** — Jobs/Show renders a
27
+ Cancel control while a job is queued/running; runs gain a distinct terminal
28
+ status `canceled(4)` (a canceled job's run no longer reports `failed`), and
29
+ a cancel arriving during the env-image build skips the container spawn (and
30
+ secret resolution) entirely.
31
+
32
+ ### Changed
33
+
34
+ - **`architect sessions agent install` emits a launchd-proof plist** — the
35
+ agent's ProgramArguments now open with the installing interpreter's absolute
36
+ ruby path (`RbConfig.ruby`) ahead of the absolute bin path, and the agent
37
+ environment carries a homebrew-inclusive PATH, so launchd's bare PATH can no
38
+ longer resolve macOS system Ruby via an env shebang (LoadError, agent exit
39
+ 1). Reinstall existing agents to migrate; wrapper-script workarounds are
40
+ obsolete.
41
+ - **`architect dispatch --as-job` requires `--job-model`** — the missing model
42
+ now fails fast at dispatch with an error naming the flag, instead of
43
+ composing a spec the server rejects with a late 422.
44
+
45
+ ### Fixed
46
+
47
+ - **Server: bulk session syncs no longer strand conversations at pending** —
48
+ transient DB-infrastructure errors (pool timeout, connection loss) inside an
49
+ import now re-raise so the queue's retry re-fires them, instead of being
50
+ swallowed as permanent failures; import concurrency is bounded below the DB
51
+ connection pool so a bulk backfill can't starve it.
52
+ - **Server: NUL bytes can no longer fail imports or live persists** — one
53
+ shared scrub now covers the claude_code, codex, and pi importers and the
54
+ live run persistor (Postgres jsonb rejects NUL outright;
55
+ `PG::UntranslatableCharacter` was failing real claude_code transcripts with
56
+ NUL in tool-block content).
57
+ - **Server: executor hardening** — a requeued job's raw Redis stream is
58
+ deleted at execution start (a surviving stale stream silently corrupted the
59
+ transcript); truncated failure evidence is scrubbed back to valid UTF-8 when
60
+ the byte boundary splits a multibyte character; the default sandbox base
61
+ image is `space-claude-base:v1` everywhere (dev bins previously defaulted to
62
+ `debian:stable-slim`).
63
+ - **Server: models-proxy timeout covers key resolution** — `op://` key
64
+ resolution now runs inside the same 10s budget as the upstream fetch, and
65
+ the providers/models action only rescues the operation's own failure
66
+ classes, so genuine bugs propagate instead of masquerading as
67
+ `upstream_error`.
68
+ - **Server: `parent_session_id` is clearable** — a re-import whose content no
69
+ longer implies a parent now nulls the stored link instead of preserving it
70
+ forever.
71
+ - **Server: theme-aware transcript turn bands** — per-turn tint colors ride
72
+ CSS custom properties with light and dark definitions (bands were
73
+ dark-calibrated and near-invisible in light mode); a mid-fetch form edit no
74
+ longer gets clobbered by the pi-extension sync on the job/profile forms; the
75
+ generated pi extension places `compat` per-model per the upstream types.
76
+
77
+ ## [5.4.0] - 2026-07-21
78
+
79
+ ### Changed
80
+
81
+ - **`architect sessions agent install` resolves an `op://` token at install
82
+ time** — the ref is resolved once via `op read` and the raw value is written
83
+ to the plist's `EnvironmentVariables` dict (`SPACE_ARCHITECT_INGEST_TOKEN`),
84
+ with the plist written mode 0600 and ProgramArguments carrying no token
85
+ material. Interval runs no longer invoke `op` at all, so a locked 1Password
86
+ can no longer fail the sync (previously the ref rode argv and every fire ran
87
+ `op read`). Reinstall existing agents to migrate; a non-`op://` token passes
88
+ through to the env dict unresolved.
89
+
90
+ ### Added
91
+
92
+ - **`architect sessions sync` env-token fallback** — `--token` is now
93
+ optional: when absent, the token is read from `SPACE_ARCHITECT_INGEST_TOKEN`.
94
+ An explicit `--token` always wins; with neither present, the error names
95
+ both sources.
96
+ - **Server: conversation session links** — importers preserve a session file's
97
+ own `session_id` and record a differing content-derived id as
98
+ `parent_session_id`; conversations resolve `parent`/`children` links
99
+ (owner-scoped) and Conversations/Show renders "part of" / "Subagent
100
+ transcripts" navigation between them.
101
+ - **Server: denormalized `turns_count` on conversations** — the conversations
102
+ index reads a counter column recomputed at import instead of loading every
103
+ conversation's messages, fixing index-page timeouts (502s) at
104
+ thousand-conversation scale.
105
+
8
106
  ## [5.3.0] - 2026-07-20
9
107
 
10
108
  ### Added
@@ -939,6 +939,7 @@ module Space::Architect
939
939
  resolve_dispatch_harness(lane_entry, model: model, harness: harness, effort: effort, force: force, err: err)
940
940
  raise Space::Core::Error, "--as-job only supports the claude-code harness (lane '#{lane}' resolves to '#{resolved_harness}')" \
941
941
  unless resolved_harness == "claude-code"
942
+ raise Space::Core::Error, "--job-model is required with --as-job" unless job_model
942
943
 
943
944
  id = iteration_id(entry)
944
945
  wt_path = space.path.join(lane_entry["worktree"] || "build/#{id}-#{lane}/wt")
@@ -349,6 +349,7 @@ module Space::Architect
349
349
  if as_job
350
350
  Jobs.require_credentials!(host, token)
351
351
  raise Space::Core::Error, "--backend-url is required with --as-job" unless backend_url
352
+ raise Space::Core::Error, "--job-model is required with --as-job" unless job_model
352
353
  raise Space::Core::Error, "--as-job cannot be combined with --push-url/--push-token/--push-host/--detach" \
353
354
  if push_url || push_token || push_host || detach
354
355
  end
@@ -884,8 +885,11 @@ module Space::Architect
884
885
  if jobs.empty?
885
886
  terminal.say "No jobs"
886
887
  else
887
- rows = jobs.map { |j| [j["id"], j["status"], j["run_id"], j["created_at"]] }
888
- terminal.say terminal.table(%w[ID Status RunID CreatedAt], rows)
888
+ rows = jobs.map do |j|
889
+ [j["id"], j["status"], j["run_id"], j["created_at"],
890
+ j["harness"], j["model"], j.dig("provenance", "lane")]
891
+ end
892
+ terminal.say terminal.table(%w[ID Status RunID CreatedAt Harness Model Lane], rows)
889
893
  end
890
894
  CLI.record_outcome(Outcome.new(exit_code: 0))
891
895
  end
@@ -950,13 +954,13 @@ module Space::Architect
950
954
  # explicitly rather than relying on dry-cli's `required:` option flag.
951
955
  def self.require_credentials!(host, token)
952
956
  raise Space::Core::Error, "--host is required" unless host
953
- raise Space::Core::Error, "--token is required" unless token
957
+ raise Space::Core::Error, "--token or #{SessionSync::TOKEN_ENV} is required" unless token
954
958
  end
955
959
 
956
960
  class Sync < BaseCommand
957
961
  desc "Scan pi/claude session files and upload new/grown conversations to a space-server"
958
962
  option :host, required: true, desc: "Base URL of the space-server"
959
- option :token, required: true, desc: "Bearer token for authorization (an op:// ref is resolved once via `op read`)"
963
+ 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`)"
960
964
  option :state_file, default: nil, desc: "Cursor YAML path (default: $XDG_STATE_HOME/space-architect/session-sync.yaml)"
961
965
  option :pi_root, default: nil, desc: "Override the pi sessions root (default: ~/.pi/agent/sessions)"
962
966
  option :claude_root, default: nil, desc: "Override the claude projects root (default: ~/.claude/projects)"
@@ -965,9 +969,10 @@ module Space::Architect
965
969
  def call(host: nil, token: nil, state_file: nil, pi_root: nil, claude_root: nil, dry_run: false, **opts)
966
970
  setup_terminal(**opts.slice(:color, :colors))
967
971
  handle_errors do
968
- Sessions.require_credentials!(host, token)
972
+ resolved_token = token || ENV[SessionSync::TOKEN_ENV]
973
+ Sessions.require_credentials!(host, resolved_token)
969
974
  runner = SessionSync::Runner.new(
970
- client: ConversationsClient.new(host, token),
975
+ client: ConversationsClient.new(host, resolved_token),
971
976
  state_path: state_file || SessionSync.default_state_file,
972
977
  pi_root: pi_root || SessionSync.default_pi_root,
973
978
  claude_root: claude_root || SessionSync.default_claude_root,
@@ -997,24 +1002,27 @@ module Space::Architect
997
1002
  include Helpers
998
1003
  desc "Install the launchd agent that periodically runs `architect sessions sync`"
999
1004
  option :host, required: true, desc: "Base URL of the space-server"
1000
- option :token, required: true, desc: "Bearer token for authorization (an op:// ref is stored as-is in the plist)"
1005
+ 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)"
1001
1006
  option :interval, default: "900", desc: "StartInterval in seconds (default 900)"
1002
1007
 
1003
1008
  def call(host: nil, token: nil, interval: "900", **opts)
1004
1009
  setup_terminal(**opts.slice(:color, :colors))
1005
1010
  handle_errors do
1006
1011
  Sessions.require_credentials!(host, token)
1012
+ resolved_token = token.start_with?("op://") ? SessionSync.resolve_token(token) : token
1007
1013
  pp = plist_path
1008
1014
  xml = SessionSync::Plist.call(
1009
1015
  label: SessionSync::LABEL,
1010
1016
  refresh_interval: interval.to_i,
1011
1017
  log_dir: SessionSync.log_dir,
1012
1018
  bin_path: SessionSync::BinPath.detect,
1019
+ ruby_bin: RbConfig.ruby,
1013
1020
  host: host,
1014
- token: token
1021
+ env: {SessionSync::TOKEN_ENV => resolved_token}
1015
1022
  )
1016
1023
  FileUtils.mkdir_p(File.dirname(pp))
1017
1024
  File.write(pp, xml)
1025
+ File.chmod(0o600, pp)
1018
1026
 
1019
1027
  result = Space::Src::Launchd::Agent.new(label: SessionSync::LABEL).install(pp)
1020
1028
  raise Space::Core::Error, "bootstrap failed: #{format_failure(result.failure)}" if result.failure?
@@ -16,7 +16,7 @@ module Space::Architect
16
16
  @host = host.chomp("/")
17
17
  @token = token
18
18
  @client = client
19
- @op_resolver = op_resolver || method(:op_read)
19
+ @op_resolver = op_resolver || SessionSync.method(:resolve_token)
20
20
  @resolved_token = nil
21
21
  end
22
22
 
@@ -62,13 +62,6 @@ module Space::Architect
62
62
  @resolved_token ||= @token.start_with?("op://") ? @op_resolver.call(@token) : @token
63
63
  end
64
64
 
65
- def op_read(ref)
66
- value = IO.popen(["op", "read", ref], &:read)
67
- raise Space::Core::Error, "op read failed for #{ref}" unless $?.success?
68
-
69
- value.chomp
70
- end
71
-
72
65
  def build_multipart(boundary, path, session_id)
73
66
  crlf = "\r\n"
74
67
  filename = File.basename(path)
@@ -3,12 +3,15 @@
3
3
  module Space::Architect
4
4
  module SessionSync
5
5
  # Hand-rolled launchd plist emitter for the session-sync agent, imitating
6
- # Space::Src::Launchd::Plist's shape. Unlike that emitter, this one does
7
- # NOT wrap the invocation in mise/ruby toolchain resolution: the
8
- # `architect` executable is a rubygems binstub (or the dev exe/architect
9
- # script, which sets up Bundler itself) and needs no toolchain pinning to
10
- # run non-interactively, so ProgramArguments invokes it directly.
6
+ # Space::Src::Launchd::Plist's shape. Under launchd's bare PATH, a bare
7
+ # env-relative ruby binstub shebang resolves to macOS system Ruby instead
8
+ # of the caller's toolchain, so ProgramArguments names the interpreter
9
+ # explicitly (ruby_bin) rather than relying on shebang resolution no
10
+ # mise wrapping needed, since the sync itself pins no toolchain at run
11
+ # time.
11
12
  class Plist
13
+ DEFAULT_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
14
+
12
15
  HEADER = <<~XML
13
16
  <?xml version="1.0" encoding="UTF-8"?>
14
17
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -22,19 +25,22 @@ module Space::Architect
22
25
  # @param refresh_interval [Integer] StartInterval in seconds (must be > 0).
23
26
  # @param log_dir [String] Absolute directory for stdout/stderr logs.
24
27
  # @param bin_path [String] Absolute path to the architect bin script.
28
+ # @param ruby_bin [String] Absolute path to the ruby to run bin_path under.
25
29
  # @param host [String] --host value passed to `sessions sync`.
26
- # @param token [String] --token value passed to `sessions sync` (an op:// ref
27
- # is passed through verbatim never resolved here).
30
+ # @param env [Hash] EnvironmentVariables dict; must contain a non-empty
31
+ # SessionSync::TOKEN_ENV value (resolved by the caller
32
+ # never resolved here). PATH defaults to DEFAULT_PATH
33
+ # unless the caller supplies its own.
28
34
  # @return [String] the full plist XML.
29
- def call(label:, refresh_interval:, log_dir:, bin_path:, host:, token:)
35
+ def call(label:, refresh_interval:, log_dir:, bin_path:, ruby_bin:, host:, env:)
30
36
  raise ArgumentError, "label is required" if label.to_s.empty?
31
37
  raise ArgumentError, "refresh_interval must be > 0" unless refresh_interval.is_a?(Integer) && refresh_interval > 0
32
- %w[log_dir bin_path].each do |k|
38
+ %w[log_dir bin_path ruby_bin].each do |k|
33
39
  v = binding.local_variable_get(k)
34
40
  raise ArgumentError, "#{k} must be absolute (got #{v.inspect})" unless v.is_a?(String) && File.absolute_path?(v)
35
41
  end
36
42
  raise ArgumentError, "host is required" if host.to_s.empty?
37
- raise ArgumentError, "token is required" if token.to_s.empty?
43
+ raise ArgumentError, "env[#{SessionSync::TOKEN_ENV}] is required" if env.to_h[SessionSync::TOKEN_ENV].to_s.empty?
38
44
 
39
45
  out_log = File.join(log_dir, "#{label}.out.log")
40
46
  err_log = File.join(log_dir, "#{label}.err.log")
@@ -42,12 +48,13 @@ module Space::Architect
42
48
  body = +""
43
49
  body << key("Label") << string(label) << "\n"
44
50
  body << key("ProgramArguments") << "\n" << array([
51
+ ruby_bin,
45
52
  bin_path,
46
53
  "sessions",
47
54
  "sync",
48
- "--host", host,
49
- "--token", token
55
+ "--host", host
50
56
  ])
57
+ body << key("EnvironmentVariables") << "\n" << dict({"PATH" => DEFAULT_PATH}.merge(env.to_h))
51
58
  body << key("StartInterval") << integer(refresh_interval) << "\n"
52
59
  body << key("RunAtLoad") << boolean(true) << "\n"
53
60
  body << key("ProcessType") << string("Background") << "\n"
@@ -89,6 +96,15 @@ module Space::Architect
89
96
  items.each { |arg| out << " <string>#{escape(arg)}</string>\n" }
90
97
  out << " </array>"
91
98
  end
99
+
100
+ def dict(hash)
101
+ out = +" <dict>\n"
102
+ hash.each do |k, v|
103
+ out << " <key>#{escape(k)}</key>\n"
104
+ out << " <string>#{escape(v)}</string>\n"
105
+ end
106
+ out << " </dict>"
107
+ end
92
108
  end
93
109
  end
94
110
  end
@@ -15,9 +15,20 @@ module Space::Architect
15
15
  module SessionSync
16
16
  LABEL = "io.github.jetpks.space-architect.session-sync"
17
17
  APP_NAME = "space-architect"
18
+ TOKEN_ENV = "SPACE_ARCHITECT_INGEST_TOKEN"
18
19
 
19
20
  module_function
20
21
 
22
+ # The one op-read seam under lib/: resolves an op:// ref via `op read`.
23
+ # Shared by ConversationsClient's default op_resolver and
24
+ # Sessions::Agent::Install (both test-injectable around this call).
25
+ def resolve_token(ref)
26
+ value = IO.popen(["op", "read", ref], &:read)
27
+ raise Space::Core::Error, "op read failed for #{ref}" unless $?.success?
28
+
29
+ value.chomp
30
+ end
31
+
21
32
  def state_dir(env: ENV)
22
33
  File.join(Space::Core::XDG.state_home(env: env).to_s, APP_NAME)
23
34
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Core
5
- VERSION = "5.3.0"
5
+ VERSION = "5.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: space-architect
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.0
4
+ version: 5.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs