@zalom/plastic 1.5.0 → 1.6.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.
- package/package.json +1 -1
- package/scripts/doctor.rb +76 -1
- package/scripts/lib/agent_models.rb +25 -0
- package/scripts/lib/installer_core.rb +13 -6
package/package.json
CHANGED
package/scripts/doctor.rb
CHANGED
|
@@ -14,6 +14,7 @@ require "yaml"
|
|
|
14
14
|
require "time"
|
|
15
15
|
require "date"
|
|
16
16
|
require "digest"
|
|
17
|
+
require "rubygems"
|
|
17
18
|
|
|
18
19
|
require_relative "lib/qmd_sync"
|
|
19
20
|
require_relative "lib/intent_validator"
|
|
@@ -71,13 +72,32 @@ class Doctor
|
|
|
71
72
|
doctor.rb
|
|
72
73
|
].freeze
|
|
73
74
|
|
|
75
|
+
# The apply_patch PreToolUse veto only fires from Codex v0.123.0 onward
|
|
76
|
+
# (PR #18391, "emit hooks for apply_patch edits"). Below it the veto
|
|
77
|
+
# silently no-ops (intent 184).
|
|
78
|
+
CODEX_HOOKS_FLOOR = "0.123.0"
|
|
79
|
+
|
|
74
80
|
attr_reader :plastic_home, :agents
|
|
75
81
|
|
|
82
|
+
# Testable shell-out default, mirroring QmdSync.default_runner: a real
|
|
83
|
+
# Open3.capture3 call in production, swappable for a fake in tests so no
|
|
84
|
+
# test needs a real codex binary or a PATH mutation.
|
|
85
|
+
def self.default_runner
|
|
86
|
+
lambda do |args|
|
|
87
|
+
require "open3"
|
|
88
|
+
out, _err, status = Open3.capture3("codex", *args)
|
|
89
|
+
[out, status.success?]
|
|
90
|
+
rescue Errno::ENOENT
|
|
91
|
+
["", false] # codex not on PATH: undetectable, fail open
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
76
95
|
def initialize(plastic_home: DEFAULT_PLASTIC_HOME, agents: DEFAULT_AGENTS,
|
|
77
|
-
bookend_amnesty: LEGACY_BOOKEND_AMNESTY)
|
|
96
|
+
bookend_amnesty: LEGACY_BOOKEND_AMNESTY, runner: Doctor.default_runner)
|
|
78
97
|
@plastic_home = plastic_home
|
|
79
98
|
@agents = agents
|
|
80
99
|
@bookend_amnesty = bookend_amnesty
|
|
100
|
+
@runner = runner
|
|
81
101
|
end
|
|
82
102
|
|
|
83
103
|
# --- Flag parsing ---
|
|
@@ -1300,6 +1320,7 @@ class Doctor
|
|
|
1300
1320
|
checks << codex_hooks_implemented_check(config)
|
|
1301
1321
|
checks << codex_hook_trust_advisory_check if hooks_check[:status] == "pass"
|
|
1302
1322
|
codex_config_toml_advisory_check(config).tap { |c| checks << c if c }
|
|
1323
|
+
codex_version_floor_check(config).tap { |c| checks << c if c }
|
|
1303
1324
|
|
|
1304
1325
|
checks
|
|
1305
1326
|
end
|
|
@@ -1555,6 +1576,60 @@ class Doctor
|
|
|
1555
1576
|
)
|
|
1556
1577
|
end
|
|
1557
1578
|
|
|
1579
|
+
# Codex version-floor advisory (intent 184): READ ONLY. The apply_patch PreToolUse
|
|
1580
|
+
# veto (scripts/lib/hook_registry.rb, scripts/codex-hook) only fires from Codex
|
|
1581
|
+
# v0.123.0 (PR #18391); below it the veto silently no-ops. version.json is Codex's
|
|
1582
|
+
# update-checker cache and holds no installed version, so the only install-method-
|
|
1583
|
+
# agnostic source is `codex --version`, shelled out through the injected runner.
|
|
1584
|
+
# Four honest branches (intent 208, no pass-by-construction): absent home -> nil;
|
|
1585
|
+
# present but undetectable -> distinct warn; below floor -> warn; at/above -> pass.
|
|
1586
|
+
def codex_version_floor_check(config)
|
|
1587
|
+
return nil unless File.exist?(config[:home_dir])
|
|
1588
|
+
|
|
1589
|
+
stdout, ok = @runner.call(["--version"])
|
|
1590
|
+
version = ok ? codex_version_from_output(stdout) : nil
|
|
1591
|
+
parsed = version && safe_version(version)
|
|
1592
|
+
|
|
1593
|
+
if parsed.nil?
|
|
1594
|
+
return check(
|
|
1595
|
+
category: "agent_registration", name: "codex_version_floor", status: "warn",
|
|
1596
|
+
message: "Could not determine the installed Codex version (`codex --version` did not " \
|
|
1597
|
+
"return a parseable version); Plastic cannot confirm the apply_patch hooks " \
|
|
1598
|
+
"floor v#{CODEX_HOOKS_FLOOR} is met, so its gate may silently no-op",
|
|
1599
|
+
fixable: false,
|
|
1600
|
+
fix_hint: "Ensure `codex` is on PATH and `codex --version` >= #{CODEX_HOOKS_FLOOR}"
|
|
1601
|
+
)
|
|
1602
|
+
end
|
|
1603
|
+
|
|
1604
|
+
if parsed < safe_version(CODEX_HOOKS_FLOOR)
|
|
1605
|
+
return check(
|
|
1606
|
+
category: "agent_registration", name: "codex_version_floor", status: "warn",
|
|
1607
|
+
message: "Installed Codex #{version} predates v#{CODEX_HOOKS_FLOOR}; the apply_patch " \
|
|
1608
|
+
"PreToolUse veto only exists from v#{CODEX_HOOKS_FLOOR} (PR #18391), so Plastic's " \
|
|
1609
|
+
"gate silently no-ops on this install",
|
|
1610
|
+
fixable: false,
|
|
1611
|
+
fix_hint: "Upgrade Codex to v#{CODEX_HOOKS_FLOOR} or newer with your install method " \
|
|
1612
|
+
"(npm i -g @openai/codex, mise, homebrew, or cargo)"
|
|
1613
|
+
)
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
check(
|
|
1617
|
+
category: "agent_registration", name: "codex_version_floor", status: "pass",
|
|
1618
|
+
message: "Codex #{version} meets the apply_patch hooks floor v#{CODEX_HOOKS_FLOOR}"
|
|
1619
|
+
)
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
def codex_version_from_output(stdout)
|
|
1623
|
+
m = stdout.to_s.match(/(\d+\.\d+\.\d+(?:[.\-+][0-9A-Za-z.\-+]*)?)/)
|
|
1624
|
+
m && m[1]
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
def safe_version(str)
|
|
1628
|
+
Gem::Version.new(str.to_s)
|
|
1629
|
+
rescue ArgumentError
|
|
1630
|
+
nil
|
|
1631
|
+
end
|
|
1632
|
+
|
|
1558
1633
|
# --- Check category 4: Core files ---
|
|
1559
1634
|
|
|
1560
1635
|
def check_core_files(agent_key)
|
|
@@ -31,6 +31,10 @@ module AgentModels
|
|
|
31
31
|
# dispatched by the auto pipeline, not part of TIER_DEFAULTS. Claude-only for
|
|
32
32
|
# this release (generate_codex_agents skips both by name; the Codex advisor
|
|
33
33
|
# case is intent 186, not a permanent exclusion).
|
|
34
|
+
#
|
|
35
|
+
# Intent 186 DEFINES the advisor Codex pairing but keeps emission deferred: when the skip is
|
|
36
|
+
# lifted, plastic-advisor pairs to gpt-5.6-sol at xhigh (the deepest) and plastic-faux-advisor
|
|
37
|
+
# to gpt-5.6-terra at high (cheaper). Neither is in TIER_DEFAULTS and neither is auto-dispatched.
|
|
34
38
|
CONSULTATION_AGENTS = %w[plastic-advisor plastic-faux-advisor].freeze
|
|
35
39
|
|
|
36
40
|
# Codex reasoning-effort per tier alias (intent 102a). model_reasoning_effort is a
|
|
@@ -45,6 +49,21 @@ module AgentModels
|
|
|
45
49
|
"haiku" => "low"
|
|
46
50
|
}.freeze
|
|
47
51
|
|
|
52
|
+
# Codex model id per tier alias (intent 186). Codex has NO vendor alias layer: every model id
|
|
53
|
+
# is a literal versioned string that rots (gpt-5.2 / gpt-5.3-codex already deprecated), which is
|
|
54
|
+
# why 116 D1 / 102a Decision B refused to pin a raw id per role file. This resolves that by
|
|
55
|
+
# centralizing every id in ONE map: Plastic owns the alias, so per-role identity costs a single
|
|
56
|
+
# line to refresh on a Codex deprecation plus a Plastic release, and no per-role file carries a
|
|
57
|
+
# raw id. opus (deepest reasoning tier) -> the flagship Sol; sonnet (execution tier) -> the
|
|
58
|
+
# balanced Terra; haiku (lightest) -> the fast/cheap Luna. Paired with EFFORT_BY_ALIAS so
|
|
59
|
+
# reasoning roles get a stronger model AND higher effort than executors. This is a shipped
|
|
60
|
+
# DEFAULT, fully overridable via agents.models.codex.<name>.
|
|
61
|
+
CODEX_MODEL_BY_ALIAS = {
|
|
62
|
+
"opus" => "gpt-5.6-sol",
|
|
63
|
+
"sonnet" => "gpt-5.6-terra",
|
|
64
|
+
"haiku" => "gpt-5.6-luna"
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
48
67
|
module_function
|
|
49
68
|
|
|
50
69
|
# Pull { basename => model } out of a loaded config hash's `agents.models`
|
|
@@ -85,4 +104,10 @@ module AgentModels
|
|
|
85
104
|
def effort_for(value)
|
|
86
105
|
EFFORT_BY_ALIAS[value.to_s]
|
|
87
106
|
end
|
|
107
|
+
|
|
108
|
+
# The Codex model id for a Plastic tier alias, or nil for any value that is not one of the three
|
|
109
|
+
# shipped aliases (the caller then treats the value as a literal Codex model id, or omits it).
|
|
110
|
+
def codex_model_for(value)
|
|
111
|
+
CODEX_MODEL_BY_ALIAS[value.to_s]
|
|
112
|
+
end
|
|
88
113
|
end
|
|
@@ -613,8 +613,8 @@ class InstallerCore
|
|
|
613
613
|
end
|
|
614
614
|
|
|
615
615
|
# Render one repo agents/*.md into a deterministic Codex agent TOML document. Fixed field
|
|
616
|
-
# order (name, description,
|
|
617
|
-
# byte-identical (idempotency).
|
|
616
|
+
# order (name, description, the model field(s) from codex_model_fields, developer_instructions)
|
|
617
|
+
# so regenerate is byte-identical (idempotency).
|
|
618
618
|
def render_codex_agent_toml(source_path, override)
|
|
619
619
|
front, body = split_frontmatter(File.read(source_path))
|
|
620
620
|
name = (front["name"] || File.basename(source_path, ".md")).to_s
|
|
@@ -641,14 +641,21 @@ class InstallerCore
|
|
|
641
641
|
end
|
|
642
642
|
end
|
|
643
643
|
|
|
644
|
-
# The
|
|
645
|
-
#
|
|
646
|
-
#
|
|
644
|
+
# The model-selection line(s). A known tier alias (opus/sonnet/haiku) emits BOTH a `model` line
|
|
645
|
+
# (from AgentModels.codex_model_for, the intent-186 per-role Codex identity) and a
|
|
646
|
+
# model_reasoning_effort line, model first for deterministic byte-identical regenerate. Any other
|
|
647
|
+
# non-empty value is a literal Codex model id emitted verbatim as `model` only. Empty -> no line
|
|
648
|
+
# (the agent inherits the session default). If an alias somehow lacks a mapped model, the effort
|
|
649
|
+
# line still emits alone (backward-safe).
|
|
647
650
|
def codex_model_fields(effective)
|
|
648
651
|
return "" if effective.nil? || effective.to_s.empty?
|
|
649
652
|
effort = AgentModels.effort_for(effective)
|
|
650
653
|
if effort
|
|
651
|
-
|
|
654
|
+
lines = []
|
|
655
|
+
model = AgentModels.codex_model_for(effective)
|
|
656
|
+
lines << %(model = "#{toml_inline_escape(model)}") if model && !model.to_s.empty?
|
|
657
|
+
lines << %(model_reasoning_effort = "#{effort}")
|
|
658
|
+
lines.join("\n")
|
|
652
659
|
else
|
|
653
660
|
%(model = "#{toml_inline_escape(effective.to_s)}")
|
|
654
661
|
end
|