@zalom/plastic 1.8.0 → 1.10.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 +94 -556
- package/README.md +18 -6
- package/agents/plastic-enforcer.md +3 -2
- package/agents/plastic-intent-curator.md +2 -2
- package/agents/plastic-intent-discovery.md +7 -0
- package/bin/plastic.js +17 -8
- package/deprecations.yml +10 -2
- 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 +3 -0
- package/hooks/future-intent-check +2 -2
- package/hooks/gate-check +3 -3
- package/hooks/hooks.json +4 -44
- package/hooks/power-tools +8 -0
- package/hooks/session-start +1 -1
- package/package.json +1 -2
- package/scripts/codex-hook +57 -118
- package/scripts/doctor.rb +146 -1012
- package/scripts/exec-worktree +103 -0
- package/scripts/hash-intent +1 -1
- package/scripts/hook-bash-gate +19 -0
- package/scripts/hook-code-gate +16 -28
- package/scripts/hook-continue +2 -2
- package/scripts/hook-create-gate +13 -57
- package/scripts/hook-edit-gates +58 -0
- package/scripts/hook-gate-check +17 -0
- package/scripts/hook-links-gate +11 -31
- package/scripts/hook-lock-gate +19 -60
- package/scripts/hook-power-tools +38 -0
- package/scripts/hook-savepoint-pre +11 -11
- package/scripts/hook-session-start +21 -15
- package/scripts/lib/apply_patch_envelope.rb +46 -13
- package/scripts/lib/bridge.rb +85 -17
- package/scripts/lib/codex_edit_gates.rb +138 -0
- package/scripts/lib/doctor_core.rb +1087 -0
- package/scripts/lib/edit_gates.rb +398 -0
- package/scripts/lib/exec_worktree.rb +325 -0
- package/scripts/lib/harness_text.rb +57 -0
- package/scripts/lib/hook_registry.rb +64 -28
- package/scripts/lib/installer_core.rb +166 -24
- package/scripts/lib/lock.rb +196 -47
- package/scripts/lib/qmd_hook.rb +21 -41
- package/scripts/lib/qmd_sync.rb +0 -15
- package/scripts/lib/revisions_writer.rb +1 -1
- 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 +17 -12
- 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 +52 -32
- package/skills/auto/evals/evals.json +2 -2
- 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 +22 -3
- package/skills/auto/references/tiers.md +24 -2
- package/skills/conventions/SKILL.md +31 -0
- package/skills/conventions/references/completion-and-done.md +43 -0
- package/skills/conventions/references/gates-and-enforcement.md +39 -0
- package/skills/conventions/references/knowledge-graph.md +47 -0
- package/skills/conventions/references/lifecycle-and-savepoints.md +11 -0
- package/skills/conventions/references/locks-and-worktrees.md +113 -0
- package/skills/conventions/references/maintenance-and-revisions.md +170 -0
- package/skills/conventions/references/roadmaps.md +44 -0
- package/skills/conventions/references/tiers-and-dispatch.md +135 -0
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/doctor/SKILL.md +15 -6
- 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 +4 -2
- package/skills/intent-continuing/SKILL.md +4 -0
- package/skills/intent-creating/SKILL.md +13 -8
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-discovering/SKILL.md +10 -3
- package/skills/intent-ending/SKILL.md +12 -7
- package/skills/intent-executing/SKILL.md +37 -19
- package/skills/intent-grilling/SKILL.md +5 -3
- package/skills/intent-linking/SKILL.md +4 -0
- package/skills/intent-locking/SKILL.md +4 -0
- package/skills/intent-planning/SKILL.md +14 -3
- package/skills/intent-researching/SKILL.md +0 -2
- package/skills/intent-savepoint/SKILL.md +4 -0
- package/skills/intent-speccing/SKILL.md +4 -0
- package/skills/intent-starting/SKILL.md +20 -2
- package/skills/project-creating/SKILL.md +0 -2
- package/skills/project-creating/references/project-scaffolding.md +3 -3
- 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/roadmap/SKILL.md +4 -0
- package/skills/roadmap-continuing/SKILL.md +4 -0
- package/skills/skill-creating/SKILL.md +8 -2
- package/skills/skill-creating/references/defaults-first.md +23 -0
- package/skills/skill-creating/references/hooks.md +4 -1
- package/skills/store-curating/SKILL.md +8 -0
- package/skills/store-indexing/SKILL.md +16 -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 +10 -8
- 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/PLASTIC-reference.md +0 -138
- package/hooks/code-gate +0 -27
- package/hooks/create-gate +0 -3
- package/hooks/links-gate +0 -3
- package/hooks/lock-gate +0 -21
- package/hooks/qmd-search +0 -8
- package/hooks/retrieval-gate +0 -10
- package/hooks/savepoint-pre +0 -10
- package/scripts/hook-qmd-search +0 -44
- package/scripts/hook-retrieval-gate +0 -148
- package/scripts/lib/retrieval_gate.rb +0 -211
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:
|
|
@@ -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
|
|
|
@@ -183,8 +194,9 @@ behind releases are part of the repository, not a hidden process.
|
|
|
183
194
|
|
|
184
195
|
## Conventions
|
|
185
196
|
|
|
186
|
-
|
|
187
|
-
and overwritten on every update.
|
|
197
|
+
The always-on core conventions live in `PLASTIC.md`, distributed to `~/.plastic/PLASTIC.md`
|
|
198
|
+
and overwritten on every update. Deeper doctrine lives in the `plastic-conventions` skill,
|
|
199
|
+
loaded on demand by the skills that need it. Project-specific rules live in `AGENTS.md`.
|
|
188
200
|
Run `plastic-doctor` to check installation health; it compares files against
|
|
189
201
|
the manifests, checks store state, and runs automatically after every update.
|
|
190
202
|
|
|
@@ -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.
|
|
@@ -27,9 +27,9 @@ You are the Plastic Intent Curator. Your role is to maintain the health and navi
|
|
|
27
27
|
5. On a terminal-state transition (Completed OR Abandoned), do these things:
|
|
28
28
|
a. Author a real `outcome.md` in the intent directory from `~/.plastic/templates/outcome.md`, with the frontmatter `disposition: delivered` for a completed intent or `disposition: abandoned` for an abandoned one. `outcome.md` is MANDATORY at every terminal, delivered and abandoned alike: on abandon it records the abandonment reason and replaces the scaffolded placeholder sentinel (never leave `outcome.md` a placeholder at a terminal).
|
|
29
29
|
b. Call `plastic-intent-ending` for the terminal-transition close (INDEX move, savepoint `Done` bookend, store commit, disarm, and the QMD reindex last): `ruby ~/.plastic/scripts/end-intent --store <store> --id <id> --disposition delivered|abandoned`, then follow that skill's own disarm and reindex steps. Never restate the INDEX/savepoint/reindex one-liners here.
|
|
30
|
-
6. Structural maintenance is move-and-record, and it is NEVER done without its receipt: remove the misplaced section, file, or ref from its artifact, then create or append `revisions.md` in that intent directory (copy the FORM from `~/.plastic/templates/revisions.md`) IN THE SAME PASS as the edit. If you cannot write `revisions.md` for any reason (permissions, a read-only path), you MUST NOT make the structural edit either - report the blocker instead of leaving an unrecorded change (this mirrors the tool-side rule: project-links, rebuild-graph, and restore-intent-v1 refuse rather than write a change with no receipt; you hold yourself to the same rule by hand). One entry per relocated item, newest at the bottom: a `## Revision vN - YYYY-MM-DD-HH:MM` header, a one-sentence `Why` ending with `[rule: <tag>]`, `Prior location`, and either `Content held` (verbatim) or a one-line `Change` for a frontmatter edit. For a stray file, embed its full content and delete the original. The violation-tag catalog is canonical in
|
|
30
|
+
6. Structural maintenance is move-and-record, and it is NEVER done without its receipt: remove the misplaced section, file, or ref from its artifact, then create or append `revisions.md` in that intent directory (copy the FORM from `~/.plastic/templates/revisions.md`) IN THE SAME PASS as the edit. If you cannot write `revisions.md` for any reason (permissions, a read-only path), you MUST NOT make the structural edit either - report the blocker instead of leaving an unrecorded change (this mirrors the tool-side rule: project-links, rebuild-graph, and restore-intent-v1 refuse rather than write a change with no receipt; you hold yourself to the same rule by hand). One entry per relocated item, newest at the bottom: a `## Revision vN - YYYY-MM-DD-HH:MM` header, a one-sentence `Why` ending with `[rule: <tag>]`, `Prior location`, and either `Content held` (verbatim) or a one-line `Change` for a frontmatter edit. For a stray file, embed its full content and delete the original. The violation-tag catalog is canonical in `plastic-conventions > references/maintenance-and-revisions.md`. A graph edit must move TOWARD ground truth (drop a dangling/false edge, add a reciprocity-forced or documented-real one) and must NEVER invent a relationship - "might be related" is never a valid `[rule:]` reason (`plastic-conventions > references/maintenance-and-revisions.md`, WORK vs MAINTENANCE).
|
|
31
31
|
7. Before performing structural maintenance on ANY intent that is NOT the one your own session is currently delivering under its own held delivery lock, you must:
|
|
32
|
-
a. Check the target's lock freshness: `ruby ~/.plastic/scripts/plastic-lock status --intent-dir <target-intent-dir>` and read the `lock_fresh` field of its JSON output. If `true`, DEFER: make no edit to that intent, and report it as skipped (an active delivery is in progress). This is DETECT-ONLY - you never acquire, create, or hold any lock of your own for maintenance (
|
|
32
|
+
a. Check the target's lock freshness: `ruby ~/.plastic/scripts/plastic-lock status --intent-dir <target-intent-dir>` and read the `lock_fresh` field of its JSON output. If `true`, DEFER: make no edit to that intent, and report it as skipped (an active delivery is in progress). This is DETECT-ONLY - you never acquire, create, or hold any lock of your own for maintenance (`plastic-conventions > references/maintenance-and-revisions.md`, WORK vs MAINTENANCE; there is exactly one lock in Plastic, the delivery lock).
|
|
33
33
|
b. Require a clean store working tree before starting: `git -C ~/.plastic status --porcelain` (or the project store's own root, if not global) must be empty. If it is not, STOP and report the dirty paths rather than risk sweeping an unrelated concurrent change into your own commit; do not proceed until the tree is clean.
|
|
34
34
|
c. Create a fresh branch from the current tip of that repo's main: `git -C <repo-root> checkout -b maintenance/curator-<UTC-timestamp> main`.
|
|
35
35
|
d. Make the scoped edit plus its `revisions.md` receipt (step 6 above), touching nothing else.
|
|
@@ -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/deprecations.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plastic deprecation notices
|
|
2
2
|
# Each entry is shown to the user during SessionStart.
|
|
3
|
-
# See
|
|
3
|
+
# See the plastic-releasing skill for the full convention.
|
|
4
4
|
#
|
|
5
5
|
# Schema:
|
|
6
6
|
# id: unique-slug (used for dismissal tracking)
|
|
@@ -11,4 +11,12 @@
|
|
|
11
11
|
# removal: semver where feature is removed
|
|
12
12
|
# link: optional URL to migration guide
|
|
13
13
|
|
|
14
|
-
deprecations:
|
|
14
|
+
deprecations:
|
|
15
|
+
- id: plastic-reference-md-absorbed
|
|
16
|
+
severity: info
|
|
17
|
+
summary: "PLASTIC-reference.md has been absorbed into the plastic-conventions skill and removed; its content now lives in that skill's reference chapters."
|
|
18
|
+
migration_steps:
|
|
19
|
+
- "No action is required. Running plastic update removes the leftover ~/.plastic/PLASTIC-reference.md automatically."
|
|
20
|
+
- "If you hand-copied or hand-edited that file, delete your own copy; it is no longer read by Plastic."
|
|
21
|
+
introduced: "1.9.0"
|
|
22
|
+
removal: "1.11.0"
|
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
ADDED
|
@@ -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/hooks.json
CHANGED
|
@@ -35,38 +35,8 @@
|
|
|
35
35
|
"hooks": [
|
|
36
36
|
{
|
|
37
37
|
"type": "command",
|
|
38
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\"
|
|
39
|
-
"statusMessage": "Checking
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"type": "command",
|
|
43
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" lock-gate",
|
|
44
|
-
"statusMessage": "Checking lock gate..."
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"matcher": "Write|Edit",
|
|
50
|
-
"hooks": [
|
|
51
|
-
{
|
|
52
|
-
"type": "command",
|
|
53
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" savepoint-pre",
|
|
54
|
-
"statusMessage": "Recording stage start..."
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"type": "command",
|
|
58
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" links-gate",
|
|
59
|
-
"statusMessage": "Checking Links gate..."
|
|
60
|
-
}
|
|
61
|
-
]
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
"matcher": "Write|Edit|mcp__serena__replace_content|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|mcp__serena__safe_delete_symbol|mcp__serena__rename_symbol",
|
|
65
|
-
"hooks": [
|
|
66
|
-
{
|
|
67
|
-
"type": "command",
|
|
68
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" create-gate",
|
|
69
|
-
"statusMessage": "Checking create gate..."
|
|
38
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" edit-gates",
|
|
39
|
+
"statusMessage": "Checking Plastic gates..."
|
|
70
40
|
}
|
|
71
41
|
]
|
|
72
42
|
},
|
|
@@ -79,16 +49,6 @@
|
|
|
79
49
|
"statusMessage": "Checking lifecycle gate..."
|
|
80
50
|
}
|
|
81
51
|
]
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
"matcher": "Bash|Read|Grep|Glob",
|
|
85
|
-
"hooks": [
|
|
86
|
-
{
|
|
87
|
-
"type": "command",
|
|
88
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" retrieval-gate",
|
|
89
|
-
"statusMessage": "Checking retrieval gate..."
|
|
90
|
-
}
|
|
91
|
-
]
|
|
92
52
|
}
|
|
93
53
|
],
|
|
94
54
|
"PostToolUse": [
|
|
@@ -139,8 +99,8 @@
|
|
|
139
99
|
"hooks": [
|
|
140
100
|
{
|
|
141
101
|
"type": "command",
|
|
142
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\"
|
|
143
|
-
"statusMessage": "
|
|
102
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" power-tools",
|
|
103
|
+
"statusMessage": "Checking power tools..."
|
|
144
104
|
}
|
|
145
105
|
]
|
|
146
106
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# power-tools mandate hook launcher (intents 66, 66b, 187, 246). No-op when there is no global store.
|
|
3
|
+
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
4
|
+
if [ ! -f "$GLOBAL_INDEX" ]; then
|
|
5
|
+
exit 0
|
|
6
|
+
fi
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
exec env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-power-tools"
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalom/plastic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Intent-driven idea development system for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"agents/",
|
|
39
39
|
"templates/",
|
|
40
40
|
"PLASTIC.md",
|
|
41
|
-
"PLASTIC-reference.md",
|
|
42
41
|
"deprecations.yml"
|
|
43
42
|
]
|
|
44
43
|
}
|
package/scripts/codex-hook
CHANGED
|
@@ -3,33 +3,36 @@
|
|
|
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
|
-
# future-intent-check | auto-arm |
|
|
9
|
-
# shell-tool
|
|
8
|
+
# future-intent-check | auto-arm | power-tools | savepoint
|
|
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
|
-
#
|
|
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
|
|
33
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,
|
|
@@ -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
|
|
@@ -50,9 +50,9 @@ require_relative "lib/links_gate"
|
|
|
50
50
|
# caller that merges the dispatcher's own stdout and stderr.
|
|
51
51
|
$stdout.sync = true
|
|
52
52
|
|
|
53
|
-
STATE_HOOKS = %w[session-start check-update continue future-intent-check auto-arm
|
|
53
|
+
STATE_HOOKS = %w[session-start check-update continue future-intent-check auto-arm power-tools savepoint].freeze
|
|
54
54
|
STATE_TIMEOUT = 5
|
|
55
|
-
SHELL_HOOKS = %w[bash-gate
|
|
55
|
+
SHELL_HOOKS = %w[bash-gate].freeze
|
|
56
56
|
|
|
57
57
|
gate = ARGV[0].to_s
|
|
58
58
|
raw = ($stdin.read rescue nil)
|
|
@@ -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
|
|
@@ -78,22 +78,17 @@ if STATE_HOOKS.include?(gate)
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
if SHELL_HOOKS.include?(gate)
|
|
81
|
-
# bash-gate
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# ApplyPatchEnvelope.parse below (see header comment).
|
|
81
|
+
# bash-gate (intent 203): a shell command has no apply_patch diff envelope, so
|
|
82
|
+
# this branch execs the SAME scripts/hook-bash-gate file Claude already runs,
|
|
83
|
+
# unmodified, and relays its exit code and stderr. It must never fall through
|
|
84
|
+
# to ApplyPatchEnvelope.parse below (see header comment).
|
|
86
85
|
cwd = payload["cwd"].to_s
|
|
87
86
|
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
88
87
|
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
89
88
|
script = File.join(__dir__, "hook-#{gate}")
|
|
90
89
|
argv = [script]
|
|
91
|
-
# hook-retrieval-gate reads plastic_home off ARGV[0] (mirrors hooks/retrieval-gate's
|
|
92
|
-
# own launcher, which passes "$HOME/.plastic"); hook-bash-gate takes no args, it
|
|
93
|
-
# resolves Dir.home directly.
|
|
94
|
-
argv << ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") } if gate == "retrieval-gate"
|
|
95
90
|
out, err, status = begin
|
|
96
|
-
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) }
|
|
97
92
|
rescue StandardError
|
|
98
93
|
["", "", nil] # fail open: script missing, unexecutable, crashed, or too slow
|
|
99
94
|
end
|
|
@@ -102,6 +97,8 @@ if SHELL_HOOKS.include?(gate)
|
|
|
102
97
|
exit(status ? status.exitstatus : 0)
|
|
103
98
|
end
|
|
104
99
|
|
|
100
|
+
require_relative "lib/apply_patch_envelope"
|
|
101
|
+
|
|
105
102
|
command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
|
|
106
103
|
exit 0 if command.nil? # fail-open: no tool_input at all (unrecognized gate name or non-tool-call payload)
|
|
107
104
|
ops = ApplyPatchEnvelope.parse(command)
|
|
@@ -109,54 +106,30 @@ exit 0 if ops.empty? # fail-open: nothing parseable to gate
|
|
|
109
106
|
|
|
110
107
|
CORES = __dir__ # ~/.plastic/scripts
|
|
111
108
|
def run_core(name, *argv)
|
|
112
|
-
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)
|
|
113
110
|
[out, $?.exitstatus]
|
|
114
111
|
end
|
|
115
112
|
|
|
116
|
-
def intent_file?(path)
|
|
117
|
-
abs = File.expand_path(path)
|
|
118
|
-
dir = File.dirname(abs)
|
|
119
|
-
dir.match?(%r{/store/[^/]+--[^/]+\z}) && File.basename(abs) == "#{File.basename(dir)}.md"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
113
|
case gate
|
|
123
|
-
when "
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# slash.
|
|
142
|
-
ops.each do |o|
|
|
143
|
-
out, _ = run_core("hook-lock-gate", o.path, session, "codex")
|
|
144
|
-
if out.include?('"permissionDecision":"deny"') || out.include?('"permissionDecision": "deny"')
|
|
145
|
-
print out
|
|
146
|
-
exit 0
|
|
147
|
-
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
|
|
148
132
|
end
|
|
149
|
-
exit 0
|
|
150
|
-
|
|
151
|
-
when "code-gate"
|
|
152
|
-
ops.each do |o|
|
|
153
|
-
out, code = run_core("hook-code-gate", o.path, session, o.added_content.to_s)
|
|
154
|
-
if code == 2
|
|
155
|
-
$stderr.print out
|
|
156
|
-
exit 2
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
exit 0
|
|
160
133
|
|
|
161
134
|
when "gate-check"
|
|
162
135
|
last_allow = nil
|
|
@@ -171,40 +144,6 @@ when "gate-check"
|
|
|
171
144
|
print last_allow if last_allow
|
|
172
145
|
exit 0
|
|
173
146
|
|
|
174
|
-
when "savepoint-pre"
|
|
175
|
-
ops.each { |o| run_core("hook-savepoint-pre", o.path) }
|
|
176
|
-
exit 0
|
|
177
|
-
|
|
178
|
-
when "links-gate"
|
|
179
|
-
# Write-time belt for the PLASTIC.md ## Links contract (intent 192), reusing
|
|
180
|
-
# the SAME LinksGate.decision Claude's hook-links-gate already drives, so
|
|
181
|
-
# both harnesses share one decision function. before_content is the real
|
|
182
|
-
# on-disk file (File.read, or "" when it does not yet exist); after_content
|
|
183
|
-
# is the proposed result, o.added_content.
|
|
184
|
-
#
|
|
185
|
-
# DISCLOSED, NARROWER judgment than Claude's version (intent 198, Decision
|
|
186
|
-
# D3): ApplyPatchEnvelope.parse only ever captures a diff's ADDED lines,
|
|
187
|
-
# never its removed or context lines, so for an Update whose diff is a
|
|
188
|
-
# partial hunk rather than a full-file rewrite, after_content may not be the
|
|
189
|
-
# complete proposed file. This is the same class of disclosed limitation
|
|
190
|
-
# create-gate already carries for Update/Delete/Move (best effort, never a
|
|
191
|
-
# hard crash). links-gate has no PostToolUse backstop to defer to on Claude
|
|
192
|
-
# either, so running this best-effort compare on Update ops too is strictly
|
|
193
|
-
# more coverage than the prior total fail-open.
|
|
194
|
-
ops.each do |o|
|
|
195
|
-
next unless intent_file?(o.path)
|
|
196
|
-
|
|
197
|
-
before_content = File.exist?(o.path) ? File.read(o.path) : ""
|
|
198
|
-
plastic_home = ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
|
|
199
|
-
reason = LinksGate.decision(file_path: o.path, before_content: before_content,
|
|
200
|
-
after_content: o.added_content.to_s, plastic_home: plastic_home)
|
|
201
|
-
next unless reason
|
|
202
|
-
|
|
203
|
-
$stderr.puts reason
|
|
204
|
-
exit 2
|
|
205
|
-
end
|
|
206
|
-
exit 0
|
|
207
|
-
|
|
208
147
|
else
|
|
209
148
|
exit 0
|
|
210
149
|
end
|