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 +4 -4
- data/CHANGELOG.md +98 -0
- data/lib/space_architect/architect_project.rb +1 -0
- data/lib/space_architect/cli/architect.rb +16 -8
- data/lib/space_architect/conversations_client.rb +1 -8
- data/lib/space_architect/session_sync/plist.rb +28 -12
- data/lib/space_architect/session_sync.rb +11 -0
- data/lib/space_core/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e170b3f69d5b2a6bdeb9422aedc341df20148e7085ea38b120b43508a9aaf3d0
|
|
4
|
+
data.tar.gz: 8ed8737fdeacf88a4ef1917fcf2a695e3d7f6f679eb1c6745094179bf57a8166
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
888
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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(:
|
|
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.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
|
27
|
-
#
|
|
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:,
|
|
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, "
|
|
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
|
data/lib/space_core/version.rb
CHANGED