@zalom/plastic 1.9.0 → 1.11.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.
Files changed (96) hide show
  1. package/PLASTIC.md +23 -8
  2. package/README.md +16 -5
  3. package/agents/plastic-enforcer.md +3 -2
  4. package/agents/plastic-intent-discovery.md +7 -0
  5. package/bin/plastic.js +17 -8
  6. package/hooks/auto-arm +2 -2
  7. package/hooks/bash-gate +1 -1
  8. package/hooks/check-update +1 -1
  9. package/hooks/continue +2 -2
  10. package/hooks/edit-gates +1 -1
  11. package/hooks/future-intent-check +2 -2
  12. package/hooks/gate-check +3 -3
  13. package/hooks/power-tools +1 -1
  14. package/hooks/session-start +1 -1
  15. package/package.json +1 -1
  16. package/scripts/codex-hook +50 -106
  17. package/scripts/doctor.rb +132 -1080
  18. package/scripts/exec-worktree +103 -0
  19. package/scripts/hash-intent +1 -1
  20. package/scripts/hook-bash-gate +19 -0
  21. package/scripts/hook-code-gate +4 -1
  22. package/scripts/hook-continue +2 -2
  23. package/scripts/hook-create-gate +6 -3
  24. package/scripts/hook-gate-check +17 -0
  25. package/scripts/hook-links-gate +4 -1
  26. package/scripts/hook-lock-gate +7 -3
  27. package/scripts/hook-savepoint-pre +4 -1
  28. package/scripts/hook-session-start +21 -15
  29. package/scripts/lib/apply_patch_envelope.rb +46 -13
  30. package/scripts/lib/bridge.rb +83 -15
  31. package/scripts/lib/codex_edit_gates.rb +138 -0
  32. package/scripts/lib/doctor_core.rb +1087 -0
  33. package/scripts/lib/edit_gates.rb +61 -5
  34. package/scripts/lib/exec_worktree.rb +325 -0
  35. package/scripts/lib/harness_text.rb +57 -0
  36. package/scripts/lib/hook_registry.rb +32 -28
  37. package/scripts/lib/installer_core.rb +67 -7
  38. package/scripts/lib/lock.rb +196 -47
  39. package/scripts/lib/ruby_probe.rb +60 -0
  40. package/scripts/lib/scaffold_intent.rb +392 -0
  41. package/scripts/lib/spec_header.rb +83 -0
  42. package/scripts/lib/start_intent.rb +296 -0
  43. package/scripts/lib/verify_intent.rb +262 -0
  44. package/scripts/lib/worktree.rb +15 -1
  45. package/scripts/link-suggest +1 -1
  46. package/scripts/maintenance-run +5 -5
  47. package/scripts/migrate-to-global +2 -2
  48. package/scripts/restore-intent-v1 +1 -1
  49. package/scripts/scaffold-intent +120 -0
  50. package/scripts/start-intent +89 -0
  51. package/scripts/verify-intent +73 -0
  52. package/skills/agent-advisor/SKILL.md +5 -5
  53. package/skills/auto/SKILL.md +42 -32
  54. package/skills/auto/references/agent-architecture.md +1 -1
  55. package/skills/auto/references/agent-report-contract.md +1 -1
  56. package/skills/auto/references/human-report-contract.md +25 -8
  57. package/skills/auto/references/tiers.md +24 -2
  58. package/skills/conventions/references/completion-and-done.md +3 -0
  59. package/skills/conventions/references/gates-and-enforcement.md +28 -12
  60. package/skills/conventions/references/locks-and-worktrees.md +3 -3
  61. package/skills/conventions/references/tiers-and-dispatch.md +10 -9
  62. package/skills/dashboard/SKILL.md +1 -1
  63. package/skills/doctor/SKILL.md +6 -5
  64. package/skills/doctor/references/gates-stuck-detection.md +13 -8
  65. package/skills/doctor/report.md +1 -1
  66. package/skills/install/SKILL.md +1 -1
  67. package/skills/intent-brainstorming/SKILL.md +0 -2
  68. package/skills/intent-creating/SKILL.md +6 -6
  69. package/skills/intent-creating/references/lifecycle.md +1 -1
  70. package/skills/intent-discovering/SKILL.md +10 -3
  71. package/skills/intent-ending/SKILL.md +8 -7
  72. package/skills/intent-executing/SKILL.md +27 -19
  73. package/skills/intent-grilling/SKILL.md +5 -3
  74. package/skills/intent-planning/SKILL.md +7 -3
  75. package/skills/intent-researching/SKILL.md +0 -2
  76. package/skills/intent-starting/SKILL.md +10 -2
  77. package/skills/project-creating/SKILL.md +0 -2
  78. package/skills/project-creating/references/project-scaffolding.md +1 -1
  79. package/skills/releasing/SKILL.md +1 -1
  80. package/skills/releasing/references/promotion-and-tagging.md +14 -8
  81. package/skills/releasing/references/release-lines.md +1 -1
  82. package/skills/skill-creating/SKILL.md +5 -2
  83. package/skills/store-indexing/SKILL.md +8 -5
  84. package/skills/store-indexing/references/zettelkasten-linking.md +1 -1
  85. package/skills/tutorial/references/track-1-guided.md +2 -2
  86. package/skills/tutorial/references/track-2-auto.md +9 -6
  87. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +1 -1
  88. package/skills/uninstall/SKILL.md +6 -9
  89. package/templates/agents.md +12 -12
  90. package/templates/config.yml +6 -7
  91. package/templates/index.md +6 -3
  92. package/templates/spec.md +1 -1
  93. package/skills/humanizer/SKILL.md +0 -40
  94. package/skills/humanizer/evals/evals.json +0 -70
  95. package/skills/humanizer/references/always-on-snippet.md +0 -9
  96. package/skills/humanizer/references/examples.md +0 -48
@@ -0,0 +1,138 @@
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