@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.
- package/PLASTIC.md +23 -8
- package/README.md +16 -5
- package/agents/plastic-enforcer.md +3 -2
- package/agents/plastic-intent-discovery.md +7 -0
- package/bin/plastic.js +17 -8
- package/hooks/auto-arm +2 -2
- package/hooks/bash-gate +1 -1
- package/hooks/check-update +1 -1
- package/hooks/continue +2 -2
- package/hooks/edit-gates +1 -1
- package/hooks/future-intent-check +2 -2
- package/hooks/gate-check +3 -3
- package/hooks/power-tools +1 -1
- package/hooks/session-start +1 -1
- package/package.json +1 -1
- package/scripts/codex-hook +50 -106
- package/scripts/doctor.rb +132 -1080
- package/scripts/exec-worktree +103 -0
- package/scripts/hash-intent +1 -1
- package/scripts/hook-bash-gate +19 -0
- package/scripts/hook-code-gate +4 -1
- package/scripts/hook-continue +2 -2
- package/scripts/hook-create-gate +6 -3
- package/scripts/hook-gate-check +17 -0
- package/scripts/hook-links-gate +4 -1
- package/scripts/hook-lock-gate +7 -3
- package/scripts/hook-savepoint-pre +4 -1
- package/scripts/hook-session-start +21 -15
- package/scripts/lib/apply_patch_envelope.rb +46 -13
- package/scripts/lib/bridge.rb +83 -15
- package/scripts/lib/codex_edit_gates.rb +138 -0
- package/scripts/lib/doctor_core.rb +1087 -0
- package/scripts/lib/edit_gates.rb +61 -5
- package/scripts/lib/exec_worktree.rb +325 -0
- package/scripts/lib/harness_text.rb +57 -0
- package/scripts/lib/hook_registry.rb +32 -28
- package/scripts/lib/installer_core.rb +67 -7
- package/scripts/lib/lock.rb +196 -47
- package/scripts/lib/ruby_probe.rb +60 -0
- package/scripts/lib/scaffold_intent.rb +392 -0
- package/scripts/lib/spec_header.rb +83 -0
- package/scripts/lib/start_intent.rb +296 -0
- package/scripts/lib/verify_intent.rb +262 -0
- package/scripts/lib/worktree.rb +15 -1
- package/scripts/link-suggest +1 -1
- package/scripts/maintenance-run +5 -5
- package/scripts/migrate-to-global +2 -2
- package/scripts/restore-intent-v1 +1 -1
- package/scripts/scaffold-intent +120 -0
- package/scripts/start-intent +89 -0
- package/scripts/verify-intent +73 -0
- package/skills/agent-advisor/SKILL.md +5 -5
- package/skills/auto/SKILL.md +42 -32
- package/skills/auto/references/agent-architecture.md +1 -1
- package/skills/auto/references/agent-report-contract.md +1 -1
- package/skills/auto/references/human-report-contract.md +25 -8
- package/skills/auto/references/tiers.md +24 -2
- package/skills/conventions/references/completion-and-done.md +3 -0
- package/skills/conventions/references/gates-and-enforcement.md +28 -12
- package/skills/conventions/references/locks-and-worktrees.md +3 -3
- package/skills/conventions/references/tiers-and-dispatch.md +10 -9
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/doctor/SKILL.md +6 -5
- package/skills/doctor/references/gates-stuck-detection.md +13 -8
- package/skills/doctor/report.md +1 -1
- package/skills/install/SKILL.md +1 -1
- package/skills/intent-brainstorming/SKILL.md +0 -2
- package/skills/intent-creating/SKILL.md +6 -6
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-discovering/SKILL.md +10 -3
- package/skills/intent-ending/SKILL.md +8 -7
- package/skills/intent-executing/SKILL.md +27 -19
- package/skills/intent-grilling/SKILL.md +5 -3
- package/skills/intent-planning/SKILL.md +7 -3
- package/skills/intent-researching/SKILL.md +0 -2
- package/skills/intent-starting/SKILL.md +10 -2
- package/skills/project-creating/SKILL.md +0 -2
- package/skills/project-creating/references/project-scaffolding.md +1 -1
- package/skills/releasing/SKILL.md +1 -1
- package/skills/releasing/references/promotion-and-tagging.md +14 -8
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/skill-creating/SKILL.md +5 -2
- package/skills/store-indexing/SKILL.md +8 -5
- package/skills/store-indexing/references/zettelkasten-linking.md +1 -1
- package/skills/tutorial/references/track-1-guided.md +2 -2
- package/skills/tutorial/references/track-2-auto.md +9 -6
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +1 -1
- package/skills/uninstall/SKILL.md +6 -9
- package/templates/agents.md +12 -12
- package/templates/config.yml +6 -7
- package/templates/index.md +6 -3
- package/templates/spec.md +1 -1
- package/skills/humanizer/SKILL.md +0 -40
- package/skills/humanizer/evals/evals.json +0 -70
- package/skills/humanizer/references/always-on-snippet.md +0 -9
- package/skills/humanizer/references/examples.md +0 -48
package/PLASTIC.md
CHANGED
|
@@ -31,7 +31,7 @@ store/
|
|
|
31
31
|
spec.md # optional - specification (Why deliverable)
|
|
32
32
|
plan.md # optional - implementation plan (How deliverable)
|
|
33
33
|
checklist.md # optional - execution registry (How deliverable)
|
|
34
|
-
outcome.md #
|
|
34
|
+
outcome.md # detailed result (Exec deliverable) - mandatory at a terminal (Completed and Abandoned alike)
|
|
35
35
|
actions/ # optional - real action files (How deliverable, at least one required)
|
|
36
36
|
resources/ # optional - research, references, screenshots, diagrams
|
|
37
37
|
savepoint.md # optional - deterministic cycle-step ledger (auto-written)
|
|
@@ -61,7 +61,7 @@ tags: [plastic, architecture]
|
|
|
61
61
|
|
|
62
62
|
- Context contract: load `sources` strongly (they are what the intent was built from);
|
|
63
63
|
traverse `chain` lightly for discovery. See
|
|
64
|
-
docs/concepts/how-plastic-sources-and-chains-intents.md for the full model.
|
|
64
|
+
[`how-plastic-sources-and-chains-intents.md`](https://github.com/zalom/plastic/blob/main/docs/concepts/how-plastic-sources-and-chains-intents.md) for the full model.
|
|
65
65
|
- IDs use Luhmann's alternating convention: `1` → `1a` → `1a1` → `1a1a`
|
|
66
66
|
- Multiple branches increment: `1a`, `1b`, `1c`
|
|
67
67
|
|
|
@@ -73,7 +73,7 @@ of influence (sources/chain/tags) and the `## Links` projection rules.
|
|
|
73
73
|
| Stage | Section | Deliverable | Detail |
|
|
74
74
|
|-------|---------|-------------|--------|
|
|
75
75
|
| **What** | `## Intent` | `{ID}--{slug}.md` | `plastic-intent-creating` |
|
|
76
|
-
| **Why** | `## Context` + Decisions | `spec.md` | `plastic-intent-brainstorming` |
|
|
76
|
+
| **Why** | `## Context` + Decisions | `spec.md` | `plastic-intent-brainstorming` enriches Context/Decisions; `plastic-intent-speccing` writes `spec.md` |
|
|
77
77
|
| **How** | Planning | `plan.md` + `actions/ACTION_N.md` (at least one) + `checklist.md` | `plastic-intent-planning` |
|
|
78
78
|
| **Exec** | Execution | `outcome.md` | `plastic-intent-executing` |
|
|
79
79
|
|
|
@@ -113,6 +113,13 @@ M = one subsystem (about a day); L = cross-cutting or novel design.
|
|
|
113
113
|
Tier is recorded as a `Tier: S|M|L` line at the top of spec.md. It is convention-only,
|
|
114
114
|
read by the orchestrator, not enforced by any gate or by doctor.
|
|
115
115
|
|
|
116
|
+
Settledness is recorded separately, as a `Settled: yes (<reason>)` line directly beneath the
|
|
117
|
+
`Tier:` line, above the `# Spec:` heading. It records that a design has already done its
|
|
118
|
+
thinking, so a later stage can trust it without deriving it again. An absent line means not
|
|
119
|
+
settled; there is no `Settled: no` form. Settledness and scope are independent, so a large
|
|
120
|
+
intent can be settled and never becomes a smaller tier. `scripts/lib/spec_header.rb` is the
|
|
121
|
+
only parser of both lines; nothing else reads the grammar.
|
|
122
|
+
|
|
116
123
|
See `plastic-conventions > references/tiers-and-dispatch.md` for topology by tier, what never
|
|
117
124
|
gets cut, and guided mode.
|
|
118
125
|
|
|
@@ -194,7 +201,7 @@ Format: `ID--three-to-five-words` (all stores).
|
|
|
194
201
|
- Root intents: sequential numbers (`1`, `2`, `3`)
|
|
195
202
|
- Branches alternate: `1` → `1a` → `1a1` → `1a1a`
|
|
196
203
|
- Intent file matches directory: `1a1--slug/1a1--slug.md`
|
|
197
|
-
- Next ID: `
|
|
204
|
+
- Next ID: `ruby ~/.plastic/scripts/folgezettel-id <parent_id> <store_path>`
|
|
198
205
|
|
|
199
206
|
See `plastic-conventions > references/knowledge-graph.md` for the branch-vs-root semantic
|
|
200
207
|
decision: when to branch versus start a root.
|
|
@@ -286,12 +293,20 @@ One line each. On Claude the five edit-path gates (savepoint-pre, lock-gate, cod
|
|
|
286
293
|
links-gate, create-gate) run inside one dispatcher process per Write or Edit, in that fixed
|
|
287
294
|
order with the first deny winning; what each gate checks is unchanged.
|
|
288
295
|
|
|
289
|
-
- **
|
|
290
|
-
|
|
296
|
+
- **savepoint-pre** appends the savepoint ledger line before a write into an intent dir; it
|
|
297
|
+
records and never denies.
|
|
291
298
|
- **lock-gate** admits only the intent's lock owner or a registered delegate to write into an
|
|
292
299
|
active intent directory.
|
|
293
|
-
- **
|
|
294
|
-
|
|
300
|
+
- **code-gate** denies a code edit before How is delivered and a code edit outside the
|
|
301
|
+
provisioned worktree (stage rule or worktree rule, first match wins), with the audited
|
|
302
|
+
`# plastic-ok` escape.
|
|
303
|
+
- **links-gate** enforces the derived `## Links` projection contract at write time.
|
|
304
|
+
- **create-gate** validates the proposed intent file at What write-time.
|
|
305
|
+
- **bash-gate** (on the Bash matcher) intercepts a write attempted through a bash or
|
|
306
|
+
interpreter one-liner, with the same audited escape.
|
|
307
|
+
- **gate-check** (PostToolUse) enforces lifecycle stage order after each write.
|
|
308
|
+
- **future-intent-check** (UserPromptSubmit) surfaces parked future intents whose keywords
|
|
309
|
+
match the user's message; it informs and never denies.
|
|
295
310
|
|
|
296
311
|
See `plastic-conventions > references/gates-and-enforcement.md` for the escape and logging
|
|
297
312
|
detail.
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ It solves problems that builder already feels:
|
|
|
63
63
|
One command installs Plastic for your agent:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
npx @zalom/plastic --claude
|
|
66
|
+
npx -y @zalom/plastic install --claude
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Then load the conventions and begin:
|
|
@@ -139,7 +139,7 @@ Walk this once in
|
|
|
139
139
|
| Intent | `plastic-continuing`, `plastic-intent-creating`, `plastic-intent-starting`, `plastic-intent-brainstorming`, `plastic-intent-grilling`, `plastic-intent-speccing`, `plastic-intent-planning`, `plastic-intent-executing`, `plastic-intent-ending`, `plastic-intent-continuing`, `plastic-intent-researching` |
|
|
140
140
|
| Project and delivery | `plastic-project-creating`, `plastic-project-continuing`, `plastic-roadmap`, `plastic-roadmap-continuing`, `plastic-releasing` |
|
|
141
141
|
| Skill | `plastic-skill-creating`, `plastic-skill-evaluating` |
|
|
142
|
-
| Product | `plastic-install`, `plastic-update`, `plastic-uninstall`, `plastic-rollback`, `plastic-doctor
|
|
142
|
+
| Product | `plastic-install`, `plastic-update`, `plastic-uninstall`, `plastic-rollback`, `plastic-doctor` |
|
|
143
143
|
|
|
144
144
|
See [`docs/guides/index.md`](docs/guides/index.md) for task-oriented walkthroughs.
|
|
145
145
|
|
|
@@ -149,7 +149,8 @@ plan a machine can build from exactly. Read
|
|
|
149
149
|
|
|
150
150
|
## Compatibility and ownership
|
|
151
151
|
|
|
152
|
-
- Native installers for Claude
|
|
152
|
+
- Native installers for Claude Code and Codex CLI, both from npm. Hermes is a packaging
|
|
153
|
+
target for now. See [harness support](docs/reference/harness-adapters.md).
|
|
153
154
|
- Plain Markdown plus Git. The work stays in files you own.
|
|
154
155
|
- Personal stores by default.
|
|
155
156
|
- Guided delivery with a human at every gate, or autonomous delivery when you
|
|
@@ -160,8 +161,18 @@ plan a machine can build from exactly. Read
|
|
|
160
161
|
the frontier reasoning discipline inline, much cheaper; Fable 5 is the frontier model
|
|
161
162
|
itself. Choose the default at install, switch it any time.
|
|
162
163
|
|
|
163
|
-
Plastic needs Ruby
|
|
164
|
-
|
|
164
|
+
Plastic needs Ruby 3.0 or later and Node.js 18 or later. Most Linux systems already
|
|
165
|
+
have a new enough Ruby. macOS ships Ruby 2.6, which is too old, so a clean Mac needs
|
|
166
|
+
a newer one first:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
curl https://mise.run | sh # only if mise is not installed yet
|
|
170
|
+
mise use --global ruby@3.3
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The installer checks this before it does anything and tells you the same thing if the
|
|
174
|
+
Ruby it finds is too old. Bun users can run `bunx` in place of `npx`; Bun is never
|
|
175
|
+
required.
|
|
165
176
|
|
|
166
177
|
## Built with Plastic
|
|
167
178
|
|
|
@@ -68,13 +68,14 @@ ceremony, never these guarantees.
|
|
|
68
68
|
1. Arm the gate, then dispatch the brainstorming specialist; gate its `## Context` + `### Decisions`
|
|
69
69
|
2. Dispatch the spec-specialist; gate `spec.md`. Then the planner; gate `plan.md` + `checklist.md`
|
|
70
70
|
3. Dispatch the executor; require a green suite. Sequential, one team per intent, on one branch when files are shared
|
|
71
|
-
4. Dispatch and review by default through Plastic's native engine, `plastic-intent-executing` (implementer plus two-stage review
|
|
71
|
+
4. Dispatch and review by default through Plastic's native engine, `plastic-intent-executing` (no external plugin): at S and M, one executor dispatch for the whole consolidated action; at L, implementer plus two-stage review per task. If `superpowers:subagent-driven-development` and `superpowers:dispatching-parallel-agents` are available, or the user asks for them, delegate to them as an enhancement
|
|
72
72
|
5. At the final gate, dispatch an independent reviewer subagent, then complete the intent
|
|
73
73
|
|
|
74
74
|
## Human-facing stage reporting
|
|
75
75
|
|
|
76
76
|
At each gate, the orchestrator briefs the human in EM-to-CTO voice: impact first, the one risk
|
|
77
|
-
that matters, then the decision left to them.
|
|
77
|
+
that matters, then the decision left to them. This is the depth at M and L; at S the briefing
|
|
78
|
+
fires once, at How. The shape and per-stage content live in
|
|
78
79
|
`skills/auto/references/human-report-contract.md`; follow it rather than improvising a report.
|
|
79
80
|
This is separate from the intent 74 report contract (`skills/auto/references/agent-report-contract.md`),
|
|
80
81
|
which is the internal, structured handoff a dispatched specialist sends back to the orchestrator.
|
|
@@ -11,6 +11,13 @@ activation, after the lock is armed and before Why begins, under the lock as
|
|
|
11
11
|
the owner session, you gather the context that already exists and deposit it
|
|
12
12
|
for the Why stage to consume.
|
|
13
13
|
|
|
14
|
+
## Skip precondition (Tier S only)
|
|
15
|
+
If a `Tier: S` line is already stamped at the top of `spec.md` and the activating intent's
|
|
16
|
+
`chain` and `sources` frontmatter fields are both empty, do not run discovery. Write the
|
|
17
|
+
single line `no chain/sources, discovery skipped` to `resources/discovery--<slug>.md` and
|
|
18
|
+
stop. Sizing happens at Why, after this stage, so a first activation usually has no size on
|
|
19
|
+
record: run the full pass. Never guess a size to unlock the skip.
|
|
20
|
+
|
|
14
21
|
## Responsibilities
|
|
15
22
|
1. **Read the intent's links.** Load the activating intent file's `chain` and
|
|
16
23
|
`sources` frontmatter fields.
|
package/bin/plastic.js
CHANGED
|
@@ -46,16 +46,25 @@ if (!existsSync(script)) {
|
|
|
46
46
|
try {
|
|
47
47
|
execFileSync('ruby', [script, ...rest], {
|
|
48
48
|
stdio: 'inherit',
|
|
49
|
-
|
|
49
|
+
// Clear RUBYOPT last, after the spread, so a global setting (for example --yjit)
|
|
50
|
+
// cannot reach a ruby that does not know the flag. A machine with an old ruby
|
|
51
|
+
// must get preflight's real message, not a crash.
|
|
52
|
+
env: { ...process.env, PLASTIC_PACKAGE_ROOT: packageRoot, RUBYOPT: '' },
|
|
50
53
|
})
|
|
51
54
|
} catch (err) {
|
|
52
55
|
if (err.status) process.exit(err.status)
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
// We only get here when ruby could not be executed at all. A ruby that runs but is
|
|
57
|
+
// too old exits with a status, handled on the line above, and prints its own real
|
|
58
|
+
// message from scripts/lib/preflight.rb naming the version actually found.
|
|
59
|
+
if (err.code === 'ENOENT') {
|
|
60
|
+
console.error('Plastic needs Ruby 3.0.0 or newer to run its scripts (ruby was not found on PATH).')
|
|
61
|
+
console.error('Install a pinned Ruby with mise:')
|
|
62
|
+
console.error(' curl https://mise.run | sh # only if mise is not installed yet')
|
|
63
|
+
console.error(' mise use --global ruby@3.3')
|
|
64
|
+
console.error('Then re-run the Plastic installer.')
|
|
65
|
+
} else {
|
|
66
|
+
console.error(`Plastic could not run ruby: ${err.message}`)
|
|
67
|
+
console.error('Check that `ruby -v` works in this shell, then re-run the Plastic installer.')
|
|
68
|
+
}
|
|
60
69
|
process.exit(1)
|
|
61
70
|
}
|
package/hooks/auto-arm
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
INPUT=$(cat)
|
|
3
|
-
MESSAGE=$(echo "$INPUT" | ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"].to_s' 2>/dev/null || echo "")
|
|
3
|
+
MESSAGE=$(echo "$INPUT" | env -u RUBYOPT ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"].to_s' 2>/dev/null || echo "")
|
|
4
4
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
ruby "$SCRIPT_DIR/../scripts/hook-auto-arm" "$MESSAGE"
|
|
5
|
+
env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-auto-arm" "$MESSAGE"
|
package/hooks/bash-gate
CHANGED
package/hooks/check-update
CHANGED
|
@@ -31,7 +31,7 @@ fi
|
|
|
31
31
|
exit 0
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
|
-
TARGET=$(printf '%s' "$TAGS" | ruby "$SELECTOR" "$CURRENT" 2>/dev/null)
|
|
34
|
+
TARGET=$(printf '%s' "$TAGS" | env -u RUBYOPT ruby "$SELECTOR" "$CURRENT" 2>/dev/null)
|
|
35
35
|
CHECKED=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
36
36
|
|
|
37
37
|
if [ -n "$TARGET" ]; then
|
package/hooks/continue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
INPUT=$(cat)
|
|
3
|
-
MESSAGE=$(echo "$INPUT" | ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"]' 2>/dev/null || echo "")
|
|
3
|
+
MESSAGE=$(echo "$INPUT" | env -u RUBYOPT ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"]' 2>/dev/null || echo "")
|
|
4
4
|
|
|
5
5
|
if echo "$MESSAGE" | grep -iq '\bcontinue\b'; then
|
|
6
6
|
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
@@ -14,7 +14,7 @@ if echo "$MESSAGE" | grep -iq '\bcontinue\b'; then
|
|
|
14
14
|
MODE="global"
|
|
15
15
|
|
|
16
16
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
-
OUTPUT=$(ruby "$SCRIPT_DIR/../scripts/hook-continue" "$INDEX" "$STORE_ROOT" "$MODE" 2>/dev/null)
|
|
17
|
+
OUTPUT=$(env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-continue" "$INDEX" "$STORE_ROOT" "$MODE" 2>/dev/null)
|
|
18
18
|
|
|
19
19
|
if [ -n "$OUTPUT" ]; then
|
|
20
20
|
echo "$OUTPUT"
|
package/hooks/edit-gates
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
INPUT=$(cat)
|
|
3
|
-
MESSAGE=$(echo "$INPUT" | ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"].downcase' 2>/dev/null || echo "")
|
|
3
|
+
MESSAGE=$(echo "$INPUT" | env -u RUBYOPT ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"].downcase' 2>/dev/null || echo "")
|
|
4
4
|
|
|
5
5
|
if [ ${#MESSAGE} -lt 10 ]; then
|
|
6
6
|
exit 0
|
|
@@ -22,4 +22,4 @@ else
|
|
|
22
22
|
fi
|
|
23
23
|
|
|
24
24
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
-
ruby "$SCRIPT_DIR/../scripts/hook-future-intent-check" "$STORE_ROOT" "$MESSAGE"
|
|
25
|
+
env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-future-intent-check" "$STORE_ROOT" "$MESSAGE"
|
package/hooks/gate-check
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
INPUT=$(cat)
|
|
3
|
-
FILE_PATH=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("tool_params", "file_path") || data.dig("tool_input", "file_path") || ""' 2>/dev/null)
|
|
3
|
+
FILE_PATH=$(echo "$INPUT" | env -u RUBYOPT ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("tool_params", "file_path") || data.dig("tool_input", "file_path") || ""' 2>/dev/null)
|
|
4
4
|
|
|
5
5
|
if [ -z "$FILE_PATH" ]; then
|
|
6
6
|
exit 0
|
|
7
7
|
fi
|
|
8
8
|
|
|
9
|
-
SESSION_ID=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("session_id") || ""' 2>/dev/null)
|
|
9
|
+
SESSION_ID=$(echo "$INPUT" | env -u RUBYOPT ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("session_id") || ""' 2>/dev/null)
|
|
10
10
|
|
|
11
11
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
-
ruby "$SCRIPT_DIR/../scripts/hook-gate-check" "$FILE_PATH" "$SESSION_ID"
|
|
12
|
+
env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-gate-check" "$FILE_PATH" "$SESSION_ID"
|
package/hooks/power-tools
CHANGED
package/hooks/session-start
CHANGED
|
@@ -6,4 +6,4 @@ if [ ! -f "$GLOBAL_INDEX" ]; then
|
|
|
6
6
|
fi
|
|
7
7
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
-
ruby "$SCRIPT_DIR/../scripts/hook-session-start" "$GLOBAL_INDEX" "$HOME/.plastic" "global" "${CLAUDE_PLUGIN_ROOT:-}"
|
|
9
|
+
env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-session-start" "$GLOBAL_INDEX" "$HOME/.plastic" "global" "${CLAUDE_PLUGIN_ROOT:-}"
|
package/package.json
CHANGED
package/scripts/codex-hook
CHANGED
|
@@ -3,34 +3,37 @@
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
#
|
|
5
5
|
# Usage: codex-hook <gate>
|
|
6
|
-
# file-mutation gates:
|
|
6
|
+
# file-mutation gates: edit-gates | gate-check
|
|
7
7
|
# live-state hooks (intent 199): session-start | check-update | continue |
|
|
8
8
|
# future-intent-check | auto-arm | power-tools | savepoint
|
|
9
9
|
# shell-tool gate (intent 203): bash-gate
|
|
10
10
|
#
|
|
11
|
-
# The Codex input adapter (intent 102, extended by 199
|
|
12
|
-
# hook stdin payload once. Three shapes exist and the gate
|
|
13
|
-
# which path runs (Codex's own hooks.json registration
|
|
14
|
-
# cross): the
|
|
15
|
-
# apply_patch command in tool_input.command [guide Part 4],
|
|
16
|
-
# envelope via ApplyPatchEnvelope, and
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# the
|
|
33
|
-
#
|
|
11
|
+
# The Codex input adapter (intent 102, extended by 199, 203, and collapsed by
|
|
12
|
+
# 251). Reads a Codex hook stdin payload once. Three shapes exist and the gate
|
|
13
|
+
# name alone selects which path runs (Codex's own hooks.json registration
|
|
14
|
+
# guarantees they never cross): the apply_patch path reads session_id at top
|
|
15
|
+
# level and the apply_patch command in tool_input.command [guide Part 4],
|
|
16
|
+
# parses the diff envelope once via ApplyPatchEnvelope, and runs all five gates
|
|
17
|
+
# (savepoint-pre, lock-gate, code-gate, links-gate, create-gate) IN-PROCESS
|
|
18
|
+
# through scripts/lib/codex_edit_gates.rb, which drives the same
|
|
19
|
+
# scripts/lib/edit_gates.rb functions Claude's merged dispatcher drives
|
|
20
|
+
# (mirroring intent 244). This replaced five registered PreToolUse commands and
|
|
21
|
+
# three nested run_core children with one process. The live-state hooks carry
|
|
22
|
+
# no tool_input at all (they are not tool calls); the dispatcher execs the SAME
|
|
23
|
+
# launcher file Claude already runs for that hook (hooks/<name>), which is
|
|
24
|
+
# already harness-agnostic (resolves ~/.plastic off $HOME, reads only the
|
|
25
|
+
# common stdin fields the guide confirms Codex shares with Claude for these
|
|
26
|
+
# three events), so the body is reused, not rewritten (D3). The shell-tool gate
|
|
27
|
+
# (bash-gate) reports tool_name: "Bash" with the command in tool_input.command,
|
|
28
|
+
# but MUST NOT be routed through ApplyPatchEnvelope.parse below: a plain shell
|
|
29
|
+
# command has no *** Begin Patch/*** End Patch envelope, so it would parse to
|
|
30
|
+
# an empty op list and hit this file's own `exit 0 if ops.empty?` line,
|
|
31
|
+
# silently allowing every Bash call and reopening the exact hole intent 203
|
|
32
|
+
# closes. So it execs the SAME scripts/hook-bash-gate file Claude already runs,
|
|
33
|
+
# unmodified, the identical "drive the body, relay its output" pattern used for
|
|
34
|
+
# the live-state hooks. The one adaptation shared by both the live-state and
|
|
35
|
+
# shell-tool paths is threading the payload's session_id into
|
|
36
|
+
# CLAUDE_CODE_SESSION_ID, since Codex's own process env never carries it, plus
|
|
34
37
|
# a bounded timeout: hooks/check-update backgrounds a real npm network call
|
|
35
38
|
# without redirecting its output away from the inherited stdout/stderr pipes,
|
|
36
39
|
# and Codex invokes hooks synchronously, so without a bound a slow network call
|
|
@@ -39,9 +42,6 @@ require "json"
|
|
|
39
42
|
require "open3"
|
|
40
43
|
require "rbconfig"
|
|
41
44
|
require "timeout"
|
|
42
|
-
require_relative "lib/apply_patch_envelope"
|
|
43
|
-
require_relative "lib/intent_validator"
|
|
44
|
-
require_relative "lib/links_gate"
|
|
45
45
|
|
|
46
46
|
# Sync stdout so a relayed launcher's stdout and stderr land on the merged
|
|
47
47
|
# pipe in call order. Ruby buffers STDOUT (but not STDERR) when it is not a
|
|
@@ -68,7 +68,7 @@ if STATE_HOOKS.include?(gate)
|
|
|
68
68
|
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
69
69
|
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
70
70
|
out, err, status = begin
|
|
71
|
-
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env, launcher, stdin_data: raw, chdir: cwd) }
|
|
71
|
+
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env.merge("RUBYOPT" => nil), launcher, stdin_data: raw, chdir: cwd) }
|
|
72
72
|
rescue StandardError
|
|
73
73
|
["", "", nil] # fail open: launcher missing, unexecutable, crashed, or too slow
|
|
74
74
|
end
|
|
@@ -88,7 +88,7 @@ if SHELL_HOOKS.include?(gate)
|
|
|
88
88
|
script = File.join(__dir__, "hook-#{gate}")
|
|
89
89
|
argv = [script]
|
|
90
90
|
out, err, status = begin
|
|
91
|
-
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env, RbConfig.ruby, *argv, stdin_data: raw, chdir: cwd) }
|
|
91
|
+
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env.merge("RUBYOPT" => nil), RbConfig.ruby, *argv, stdin_data: raw, chdir: cwd) }
|
|
92
92
|
rescue StandardError
|
|
93
93
|
["", "", nil] # fail open: script missing, unexecutable, crashed, or too slow
|
|
94
94
|
end
|
|
@@ -97,6 +97,8 @@ if SHELL_HOOKS.include?(gate)
|
|
|
97
97
|
exit(status ? status.exitstatus : 0)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
require_relative "lib/apply_patch_envelope"
|
|
101
|
+
|
|
100
102
|
command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
|
|
101
103
|
exit 0 if command.nil? # fail-open: no tool_input at all (unrecognized gate name or non-tool-call payload)
|
|
102
104
|
ops = ApplyPatchEnvelope.parse(command)
|
|
@@ -104,54 +106,30 @@ exit 0 if ops.empty? # fail-open: nothing parseable to gate
|
|
|
104
106
|
|
|
105
107
|
CORES = __dir__ # ~/.plastic/scripts
|
|
106
108
|
def run_core(name, *argv)
|
|
107
|
-
out = IO.popen([RbConfig.ruby, File.join(CORES, name), *argv], "r", err: [:child, :out], &:read)
|
|
109
|
+
out = IO.popen([{ "RUBYOPT" => nil }, RbConfig.ruby, File.join(CORES, name), *argv], "r", err: [:child, :out], &:read)
|
|
108
110
|
[out, $?.exitstatus]
|
|
109
111
|
end
|
|
110
112
|
|
|
111
|
-
def intent_file?(path)
|
|
112
|
-
abs = File.expand_path(path)
|
|
113
|
-
dir = File.dirname(abs)
|
|
114
|
-
dir.match?(%r{/store/[^/]+--[^/]+\z}) && File.basename(abs) == "#{File.basename(dir)}.md"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
113
|
case gate
|
|
118
|
-
when "
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# slash.
|
|
137
|
-
ops.each do |o|
|
|
138
|
-
out, _ = run_core("hook-lock-gate", o.path, session, "codex")
|
|
139
|
-
if out.include?('"permissionDecision":"deny"') || out.include?('"permissionDecision": "deny"')
|
|
140
|
-
print out
|
|
141
|
-
exit 0
|
|
142
|
-
end
|
|
114
|
+
when "edit-gates"
|
|
115
|
+
# The ONE registered PreToolUse command on the apply_patch matcher (intent
|
|
116
|
+
# 251). Five gates, in-process, in Claude's evaluation order, first deny wins,
|
|
117
|
+
# each deny shape reproduced exactly: stderr plus exit 2 for code-gate,
|
|
118
|
+
# links-gate and create-gate; stdout permissionDecision JSON plus exit 0 for
|
|
119
|
+
# lock-gate. Fail-open at both levels, per gate inside EditGates.dispatch and
|
|
120
|
+
# again at this file's own top level.
|
|
121
|
+
begin
|
|
122
|
+
require_relative "lib/codex_edit_gates"
|
|
123
|
+
exit CodexEditGates.dispatch(ops: ops, payload: payload)
|
|
124
|
+
rescue StandardError, ScriptError => e
|
|
125
|
+
# Dispatcher-internal failure: never impersonate a deny. ScriptError (a
|
|
126
|
+
# sibling of StandardError, not a subclass) covers LoadError: a future
|
|
127
|
+
# manifest gap (a lib file required here but not distributed, exactly the
|
|
128
|
+
# bug the full suite caught during this intent's own delivery) must fail
|
|
129
|
+
# open with a gate decision, never exit 1 with no decision at all.
|
|
130
|
+
$stderr.puts "plastic codex edit-gates error: #{e.message}"
|
|
131
|
+
exit 0
|
|
143
132
|
end
|
|
144
|
-
exit 0
|
|
145
|
-
|
|
146
|
-
when "code-gate"
|
|
147
|
-
ops.each do |o|
|
|
148
|
-
out, code = run_core("hook-code-gate", o.path, session, o.added_content.to_s)
|
|
149
|
-
if code == 2
|
|
150
|
-
$stderr.print out
|
|
151
|
-
exit 2
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
exit 0
|
|
155
133
|
|
|
156
134
|
when "gate-check"
|
|
157
135
|
last_allow = nil
|
|
@@ -166,40 +144,6 @@ when "gate-check"
|
|
|
166
144
|
print last_allow if last_allow
|
|
167
145
|
exit 0
|
|
168
146
|
|
|
169
|
-
when "savepoint-pre"
|
|
170
|
-
ops.each { |o| run_core("hook-savepoint-pre", o.path) }
|
|
171
|
-
exit 0
|
|
172
|
-
|
|
173
|
-
when "links-gate"
|
|
174
|
-
# Write-time belt for the PLASTIC.md ## Links contract (intent 192), reusing
|
|
175
|
-
# the SAME LinksGate.decision Claude's hook-links-gate already drives, so
|
|
176
|
-
# both harnesses share one decision function. before_content is the real
|
|
177
|
-
# on-disk file (File.read, or "" when it does not yet exist); after_content
|
|
178
|
-
# is the proposed result, o.added_content.
|
|
179
|
-
#
|
|
180
|
-
# DISCLOSED, NARROWER judgment than Claude's version (intent 198, Decision
|
|
181
|
-
# D3): ApplyPatchEnvelope.parse only ever captures a diff's ADDED lines,
|
|
182
|
-
# never its removed or context lines, so for an Update whose diff is a
|
|
183
|
-
# partial hunk rather than a full-file rewrite, after_content may not be the
|
|
184
|
-
# complete proposed file. This is the same class of disclosed limitation
|
|
185
|
-
# create-gate already carries for Update/Delete/Move (best effort, never a
|
|
186
|
-
# hard crash). links-gate has no PostToolUse backstop to defer to on Claude
|
|
187
|
-
# either, so running this best-effort compare on Update ops too is strictly
|
|
188
|
-
# more coverage than the prior total fail-open.
|
|
189
|
-
ops.each do |o|
|
|
190
|
-
next unless intent_file?(o.path)
|
|
191
|
-
|
|
192
|
-
before_content = File.exist?(o.path) ? File.read(o.path) : ""
|
|
193
|
-
plastic_home = ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
|
|
194
|
-
reason = LinksGate.decision(file_path: o.path, before_content: before_content,
|
|
195
|
-
after_content: o.added_content.to_s, plastic_home: plastic_home)
|
|
196
|
-
next unless reason
|
|
197
|
-
|
|
198
|
-
$stderr.puts reason
|
|
199
|
-
exit 2
|
|
200
|
-
end
|
|
201
|
-
exit 0
|
|
202
|
-
|
|
203
147
|
else
|
|
204
148
|
exit 0
|
|
205
149
|
end
|