@zalom/plastic 1.14.0 → 2.0.0-alpha.1
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 +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/check-update +12 -3
- package/hooks/close +5 -0
- package/hooks/hooks.json +9 -49
- package/hooks/record +4 -0
- package/package.json +3 -2
- package/scripts/agent-report +8 -9
- package/scripts/append-ledger +195 -0
- package/scripts/codex-hook +101 -95
- package/scripts/dashboard.rb +4 -4
- package/scripts/doctor.rb +106 -43
- package/scripts/end-intent +77 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +242 -0
- package/scripts/hook-close +34 -0
- package/scripts/hook-record +149 -0
- package/scripts/hook-session-start +95 -12
- package/scripts/install.rb +8 -6
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +283 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/doctor_core.rb +17 -88
- package/scripts/lib/doctor_session_ledger.rb +106 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +44 -107
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +19 -30
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +320 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +84 -0
- package/scripts/lib/session_git.rb +469 -0
- package/scripts/lib/session_ledger.rb +497 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +94 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +42 -3
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +5 -5
- package/scripts/update.rb +7 -3
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +218 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +8 -8
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +47 -55
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +131 -89
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +2 -2
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +28 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +12 -49
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/project.yml +7 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
package/scripts/hook-create-gate
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
|
|
5
|
-
# PreToolUse create gate (intent 60b): validate the PROPOSED content of a Write
|
|
6
|
-
# to an intent file before it lands. Fires when the target path is an intent file
|
|
7
|
-
# inside its own equally-named directory (store/<id>--<slug>/<id>--<slug>.md). It
|
|
8
|
-
# validates the proposed content from the hook payload (tool_input.content), NOT
|
|
9
|
-
# the on-disk file (which does not exist yet at PreToolUse), using IntentValidator
|
|
10
|
-
# for born-complete frontmatter plus the sanctioned section structure.
|
|
11
|
-
#
|
|
12
|
-
# It depends ONLY on the stdin path + content, never on the auto-bridge or any
|
|
13
|
-
# session id, so it runs unconditionally (headless / background sessions).
|
|
14
|
-
# It validates ONLY the intent file, never sentinel placeholder lifecycle files
|
|
15
|
-
# (spec.md/plan.md/etc.); the path matcher excludes them.
|
|
16
|
-
#
|
|
17
|
-
# Exit 0 = allow. Exit 2 = block (reason on stderr, shown to the agent).
|
|
18
|
-
#
|
|
19
|
-
# Reads the Claude Code PreToolUse payload as JSON on STDIN:
|
|
20
|
-
# { "tool_input": { "file_path": "...", "content": "..." } }
|
|
21
|
-
# Empty / unparseable / non-matching path => exit 0 (cannot judge, allow).
|
|
22
|
-
# Three payload shapes on a matching path (intent 108, D7):
|
|
23
|
-
# Write (content): validate the proposed content.
|
|
24
|
-
# Edit (old_string): simulate the replacement on the on-disk file and
|
|
25
|
-
# validate the RESULT; a missing file blocks.
|
|
26
|
-
# Pathless MCP mutation: validate the CURRENT on-disk file (the PostToolUse
|
|
27
|
-
# gate-check backstop validates the result); a
|
|
28
|
-
# missing file blocks (fail-safe).
|
|
29
|
-
#
|
|
30
|
-
# Thin CLI wrapper (intent 244): the gate logic itself lives in
|
|
31
|
-
# scripts/lib/edit_gates.rb, shared with the merged Claude dispatcher
|
|
32
|
-
# (scripts/hook-edit-gates) and, since intent 251, the merged Codex dispatcher
|
|
33
|
-
# (scripts/lib/codex_edit_gates.rb). This wrapper has no production caller on
|
|
34
|
-
# either harness anymore; it remains as the per-gate isolation surface for the
|
|
35
|
-
# hook test suite. Deliberately WITHOUT a top-level rescue, matching this
|
|
36
|
-
# script's behavior before the extraction: an internal exception here has
|
|
37
|
-
# never been caught.
|
|
38
|
-
|
|
39
|
-
require "json"
|
|
40
|
-
require_relative "lib/edit_gates"
|
|
41
|
-
|
|
42
|
-
raw = $stdin.read rescue nil
|
|
43
|
-
exit 0 if raw.nil? || raw.strip.empty?
|
|
44
|
-
|
|
45
|
-
payload = JSON.parse(raw) rescue nil
|
|
46
|
-
exit 0 unless payload.is_a?(Hash)
|
|
47
|
-
|
|
48
|
-
outcome = EditGates.create_gate(EditGates.context_from(payload))
|
|
49
|
-
exit 0 unless outcome
|
|
50
|
-
outcome.lines.each { |line| $stderr.puts line }
|
|
51
|
-
exit 2
|
package/scripts/hook-edit-gates
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
|
|
5
|
-
# The merged Claude PreToolUse edit-path dispatcher (intent 244). One registered
|
|
6
|
-
# hook, one Ruby process, one stdin parse, five gates in-process, in the fixed
|
|
7
|
-
# order savepoint-pre, lock-gate, code-gate, links-gate, create-gate (spec D-a).
|
|
8
|
-
# The first deny ends evaluation and its own shape is reproduced exactly: stderr
|
|
9
|
-
# plus exit 2 for code-gate/links-gate/create-gate, stdout permissionDecision
|
|
10
|
-
# JSON plus exit 0 for lock-gate (spec D-b). savepoint-pre runs first so its
|
|
11
|
-
# `started` ledger append stays unconditional, including on a call another gate
|
|
12
|
-
# then blocks (spec D-c).
|
|
13
|
-
#
|
|
14
|
-
# Fail-open everywhere (spec D-i): each gate runs inside its own rescue in
|
|
15
|
-
# EditGates.dispatch, and this file's top level rescues to exit 0, so a gate bug
|
|
16
|
-
# never denies and never silences a sibling gate.
|
|
17
|
-
#
|
|
18
|
-
# LOAD-BEARING SHAPE: doctor's claude_hooks_implemented_check reads THIS FILE as
|
|
19
|
-
# plain text and extracts the `when "<gate>"` labels of the `case gate` statement
|
|
20
|
-
# below, because a script with top-level side effects can never be required to
|
|
21
|
-
# introspect it (the same technique intent 200 used for scripts/codex-hook). If
|
|
22
|
-
# the case statement is reshaped, update that check in scripts/doctor.rb in the
|
|
23
|
-
# same change, or it will fail loudly by design.
|
|
24
|
-
|
|
25
|
-
require "json"
|
|
26
|
-
require_relative "lib/edit_gates"
|
|
27
|
-
|
|
28
|
-
def route(gate, ctx)
|
|
29
|
-
case gate
|
|
30
|
-
when "savepoint-pre" then EditGates.savepoint_pre(ctx)
|
|
31
|
-
when "lock-gate" then EditGates.lock_gate(ctx)
|
|
32
|
-
when "code-gate" then EditGates.code_gate(ctx)
|
|
33
|
-
when "links-gate" then EditGates.links_gate(ctx)
|
|
34
|
-
when "create-gate" then EditGates.create_gate(ctx)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
begin
|
|
39
|
-
raw = begin
|
|
40
|
-
$stdin.read
|
|
41
|
-
rescue StandardError
|
|
42
|
-
nil
|
|
43
|
-
end
|
|
44
|
-
exit 0 if raw.nil? || raw.strip.empty?
|
|
45
|
-
|
|
46
|
-
payload = begin
|
|
47
|
-
JSON.parse(raw)
|
|
48
|
-
rescue StandardError
|
|
49
|
-
nil
|
|
50
|
-
end
|
|
51
|
-
exit 0 unless payload.is_a?(Hash)
|
|
52
|
-
|
|
53
|
-
exit EditGates.dispatch(ctx: EditGates.context_from(payload), route: method(:route))
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
# Dispatcher-internal failure: never impersonate a deny (spec D-i).
|
|
56
|
-
$stderr.puts "plastic edit-gates error: #{e.message}"
|
|
57
|
-
exit 0
|
|
58
|
-
end
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# Usage: hook-future-intent-check <store_root> <message>
|
|
4
|
-
# Outputs hook JSON when the user's message matches future intent keywords.
|
|
5
|
-
# Exits silently if no matches.
|
|
6
|
-
|
|
7
|
-
require "json"
|
|
8
|
-
require "yaml"
|
|
9
|
-
|
|
10
|
-
store_root, message = ARGV
|
|
11
|
-
exit 0 unless store_root && message
|
|
12
|
-
|
|
13
|
-
index_path = "#{store_root}/INDEX.md"
|
|
14
|
-
exit 0 unless File.exist?(index_path)
|
|
15
|
-
|
|
16
|
-
# --- Find future intent dirs ---
|
|
17
|
-
|
|
18
|
-
lines = File.readlines(index_path)
|
|
19
|
-
future_dirs = []
|
|
20
|
-
section = nil
|
|
21
|
-
|
|
22
|
-
lines.each do |line|
|
|
23
|
-
section = :future if line.start_with?("## Future")
|
|
24
|
-
section = nil if line.start_with?("## ") && !line.start_with?("## Future")
|
|
25
|
-
next unless section == :future && line.strip.start_with?("- [")
|
|
26
|
-
|
|
27
|
-
if line =~ /store\/([\w-]+)\//
|
|
28
|
-
future_dirs << $1
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
exit 0 if future_dirs.empty?
|
|
33
|
-
|
|
34
|
-
# --- Match keywords against message ---
|
|
35
|
-
|
|
36
|
-
matches = []
|
|
37
|
-
message_words = message.split(/\W+/).reject { |w| w.length < 4 }
|
|
38
|
-
|
|
39
|
-
future_dirs.each do |dir|
|
|
40
|
-
intent_path = "#{store_root}/store/#{dir}/#{dir}.md"
|
|
41
|
-
next unless File.exist?(intent_path)
|
|
42
|
-
|
|
43
|
-
content = File.read(intent_path)
|
|
44
|
-
|
|
45
|
-
tags = []
|
|
46
|
-
if content =~ /^tags:\s*\[([^\]]+)\]/
|
|
47
|
-
tags = $1.split(",").map(&:strip).map(&:downcase)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
intent_name = ""
|
|
51
|
-
if content =~ /^intent:\s*["']?(.+?)["']?\s*$/
|
|
52
|
-
intent_name = $1.downcase
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
intent_id = ""
|
|
56
|
-
if content =~ /^id:\s*["']?([a-z0-9]+)["']?\s*$/
|
|
57
|
-
intent_id = $1
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
keywords = tags + intent_name.split(/\W+/).reject { |w| w.length < 4 }
|
|
61
|
-
keywords = keywords.map(&:downcase).uniq
|
|
62
|
-
|
|
63
|
-
matched_keywords = keywords.select { |kw| message.include?(kw) }
|
|
64
|
-
name_matches = message_words.select { |w| intent_name.include?(w) }
|
|
65
|
-
matched_keywords = (matched_keywords + name_matches).uniq
|
|
66
|
-
|
|
67
|
-
if matched_keywords.length >= 1
|
|
68
|
-
index_line = lines.find { |l| l.include?(dir) }&.strip || "#{intent_id} — #{intent_name}"
|
|
69
|
-
matches << { "index_line" => index_line, "keywords" => matched_keywords }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
exit 0 if matches.empty?
|
|
74
|
-
|
|
75
|
-
# --- Build context ---
|
|
76
|
-
|
|
77
|
-
parts = []
|
|
78
|
-
parts << "PLASTIC — Future intents related to this message:\n"
|
|
79
|
-
matches.each do |m|
|
|
80
|
-
parts << "#{m["index_line"]} (matched: #{m["keywords"].join(", ")})"
|
|
81
|
-
end
|
|
82
|
-
parts << "\nConsider asking the user if they want to activate any of these, or note the connection."
|
|
83
|
-
|
|
84
|
-
payload = {
|
|
85
|
-
"hookSpecificOutput" => {
|
|
86
|
-
"hookEventName" => "UserPromptSubmit",
|
|
87
|
-
"additionalContext" => parts.join("\n")
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
puts JSON.generate(payload)
|
package/scripts/hook-gate-check
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# Usage: hook-gate-check <file_path> [session_id]
|
|
4
|
-
# PostToolUse gate enforcement for Plastic intent lifecycle.
|
|
5
|
-
# Checks if a written file triggers a stage-transition gate.
|
|
6
|
-
# Exit 0 = allow (optionally with transition context)
|
|
7
|
-
# Exit 2 = block (gate violation)
|
|
8
|
-
|
|
9
|
-
require "json"
|
|
10
|
-
require_relative "lib/bridge"
|
|
11
|
-
require_relative "lib/intent_validator"
|
|
12
|
-
require_relative "lib/lock"
|
|
13
|
-
|
|
14
|
-
file_path = ARGV[0]
|
|
15
|
-
exit 0 unless file_path && !file_path.empty?
|
|
16
|
-
|
|
17
|
-
session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]
|
|
18
|
-
|
|
19
|
-
file_path_abs = File.expand_path(file_path)
|
|
20
|
-
|
|
21
|
-
# --- Decoupled savepoint ledger (intent 34, hardened in intent 52) ---
|
|
22
|
-
# The savepoint is derived purely from the file path's intent directory, BEFORE
|
|
23
|
-
# any bridge resolution. A missing bridge must never skip the savepoint.
|
|
24
|
-
intent_dir_abs = Bridge.intent_dir_for(file_path_abs)
|
|
25
|
-
if intent_dir_abs
|
|
26
|
-
begin
|
|
27
|
-
Bridge.append_savepoint(intent_dir_abs, file_path_abs)
|
|
28
|
-
# When checklist.md lands, How ends and Exec begins: emit the `Exec started`
|
|
29
|
-
# companion in the same event (intent 81). Guard on a real (non-placeholder)
|
|
30
|
-
# checklist so a scaffold sentinel does not trip it.
|
|
31
|
-
if File.basename(file_path_abs) == "checklist.md" && Bridge.stage_file_present?(file_path_abs)
|
|
32
|
-
Bridge.append_exec_started(intent_dir_abs)
|
|
33
|
-
end
|
|
34
|
-
rescue StandardError
|
|
35
|
-
# ignore — rebuildable from disk
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# --- Artifact-validity backstop (intent 4a1c1) ---
|
|
40
|
-
# When the written file IS the intent file itself (the `<id>--<slug>.md` directly
|
|
41
|
-
# inside `store/<id>--<slug>/`), run IntentValidator on it. This is headless-safe:
|
|
42
|
-
# it depends only on the file path and disk, never on a bridge or session. In the
|
|
43
|
-
# PostToolUse model the write has already happened, so we cannot prevent it; the
|
|
44
|
-
# loud non-zero exit + stderr is the rejection signal. NOT for spec.md/plan.md/
|
|
45
|
-
# checklist.md/outcome.md/savepoint.md — those are validated by the stage gates.
|
|
46
|
-
if intent_dir_abs && File.basename(file_path_abs) == File.basename(Bridge.intent_file(intent_dir_abs))
|
|
47
|
-
result = IntentValidator.validate(intent_dir_abs)
|
|
48
|
-
unless result[:ok]
|
|
49
|
-
warn "PLASTIC ARTIFACT INVALID — #{File.basename(file_path_abs)} is not born complete:"
|
|
50
|
-
result[:missing].each { |field| warn " missing required field: #{field}" }
|
|
51
|
-
result[:errors].each { |error| warn " #{error}" }
|
|
52
|
-
warn "Fix the frontmatter; the intent is not valid until every required field is present and sources/chain are well-formed."
|
|
53
|
-
exit 1
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# --- Find bridge file ---
|
|
58
|
-
bridge_data = Bridge.discover_bridge(session: session, cwd: Dir.pwd)
|
|
59
|
-
|
|
60
|
-
# No bridge = no active intent = no stage enforcement. Savepoint already handled.
|
|
61
|
-
exit 0 unless bridge_data
|
|
62
|
-
session = bridge_data["session"]
|
|
63
|
-
|
|
64
|
-
# --- Resolve intent directory ---
|
|
65
|
-
|
|
66
|
-
intent_info = bridge_data["intent"]
|
|
67
|
-
exit 0 unless intent_info
|
|
68
|
-
|
|
69
|
-
store = intent_info["store"]
|
|
70
|
-
dir = intent_info["dir"]
|
|
71
|
-
intent_dir = "#{store}/#{dir}"
|
|
72
|
-
|
|
73
|
-
# Normalize both paths for comparison
|
|
74
|
-
bridge_intent_dir_abs = File.expand_path(intent_dir)
|
|
75
|
-
|
|
76
|
-
# Lease heartbeat (intent 108, D1): every write by the session that owns (or
|
|
77
|
-
# delegates under) the lock refreshes the delivery.lock mtime. Best-effort:
|
|
78
|
-
# a heartbeat failure must never break the gate hook.
|
|
79
|
-
begin
|
|
80
|
-
Lock.heartbeat(bridge_intent_dir_abs, session: session)
|
|
81
|
-
rescue StandardError
|
|
82
|
-
# ignore
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Not inside intent dir = not our business
|
|
86
|
-
exit 0 unless file_path_abs.start_with?("#{bridge_intent_dir_abs}/")
|
|
87
|
-
|
|
88
|
-
# --- Determine if this is a stage-transition file ---
|
|
89
|
-
|
|
90
|
-
relative = file_path_abs.sub("#{bridge_intent_dir_abs}/", "")
|
|
91
|
-
basename = File.basename(file_path)
|
|
92
|
-
|
|
93
|
-
stage_files = %w[spec.md plan.md checklist.md outcome.md]
|
|
94
|
-
is_stage_file = stage_files.include?(basename)
|
|
95
|
-
is_action_file = relative.start_with?("actions/")
|
|
96
|
-
|
|
97
|
-
if is_stage_file || is_action_file
|
|
98
|
-
# Run gate check
|
|
99
|
-
error = Bridge.check_gate(bridge_intent_dir_abs, file_path_abs)
|
|
100
|
-
|
|
101
|
-
if error
|
|
102
|
-
# Block log (intent 229): same hand-rolled best-effort write as
|
|
103
|
-
# hook-bash-gate's; this script keeps its current requires.
|
|
104
|
-
begin
|
|
105
|
-
require "fileutils"
|
|
106
|
-
require "time"
|
|
107
|
-
blocklog = File.join(Dir.home, ".plastic", ".cache", "gate-blocks.log")
|
|
108
|
-
FileUtils.mkdir_p(File.dirname(blocklog))
|
|
109
|
-
File.open(blocklog, "a") do |io|
|
|
110
|
-
io.puts([Time.now.utc.iso8601, "gate-check", session.to_s,
|
|
111
|
-
Bridge.intent_id_from_dir(bridge_intent_dir_abs).to_s,
|
|
112
|
-
file_path_abs.to_s.gsub(/\s+/, " ").strip,
|
|
113
|
-
error.to_s.gsub(/\s+/, " ").strip].join("\t"))
|
|
114
|
-
end
|
|
115
|
-
rescue StandardError
|
|
116
|
-
# the block still applies; logging is best-effort
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Gate violation — block the write
|
|
120
|
-
bridge_data["build"]["gate_failures"] = (bridge_data["build"]["gate_failures"] || 0) + 1
|
|
121
|
-
Bridge.write(session, bridge_data)
|
|
122
|
-
|
|
123
|
-
payload = {
|
|
124
|
-
"decision" => "block",
|
|
125
|
-
"reason" => "PLASTIC GATE — #{error}"
|
|
126
|
-
}
|
|
127
|
-
puts JSON.generate(payload)
|
|
128
|
-
exit 2
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Gate passed — update bridge with new stage
|
|
132
|
-
old_stage = bridge_data["build"]["stage"]
|
|
133
|
-
new_stage = Bridge.derive_stage(bridge_intent_dir_abs)
|
|
134
|
-
new_missing = Bridge.missing_for_stage(new_stage) - Bridge.has_files(bridge_intent_dir_abs)
|
|
135
|
-
|
|
136
|
-
bridge_data["build"]["stage"] = new_stage
|
|
137
|
-
bridge_data["build"]["has"] = Bridge.has_files(bridge_intent_dir_abs)
|
|
138
|
-
bridge_data["build"]["missing"] = new_missing
|
|
139
|
-
bridge_data["build"]["gate_failures"] = 0
|
|
140
|
-
bridge_data["build"]["last_activity"] = Time.now.utc.iso8601
|
|
141
|
-
|
|
142
|
-
if old_stage != new_stage
|
|
143
|
-
bridge_data["observe"]["last_transition"] = "#{old_stage} → #{new_stage}"
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
Bridge.write(session, bridge_data)
|
|
147
|
-
|
|
148
|
-
# Build transition context — ONE concise sentence (intent 84, Lever 1),
|
|
149
|
-
# preserving the `Next: ...` hint. Formatting is pure in Bridge.gate_narration.
|
|
150
|
-
context = Bridge.gate_narration(
|
|
151
|
-
old_stage: old_stage, new_stage: new_stage,
|
|
152
|
-
basename: basename, new_missing: new_missing
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
payload = {
|
|
156
|
-
"hookSpecificOutput" => {
|
|
157
|
-
"hookEventName" => "PostToolUse",
|
|
158
|
-
"additionalContext" => context
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
puts JSON.generate(payload)
|
|
162
|
-
exit 0
|
|
163
|
-
else
|
|
164
|
-
# Regular file in intent dir — just update last_activity. The savepoint for
|
|
165
|
-
# the What milestone was already handled above by the decoupled ledger write.
|
|
166
|
-
bridge_data["build"]["last_activity"] = Time.now.utc.iso8601
|
|
167
|
-
Bridge.write(session, bridge_data)
|
|
168
|
-
exit 0
|
|
169
|
-
end
|
package/scripts/hook-links-gate
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
|
|
5
|
-
# PreToolUse links gate (intent 192): denies an Edit/Write whose payload
|
|
6
|
-
# changes a REAL ## Links section to anything other than exactly what
|
|
7
|
-
# LinksProjection would emit for the file's own frontmatter. This is the
|
|
8
|
-
# write-time belt for the PLASTIC.md ## Links contract; scripts/project-links
|
|
9
|
-
# is the safe-repair path this gate always leaves open (it runs via Bash, a
|
|
10
|
-
# tool this gate does not intercept).
|
|
11
|
-
#
|
|
12
|
-
# Reads the PreToolUse payload as JSON on STDIN:
|
|
13
|
-
# { "tool_input": { "file_path":, "content": | "old_string"/"new_string"/"replace_all": } }
|
|
14
|
-
# Exit 0 = allow (including every "cannot judge" case). Exit 2 = deny (message
|
|
15
|
-
# on stderr).
|
|
16
|
-
#
|
|
17
|
-
# Thin CLI wrapper (intent 244): the gate logic itself lives in
|
|
18
|
-
# scripts/lib/edit_gates.rb, shared with the merged Claude dispatcher
|
|
19
|
-
# (scripts/hook-edit-gates) and, since intent 251, the merged Codex dispatcher
|
|
20
|
-
# (scripts/lib/codex_edit_gates.rb). This wrapper has no production caller on
|
|
21
|
-
# either harness anymore; it remains as the per-gate isolation surface for the
|
|
22
|
-
# hook test suite.
|
|
23
|
-
|
|
24
|
-
require "json"
|
|
25
|
-
require_relative "lib/edit_gates"
|
|
26
|
-
|
|
27
|
-
begin
|
|
28
|
-
raw = begin
|
|
29
|
-
$stdin.read
|
|
30
|
-
rescue StandardError
|
|
31
|
-
nil
|
|
32
|
-
end
|
|
33
|
-
exit 0 if raw.nil? || raw.strip.empty?
|
|
34
|
-
|
|
35
|
-
payload = begin
|
|
36
|
-
JSON.parse(raw)
|
|
37
|
-
rescue StandardError
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
exit 0 unless payload.is_a?(Hash)
|
|
41
|
-
|
|
42
|
-
outcome = EditGates.links_gate(EditGates.context_from(payload))
|
|
43
|
-
exit 0 unless outcome
|
|
44
|
-
outcome.lines.each { |line| $stderr.puts line }
|
|
45
|
-
exit 2
|
|
46
|
-
rescue => e
|
|
47
|
-
# Hook-internal failure only: do NOT impersonate a user-deny. Log and fail open
|
|
48
|
-
# here so a gate bug (an unreadable UNRELATED store dir, a resolver crash, or
|
|
49
|
-
# anything else) never bricks editing; mirrors hook-lock-gate's own top-level
|
|
50
|
-
# rescue (ruling: the gate must fail open, never closed, on a computation it
|
|
51
|
-
# cannot complete).
|
|
52
|
-
$stderr.puts "plastic links-gate error: #{e.message}"
|
|
53
|
-
exit 0
|
|
54
|
-
end
|
package/scripts/hook-lock-gate
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
#
|
|
5
|
-
# Usage: hook-lock-gate <file_path> [session_id] [harness]
|
|
6
|
-
# Fail-CLOSED PreToolUse gate (intent 96). Blocks a mutating write to an active
|
|
7
|
-
# intent's lifecycle dir when THIS session holds no live lock. Project code is NOT
|
|
8
|
-
# gated here (D2). Emits the PreToolUse JSON deny contract at exit 0; reserves
|
|
9
|
-
# non-zero exit for hook-internal errors only. Matcher Write|Edit|NotebookEdit, so
|
|
10
|
-
# reads never reach here.
|
|
11
|
-
#
|
|
12
|
-
# Thin CLI wrapper (intent 244): the gate logic itself lives in
|
|
13
|
-
# scripts/lib/edit_gates.rb, shared with the merged Claude dispatcher
|
|
14
|
-
# (scripts/hook-edit-gates) and, since intent 251, the merged Codex dispatcher
|
|
15
|
-
# (scripts/lib/codex_edit_gates.rb). This wrapper has no production caller on
|
|
16
|
-
# either harness anymore; it remains as the per-gate isolation surface for the
|
|
17
|
-
# hook test suite.
|
|
18
|
-
|
|
19
|
-
require_relative "lib/edit_gates"
|
|
20
|
-
|
|
21
|
-
begin
|
|
22
|
-
file_path = ARGV[0]
|
|
23
|
-
exit 0 unless file_path && !file_path.empty?
|
|
24
|
-
session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]
|
|
25
|
-
# Usage: hook-lock-gate <file_path> [session_id] [harness]. Claude's own bash
|
|
26
|
-
# shim never passes a third arg, so harness falls through to Lock's default
|
|
27
|
-
# (claude, slash form). Since intent 251 the merged Codex library
|
|
28
|
-
# (scripts/lib/codex_edit_gates.rb) sets harness: "codex" directly on the
|
|
29
|
-
# Context it builds; no caller of this wrapper passes "codex" anymore.
|
|
30
|
-
harness = (ARGV[2] unless ARGV[2].to_s.empty?) || "claude"
|
|
31
|
-
|
|
32
|
-
ctx = EditGates::Context.new(
|
|
33
|
-
tool_name: "Write", session: session, gate_path: file_path, file_path: file_path,
|
|
34
|
-
harness: harness,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
outcome = EditGates.lock_gate(ctx)
|
|
38
|
-
exit 0 unless outcome
|
|
39
|
-
print outcome.stdout
|
|
40
|
-
exit 0
|
|
41
|
-
rescue => e
|
|
42
|
-
# Hook-internal failure only: do NOT impersonate a user-deny. Log and fail open
|
|
43
|
-
# here so a gate bug never bricks editing; the fail-CLOSED intent is about MISSING
|
|
44
|
-
# locks, not crashes.
|
|
45
|
-
$stderr.puts "plastic lock-gate error: #{e.message}"
|
|
46
|
-
exit 0
|
|
47
|
-
end
|
package/scripts/hook-power-tools
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
|
|
5
|
-
# power-tools UserPromptSubmit hook (intents 66, 66b, 187, 246). Emits the
|
|
6
|
-
# PowerTools mandate, one recommendation line per present tool, as
|
|
7
|
-
# hookSpecificOutput. Silent no-op (exit 0) when no power tool is present or
|
|
8
|
-
# anything goes wrong. It parses no prompt: the mandate depends on tool presence
|
|
9
|
-
# alone, so there is nothing to gate on prompt triviality. Intent 246 removed the
|
|
10
|
-
# qmd hit search this hook used to run first.
|
|
11
|
-
require "json"
|
|
12
|
-
require "timeout"
|
|
13
|
-
require_relative "lib/qmd_hook"
|
|
14
|
-
|
|
15
|
-
# Drain stdin so the caller's write never blocks or breaks on a closed pipe. The
|
|
16
|
-
# payload is deliberately not parsed.
|
|
17
|
-
begin
|
|
18
|
-
STDIN.read
|
|
19
|
-
rescue StandardError
|
|
20
|
-
nil
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
context = begin
|
|
24
|
-
Timeout.timeout(2) do
|
|
25
|
-
QmdHook.run(cwd: Dir.pwd)
|
|
26
|
-
end
|
|
27
|
-
rescue Exception
|
|
28
|
-
nil
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
exit 0 if context.nil? || context.strip.empty?
|
|
32
|
-
|
|
33
|
-
puts JSON.generate(
|
|
34
|
-
"hookSpecificOutput" => {
|
|
35
|
-
"hookEventName" => "UserPromptSubmit",
|
|
36
|
-
"additionalContext" => context,
|
|
37
|
-
}
|
|
38
|
-
)
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
# Usage: hook-savepoint-pre <file_path>
|
|
5
|
-
#
|
|
6
|
-
# PreToolUse savepoint trigger (intent 81). When a stage-opening lifecycle file
|
|
7
|
-
# (spec.md => Why, plan.md => How) is ABOUT to be written into an intent dir,
|
|
8
|
-
# append the pre-stage `started` ledger line so the ledger records "this stage
|
|
9
|
-
# was entered" before its artifact lands.
|
|
10
|
-
#
|
|
11
|
-
# Like the PostToolUse decoupled savepoint write (intent 52), it is derived from
|
|
12
|
-
# the file path alone: no bridge, no session, fires headless. It NEVER blocks a
|
|
13
|
-
# write: any non-match or failure exits 0. The append is idempotent and only
|
|
14
|
-
# fires while the stage is genuinely starting (the artifact is not yet a real,
|
|
15
|
-
# non-placeholder file), so re-edits add nothing.
|
|
16
|
-
#
|
|
17
|
-
# Thin CLI wrapper (intent 244): the gate logic itself lives in
|
|
18
|
-
# scripts/lib/edit_gates.rb, shared with the merged Claude dispatcher
|
|
19
|
-
# (scripts/hook-edit-gates) and, since intent 251, the merged Codex dispatcher
|
|
20
|
-
# (scripts/lib/codex_edit_gates.rb). This wrapper has no production caller on
|
|
21
|
-
# either harness anymore; it remains as the per-gate isolation surface for the
|
|
22
|
-
# hook test suite.
|
|
23
|
-
|
|
24
|
-
require_relative "lib/edit_gates"
|
|
25
|
-
|
|
26
|
-
file_path = ARGV[0]
|
|
27
|
-
exit 0 if file_path.nil? || file_path.empty?
|
|
28
|
-
|
|
29
|
-
EditGates.savepoint_pre(
|
|
30
|
-
EditGates::Context.new(tool_name: "Write", file_path: file_path, harness: "claude")
|
|
31
|
-
)
|
|
32
|
-
exit 0
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
# encoding: UTF-8
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# CodexEditGates (intent 251): the merged Codex apply_patch edit path. One
|
|
5
|
-
# process, one stdin parse, one envelope parse, five gates in-process.
|
|
6
|
-
#
|
|
7
|
-
# Before this, ~/.codex/hooks.json registered five separate PreToolUse commands
|
|
8
|
-
# on the apply_patch matcher and three of them spawned a further run_core child
|
|
9
|
-
# per file operation: eight processes per edit, ten across a full edit cycle.
|
|
10
|
-
# This is the Codex mirror of what intent 244 did for Claude (spec D-e of that
|
|
11
|
-
# intent parked it as a successor).
|
|
12
|
-
#
|
|
13
|
-
# Every DECISION here comes from scripts/lib/edit_gates.rb, the same functions
|
|
14
|
-
# scripts/hook-edit-gates drives for Claude. This file contributes exactly three
|
|
15
|
-
# Codex-specific things and nothing else:
|
|
16
|
-
# 1. the per-operation Context (the patch envelope carries a path plus ADDED
|
|
17
|
-
# lines, not Claude's file_path plus content pair),
|
|
18
|
-
# 2. the add-only rule for create-gate (Update/Delete/Move defer to the
|
|
19
|
-
# PostToolUse gate-check backstop, intent 102 spec Decision 5), and
|
|
20
|
-
# 3. the two-pass evaluation that keeps savepoint-pre's ledger append
|
|
21
|
-
# unconditional across a multi-operation patch (intent 244 spec D-c).
|
|
22
|
-
#
|
|
23
|
-
# NO SUBPROCESS MAY EVER BE ADDED TO THIS FILE. That is the whole point of the
|
|
24
|
-
# intent, and test/codex_edit_gates_test.rb asserts it against this source.
|
|
25
|
-
require "stringio"
|
|
26
|
-
require_relative "edit_gates"
|
|
27
|
-
require_relative "hook_registry"
|
|
28
|
-
|
|
29
|
-
module CodexEditGates
|
|
30
|
-
module_function
|
|
31
|
-
|
|
32
|
-
# Pass 1 and pass 2 of the two-pass evaluation (spec D3). Both slices preserve
|
|
33
|
-
# HookRegistry::CODEX_GATE_TOOLS key order, which is Claude's evaluation order
|
|
34
|
-
# (spec D5): savepoint-pre, lock-gate, code-gate, links-gate, create-gate.
|
|
35
|
-
SAVEPOINT_TOOLS = HookRegistry::CODEX_GATE_TOOLS.select { |gate, _| gate == "savepoint-pre" }.freeze
|
|
36
|
-
DENY_TOOLS = HookRegistry::CODEX_GATE_TOOLS.reject { |gate, _| gate == "savepoint-pre" }.freeze
|
|
37
|
-
|
|
38
|
-
# One Context per file operation. gate_path and file_path both carry the
|
|
39
|
-
# envelope's raw path, exactly as the five popened cores received it in ARGV[0]
|
|
40
|
-
# before the merge. content is always a String (never nil), because
|
|
41
|
-
# ApplyPatchEnvelope gives a Delete op a nil added_content and today's
|
|
42
|
-
# dispatcher already called .to_s on it at every call site. harness is "codex"
|
|
43
|
-
# so lock-gate and the claim gate render deny reasons in the $plastic-<name>
|
|
44
|
-
# form (intent 201 D2).
|
|
45
|
-
def context_for(payload, op, env: ENV)
|
|
46
|
-
session = payload["session_id"]
|
|
47
|
-
session = env["CLAUDE_CODE_SESSION_ID"] if session.to_s.empty?
|
|
48
|
-
|
|
49
|
-
EditGates::Context.new(
|
|
50
|
-
tool_name: payload["tool_name"],
|
|
51
|
-
session: (session unless session.to_s.empty?),
|
|
52
|
-
gate_path: op.path,
|
|
53
|
-
file_path: op.path,
|
|
54
|
-
savepoint_path: op.path,
|
|
55
|
-
content: op.added_content.to_s,
|
|
56
|
-
harness: "codex",
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# create-gate is a pre-write veto for ADD operations only (spec D2).
|
|
61
|
-
# EditGates.create_gate has no such rule: fed a partial Update hunk as content
|
|
62
|
-
# it would run IntentValidator on a fragment and FALSE-DENY a legitimate write.
|
|
63
|
-
def create_gate(ctx, op)
|
|
64
|
-
return EditGates::ALLOW unless op.op == :add
|
|
65
|
-
EditGates.create_gate(ctx)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def route_for(op)
|
|
69
|
-
lambda do |gate, ctx|
|
|
70
|
-
case gate
|
|
71
|
-
when "savepoint-pre" then EditGates.savepoint_pre(ctx)
|
|
72
|
-
when "lock-gate" then EditGates.lock_gate(ctx)
|
|
73
|
-
when "code-gate" then EditGates.code_gate(ctx)
|
|
74
|
-
when "links-gate" then EditGates.links_gate(ctx)
|
|
75
|
-
when "create-gate" then create_gate(ctx, op)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# True iff tool_name is a NON-EMPTY string that no CODEX_GATE_TOOLS entry
|
|
81
|
-
# matches. An empty/missing tool_name already means "the harness matched but
|
|
82
|
-
# did not name the tool" and every gate runs (EditGates.tool_applies?'s own
|
|
83
|
-
# empty-name rule); this is the OTHER unmatched case, a non-empty tool_name
|
|
84
|
-
# this dispatcher does not recognize. Checked once against "apply_patch",
|
|
85
|
-
# since every CODEX_GATE_TOOLS value is the identical single-entry list.
|
|
86
|
-
def unexpected_tool_name?(tool_name)
|
|
87
|
-
return false if tool_name.to_s.empty?
|
|
88
|
-
!EditGates.tool_applies?(tool_name, %w[apply_patch])
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Returns the process exit code. Two passes (spec D3):
|
|
92
|
-
# pass 1 runs savepoint-pre over EVERY op. It never denies, and running it
|
|
93
|
-
# before any deny check keeps its started ledger append unconditional, so
|
|
94
|
-
# op B's ledger line still lands when op A is blocked.
|
|
95
|
-
# pass 2 runs the four denying gates per op, first deny wins.
|
|
96
|
-
# EditGates.dispatch returns 0 both for "allowed" and for lock-gate's JSON
|
|
97
|
-
# deny, so pass 2 captures stdout per op: a non-empty buffer IS a deny, and it
|
|
98
|
-
# is printed and evaluation stops. Without that capture a second op could emit
|
|
99
|
-
# a second deny JSON.
|
|
100
|
-
#
|
|
101
|
-
# Codex only ever invokes this dispatcher from the apply_patch matcher, so by
|
|
102
|
-
# the time we get here the harness HAS already matched, regardless of what
|
|
103
|
-
# tool_name the payload itself carries. A non-empty, unrecognized tool_name
|
|
104
|
-
# (a future Codex rename, or a malformed/adversarial payload) must therefore
|
|
105
|
-
# never silently skip every gate: that is the coverage WEAKENING
|
|
106
|
-
# EditGates.tool_applies?'s own empty-name rule exists to prevent, just
|
|
107
|
-
# triggered from the opposite direction (an unrecognized name here, not a
|
|
108
|
-
# missing one there). Loud and safe: warn once to err and run all five gates
|
|
109
|
-
# exactly as if tool_name had been "apply_patch", rather than silent and
|
|
110
|
-
# permissive.
|
|
111
|
-
def dispatch(ops:, payload:, out: $stdout, err: $stderr)
|
|
112
|
-
if unexpected_tool_name?(payload["tool_name"])
|
|
113
|
-
err.puts "plastic codex edit-gates: unexpected tool_name #{payload["tool_name"].inspect}; " \
|
|
114
|
-
"the apply_patch matcher already selected this dispatcher, so all five gates run anyway"
|
|
115
|
-
payload = payload.merge("tool_name" => "apply_patch")
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
pairs = ops.map { |op| [op, context_for(payload, op)] }
|
|
119
|
-
|
|
120
|
-
pairs.each do |op, ctx|
|
|
121
|
-
EditGates.dispatch(ctx: ctx, route: route_for(op), gate_tools: SAVEPOINT_TOOLS,
|
|
122
|
-
out: StringIO.new, err: err)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
pairs.each do |op, ctx|
|
|
126
|
-
buffer = StringIO.new
|
|
127
|
-
code = EditGates.dispatch(ctx: ctx, route: route_for(op), gate_tools: DENY_TOOLS,
|
|
128
|
-
out: buffer, err: err)
|
|
129
|
-
return code unless code.zero?
|
|
130
|
-
next if buffer.string.empty?
|
|
131
|
-
|
|
132
|
-
out.print buffer.string
|
|
133
|
-
return 0
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
0
|
|
137
|
-
end
|
|
138
|
-
end
|