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 +4 -4
- data/CHANGELOG.md +89 -0
- data/README.md +2 -4
- data/lib/space_architect/architect_project.rb +3 -21
- data/lib/space_architect/cli/architect.rb +66 -295
- data/lib/space_architect/harness.rb +2 -51
- data/lib/space_architect/routing.rb +43 -0
- data/lib/space_architect/version.rb +1 -1
- data/lib/space_architect.rb +0 -4
- data/skill/architect/SKILL.md +145 -319
- data/skill/architect-action/SKILL.md +123 -0
- data/skill/architect-discuss/SKILL.md +252 -0
- data/skill/{architect → architect-discuss}/dispatch.md +5 -4
- data/skill/{architect → architect-discuss}/research.md +2 -1
- data/skill/architect-research/SKILL.md +3 -2
- data/skill/architect-status/SKILL.md +55 -0
- metadata +7 -45
- 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
- data/skill/architect-plan/SKILL.md +0 -114
- data/skill/architect-vocabulary/SKILL.md +0 -144
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e768903f0762630e4c4c510a83621cf4c82be30ea7d95026d794f90d70205f44
|
|
4
|
+
data.tar.gz: 77c7b1faae07696200558929456ab481aa5f8f061f69a0ff923fac53a4957f2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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",
|
|
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
|
|
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 ||
|
|
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-
|
|
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 "
|
|
1614
|
-
|
|
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
|