@zalom/plastic 2.0.0-alpha.26 → 2.0.0-alpha.28
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.
- package/PLASTIC.md +13 -139
- package/README.md +337 -133
- package/agents/plastic-enforcer.md +12 -12
- package/agents/plastic-executor.md +2 -1
- package/agents/plastic-node-research.md +4 -2
- package/agents/plastic-node-verify.md +1 -0
- package/agents/plastic-node-work.md +1 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +6 -8
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +35 -1
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
- package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
- package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/package.json +3 -2
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +10 -9
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +57 -49
- package/scripts/end-intent +2 -8
- package/scripts/file-session-intent +2 -1
- package/scripts/hook-capture +4 -3
- package/scripts/hook-close +2 -1
- package/scripts/hook-record +3 -2
- package/scripts/hook-savepoint +3 -2
- package/scripts/hook-session-start +5 -4
- package/scripts/hook-stop +2 -1
- package/scripts/insight-append +1 -2
- package/scripts/install.rb +7 -5
- package/scripts/lib/action_graph_shim.rb +3 -1
- package/scripts/lib/active_delivery.rb +1 -1
- package/scripts/lib/agent_models.rb +44 -26
- package/scripts/lib/arm.rb +2 -1
- package/scripts/lib/backup.rb +63 -0
- package/scripts/lib/cli/command.rb +85 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +60 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +25 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +20 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +61 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +25 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +43 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +49 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +25 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +84 -0
- package/scripts/lib/cli/legacy.rb +50 -0
- package/scripts/lib/cli/output.rb +102 -0
- package/scripts/lib/cli/scope.rb +127 -0
- package/scripts/lib/cli/table.rb +64 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +5 -4
- package/scripts/lib/compact_instructions.rb +8 -0
- package/scripts/lib/day_summary.rb +4 -3
- package/scripts/lib/doctor_core.rb +7 -32
- package/scripts/lib/doctor_session_ledger.rb +2 -1
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_measure_models.rb +4 -2
- package/scripts/lib/index_entry.rb +9 -0
- package/scripts/lib/installer_core.rb +175 -56
- package/scripts/lib/intent_screen.rb +3 -3
- package/scripts/lib/lock.rb +2 -2
- package/scripts/lib/node_file.rb +22 -2
- package/scripts/lib/node_input.rb +11 -6
- package/scripts/lib/node_return.rb +8 -3
- package/scripts/lib/preflight.rb +2 -4
- package/scripts/lib/project_config.rb +2 -1
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +2 -0
- package/scripts/lib/report_screen.rb +4 -3
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_queue.rb +2 -2
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +59 -2
- package/scripts/lib/runner_dispatch.rb +12 -8
- package/scripts/lib/runner_policy.rb +28 -10
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_git.rb +4 -3
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +2 -7
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/worktree.rb +3 -8
- package/scripts/lib/worktree_sweep.rb +3 -2
- package/scripts/link-suggest +2 -1
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +3 -12
- package/scripts/node-run +2 -1
- package/scripts/plastic-lock +3 -2
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +44 -6
- package/scripts/release-check +10 -5
- package/scripts/report-screen +1 -1
- package/scripts/session-commit +2 -1
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +6 -6
- package/templates/config.yml +13 -6
- package/templates/node-research.md +4 -2
- package/templates/render.css +10 -0
- package/bin/plastic.js +0 -70
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -297
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -64
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -180
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -305
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -156
- package/skills/intent-continuing/references/board-fill.md +0 -52
- package/skills/intent-continuing/references/boarding-matrix.md +0 -35
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -89
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -182
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -87
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -47
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -136
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -376
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -70
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -90
- package/skills/roadmap/references/file-format.md +0 -134
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -66
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
- /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plastic
|
|
4
|
+
module StoreLayout
|
|
5
|
+
GLOBAL = "global"
|
|
6
|
+
PARENTS = %w[stores projects].freeze
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def moved?(home)
|
|
11
|
+
File.directory?(File.join(home, "stores"))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def global_root(home)
|
|
15
|
+
moved?(home) ? File.join(home, "stores", GLOBAL) : home
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def global_store(home)
|
|
19
|
+
File.join(global_root(home), "store")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def projects_root(home)
|
|
23
|
+
File.join(home, moved?(home) ? "stores" : "projects")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def project_root(home, slug)
|
|
27
|
+
File.join(projects_root(home), slug)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def root(home, slug)
|
|
31
|
+
(slug == GLOBAL) ? global_root(home) : project_root(home, slug)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def project_slugs(home)
|
|
35
|
+
parent = projects_root(home)
|
|
36
|
+
return [] unless File.directory?(parent)
|
|
37
|
+
|
|
38
|
+
Dir.children(parent).reject { |entry| entry.start_with?(".") || entry == GLOBAL }.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def locate(store)
|
|
42
|
+
root = File.dirname(File.expand_path(store))
|
|
43
|
+
parent = File.dirname(root)
|
|
44
|
+
return [root, GLOBAL] unless PARENTS.include?(File.basename(parent))
|
|
45
|
+
|
|
46
|
+
[File.dirname(parent), File.basename(root)]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def home_and_scope(store)
|
|
50
|
+
home, slug = locate(store)
|
|
51
|
+
[home, (slug == GLOBAL) ? GLOBAL : "project:#{slug}"]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "fileutils"
|
|
5
6
|
require "yaml"
|
|
6
7
|
|
|
@@ -63,7 +64,7 @@ module StoreProvisioning
|
|
|
63
64
|
}
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
project_dir =
|
|
67
|
+
project_dir = Plastic::StoreLayout.project_root(plastic_home, slug)
|
|
67
68
|
store_dir = File.join(project_dir, "store")
|
|
68
69
|
FileUtils.mkdir_p(store_dir)
|
|
69
70
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "search_index"
|
|
6
|
+
require_relative "reference_archive"
|
|
7
|
+
require_relative "work_graph"
|
|
8
|
+
|
|
9
|
+
module Plastic
|
|
10
|
+
module StoreSync
|
|
11
|
+
INTENT_PATH = %r{\A(?:(?:projects|stores)/([^/]+)/)?store/([^/]+?)--}
|
|
12
|
+
ROW_HASH = "lower(hex(sha3(sqlar_uncompress(data, sz), 256)))"
|
|
13
|
+
|
|
14
|
+
def self.action(state)
|
|
15
|
+
file, row, recorded = state.values_at("file", "row", "hash")
|
|
16
|
+
return "write" if file.nil?
|
|
17
|
+
return if file == row
|
|
18
|
+
return "read" if row == recorded
|
|
19
|
+
return "write" if file == recorded
|
|
20
|
+
|
|
21
|
+
"conflict"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.plan(home)
|
|
25
|
+
known = states(home)
|
|
26
|
+
fresh = SearchIndex.files(home) - known.map { |state| state["path"] }
|
|
27
|
+
plan = known.map { |state| [action(state), state["path"]] } + fresh.map { |file| ["read", file] }
|
|
28
|
+
plan.select(&:first).sort_by(&:last)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.states(home)
|
|
32
|
+
Sqlite.call(SearchIndex.path(home), <<~SQL, readonly: true)
|
|
33
|
+
SELECT path, lower(hex(sha3(readfile(#{Sqlite.quote(home)} || '/' || path), 256))) AS file, #{ROW_HASH} AS row, hash
|
|
34
|
+
FROM doc;
|
|
35
|
+
SQL
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.apply(home, plan)
|
|
39
|
+
statements = plan.map { |action, path| send(action, home, path) }
|
|
40
|
+
statements << "INSERT INTO ft(ft) VALUES('rebuild');" if plan.any? { |action, _path| action == "read" }
|
|
41
|
+
Sqlite.call(SearchIndex.path(home), ["BEGIN;", *statements, "COMMIT;"].join("\n"))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.read(home, path)
|
|
45
|
+
"DELETE FROM doc WHERE path = #{Sqlite.quote(path)};\n#{SearchIndex.insert(home, path)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.write(home, path)
|
|
49
|
+
where = "WHERE path = #{Sqlite.quote(path)}"
|
|
50
|
+
"#{restore(home, "doc", "path")} #{where};\nUPDATE doc SET hash = #{ROW_HASH} #{where};"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.restore(home, table, column)
|
|
54
|
+
"SELECT writefile(#{Sqlite.quote(home)} || '/' || #{column}, sqlar_uncompress(data, sz)) FROM #{table}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.diff(home, path)
|
|
58
|
+
body = Sqlite.call(SearchIndex.path(home), <<~SQL, readonly: true).first["body"]
|
|
59
|
+
SELECT CAST(sqlar_uncompress(data, sz) AS TEXT) AS body FROM doc WHERE path = #{Sqlite.quote(path)};
|
|
60
|
+
SQL
|
|
61
|
+
Tempfile.create("plastic-row") do |row|
|
|
62
|
+
row.write(body)
|
|
63
|
+
row.flush
|
|
64
|
+
Open3.capture2("diff", "-u", "-L", "row", "-L", "file", row.path, File.join(home, path)).first
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.orphans(home)
|
|
69
|
+
paths = Sqlite.call(SearchIndex.path(home), "SELECT path FROM doc;", readonly: true) +
|
|
70
|
+
Sqlite.call(ReferenceArchive.path(home), "SELECT name AS path FROM sqlar;", readonly: true)
|
|
71
|
+
keys = paths.filter_map { |row| row["path"].match(INTENT_PATH) }.map { |found| "#{found[1] || "global"}:#{found[2]}" }
|
|
72
|
+
(keys.uniq - WorkGraph.known(home)).sort
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.checkout(home)
|
|
76
|
+
{SearchIndex.path(home) => %w[doc path], ReferenceArchive.path(home) => %w[sqlar name]}.flat_map do |database, (table, column)|
|
|
77
|
+
missing = Sqlite.call(database, "SELECT #{column} AS path FROM #{table};", readonly: true)
|
|
78
|
+
.map { |row| row["path"] }.reject { |path| File.exist?(File.join(home, path)) }
|
|
79
|
+
names = missing.map { |path| Sqlite.quote(path) }.join(", ")
|
|
80
|
+
Sqlite.call(database, "#{restore(home, table, column)} WHERE #{column} IN (#{names});", readonly: true)
|
|
81
|
+
missing
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "store_layout"
|
|
5
|
+
require_relative "sqlite"
|
|
6
|
+
require_relative "lock"
|
|
7
|
+
|
|
8
|
+
module Plastic
|
|
9
|
+
class StoresMove
|
|
10
|
+
Refused = Class.new(StandardError)
|
|
11
|
+
|
|
12
|
+
GLOBAL_ENTRIES = %w[store INDEX.md roadmaps].freeze
|
|
13
|
+
PATH_COLUMNS = {"knowledge_graph.db" => %w[doc path], "references.db" => %w[sqlar name]}.freeze
|
|
14
|
+
LOCKS = "{store,projects/*/store}/*/delivery.lock"
|
|
15
|
+
|
|
16
|
+
def initialize(home, qmd_index:, dry_run: false)
|
|
17
|
+
@home = home
|
|
18
|
+
@qmd_index = qmd_index
|
|
19
|
+
@dry_run = dry_run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
refuse
|
|
24
|
+
plan = [["copy", copy]] + moves.map { |from, to| ["move", "#{from} -> #{to}"] } + rewrites.map { |file| ["rewrite", file] }
|
|
25
|
+
run unless @dry_run
|
|
26
|
+
plan
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def copy
|
|
30
|
+
"#{@home}-before-stores-move"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def refuse
|
|
36
|
+
raise Refused, "#{File.join(@home, "stores")}/ exists, so this home has moved already" if StoreLayout.moved?(@home)
|
|
37
|
+
raise Refused, "a delivery lock is held: #{held.join(", ")}" unless held.empty?
|
|
38
|
+
raise Refused, "#{copy} exists; remove it or move it away first" if File.exist?(copy)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def held
|
|
42
|
+
Dir.glob(LOCKS, base: @home).select { |lock| Time.now - File.mtime(File.join(@home, lock)) < Lock::TTL_SECONDS }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def moves
|
|
46
|
+
@moves ||= GLOBAL_ENTRIES.select { |name| File.exist?(File.join(@home, name)) }.map { |name| [name, "stores/global/#{name}"] } +
|
|
47
|
+
project_entries.map { |entry| ["projects/#{entry}", "stores/#{entry}"] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def project_entries
|
|
51
|
+
Dir.exist?(projects) ? Dir.children(projects).sort : []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def projects
|
|
55
|
+
File.join(@home, "projects")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def rewrites
|
|
59
|
+
[File.join(@home, "config.yml"), @qmd_index, *PATH_COLUMNS.keys.map { |name| File.join(@home, name) }].select { |file| File.file?(file) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def run
|
|
63
|
+
files = rewrites
|
|
64
|
+
FileUtils.cp_r(@home, copy, preserve: true)
|
|
65
|
+
moves.each { |from, to| move(from, to) }
|
|
66
|
+
FileUtils.rmdir(projects) if Dir.exist?(projects)
|
|
67
|
+
files.each { |file| PATH_COLUMNS.key?(File.basename(file)) ? rewrite_rows(file) : rewrite_text(file) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def move(from, to)
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(File.join(@home, to)))
|
|
72
|
+
FileUtils.mv(File.join(@home, from), File.join(@home, to))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def rewrite_text(file)
|
|
76
|
+
FileUtils.cp(file, "#{file}.before-stores-move", preserve: true)
|
|
77
|
+
File.write(file, [@home, "~/.plastic"].inject(File.read(file)) { |text, base| relocate(text, base) })
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def relocate(text, base)
|
|
81
|
+
text.gsub(%r{#{Regexp.escape(base)}/(store|roadmaps|INDEX\.md)\b}) { "#{base}/stores/global/#{Regexp.last_match(1)}" }
|
|
82
|
+
.gsub(%r{#{Regexp.escape(base)}/projects\b}) { "#{base}/stores" }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rewrite_rows(file)
|
|
86
|
+
table, column = PATH_COLUMNS.fetch(File.basename(file))
|
|
87
|
+
Sqlite.call(file, <<~SQL)
|
|
88
|
+
UPDATE #{table} SET #{column} = 'stores/' || substr(#{column}, 10) WHERE #{column} LIKE 'projects/%';
|
|
89
|
+
UPDATE #{table} SET #{column} = 'stores/global/' || #{column} WHERE #{column} LIKE 'store/%';
|
|
90
|
+
SQL
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require_relative "worktree"
|
|
5
6
|
require_relative "scaffold_intent"
|
|
6
7
|
|
|
@@ -96,13 +97,7 @@ module VerifyIntent
|
|
|
96
97
|
# shapes are told apart by whether store's grandparent is literally named "projects" (the
|
|
97
98
|
# StoreDiscovery convention), never by hardcoding ".plastic" as a name.
|
|
98
99
|
def resolve_plastic_home_and_scope(store)
|
|
99
|
-
|
|
100
|
-
grandparent = File.dirname(parent)
|
|
101
|
-
if File.basename(grandparent) == "projects"
|
|
102
|
-
[File.dirname(grandparent), "project:#{File.basename(parent)}"]
|
|
103
|
-
else
|
|
104
|
-
[parent, "global"]
|
|
105
|
-
end
|
|
100
|
+
Plastic::StoreLayout.home_and_scope(store)
|
|
106
101
|
end
|
|
107
102
|
|
|
108
103
|
# --- check 1: doctor, scoped to the intent --------------------------------------
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# VersionNumber - dotted version comparison without RubyGems (intent 363).
|
|
5
|
+
#
|
|
6
|
+
# The launcher starts as `ruby --disable-gems`, which saves about 26 of the 40
|
|
7
|
+
# milliseconds a plain Ruby process costs, so no file a command loads may reach
|
|
8
|
+
# for Gem::Version. Parses the leading run of dotted integers, ignoring a
|
|
9
|
+
# leading "v" and anything after the digits, and compares two of them segment by
|
|
10
|
+
# segment with the shorter one zero-padded. A string with no leading integer
|
|
11
|
+
# parses to nil, so a caller answers "not found" instead of raising.
|
|
12
|
+
#
|
|
13
|
+
# It deliberately does not order pre-release suffixes: 2.0.0-alpha.27 and 2.0.0
|
|
14
|
+
# compare equal here. A caller that needs RubyGems' pre-release order, such as
|
|
15
|
+
# the release-tag reader in scripts/report-screen, keeps Gem::Version.
|
|
16
|
+
class VersionNumber
|
|
17
|
+
include Comparable
|
|
18
|
+
|
|
19
|
+
LEADING_DIGITS = /\A(\d+(?:\.\d+)*)/
|
|
20
|
+
|
|
21
|
+
attr_reader :segments
|
|
22
|
+
|
|
23
|
+
def self.parse(value)
|
|
24
|
+
match = LEADING_DIGITS.match(value.to_s.strip.sub(/\Av/, ""))
|
|
25
|
+
match && new(match[1].split(".").map(&:to_i))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(segments)
|
|
29
|
+
@segments = segments
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def <=>(other)
|
|
33
|
+
return nil unless other.is_a?(VersionNumber)
|
|
34
|
+
|
|
35
|
+
width = [segments.length, other.segments.length].max
|
|
36
|
+
padded(width) <=> other.padded(width)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
segments.join(".")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def padded(width)
|
|
46
|
+
segments + Array.new(width - segments.length, 0)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "sqlite"
|
|
5
|
+
require_relative "index_entry"
|
|
6
|
+
require_relative "store_discovery"
|
|
7
|
+
|
|
8
|
+
module Plastic
|
|
9
|
+
module WorkGraph
|
|
10
|
+
NAME = "work_graph.db"
|
|
11
|
+
SCHEMA = <<~SQL
|
|
12
|
+
CREATE TABLE intent(id TEXT, store TEXT, slug TEXT, status TEXT, title TEXT);
|
|
13
|
+
CREATE TABLE ledger(intent_id TEXT, store TEXT, at TEXT, stage TEXT, text TEXT);
|
|
14
|
+
SQL
|
|
15
|
+
STATUSES = %w[active future completed abandoned].freeze
|
|
16
|
+
LEDGER_LINE = /\A(\S+)\s{2,}(\S+)\s{2,}(.+)\z/
|
|
17
|
+
|
|
18
|
+
def self.path(home)
|
|
19
|
+
File.join(home, NAME)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build(home)
|
|
23
|
+
draft = "#{path(home)}.tmp"
|
|
24
|
+
FileUtils.rm_f(draft)
|
|
25
|
+
rows = StoreDiscovery.discover(home)[:stores].flat_map { |store| intents(store) + ledger(store) }
|
|
26
|
+
Sqlite.call(draft, [SCHEMA, "BEGIN;", *rows, "COMMIT;"].join("\n"))
|
|
27
|
+
File.rename(draft, path(home))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.intents(store)
|
|
31
|
+
return [] unless File.exist?(store[:index])
|
|
32
|
+
|
|
33
|
+
status = nil
|
|
34
|
+
File.readlines(store[:index], chomp: true, encoding: "UTF-8").filter_map do |line|
|
|
35
|
+
status = line.delete_prefix("## ").downcase if line.start_with?("## ")
|
|
36
|
+
entry = STATUSES.include?(status) && IndexEntry.match(line)
|
|
37
|
+
entry && insert("intent", entry[1], store[:slug], File.basename(File.dirname(entry[3])).split("--", 2).last, status, entry[2])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.ledger(store)
|
|
42
|
+
Dir.glob("*/savepoint.md", base: store[:store]).sort.flat_map do |file|
|
|
43
|
+
id = file.split("--").first
|
|
44
|
+
File.readlines(File.join(store[:store], file), chomp: true, encoding: "UTF-8").filter_map do |line|
|
|
45
|
+
entry = line.match(LEDGER_LINE)
|
|
46
|
+
entry && insert("ledger", id, store[:slug], entry[1], entry[2], entry[3])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.insert(table, *values)
|
|
52
|
+
"INSERT INTO #{table} VALUES(#{values.map { |value| Sqlite.quote(value.to_s) }.join(", ")});"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.known(home)
|
|
56
|
+
Sqlite.call(path(home), "SELECT store, id FROM intent;", readonly: true).map { |row| "#{row["store"]}:#{row["id"]}" }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
package/scripts/lib/worktree.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "json"
|
|
5
6
|
require "yaml"
|
|
6
7
|
require "socket"
|
|
@@ -377,14 +378,8 @@ module Worktree
|
|
|
377
378
|
# (no project repo). Mirrors qmd_sync's slug_for_store fallback.
|
|
378
379
|
def slug_for_store(store_dir, home: Dir.home)
|
|
379
380
|
return nil if blank?(store_dir)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return nil if store_dir == File.join(plastic_home, "store")
|
|
383
|
-
|
|
384
|
-
parts = store_dir.split(File::SEPARATOR)
|
|
385
|
-
idx = parts.rindex("projects")
|
|
386
|
-
return parts[idx + 1] if idx && parts[idx + 1] && parts[idx + 2] == "store"
|
|
387
|
-
nil
|
|
381
|
+
slug = Plastic::StoreLayout.locate(store_dir).last
|
|
382
|
+
(slug == Plastic::StoreLayout::GLOBAL) ? nil : slug
|
|
388
383
|
end
|
|
389
384
|
|
|
390
385
|
# Best-effort slug for the worktree dir-name from an intent dir/store path:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require_relative "worktree"
|
|
5
6
|
require_relative "index_entry"
|
|
6
7
|
|
|
@@ -55,8 +56,8 @@ module WorktreeSweep
|
|
|
55
56
|
# Matching on the full name, not just the numeric id, sidesteps id
|
|
56
57
|
# collisions across projects (ids are only unique WITHIN one store).
|
|
57
58
|
def resolve_intent_dir(plastic_home, name)
|
|
58
|
-
candidates = [File.join(plastic_home,
|
|
59
|
-
Dir.glob(File.join(plastic_home, "
|
|
59
|
+
candidates = [File.join(Plastic::StoreLayout.global_store(plastic_home), name)] +
|
|
60
|
+
Dir.glob(File.join(Plastic::StoreLayout.projects_root(plastic_home), "*", "store", name))
|
|
60
61
|
candidates.find { |d| Dir.exist?(d) }
|
|
61
62
|
end
|
|
62
63
|
|
package/scripts/link-suggest
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
# nothing. --edge is explicit and required (sources is decided by origin, never
|
|
27
27
|
# inferred); --rating is required when --edge is chain.
|
|
28
28
|
|
|
29
|
+
require_relative "lib/store_layout"
|
|
29
30
|
require_relative "lib/link_suggestions"
|
|
30
31
|
|
|
31
32
|
module LinkSuggestCLI
|
|
@@ -69,7 +70,7 @@ module LinkSuggestCLI
|
|
|
69
70
|
i += 1
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
|
-
opts[:store_dir] ||=
|
|
73
|
+
opts[:store_dir] ||= Plastic::StoreLayout.global_store(opts[:home])
|
|
73
74
|
opts
|
|
74
75
|
end
|
|
75
76
|
|
|
@@ -17,7 +17,7 @@ GLOBAL_ROOT="$HOME/.plastic"
|
|
|
17
17
|
LOCAL_PLASTIC="$PROJECT_PATH/.plastic"
|
|
18
18
|
|
|
19
19
|
if [ ! -d "$GLOBAL_ROOT" ]; then
|
|
20
|
-
echo "Error: Global Plastic not installed. Run
|
|
20
|
+
echo "Error: Global Plastic not installed. Run 'plastic install' first." >&2
|
|
21
21
|
exit 1
|
|
22
22
|
fi
|
|
23
23
|
|
package/scripts/new-intent
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
# exactly one line, `created <dir>` or `joined <dir>`; exit 0 either way,
|
|
36
36
|
# exit 1 on bad input or failed self-validation.
|
|
37
37
|
|
|
38
|
+
require_relative "lib/store_layout"
|
|
38
39
|
require "fileutils"
|
|
39
40
|
require "date"
|
|
40
41
|
require_relative "lib/savepoint"
|
|
@@ -167,17 +168,7 @@ end
|
|
|
167
168
|
# [nil, "global"] only if the layout is unrecognized, in which case Links
|
|
168
169
|
# projection falls back to single-store resolution rooted at this store.
|
|
169
170
|
def store_context(store)
|
|
170
|
-
|
|
171
|
-
parent = File.dirname(store) # `<home>` or `<home>/projects/<slug>`
|
|
172
|
-
if File.basename(store) == "store" && File.basename(File.dirname(parent)) == "projects"
|
|
173
|
-
slug = File.basename(parent)
|
|
174
|
-
home = File.dirname(File.dirname(parent)) # strip projects/<slug>
|
|
175
|
-
[home, "project:#{slug}"]
|
|
176
|
-
elsif File.basename(store) == "store"
|
|
177
|
-
[parent, "global"] # `<home>/store`
|
|
178
|
-
else
|
|
179
|
-
[parent, "global"]
|
|
180
|
-
end
|
|
171
|
+
Plastic::StoreLayout.home_and_scope(store)
|
|
181
172
|
end
|
|
182
173
|
|
|
183
174
|
# The in-scope stores under `plastic_home`, each { key:, store: }. Delegates to
|
|
@@ -283,7 +274,7 @@ def run_tmp(opts)
|
|
|
283
274
|
abort "new-intent: --sources is not valid with --tmp" unless opts[:sources].empty?
|
|
284
275
|
abort "new-intent: --tags is not valid with --tmp" unless opts[:tags].empty?
|
|
285
276
|
|
|
286
|
-
store = opts[:store] ? expand(opts[:store]) : expand(
|
|
277
|
+
store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(expand(ENV.fetch("PLASTIC_HOME", "~/.plastic")))
|
|
287
278
|
templates = opts[:templates] ? expand(opts[:templates]) : default_templates_dir
|
|
288
279
|
abort "new-intent: templates dir not found: #{templates}" unless Dir.exist?(templates)
|
|
289
280
|
|
package/scripts/node-run
CHANGED
|
@@ -201,7 +201,8 @@ module NodeRunCLI
|
|
|
201
201
|
message_path = "#{out_path}.msg"
|
|
202
202
|
|
|
203
203
|
timeout_seconds = timeout_flag ? timeout_flag.to_i : CodexAdapter.timeout_seconds(kind)
|
|
204
|
-
codex_argv = CodexAdapter.build_argv(kind: kind, worktree: worktree, output_last_message: message_path
|
|
204
|
+
codex_argv = CodexAdapter.build_argv(kind: kind, worktree: worktree, output_last_message: message_path,
|
|
205
|
+
model: fields["model"], effort: fields["effort"])
|
|
205
206
|
|
|
206
207
|
result = CodexAdapter.execute(codex_argv, stdin_data: input_bytes, timeout_seconds: timeout_seconds,
|
|
207
208
|
output_last_message_path: message_path)
|
package/scripts/plastic-lock
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
# file; that path was retired in 2.0 (intent 307) and 344 G11.
|
|
39
39
|
# Exit 0 on success/report; exit 1 when the verb is blocked.
|
|
40
40
|
|
|
41
|
+
require_relative "lib/store_layout"
|
|
41
42
|
require "json"
|
|
42
43
|
require "yaml"
|
|
43
44
|
require_relative "lib/lock"
|
|
@@ -96,7 +97,7 @@ if dir.nil? || dir.strip.empty?
|
|
|
96
97
|
exit 1
|
|
97
98
|
end
|
|
98
99
|
roots = ActiveDelivery.project_roots(plastic_home)
|
|
99
|
-
global_store =
|
|
100
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
100
101
|
dir = ActiveDelivery.resolve(global_store: global_store, project_roots: roots, session: session)
|
|
101
102
|
if dir.nil?
|
|
102
103
|
candidates = ActiveDelivery.candidate_intent_dirs(global_store: global_store, project_roots: roots).uniq
|
|
@@ -188,7 +189,7 @@ when "arm"
|
|
|
188
189
|
end
|
|
189
190
|
when "status"
|
|
190
191
|
lock = Lock.read(dir)
|
|
191
|
-
resolved = ActiveDelivery.resolve(global_store:
|
|
192
|
+
resolved = ActiveDelivery.resolve(global_store: Plastic::StoreLayout.global_store(plastic_home),
|
|
192
193
|
project_roots: ActiveDelivery.project_roots(plastic_home),
|
|
193
194
|
session: key)
|
|
194
195
|
delivering = !resolved.nil? && File.expand_path(resolved) == File.expand_path(dir)
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
# Exit codes: 0 promoted (prints the intent dir last), 1 new-intent failed,
|
|
22
22
|
# 2 usage error or nothing to promote.
|
|
23
23
|
|
|
24
|
+
require_relative "lib/store_layout"
|
|
24
25
|
require "open3"
|
|
25
26
|
require "yaml"
|
|
26
27
|
require_relative "lib/session_ledger"
|
|
@@ -61,7 +62,7 @@ usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger
|
|
|
61
62
|
usage_abort("--match is required") if opts[:match].nil? || opts[:match].empty?
|
|
62
63
|
|
|
63
64
|
plastic_home = File.expand_path(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
|
|
64
|
-
store = opts[:store] ||
|
|
65
|
+
store = opts[:store] || Plastic::StoreLayout.global_store(plastic_home)
|
|
65
66
|
templates = opts[:templates] || File.expand_path("../templates", __dir__)
|
|
66
67
|
session = opts[:session] ? SessionLedger.short_session_id(opts[:session]) : nil
|
|
67
68
|
|
|
@@ -69,7 +70,7 @@ target_store = store
|
|
|
69
70
|
if opts[:project]
|
|
70
71
|
projects = StoreProvisioning.load_projects(plastic_home)
|
|
71
72
|
usage_abort("--project #{opts[:project]} is not a registered project") unless projects.key?(opts[:project])
|
|
72
|
-
target_store = File.join(plastic_home,
|
|
73
|
+
target_store = File.join(Plastic::StoreLayout.project_root(plastic_home, opts[:project]), "store")
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
# --- pick the target line ---------------------------------------------------
|
package/scripts/read-config
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# Usage: read-config <key> [--default VALUE] [--project PATH]
|
|
2
|
+
# Usage: read-config <key> [--default VALUE] [--project PATH] [--harness claude|codex]
|
|
3
3
|
# Resolves a config value: project -> global -> built-in defaults.
|
|
4
4
|
# Nested keys use dot notation: agent.type, architect.style
|
|
5
5
|
# Returns scalar values as strings, hash/array values as JSON.
|
|
@@ -46,6 +46,7 @@ def parse_args(argv)
|
|
|
46
46
|
key = nil
|
|
47
47
|
default_val = nil
|
|
48
48
|
project_dir = nil
|
|
49
|
+
harness = nil
|
|
49
50
|
|
|
50
51
|
i = 0
|
|
51
52
|
while i < argv.length
|
|
@@ -58,13 +59,17 @@ def parse_args(argv)
|
|
|
58
59
|
abort "Error: --project requires a value" unless argv[i + 1]
|
|
59
60
|
project_dir = argv[i + 1]
|
|
60
61
|
i += 2
|
|
62
|
+
when "--harness"
|
|
63
|
+
abort "Error: --harness requires a value" unless argv[i + 1]
|
|
64
|
+
harness = argv[i + 1]
|
|
65
|
+
i += 2
|
|
61
66
|
else
|
|
62
67
|
key = argv[i]
|
|
63
68
|
i += 1
|
|
64
69
|
end
|
|
65
70
|
end
|
|
66
71
|
|
|
67
|
-
[key, default_val, project_dir]
|
|
72
|
+
[key, default_val, project_dir, harness]
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
def load_yaml(path)
|
|
@@ -119,10 +124,10 @@ if ARGV.include?("--migrate")
|
|
|
119
124
|
exit 0
|
|
120
125
|
end
|
|
121
126
|
|
|
122
|
-
key, explicit_default, project_dir = parse_args(ARGV)
|
|
127
|
+
key, explicit_default, project_dir, harness = parse_args(ARGV)
|
|
123
128
|
|
|
124
129
|
if key.nil? || key.empty?
|
|
125
|
-
$stderr.puts "Usage: read-config <key> [--default VALUE] [--project PATH]"
|
|
130
|
+
$stderr.puts "Usage: read-config <key> [--default VALUE] [--project PATH] [--harness claude|codex]"
|
|
126
131
|
exit 1
|
|
127
132
|
end
|
|
128
133
|
|
|
@@ -135,9 +140,42 @@ if project_dir
|
|
|
135
140
|
project_config = load_yaml(project_config_path)
|
|
136
141
|
end
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
def canonical_harness(value)
|
|
144
|
+
return nil if value.nil?
|
|
145
|
+
return "claude" if %w[claude claude-code].include?(value.to_s)
|
|
146
|
+
return "codex" if value.to_s == "codex"
|
|
147
|
+
|
|
148
|
+
abort "Error: --harness must be claude or codex"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def harness_agent_value(config, key, harness)
|
|
152
|
+
match = key.match(/\Aagents\.(models|efforts)\.([^.]+)\z/)
|
|
153
|
+
return nil unless match
|
|
154
|
+
|
|
155
|
+
section, agent = match.captures
|
|
156
|
+
if section == "models"
|
|
157
|
+
AgentModels.models_section(config, harness: harness)[agent]
|
|
158
|
+
else
|
|
159
|
+
AgentModels.efforts_section(config, harness)[agent]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
harness = canonical_harness(harness)
|
|
164
|
+
if harness
|
|
165
|
+
value = harness_agent_value(project_config, key, harness)
|
|
166
|
+
value = harness_agent_value(global_config, key, harness) if value.nil?
|
|
167
|
+
else
|
|
168
|
+
value = dig_key(project_config, key)
|
|
169
|
+
value = dig_key(global_config, key) if value.nil?
|
|
170
|
+
end
|
|
140
171
|
value = explicit_default if value.nil? && explicit_default
|
|
172
|
+
if value.nil? && harness && (match = key.match(/\Aagents\.(models|efforts)\.([^.]+)\z/))
|
|
173
|
+
value = if match[1] == "models"
|
|
174
|
+
AgentModels.shipped_model_for(match[2], harness: harness)
|
|
175
|
+
else
|
|
176
|
+
AgentModels::DEFAULT_EFFORT
|
|
177
|
+
end
|
|
178
|
+
end
|
|
141
179
|
value = dig_key(DEFAULTS, key) if value.nil?
|
|
142
180
|
|
|
143
181
|
puts format_value(value)
|