space-architect 2.0.2 → 4.0.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 +31 -0
- data/README.md +16 -3
- data/lib/space_architect/architect_project.rb +186 -65
- data/lib/space_architect/bug_report.rb +93 -0
- data/lib/space_architect/cli/architect.rb +104 -28
- data/lib/space_architect/harness.rb +63 -9
- data/lib/space_architect/templates/iteration.md.erb +15 -1
- data/lib/space_architect.rb +1 -0
- data/lib/space_core/cli/help.rb +88 -18
- data/lib/space_core/cli/loop_status.rb +47 -0
- data/lib/space_core/cli/repeatable_options.rb +52 -43
- data/lib/space_core/cli/status.rb +54 -13
- data/lib/space_core/commands.rb +20 -0
- data/lib/space_core/paths.rb +23 -0
- data/lib/space_core/shell_integration.rb +1 -1
- data/lib/space_core/space.rb +51 -1
- data/lib/space_core/space_store.rb +1 -1
- data/lib/space_core/terminal.rb +1 -18
- data/lib/space_core/version.rb +1 -1
- data/lib/space_core.rb +2 -0
- data/lib/space_src/cli/clone.rb +0 -1
- data/lib/space_src/cli/config.rb +0 -1
- data/lib/space_src/cli/daemon.rb +0 -1
- data/lib/space_src/cli/org.rb +0 -1
- data/lib/space_src/cli/repo.rb +0 -1
- data/lib/space_src/cli/shell.rb +0 -1
- data/lib/space_src/cli/status.rb +0 -1
- data/lib/space_src/cli/sync.rb +0 -1
- data/lib/space_src/cli.rb +0 -1
- data/skill/architect/SKILL.md +47 -19
- data/skill/architect/dispatch.md +37 -23
- metadata +5 -1
|
@@ -6,7 +6,7 @@ require "dry/cli"
|
|
|
6
6
|
# on each occurrence, so `-r a -r b` yields ["b"]. We want repeated flags to
|
|
7
7
|
# accumulate (`-r a -r b -r c` => ["a", "b", "c"]) the way git/docker-style CLIs
|
|
8
8
|
# do, while still accepting the comma form. dry-cli exposes no hook for this, so
|
|
9
|
-
# we
|
|
9
|
+
# we prepend two private seams, each mirroring dry-cli 1.4.1 with a single change:
|
|
10
10
|
#
|
|
11
11
|
# * Parser.call — concat instead of assign for array options.
|
|
12
12
|
# * Banner.extended_command_options — drop the "=VALUE1,VALUE2,.." hint that
|
|
@@ -14,62 +14,71 @@ require "dry/cli"
|
|
|
14
14
|
# matching how you actually type it (-r VALUE).
|
|
15
15
|
#
|
|
16
16
|
# These mirror the released 1.4.1 source EXACTLY (not the dry-rb main branch,
|
|
17
|
-
# which already differs)
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# which already differs), prepended onto the module's singleton class so `super`
|
|
18
|
+
# reaches dry-cli's own implementation instead of discarding it outright. Pinned
|
|
19
|
+
# via `~> 1.4`; if a future dry-cli reworks these methods, repeatable_options_test
|
|
20
|
+
# goes red and we re-sync. Rationale: notes/ruby-cli-gems-report.md.
|
|
20
21
|
module Dry
|
|
21
22
|
class CLI
|
|
22
23
|
module Parser
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# Reopened on Parser's own nesting (not CLI's) so `Result` resolves
|
|
25
|
+
# lexically, exactly as it does in dry-cli's own `Parser.call`.
|
|
26
|
+
module RepeatableOptionsPatch
|
|
27
|
+
def call(command, arguments, prog_name)
|
|
28
|
+
original_arguments = arguments.dup
|
|
29
|
+
parsed_options = {}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
OptionParser.new do |opts|
|
|
32
|
+
command.options.each do |option|
|
|
33
|
+
opts.on(*option.parser_options) do |value|
|
|
34
|
+
if option.array?
|
|
35
|
+
(parsed_options[option.name.to_sym] ||= []).concat(value)
|
|
36
|
+
else
|
|
37
|
+
parsed_options[option.name.to_sym] = value
|
|
38
|
+
end
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
|
-
end
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
opts.on_tail("-h", "--help") do
|
|
43
|
+
return Result.help
|
|
44
|
+
end
|
|
45
|
+
end.parse!(arguments)
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
parsed_options = command.default_params.merge(parsed_options)
|
|
48
|
+
parse_required_params(command, arguments, prog_name, parsed_options)
|
|
49
|
+
rescue ::OptionParser::ParseError, ValueError
|
|
50
|
+
Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"")
|
|
51
|
+
end
|
|
47
52
|
end
|
|
53
|
+
singleton_class.prepend(RepeatableOptionsPatch)
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
module Banner
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
module RepeatableOptionsPatch
|
|
58
|
+
def extended_command_options(command)
|
|
59
|
+
result = command.options.map do |option|
|
|
60
|
+
name = Inflector.dasherize(option.name)
|
|
61
|
+
name = if option.boolean?
|
|
62
|
+
"[no-]#{name}"
|
|
63
|
+
elsif option.flag?
|
|
64
|
+
name
|
|
65
|
+
else
|
|
66
|
+
# array options included: repeated flags accumulate, so show
|
|
67
|
+
# the single repeatable form rather than "=VALUE1,VALUE2,..".
|
|
68
|
+
"#{name}=VALUE"
|
|
69
|
+
end
|
|
70
|
+
name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any?
|
|
71
|
+
name = " --#{name.ljust(30)}"
|
|
72
|
+
name = "#{name} # #{option.desc}"
|
|
73
|
+
name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
|
|
74
|
+
name
|
|
75
|
+
end
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
result << " --#{"help, -h".ljust(30)} # Print this help"
|
|
78
|
+
result.join("\n")
|
|
79
|
+
end
|
|
72
80
|
end
|
|
81
|
+
singleton_class.prepend(RepeatableOptionsPatch)
|
|
73
82
|
end
|
|
74
83
|
end
|
|
75
84
|
end
|
|
@@ -5,28 +5,69 @@ class Status < BaseCommand
|
|
|
5
5
|
desc "Set a space status: active, paused, done, archived"
|
|
6
6
|
argument :rest, type: :array, required: false, desc: "[SPACE] STATUS"
|
|
7
7
|
|
|
8
|
+
# One command, overloaded by its positional(s):
|
|
9
|
+
# help token (`help`, or dry-cli's -h/--help which never reach here) → help
|
|
10
|
+
# lone keyword / `<space> <keyword>` → set
|
|
11
|
+
# bare, or a lone non-keyword identifier → report
|
|
8
12
|
def call(rest: [], **opts)
|
|
9
13
|
setup_terminal(**opts.slice(:color, :colors))
|
|
14
|
+
args = Array(rest)
|
|
15
|
+
return show_help if help_token?(args)
|
|
16
|
+
|
|
10
17
|
handle_errors do
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
if set_args?(args)
|
|
19
|
+
set_status(args)
|
|
20
|
+
elsif args.length <= 1
|
|
21
|
+
report(args.first)
|
|
22
|
+
else
|
|
23
|
+
raise Space::Core::Error, "Usage: space status [SPACE] STATUS"
|
|
16
24
|
end
|
|
17
25
|
end
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
private
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
# The bare word `help` shows the command help, like dry-cli's -h/--help (which
|
|
31
|
+
# it intercepts before dispatch). It must never set status to "help" or report.
|
|
32
|
+
def help_token?(args)
|
|
33
|
+
args == ["help"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def show_help
|
|
37
|
+
out.puts Dry::CLI::Banner.call(self.class, Dry::CLI::ProgramName.call(["status"]))
|
|
38
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A set request: `<space> <keyword>` (two args), or a lone status keyword. A
|
|
42
|
+
# lone non-keyword arg is a space identifier to REPORT, not a malformed status.
|
|
43
|
+
def set_args?(args)
|
|
44
|
+
args.length == 2 ||
|
|
45
|
+
(args.length == 1 && Space::Core::Space::VALID_STATUSES.include?(args.first))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def set_status(args)
|
|
49
|
+
identifier, status_value = args.length == 2 ? args : [nil, args.first]
|
|
50
|
+
render(store.find(identifier)) do |space|
|
|
51
|
+
space.update_status(status_value)
|
|
52
|
+
terminal.success "#{space.id} is #{space.status}"
|
|
53
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def report(identifier)
|
|
58
|
+
render(store.find(identifier)) do |space|
|
|
59
|
+
terminal.say "ID: #{space.id}"
|
|
60
|
+
terminal.say "Title: #{space.title}"
|
|
61
|
+
terminal.say "Status: #{space.status}"
|
|
62
|
+
terminal.say "Path: #{terminal.path(space.path)}"
|
|
63
|
+
terminal.say "Created: #{space.data['created_at']}"
|
|
64
|
+
terminal.say "Updated: #{space.data['updated_at']}"
|
|
65
|
+
lines = LoopStatus.lines(space.data["project"])
|
|
66
|
+
if lines
|
|
67
|
+
terminal.say ""
|
|
68
|
+
lines.each { |line| terminal.say line }
|
|
69
|
+
end
|
|
70
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
30
71
|
end
|
|
31
72
|
end
|
|
32
73
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Core
|
|
4
|
+
module Commands
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Render a multi-flag command with trailing " \" continuations broken at
|
|
8
|
+
# "--flag" boundaries, continuation lines indented two spaces. Commands
|
|
9
|
+
# without "--" flags are returned unchanged.
|
|
10
|
+
def wrap(command)
|
|
11
|
+
parts = command.split(/(?= --)/)
|
|
12
|
+
return command if parts.size <= 1
|
|
13
|
+
|
|
14
|
+
parts.each_with_index.map do |part, i|
|
|
15
|
+
segment = i.zero? ? part.rstrip : " #{part.lstrip}"
|
|
16
|
+
i < parts.size - 1 ? "#{segment} \\" : segment
|
|
17
|
+
end.join("\n")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Core
|
|
4
|
+
module Paths
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def contract(path, env: ENV)
|
|
8
|
+
value = path.to_s
|
|
9
|
+
home = XDG.home(env: env)
|
|
10
|
+
[home, realpath_or_nil(home)].compact.uniq.each do |h|
|
|
11
|
+
return "~" if value == h
|
|
12
|
+
return "~#{value.delete_prefix(h)}" if value.start_with?("#{h}/")
|
|
13
|
+
end
|
|
14
|
+
value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def realpath_or_nil(path)
|
|
18
|
+
File.realpath(path)
|
|
19
|
+
rescue SystemCallError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -295,7 +295,7 @@ module Space::Core
|
|
|
295
295
|
complete -c space -f -n "__space_architect_complete_needs_command" -a path -d "Print a space path"
|
|
296
296
|
complete -c space -f -n "__space_architect_complete_needs_command" -a use -d "Select and cd to a space with fish integration"
|
|
297
297
|
complete -c space -f -n "__space_architect_complete_needs_command" -a current -d "Show the current space"
|
|
298
|
-
complete -c space -f -n "__space_architect_complete_needs_command" -a status -d "
|
|
298
|
+
complete -c space -f -n "__space_architect_complete_needs_command" -a status -d "Report or set a space status"
|
|
299
299
|
complete -c space -f -n "__space_architect_complete_needs_command" -a config -d "Show or update config"
|
|
300
300
|
complete -c space -f -n "__space_architect_complete_needs_command" -a repo -d "Manage repos in the current space"
|
|
301
301
|
complete -c space -f -n "__space_architect_complete_needs_command" -a repos -d "Manage repos in the current space"
|
data/lib/space_core/space.rb
CHANGED
|
@@ -9,6 +9,11 @@ module Space::Core
|
|
|
9
9
|
METADATA_FILE = "space.yaml"
|
|
10
10
|
VALID_STATUSES = %w[active paused done archived].freeze
|
|
11
11
|
|
|
12
|
+
# Canonical space.yaml shape: registry under `project:` (renamed from the
|
|
13
|
+
# pre-2.0 `architect:` key). Bumped from 1 in the release that did the
|
|
14
|
+
# rename, since that release shipped no read-side alias for it.
|
|
15
|
+
SCHEMA_VERSION = 2
|
|
16
|
+
|
|
12
17
|
attr_reader :path, :data
|
|
13
18
|
|
|
14
19
|
def self.load(path)
|
|
@@ -18,13 +23,58 @@ module Space::Core
|
|
|
18
23
|
parsed = YAML.safe_load(metadata_path.read, aliases: false) || {}
|
|
19
24
|
raise Error, "Space metadata must contain a YAML mapping: #{metadata_path}" unless parsed.is_a?(Hash)
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
data = stringify_keys(parsed)
|
|
27
|
+
normalize_schema!(data, metadata_path)
|
|
28
|
+
new(Pathname.new(path), data)
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
def self.stringify_keys(hash)
|
|
25
32
|
hash.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
|
|
26
33
|
end
|
|
27
34
|
|
|
35
|
+
# Normalizes a parsed space.yaml hash to canonical schema v2, in place:
|
|
36
|
+
# - future version (> SCHEMA_VERSION) → raise, refuse to misread it.
|
|
37
|
+
# - `architect:` only (v1a) → becomes `project:`.
|
|
38
|
+
# - `project:` only (v1b or v2) → left as-is.
|
|
39
|
+
# - both present, `project:` empty and `architect:` non-empty → the
|
|
40
|
+
# corruption from the old silent-default read path; take `architect:`.
|
|
41
|
+
# - both present and non-empty: identical → keep; differing → raise
|
|
42
|
+
# rather than silently pick one and lose data.
|
|
43
|
+
# Idempotent, so a canonical v2 space is a no-op through this method.
|
|
44
|
+
def self.normalize_schema!(data, metadata_path)
|
|
45
|
+
version = data["version"]
|
|
46
|
+
if version.is_a?(Integer) && version > SCHEMA_VERSION
|
|
47
|
+
raise Error,
|
|
48
|
+
"space.yaml schema version #{version} is newer than this gem supports " \
|
|
49
|
+
"(#{SCHEMA_VERSION}); upgrade space-architect: #{metadata_path}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
legacy = data.delete("architect")
|
|
53
|
+
current = data["project"]
|
|
54
|
+
|
|
55
|
+
data["project"] =
|
|
56
|
+
if legacy.nil?
|
|
57
|
+
current
|
|
58
|
+
elsif current.nil? || empty_project_block?(current)
|
|
59
|
+
legacy
|
|
60
|
+
elsif legacy == current
|
|
61
|
+
current
|
|
62
|
+
else
|
|
63
|
+
raise Error,
|
|
64
|
+
"#{metadata_path} has both 'architect:' and 'project:' blocks with " \
|
|
65
|
+
"conflicting content; resolve manually.\narchitect: #{legacy.inspect}\n" \
|
|
66
|
+
"project: #{current.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
data["version"] = SCHEMA_VERSION
|
|
70
|
+
end
|
|
71
|
+
private_class_method :normalize_schema!
|
|
72
|
+
|
|
73
|
+
def self.empty_project_block?(block)
|
|
74
|
+
block.is_a?(Hash) && Array(block["iterations"]).empty? && block["current_iteration"].nil?
|
|
75
|
+
end
|
|
76
|
+
private_class_method :empty_project_block?
|
|
77
|
+
|
|
28
78
|
def initialize(path, data)
|
|
29
79
|
@path = Pathname.new(path)
|
|
30
80
|
@data = data
|
data/lib/space_core/terminal.rb
CHANGED
|
@@ -48,13 +48,7 @@ module Space::Core
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def path(path)
|
|
51
|
-
|
|
52
|
-
homes.each do |home|
|
|
53
|
-
return "~" if value == home
|
|
54
|
-
return "~#{value.delete_prefix(home)}" if value.start_with?("#{home}/")
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
value
|
|
51
|
+
Paths.contract(path, env: config.env)
|
|
58
52
|
end
|
|
59
53
|
|
|
60
54
|
def with_spinner(message)
|
|
@@ -108,17 +102,6 @@ module Space::Core
|
|
|
108
102
|
end
|
|
109
103
|
end
|
|
110
104
|
|
|
111
|
-
def homes
|
|
112
|
-
home = XDG.home(env: config.env)
|
|
113
|
-
[home, realpath_or_nil(home)].compact.uniq
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def realpath_or_nil(path)
|
|
117
|
-
File.realpath(path)
|
|
118
|
-
rescue SystemCallError
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
121
|
-
|
|
122
105
|
def table_row(headers, row, column_widths, header: false)
|
|
123
106
|
row.each_with_index.map do |cell, index|
|
|
124
107
|
raw = cell.to_s
|
data/lib/space_core/version.rb
CHANGED
data/lib/space_core.rb
CHANGED
|
@@ -10,6 +10,8 @@ require "space_core/warnings"
|
|
|
10
10
|
Space::Core::Warnings.disable_experimental!
|
|
11
11
|
require "space_core/atomic_write"
|
|
12
12
|
require "space_core/xdg"
|
|
13
|
+
require "space_core/paths"
|
|
14
|
+
require "space_core/commands"
|
|
13
15
|
require "space_core/config"
|
|
14
16
|
require "space_core/state"
|
|
15
17
|
require "space_core/slugger"
|
data/lib/space_src/cli/clone.rb
CHANGED
data/lib/space_src/cli/config.rb
CHANGED
data/lib/space_src/cli/daemon.rb
CHANGED
data/lib/space_src/cli/org.rb
CHANGED
data/lib/space_src/cli/repo.rb
CHANGED
data/lib/space_src/cli/shell.rb
CHANGED
data/lib/space_src/cli/status.rb
CHANGED
data/lib/space_src/cli/sync.rb
CHANGED
data/lib/space_src/cli.rb
CHANGED
data/skill/architect/SKILL.md
CHANGED
|
@@ -110,6 +110,7 @@ loop.
|
|
|
110
110
|
8. **Stop conditions:** failing verification you can't root-cause, instructions
|
|
111
111
|
conflicting with project docs, irreversible/destructive calls, or scope
|
|
112
112
|
growth beyond the iteration → checkpoint to the handoff and ask the human.
|
|
113
|
+
For bugs in the architect tooling or process itself, run `architect bug-report` — it prints a prefilled issue template and the `gh` command to file it.
|
|
113
114
|
|
|
114
115
|
## Procedure
|
|
115
116
|
|
|
@@ -120,8 +121,11 @@ loop.
|
|
|
120
121
|
gate (test/lint/typecheck/build commands) from docs or CI config.
|
|
121
122
|
- Once per environment: `claude --version` and confirm the builder model
|
|
122
123
|
resolves (`echo ok | claude -p --model <builder-model> --max-turns 1`;
|
|
123
|
-
details in `dispatch.md`).
|
|
124
|
-
|
|
124
|
+
details in `dispatch.md`). Past that one-time check, every foreground dispatch
|
|
125
|
+
self-verifies — it prints a liveness line naming the streamed model and
|
|
126
|
+
confirming the run log is growing (a WARN line instead if the streamed model
|
|
127
|
+
disagrees with the pin or the log isn't growing) — so no lone lane needs
|
|
128
|
+
launching-and-watching before the fan-out.
|
|
125
129
|
- Read `architecture/ARCHITECT.md` (the cross-iteration table of contents),
|
|
126
130
|
`architecture/BRIEF.md` if present (the durable §-numbered project contract you
|
|
127
131
|
cite as BRIEF §N), and the iteration file `architecture/I<NN>-<name>.md` for any
|
|
@@ -244,9 +248,17 @@ contract, self-contained:
|
|
|
244
248
|
task.
|
|
245
249
|
- **Lane plan** — split the iteration into 1–4 parallel lanes, each declaring
|
|
246
250
|
its **target repo + file-touch set, checked for overlap**: name the repo
|
|
247
|
-
(`repos/<repo>`) and every file each lane may touch.
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
(`repos/<repo>`) and every file each lane may touch. The machine-readable
|
|
252
|
+
declaration lives in a fenced ` ```lanes ` block in the Specification — one
|
|
253
|
+
entry per lane (`name`, `repo`, `touch` globs) — the single frozen source of
|
|
254
|
+
truth `architect freeze` records into `space.yaml` and `architect provision`
|
|
255
|
+
materializes. The touch-set now lives *with* the frozen spec by design: it
|
|
256
|
+
closes the drift where a `worktree add --touch` flag could diverge from the
|
|
257
|
+
spec's intent. The scaffold ships a commented ` ```lanes ` stub in the
|
|
258
|
+
Specification (see `templates/iteration.md.erb`) — uncomment it. Lanes in
|
|
259
|
+
*different* repos are inherently disjoint; same-repo lanes with any file
|
|
260
|
+
overlap run as one. Each lane gets its own objective, output format, and
|
|
261
|
+
boundaries. Most
|
|
250
262
|
iterations are one lane — fan out only when the work is genuinely parallel (a
|
|
251
263
|
cross-repo project often is). Two first-class patterns — runnable recipes in `dispatch.md`: **parallel +
|
|
252
264
|
fast-follow** (disjoint lanes integrate first; a fast-follow lane off
|
|
@@ -292,13 +304,24 @@ once a frozen section changed afterward.
|
|
|
292
304
|
|
|
293
305
|
### 5. Dispatch (one fresh `claude -p` per lane, worktree-isolated)
|
|
294
306
|
|
|
295
|
-
Per the mechanics in `dispatch.md
|
|
296
|
-
|
|
297
|
-
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
307
|
+
Per the mechanics in `dispatch.md`. The lane lifecycle is **declare → freeze →
|
|
308
|
+
provision → write prompts → dispatch** — every lane gets a worktree; there is no
|
|
309
|
+
dispatch-in-the-checkout path:
|
|
310
|
+
|
|
311
|
+
- **Declare** — at spec time, each lane is one entry in the Specification's
|
|
312
|
+
fenced ` ```lanes ` block (§4): `name`, `repo`, `touch` globs.
|
|
313
|
+
- **Freeze** — `architect freeze` parses that block and records each lane
|
|
314
|
+
(name, repo, touch_set) into `space.yaml`.
|
|
315
|
+
- **Provision** — `architect provision <iteration>` materializes every declared
|
|
316
|
+
lane's worktree + `lane/<id>-<lane>` branch in one shot, each off the resolved
|
|
317
|
+
base (`--base` override, else `project/<slug>` when it exists, else the repo's
|
|
318
|
+
default branch — a repo commit, distinct from the freeze, which is a space
|
|
319
|
+
commit) and recorded in `space.yaml`; it is idempotent, and `--lane <name>`
|
|
320
|
+
provisions a single lane. The manual `architect worktree add` it wraps stays
|
|
321
|
+
registered as an internal primitive for edge cases — not a step you run.
|
|
322
|
+
- `dispatch`, `integrate`, and `gate` auto-materialize a lane whose worktree is
|
|
323
|
+
missing from its frozen declaration, so a lane you didn't pre-provision can't
|
|
324
|
+
dead-end the flow.
|
|
302
325
|
|
|
303
326
|
Assemble each lane's lane-prompt (the template in `dispatch.md` + this lane's
|
|
304
327
|
section of the Specification + the frozen Acceptance Criteria) and write it to
|
|
@@ -362,9 +385,9 @@ it (never hand-resolve). A cross-repo project yields one `project/<slug>` branch
|
|
|
362
385
|
per touched repo. `main` is never touched per-iteration — `--teardown` deletes
|
|
363
386
|
only the per-lane `lane/<iteration>-<lane>` branches and worktrees, never the
|
|
364
387
|
project branch. Update the iteration index in `architecture/ARCHITECT.md`
|
|
365
|
-
(recording the `project/<slug>` branch), remove the worktrees
|
|
366
|
-
integrate … --teardown`, or `architect worktree remove <iteration>
|
|
367
|
-
commit the space.
|
|
388
|
+
(recording the `project/<slug>` branch), remove the provisioned worktrees
|
|
389
|
+
(`architect integrate … --teardown`, or `architect worktree remove <iteration>
|
|
390
|
+
<lane>`), and commit the space.
|
|
368
391
|
|
|
369
392
|
**Run the frozen gates cold** — `architect gate <iteration>` runs the frozen
|
|
370
393
|
gate commands against the integration tree and streams raw output (a runner, not
|
|
@@ -373,10 +396,15 @@ cited BRIEF §sections (per §2), then write the **Verdict** (`architect verdict
|
|
|
373
396
|
<iteration> continue|kill --from <file>`): disagreement rulings, per-AC
|
|
374
397
|
PASS/FAIL/INVALID, the KILL/CONTINUE call.
|
|
375
398
|
|
|
376
|
-
At project end,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
the
|
|
399
|
+
At project end, landing is yours, not the CLI's — the PR body is judgment
|
|
400
|
+
output, the same class as a Verdict. Per touched repo: write the PR body
|
|
401
|
+
yourself — from the iteration verdicts, the integrated diff, and the BRIEF —
|
|
402
|
+
to `build/land/<repo>-pr-body.md`, then present the paste-and-run block to the
|
|
403
|
+
human: `cd` to the repo checkout, `git push -u origin project/<slug>`, and
|
|
404
|
+
`gh pr create --base main --head project/<slug> --title … --body-file …` —
|
|
405
|
+
paths `~`-contracted, the multi-flag command broken with trailing ` \` at flag
|
|
406
|
+
boundaries. No push, no `gh` call by you or the CLI; the human runs the block
|
|
407
|
+
when the project is ready to ship.
|
|
380
408
|
|
|
381
409
|
## Maintenance
|
|
382
410
|
|