space-architect 1.1.0 → 1.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.
@@ -107,6 +107,12 @@ module SpaceArchitect
107
107
  mission = ArchitectMission.new(space: sp)
108
108
  sha = mission.freeze!(iteration)
109
109
  terminal.say "Frozen #{iteration} at #{sha}"
110
+ ac = mission.acceptance_criteria(iteration)
111
+ unless ac.to_s.strip.empty?
112
+ terminal.say ""
113
+ terminal.say "Frozen Acceptance Criteria (quote these verbatim when judging):"
114
+ terminal.say ac
115
+ end
110
116
  CLI.record_outcome(Outcome.new(exit_code: 0))
111
117
  end
112
118
  end
@@ -195,6 +201,201 @@ module SpaceArchitect
195
201
  end
196
202
  end
197
203
 
204
+ class Section < Dry::CLI::Command
205
+ include GlobalOptions
206
+ include Helpers
207
+
208
+ desc "Write a section of the iteration file and commit it (one call)"
209
+ argument :iteration, required: true, desc: "Iteration name"
210
+ argument :section, required: true, desc: "Section: grounds, specification, prompt, verdict"
211
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
212
+ option :from, default: nil, desc: "Read the section body from this file"
213
+ option :body, default: nil, desc: "Inline section body (one-liners)"
214
+ option :stdin, type: :boolean, default: false, desc: "Read the section body from stdin"
215
+ option :append, type: :boolean, default: false, desc: "Append a ### <lane> subsection instead of replacing"
216
+ option :lane, default: nil, desc: "Lane name for an appended ### subsection"
217
+
218
+ def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil, **opts)
219
+ setup_terminal(**opts.slice(:color, :colors))
220
+ handle_errors do
221
+ content = read_section_body(from: from, body: body, stdin: stdin)
222
+ render(store.find(space)) do |sp|
223
+ mission = ArchitectMission.new(space: sp)
224
+ res = mission.write_section!(iteration, section, body: content, append: append, lane: lane)
225
+ if res[:committed]
226
+ terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
227
+ terminal.say res[:diffstat] unless res[:diffstat].empty?
228
+ else
229
+ terminal.say "#{res[:heading]} written — no change to commit"
230
+ end
231
+ CLI.record_outcome(Outcome.new(exit_code: 0))
232
+ end
233
+ end
234
+ end
235
+
236
+ private
237
+
238
+ def read_section_body(from:, body:, stdin:)
239
+ return File.read(from) if from
240
+ return body if body
241
+ return $stdin.read if stdin
242
+ raise SpaceArchitect::Error, "provide the section body via --from <file>, --body <text>, or --stdin"
243
+ end
244
+ end
245
+
246
+ class Evidence < Dry::CLI::Command
247
+ include GlobalOptions
248
+ include Helpers
249
+
250
+ desc "Transcribe a lane's scratch report VERBATIM into Builder Report and commit"
251
+ argument :iteration, required: true, desc: "Iteration name"
252
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
253
+ option :lane, default: nil, desc: "Lane name (per-lane subsection; omit for a single-lane iteration)"
254
+
255
+ def call(iteration:, space: nil, lane: nil, **opts)
256
+ setup_terminal(**opts.slice(:color, :colors))
257
+ handle_errors do
258
+ render(store.find(space)) do |sp|
259
+ mission = ArchitectMission.new(space: sp)
260
+ res = mission.transcribe_evidence!(iteration, lane: lane)
261
+ terminal.say "Transcribed #{res[:lines]} lines → #{res[:sha][0, 8]}"
262
+ terminal.say "Builder STATUS: #{res[:status_line]}" if res[:status_line]
263
+ terminal.say "Now rule on the builder's PHASE 0 disagreements in the Verdict (a later session)."
264
+ CLI.record_outcome(Outcome.new(exit_code: 0))
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ class Merge < Dry::CLI::Command
271
+ include GlobalOptions
272
+ include Helpers
273
+
274
+ desc "Integrate ONE judged-passing lane (merges --no-ff; runs no gates, makes no verdict)"
275
+ argument :iteration, required: true, desc: "Iteration name"
276
+ argument :lane, required: true, desc: "Lane name (architect-judged passing)"
277
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
278
+ option :message, default: nil, desc: "Commit message for the lane's working-tree changes"
279
+
280
+ def call(iteration:, lane:, space: nil, message: nil, **opts)
281
+ setup_terminal(**opts.slice(:color, :colors))
282
+ handle_errors do
283
+ render(store.find(space)) do |sp|
284
+ mission = ArchitectMission.new(space: sp)
285
+ r = mission.merge_lane!(iteration, lane, message: message)
286
+ terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
287
+ terminal.say r[:diffstat] unless r[:diffstat].empty?
288
+ terminal.say "Gates NOT run — run `architect gate #{iteration}` against the integration branch."
289
+ CLI.record_outcome(Outcome.new(exit_code: 0))
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ class Integrate < Dry::CLI::Command
296
+ include GlobalOptions
297
+ include Helpers
298
+
299
+ desc "Integrate the architect-supplied set of passing lanes, in order (stops on conflict)"
300
+ argument :iteration, required: true, desc: "Iteration name"
301
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
302
+ option :lanes, required: true, desc: "Comma-separated passing lane names (you decide the set)"
303
+ option :teardown, type: :boolean, default: false, desc: "Remove worktrees + delete lane branches after merge"
304
+
305
+ def call(iteration:, space: nil, lanes:, teardown: false, **opts)
306
+ setup_terminal(**opts.slice(:color, :colors))
307
+ handle_errors do
308
+ render(store.find(space)) do |sp|
309
+ mission = ArchitectMission.new(space: sp)
310
+ lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
311
+ results = mission.integrate!(iteration, lanes: lane_names, teardown: teardown)
312
+ results.each do |r|
313
+ terminal.say "Merged #{r[:lane]} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
314
+ end
315
+ terminal.say "Gates NOT run — run `architect gate #{iteration}`; the verdict is the next session's."
316
+ CLI.record_outcome(Outcome.new(exit_code: 0))
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ class Gate < Dry::CLI::Command
323
+ include GlobalOptions
324
+ include Helpers
325
+
326
+ desc "Run the frozen Acceptance Criteria gate commands and stream raw output (no PASS/FAIL)"
327
+ argument :iteration, required: true, desc: "Iteration name"
328
+ argument :lane, required: false, desc: "Run in a lane worktree (default: the integration repo)"
329
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
330
+
331
+ def call(iteration:, lane: nil, space: nil, **opts)
332
+ setup_terminal(**opts.slice(:color, :colors))
333
+ handle_errors do
334
+ render(store.find(space)) do |sp|
335
+ mission = ArchitectMission.new(space: sp)
336
+ results = mission.run_gates(iteration, lane: lane)
337
+ results.each do |r|
338
+ terminal.say ""
339
+ terminal.say "── #{r[:ac].empty? ? "(gate)" : r[:ac]}: #{r[:command]} (exit #{r[:exit_code]})"
340
+ terminal.say r[:stdout].rstrip unless r[:stdout].strip.empty?
341
+ terminal.say r[:stderr].rstrip unless r[:stderr].strip.empty?
342
+ end
343
+ terminal.say ""
344
+ terminal.say "Raw gate output above — the PASS/FAIL/INVALID verdict is yours, read against the frozen thresholds."
345
+ CLI.record_outcome(Outcome.new(exit_code: 0))
346
+ end
347
+ end
348
+ end
349
+ end
350
+
351
+ class InstallSkills < Dry::CLI::Command
352
+ include GlobalOptions
353
+ include Helpers
354
+
355
+ desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
356
+ option :provider, default: "claude", desc: "Harness: claude, codex, opencode, pi"
357
+ option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
358
+ option :force, type: :boolean, default: false, desc: "Overwrite existing skills that differ"
359
+ option :dry_run, type: :boolean, default: false, desc: "Print what would happen without writing files"
360
+
361
+ def call(provider: "claude", project: false, force: false, dry_run: false, **opts)
362
+ setup_terminal(**opts.slice(:color, :colors))
363
+ handle_errors do
364
+ result = SkillInstaller.install(provider, project: project, force: force,
365
+ env: project_config.env, dry_run: dry_run)
366
+ verb = dry_run ? "Would install" : "Installed"
367
+ terminal.say "#{verb} skills for #{provider} → #{terminal.path(result[:dest_root])}"
368
+ result[:skills].each do |s|
369
+ terminal.say " #{s[:name]}: #{terminal.style_skill_action(s[:action])} (#{terminal.path(s[:path])})"
370
+ end
371
+ CLI.record_outcome(Outcome.new(exit_code: 0))
372
+ end
373
+ end
374
+ end
375
+
376
+ module Brief
377
+ class New < Dry::CLI::Command
378
+ include GlobalOptions
379
+ include Helpers
380
+
381
+ desc "Scaffold the durable mission brief (architecture/BRIEF.md)"
382
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
383
+ option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
384
+
385
+ def call(space: nil, force: false, **opts)
386
+ setup_terminal(**opts.slice(:color, :colors))
387
+ handle_errors do
388
+ render(store.find(space)) do |sp|
389
+ mission = ArchitectMission.new(space: sp)
390
+ path = mission.brief_new!(force: force)
391
+ terminal.say "Brief ready: #{terminal.path(path)}"
392
+ CLI.record_outcome(Outcome.new(exit_code: 0))
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
398
+
198
399
  module Worktree
199
400
  class Add < Dry::CLI::Command
200
401
  include GlobalOptions
@@ -208,14 +409,16 @@ module SpaceArchitect
208
409
  option :harness, default: "claude-code", desc: "Harness (claude-code, opencode)"
209
410
  option :model, default: nil, desc: "Model (required for opencode)"
210
411
  option :effort, default: nil, desc: "Reasoning effort (opencode only; sets reasoningEffort in the model config)"
412
+ option :touch, default: nil, desc: "Comma-separated file globs the lane may touch (records its touch_set for in-bounds + merge checks)"
211
413
 
212
- def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, **opts)
414
+ def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, touch: nil, **opts)
213
415
  setup_terminal(**opts.slice(:color, :colors))
214
416
  handle_errors do
215
417
  render(store.find) do |sp|
216
418
  mission = ArchitectMission.new(space: sp)
419
+ touch_set = touch ? touch.split(",").map(&:strip).reject(&:empty?) : nil
217
420
  result = mission.worktree_add(repo, iteration, lane, base: base,
218
- harness: harness, model: model, effort: effort)
421
+ harness: harness, model: model, effort: effort, touch: touch_set)
219
422
  terminal.say "Worktree: #{terminal.path(result[:worktree])}"
220
423
  terminal.say "Base SHA: #{result[:base_sha]}"
221
424
  CLI.record_outcome(Outcome.new(exit_code: 0))
@@ -376,6 +579,15 @@ SpaceArchitect::CLI::Registry.register "status", SpaceArchitect::CLI::Architect:
376
579
  SpaceArchitect::CLI::Registry.register "freeze", SpaceArchitect::CLI::Architect::Freeze
377
580
  SpaceArchitect::CLI::Registry.register "verify", SpaceArchitect::CLI::Architect::Verify
378
581
  SpaceArchitect::CLI::Registry.register "dispatch", SpaceArchitect::CLI::Architect::Dispatch
582
+ SpaceArchitect::CLI::Registry.register "section", SpaceArchitect::CLI::Architect::Section
583
+ SpaceArchitect::CLI::Registry.register "evidence", SpaceArchitect::CLI::Architect::Evidence
584
+ SpaceArchitect::CLI::Registry.register "merge", SpaceArchitect::CLI::Architect::Merge
585
+ SpaceArchitect::CLI::Registry.register "integrate", SpaceArchitect::CLI::Architect::Integrate
586
+ SpaceArchitect::CLI::Registry.register "gate", SpaceArchitect::CLI::Architect::Gate
587
+ SpaceArchitect::CLI::Registry.register "install-skills", SpaceArchitect::CLI::Architect::InstallSkills
588
+ SpaceArchitect::CLI::Registry.register "brief" do |b|
589
+ b.register "new", SpaceArchitect::CLI::Architect::Brief::New
590
+ end
379
591
  SpaceArchitect::CLI::Registry.register "worktree" do |wt|
380
592
  wt.register "add", SpaceArchitect::CLI::Architect::Worktree::Add
381
593
  wt.register "remove", SpaceArchitect::CLI::Architect::Worktree::Remove
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module SpaceArchitect
7
+ module SkillInstaller
8
+ PROVIDERS = %w[claude codex opencode pi].freeze
9
+
10
+ class << self
11
+ def source_root
12
+ Pathname.new(__dir__).parent.parent.join("skill")
13
+ end
14
+
15
+ def dest_root(provider, project:, env:, cwd: Dir.pwd)
16
+ case provider.to_s
17
+ when "claude"
18
+ base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
19
+ base.join(".claude", "skills")
20
+ when "codex"
21
+ base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
22
+ base.join(".agents", "skills")
23
+ when "opencode"
24
+ project ? Pathname.new(cwd).join(".opencode", "skills") : XDG.config_home(env: env).join("skills")
25
+ when "pi"
26
+ base = project ? Pathname.new(cwd) : Pathname.new(pi_agent_dir(env: env))
27
+ base.join("skills")
28
+ else
29
+ raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
30
+ end
31
+ end
32
+
33
+ def install(provider, project:, force:, env:, cwd: Dir.pwd, dry_run: false)
34
+ validate_provider!(provider)
35
+ dest = dest_root(provider, project: project, env: env, cwd: cwd)
36
+ results = []
37
+
38
+ source_skills.each do |skill_dir|
39
+ name = skill_dir.basename.to_s
40
+ skill_dest = dest.join(name)
41
+ results << install_skill(skill_dir, skill_dest, force: force, dry_run: dry_run)
42
+ end
43
+
44
+ { dest_root: dest, skills: results, dry_run: dry_run }
45
+ end
46
+
47
+ def source_skills
48
+ source_root.children.select(&:directory?)
49
+ end
50
+
51
+ private
52
+
53
+ def validate_provider!(provider)
54
+ return if PROVIDERS.include?(provider.to_s)
55
+
56
+ raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
57
+ end
58
+
59
+ def pi_agent_dir(env:)
60
+ Pathname.new(env.fetch("PI_CODING_AGENT_DIR", File.join(XDG.home(env: env), ".pi", "agent")))
61
+ end
62
+
63
+ def install_skill(source, dest, force:, dry_run:)
64
+ name = source.basename.to_s
65
+
66
+ if dest.exist?
67
+ if same_content?(source, dest)
68
+ return { name: name, action: :unchanged, path: dest }
69
+ end
70
+
71
+ unless force
72
+ return { name: name, action: :conflict, path: dest } if dry_run
73
+
74
+ raise Error,
75
+ "Refusing to overwrite existing skill at #{dest}. Re-run with --force."
76
+ end
77
+
78
+ unless dry_run
79
+ FileUtils.rm_rf(dest)
80
+ FileUtils.cp_r(source, dest)
81
+ end
82
+ { name: name, action: dry_run ? :would_update : :updated, path: dest }
83
+ else
84
+ unless dry_run
85
+ FileUtils.mkdir_p(dest.parent)
86
+ FileUtils.cp_r(source, dest)
87
+ end
88
+ { name: name, action: dry_run ? :would_install : :installed, path: dest }
89
+ end
90
+ end
91
+
92
+ def same_content?(source, dest)
93
+ return false unless dest.directory?
94
+
95
+ source_files = Dir.glob("#{source}/**/*").reject { |f| File.directory?(f) }
96
+ dest_files = Dir.glob("#{dest}/**/*").reject { |f| File.directory?(f) }
97
+
98
+ return false if source_files.length != dest_files.length
99
+
100
+ source_files.sort.zip(dest_files.sort).all? do |sf, df|
101
+ rel = sf.sub("#{source}/", "")
102
+ df.end_with?(rel) && File.read(sf) == File.read(df)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -4,6 +4,10 @@
4
4
  > architecture/I<NN>-<iteration>.md — this file only indexes the iterations and carries
5
5
  > mission-wide state. Keep it short (~150 lines): the next session must grok it
6
6
  > in under a minute. Not in the committed architecture = didn't happen.
7
+ >
8
+ > Durable mission contract: `architecture/BRIEF.md` (numbered §sections, cited as BRIEF §N).
9
+ > Create it with `architect brief new`. Edits to a §section are mission-scope decisions —
10
+ > log them in the Decisions log below, never as silent per-iteration drift.
7
11
 
8
12
  ## TL;DR (keep current)
9
13
 
@@ -0,0 +1,43 @@
1
+ # BRIEF — <%= @_title %>
2
+
3
+ > Durable mission contract for the Architect Loop. The numbered §sections below are the
4
+ > stable, cross-iteration address space: every iteration cites them as **BRIEF §N** in its
5
+ > Grounds, Specification, Acceptance Criteria, and Verdict (e.g. `(BRIEF §3.1)`), the way each
6
+ > gate addresses its intent back to one frozen reference. Frozen early; edits are mission-scope
7
+ > decisions logged in ARCHITECT.md's Decisions log, never silent per-iteration drift. Optional —
8
+ > a discovery mission may defer this and cite per-iteration Grounds until the shape stabilizes,
9
+ > then promote the consolidated picture here once.
10
+
11
+ ## 1. Goal & non-goals
12
+
13
+ <!-- One paragraph of goal; an explicit non-goals list. §1 is the cardinal-intent address —
14
+ iterations and verdicts cite it for the invariants that must never break. -->
15
+
16
+ ## 2. Constraints / frozen stack
17
+
18
+ <!-- Languages, gems/deps and pinned versions, platform constraints. "No new dependencies"
19
+ boundaries in iteration specs cite BRIEF §2. -->
20
+
21
+ ## 3. Domain model
22
+
23
+ <!-- The nouns and their relationships; the data/contract shapes that span iterations. -->
24
+
25
+ ### 3.1
26
+
27
+ ## 4. Layout
28
+
29
+ <!-- Where things live: directories, entry points, the seams iterations build against. -->
30
+
31
+ ## 5. Iteration map
32
+
33
+ <!-- The planned iterations, each independently shippable; the Acceptance Criteria are the
34
+ frozen proof. An iteration's Specification cites its row here (e.g. "BRIEF §5 Slice 3"). -->
35
+
36
+ ## 6. Cross-iteration risks / PHASE-0 challenge list
37
+
38
+ <!-- Known risks and the things a builder's PHASE 0 should challenge — cited from specs as
39
+ "BRIEF §6". -->
40
+
41
+ ## 7. Definition of done (whole mission)
42
+
43
+ <!-- The mission-level DoD. Iteration gates diff against "BRIEF §7 DoD". -->
@@ -9,21 +9,23 @@
9
9
 
10
10
  ## Grounds
11
11
 
12
- <!-- WHY. Research/PRD distilled: problem, decision + why, requirements,
13
- non-goals, verified facts WITH citation URLs, open questions. Optional — delete
14
- this section for iterations that needed no research. Commit: "<%= @_ordinal %>: grounds". -->
12
+ <!-- WHY. Research/spec distilled: problem, decision + why, requirements,
13
+ non-goals, verified facts WITH citation URLs, open questions. If
14
+ architecture/BRIEF.md exists, cite **BRIEF §N** instead of restating shrink this
15
+ to deltas + citations. Optional — delete this section for iterations that needed
16
+ no research. Write + commit: `architect section <%= @_name %> grounds --from <file>`. -->
15
17
 
16
18
  ## Specification
17
19
 
18
20
  <!-- WHAT / HOW — the full, self-contained delegation contract.
19
- Commit: "<%= @_ordinal %>: specification". -->
21
+ Write + commit: `architect section <%= @_name %> specification --from <file>`. -->
20
22
 
21
- - **Objective** — what to build and why (cite Grounds if present).
23
+ - **Objective** — what to build and why; cite **BRIEF §N** for durable context (e.g. `(BRIEF §3.1)`).
22
24
  - **Output format** — raw tables, numbers, commit SHAs, test output paths.
23
25
  - **Tool guidance** — exact verification commands for the target repo; the
24
26
  APIs/formats/versions to verify against live dependencies before writing code.
25
27
  - **Boundaries** — may-touch / must-not-touch / out-of-scope; no placeholders;
26
- no refactors beyond the task.
28
+ no refactors beyond the task. (Frozen stack: cite `BRIEF §2`.)
27
29
  - **Lane plan** — 1–4 lanes, each declaring: target repo `repos/<repo>`,
28
30
  file-touch set (overlap-checked), objective, output format, boundaries.
29
31
  - **Effort** — `think hard` … `ultrathink` per lane, with one line of why.
@@ -34,33 +36,39 @@ Commit: "<%= @_ordinal %>: specification". -->
34
36
  commits this file and records its SHA as freeze_sha. Read-only afterward — any
35
37
  change to Grounds/Specification/Acceptance Criteria = automatic iteration FAIL. -->
36
38
 
37
- | AC# | Command | Threshold |
38
- |-----|---------|-----------|
39
- | | | |
39
+ > Gate-pass is necessary, not sufficient: the architect also reads the diff against the
40
+ > cited **BRIEF §sections** intent and the §1 cardinal invariant before the verdict.
41
+
42
+ | AC# | Command | Threshold | Brief § |
43
+ |-----|---------|-----------|---------|
44
+ | | | | |
40
45
 
41
46
  ## Builder Prompt
42
47
 
43
48
  <!-- The exact lane-prompt(s) dispatched, recorded as provenance. One ###
44
49
  subsection per lane. A copy is written to build/<%= @_ordinal %>-<%= @_name %>-<lane>/prompt.md
45
- for stdin dispatch. Commit: "<%= @_ordinal %>: dispatched". -->
50
+ for stdin dispatch. Record + commit:
51
+ `architect section <%= @_name %> prompt --append --lane <lane> --from build/<%= @_ordinal %>-<%= @_name %>-<lane>/prompt.md`. -->
46
52
 
47
53
  ## Builder Report
48
54
 
49
55
  <!-- RAW EVIDENCE ONLY — tables, numbers, command output. The builder writes this
50
56
  to build/<%= @_ordinal %>-<%= @_name %>-<lane>/report.md; the architect transcribes it here
51
- VERBATIM (no interpretation). One ### subsection per lane; include the builder's
52
- PHASE 0 disagreements and its STATUS line. Commit: "<%= @_ordinal %>: evidence". -->
57
+ VERBATIM with `architect evidence <%= @_name %> --lane <lane>` (byte-for-byte, no
58
+ interpretation). One ### subsection per lane; includes the builder's PHASE 0
59
+ disagreements and its STATUS line. -->
53
60
 
54
61
  ## Verdict
55
62
 
56
63
  <!-- ARCHITECT JUDGMENT, written in a LATER session than the dispatch.
57
- Commit: "<%= @_ordinal %>: verdict". -->
64
+ Write + commit: `architect section <%= @_name %> verdict --from <file>`. -->
58
65
 
59
- - **Disagreement rulings** — each PHASE 0 disagreement: ACCEPT / REJECT / MODIFY + why.
66
+ - **Disagreement rulings** — each PHASE 0 disagreement: ACCEPT / REJECT / MODIFY + why; cite the
67
+ BRIEF §section in tension beside file:line.
60
68
  - **Acceptance Criteria integrity** — frozen sections unchanged since freeze (`architect verify <%= @_name %>`)?
61
69
 
62
- | AC# | Raw result | Verdict |
63
- |-----|------------|---------|
64
- | | | PASS/FAIL/INVALID |
70
+ | AC# | Raw result | Brief § | Verdict |
71
+ |-----|------------|---------|---------|
72
+ | | | | PASS/FAIL/INVALID |
65
73
 
66
- - **Iteration** — KILL / CONTINUE + the single decisive reason.
74
+ - **Iteration** — KILL / CONTINUE + the single decisive reason (e.g. "diff vs BRIEF §1/§3.3 faithful — CONTINUE").
@@ -74,6 +74,21 @@ module SpaceArchitect
74
74
  end.wait
75
75
  end
76
76
 
77
+ def style_skill_action(action)
78
+ case action.to_s
79
+ when "installed", "updated"
80
+ pastel.green(action)
81
+ when "would_install", "would_update"
82
+ pastel.cyan(action)
83
+ when "unchanged"
84
+ pastel.bright_black(action)
85
+ when "conflict"
86
+ pastel.yellow(action)
87
+ else
88
+ action.to_s
89
+ end
90
+ end
91
+
77
92
  private
78
93
 
79
94
  def color_mode
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpaceArchitect
4
- VERSION = "1.1.0"
4
+ VERSION = "1.3.0"
5
5
  end
@@ -19,6 +19,7 @@ require_relative "space_architect/git_client"
19
19
  require_relative "space_architect/mise_client"
20
20
  require_relative "space_architect/space_store"
21
21
  require_relative "space_architect/shell_integration"
22
+ require_relative "space_architect/skill_installer"
22
23
  require_relative "space_architect/terminal"
23
24
  require_relative "space_architect/harness"
24
25
  require_relative "space_architect/dispatcher"