space-architect 5.4.0 → 5.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20aaf646a8056b8d5975cb789f55a49f9f9c0ca5deca38f9866a0e3e220e8904
4
- data.tar.gz: a89104abc977aaeae263c09f922800252867df448050262c557d6f60780275f0
3
+ metadata.gz: 71935f28a32069832af5b4d25f368d33c7efc505267a98c4f4b49f75a0084861
4
+ data.tar.gz: 2b067fdbdd5a950b204ef9fb16a3484a634e0b6acd2f9ca467e8f47adec29902
5
5
  SHA512:
6
- metadata.gz: f0a104ab6b75ec7170e5c4bc7a57c4406f002e167ba13b3d913d21a97f3c020a13d3912d2f2fada42edcd0ffeac876a8fdff2fe0c38ad163f617700927767493
7
- data.tar.gz: ea287608ae6cb4a560d3b38e51e4d8b0c3d4dc04985e429046fe83b1588c8f7dd34608a7ede28ed45457ff8e0456226d4130ddfb6b9541dde875cc3976898a1b
6
+ metadata.gz: 66a47fc8027809c4132ba9e5365694ab530d646be68b0cacec80d527e81dcb28f7d192afb4e76ea1e9b0489bada20013af2c32da066075e28f39bb407fc89243
7
+ data.tar.gz: f10a02847f4dafb408b98eb6a79f4b911bc0db7bf1856fbef18b751928f2e824ee5a5596db82ca94641f2fa5f55b037a671d9fed4daf0c8b54a374735b5d4baf
data/CHANGELOG.md CHANGED
@@ -5,6 +5,91 @@ 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.1] - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - **Lane in-bounds check no longer false-FAILs on dotfile directories** — the
13
+ `touch_set` matcher built its `fnmatch` flags without `File::FNM_DOTMATCH`, so
14
+ a `dir/**` touch glob never matched a path with a dot-prefixed segment. Any
15
+ lane creating `.github/workflows/ci.yml` *inside its own declared directory*
16
+ was reported out-of-bounds by `architect verify` check (d) and hard-blocked by
17
+ `architect integrate` ("wrote outside its declared touch set"), with no
18
+ override — the touch set is frozen at spec time, so neither side of the
19
+ comparison could be adjusted legitimately and the merges had to be done by
20
+ hand. `merge_lane!`'s conflict classification carried the identical blind spot;
21
+ both sites now share one `in_touch_set?` predicate so the flags cannot drift
22
+ again.
23
+
24
+ ## [5.5.0] - 2026-07-22
25
+
26
+ ### Added
27
+
28
+ - **`architect jobs list` provenance columns** — the table gains Harness /
29
+ Model / Lane (sourced from the list JSON's existing fields), blank when an
30
+ older row lacks them, so parallel lanes are tellable apart at a glance.
31
+ - **Server: pagination on the jobs, runs, and conversations indexes** — 50
32
+ rows per page with a `?page=N` param and Prev/Next controls; runs and
33
+ conversations were previously unbounded fetches. Conversations order
34
+ newest-updated first, and the index still never loads messages.
35
+ - **Server: job re-run prefill** — `GET /jobs/new?from=<job id>` (owner-gated)
36
+ prefills the new-job form field-for-field from that job's spec via the same
37
+ mapping profile application uses; Jobs/Show gains a "Run again" link.
38
+ - **Server: embedded live run view** — the run stream (SSE + rendering) is
39
+ extracted into a shared `RunStream` component and embedded on Jobs/Show
40
+ whenever the job has a run, so watching a job live no longer requires
41
+ leaving the job page.
42
+ - **Server: browser cancel + honest canceled lifecycle** — Jobs/Show renders a
43
+ Cancel control while a job is queued/running; runs gain a distinct terminal
44
+ status `canceled(4)` (a canceled job's run no longer reports `failed`), and
45
+ a cancel arriving during the env-image build skips the container spawn (and
46
+ secret resolution) entirely.
47
+
48
+ ### Changed
49
+
50
+ - **`architect sessions agent install` emits a launchd-proof plist** — the
51
+ agent's ProgramArguments now open with the installing interpreter's absolute
52
+ ruby path (`RbConfig.ruby`) ahead of the absolute bin path, and the agent
53
+ environment carries a homebrew-inclusive PATH, so launchd's bare PATH can no
54
+ longer resolve macOS system Ruby via an env shebang (LoadError, agent exit
55
+ 1). Reinstall existing agents to migrate; wrapper-script workarounds are
56
+ obsolete.
57
+ - **`architect dispatch --as-job` requires `--job-model`** — the missing model
58
+ now fails fast at dispatch with an error naming the flag, instead of
59
+ composing a spec the server rejects with a late 422.
60
+
61
+ ### Fixed
62
+
63
+ - **Server: bulk session syncs no longer strand conversations at pending** —
64
+ transient DB-infrastructure errors (pool timeout, connection loss) inside an
65
+ import now re-raise so the queue's retry re-fires them, instead of being
66
+ swallowed as permanent failures; import concurrency is bounded below the DB
67
+ connection pool so a bulk backfill can't starve it.
68
+ - **Server: NUL bytes can no longer fail imports or live persists** — one
69
+ shared scrub now covers the claude_code, codex, and pi importers and the
70
+ live run persistor (Postgres jsonb rejects NUL outright;
71
+ `PG::UntranslatableCharacter` was failing real claude_code transcripts with
72
+ NUL in tool-block content).
73
+ - **Server: executor hardening** — a requeued job's raw Redis stream is
74
+ deleted at execution start (a surviving stale stream silently corrupted the
75
+ transcript); truncated failure evidence is scrubbed back to valid UTF-8 when
76
+ the byte boundary splits a multibyte character; the default sandbox base
77
+ image is `space-claude-base:v1` everywhere (dev bins previously defaulted to
78
+ `debian:stable-slim`).
79
+ - **Server: models-proxy timeout covers key resolution** — `op://` key
80
+ resolution now runs inside the same 10s budget as the upstream fetch, and
81
+ the providers/models action only rescues the operation's own failure
82
+ classes, so genuine bugs propagate instead of masquerading as
83
+ `upstream_error`.
84
+ - **Server: `parent_session_id` is clearable** — a re-import whose content no
85
+ longer implies a parent now nulls the stored link instead of preserving it
86
+ forever.
87
+ - **Server: theme-aware transcript turn bands** — per-turn tint colors ride
88
+ CSS custom properties with light and dark definitions (bands were
89
+ dark-calibrated and near-invisible in light mode); a mid-fetch form edit no
90
+ longer gets clobbered by the pi-extension sync on the job/profile forms; the
91
+ generated pi extension places `compat` per-model per the upstream types.
92
+
8
93
  ## [5.4.0] - 2026-07-21
9
94
 
10
95
  ### Changed
@@ -42,6 +42,13 @@ module Space::Architect
42
42
  # Hard per-gate timeout. Generous relative to the full suite (~55s).
43
43
  DEFAULT_GATE_TIMEOUT = 900
44
44
 
45
+ # Flags for matching a changed path against a lane's touch_set globs.
46
+ # PATHNAME keeps a single `*` from crossing `/`; EXTGLOB enables `{a,b}`;
47
+ # DOTMATCH lets a glob reach dotfile segments, so a `dir/**` touch set covers
48
+ # `dir/.github/workflows/ci.yml` — the standard deliverable for a lane preparing
49
+ # a directory to become a repo root.
50
+ TOUCH_FNM = File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH
51
+
45
52
  # Legacy sentinel: worktree_add used to seed prompt.md with this placeholder
46
53
  # (dropped — the blind-overwrite tripped harness read-before-write guards, #48).
47
54
  # dispatch still refuses to launch on this content, so stubs in old spaces
@@ -404,10 +411,7 @@ module Space::Architect
404
411
  git_capture("-C", repo_path.to_s, "merge", "--abort")
405
412
  conflict_files = conflicts.split
406
413
  lane_touch_set = lane_entry["touch_set"] || []
407
- fnm = File::FNM_PATHNAME | File::FNM_EXTGLOB
408
- outside = conflict_files.reject do |f|
409
- lane_touch_set.any? { |g| File.fnmatch(g, f, fnm) || (g.end_with?("/**") && File.fnmatch("#{g}/*", f, fnm)) }
410
- end
414
+ outside = conflict_files.reject { |f| in_touch_set?(f, lane_touch_set) }
411
415
  if !lane_touch_set.empty? && outside.empty?
412
416
  raise Space::Core::Error,
413
417
  "Merge conflict integrating lane '#{lane}' (#{conflict_files.join(", ")}) — the lane plan was " \
@@ -939,6 +943,7 @@ module Space::Architect
939
943
  resolve_dispatch_harness(lane_entry, model: model, harness: harness, effort: effort, force: force, err: err)
940
944
  raise Space::Core::Error, "--as-job only supports the claude-code harness (lane '#{lane}' resolves to '#{resolved_harness}')" \
941
945
  unless resolved_harness == "claude-code"
946
+ raise Space::Core::Error, "--job-model is required with --as-job" unless job_model
942
947
 
943
948
  id = iteration_id(entry)
944
949
  wt_path = space.path.join(lane_entry["worktree"] || "build/#{id}-#{lane}/wt")
@@ -1297,18 +1302,23 @@ module Space::Architect
1297
1302
  i += 1
1298
1303
  changed << orig if orig && !orig.empty?
1299
1304
  end
1300
- fnm = File::FNM_PATHNAME | File::FNM_EXTGLOB
1301
- changed.all? do |f|
1302
- touch_set.any? do |g|
1303
- File.fnmatch(g, f, fnm) ||
1304
- (g.end_with?("/**") && File.fnmatch("#{g}/*", f, fnm))
1305
- end
1306
- end
1305
+ changed.all? { |f| in_touch_set?(f, touch_set) }
1307
1306
  end
1308
1307
 
1309
1308
  checks
1310
1309
  end
1311
1310
 
1311
+ # Is a changed path inside a lane's declared touch set? Single-sourced so the
1312
+ # in-bounds check (d) and merge_lane!'s conflict classification can never drift.
1313
+ # A trailing `dir/**` is matched twice: bare (PATHNAME stops it at direct
1314
+ # children) and as `dir/**/*`, whose whole-component `**/` does cross `/`.
1315
+ def in_touch_set?(path, globs)
1316
+ globs.any? do |g|
1317
+ File.fnmatch(g, path, TOUCH_FNM) ||
1318
+ (g.end_with?("/**") && File.fnmatch("#{g}/*", path, TOUCH_FNM))
1319
+ end
1320
+ end
1321
+
1312
1322
  # Replace (or, with append:, extend) the body of a "## Heading" section, leaving
1313
1323
  # every other section byte-untouched. Append replaces a placeholder body (only a
1314
1324
  # template comment) the first time, then stacks subsections after it. A supplied
@@ -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
@@ -1012,6 +1016,7 @@ module Space::Architect
1012
1016
  refresh_interval: interval.to_i,
1013
1017
  log_dir: SessionSync.log_dir,
1014
1018
  bin_path: SessionSync::BinPath.detect,
1019
+ ruby_bin: RbConfig.ruby,
1015
1020
  host: host,
1016
1021
  env: {SessionSync::TOKEN_ENV => resolved_token}
1017
1022
  )
@@ -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,15 +25,17 @@ 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
30
  # @param env [Hash] EnvironmentVariables dict; must contain a non-empty
27
31
  # SessionSync::TOKEN_ENV value (resolved by the caller —
28
- # never resolved here).
32
+ # never resolved here). PATH defaults to DEFAULT_PATH
33
+ # unless the caller supplies its own.
29
34
  # @return [String] the full plist XML.
30
- def call(label:, refresh_interval:, log_dir:, bin_path:, host:, env:)
35
+ def call(label:, refresh_interval:, log_dir:, bin_path:, ruby_bin:, host:, env:)
31
36
  raise ArgumentError, "label is required" if label.to_s.empty?
32
37
  raise ArgumentError, "refresh_interval must be > 0" unless refresh_interval.is_a?(Integer) && refresh_interval > 0
33
- %w[log_dir bin_path].each do |k|
38
+ %w[log_dir bin_path ruby_bin].each do |k|
34
39
  v = binding.local_variable_get(k)
35
40
  raise ArgumentError, "#{k} must be absolute (got #{v.inspect})" unless v.is_a?(String) && File.absolute_path?(v)
36
41
  end
@@ -43,12 +48,13 @@ module Space::Architect
43
48
  body = +""
44
49
  body << key("Label") << string(label) << "\n"
45
50
  body << key("ProgramArguments") << "\n" << array([
51
+ ruby_bin,
46
52
  bin_path,
47
53
  "sessions",
48
54
  "sync",
49
55
  "--host", host
50
56
  ])
51
- body << key("EnvironmentVariables") << "\n" << dict(env)
57
+ body << key("EnvironmentVariables") << "\n" << dict({"PATH" => DEFAULT_PATH}.merge(env.to_h))
52
58
  body << key("StartInterval") << integer(refresh_interval) << "\n"
53
59
  body << key("RunAtLoad") << boolean(true) << "\n"
54
60
  body << key("ProcessType") << string("Background") << "\n"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Core
5
- VERSION = "5.4.0"
5
+ VERSION = "5.5.1"
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.4.0
4
+ version: 5.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs