@zalom/plastic 1.0.0-alpha.7 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/doctor.rb +43 -0
- package/scripts/install.rb +25 -0
- package/scripts/lib/bridge.rb +34 -0
- package/skills/auto/SKILL.md +18 -4
- package/skills/releasing/SKILL.md +88 -17
- package/templates/project.yml +5 -0
package/package.json
CHANGED
package/scripts/doctor.rb
CHANGED
|
@@ -681,6 +681,49 @@ def check_project_stores
|
|
|
681
681
|
)
|
|
682
682
|
end
|
|
683
683
|
|
|
684
|
+
# project_yml_exists
|
|
685
|
+
project_yml_path = File.join(PLASTIC_HOME, "projects", slug, "project.yml")
|
|
686
|
+
project_yml_data = nil
|
|
687
|
+
|
|
688
|
+
if File.exist?(project_yml_path)
|
|
689
|
+
checks << check(
|
|
690
|
+
category: "project_stores", name: "project_yml_exists", status: "pass",
|
|
691
|
+
message: "project.yml exists for project '#{slug}'"
|
|
692
|
+
)
|
|
693
|
+
project_yml_data = load_yaml_safe(project_yml_path)
|
|
694
|
+
else
|
|
695
|
+
checks << check(
|
|
696
|
+
category: "project_stores", name: "project_yml_exists", status: "warn",
|
|
697
|
+
message: "project.yml missing for project '#{slug}'",
|
|
698
|
+
fixable: true, fix_hint: "Create project.yml from template — see plastic:creating-project"
|
|
699
|
+
)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# governing_docs_exist
|
|
703
|
+
if project_yml_data.is_a?(Hash) && project_yml_data["governing_docs"].is_a?(Array) && !project_yml_data["governing_docs"].empty?
|
|
704
|
+
project_path = project_info.is_a?(Hash) ? project_info["path"] : nil
|
|
705
|
+
|
|
706
|
+
if project_path
|
|
707
|
+
missing_docs = project_yml_data["governing_docs"].reject do |doc_path|
|
|
708
|
+
File.exist?(File.join(project_path, doc_path))
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
if missing_docs.empty?
|
|
712
|
+
checks << check(
|
|
713
|
+
category: "project_stores", name: "governing_docs_exist", status: "pass",
|
|
714
|
+
message: "All governing docs exist for project '#{slug}'"
|
|
715
|
+
)
|
|
716
|
+
else
|
|
717
|
+
checks << check(
|
|
718
|
+
category: "project_stores", name: "governing_docs_exist", status: "warn",
|
|
719
|
+
message: "#{missing_docs.size} governing doc(s) missing for project '#{slug}'",
|
|
720
|
+
details: missing_docs,
|
|
721
|
+
fixable: false
|
|
722
|
+
)
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
684
727
|
# cross_references — if project has `parent` field, check global store intent tags
|
|
685
728
|
parent_id = project_info.is_a?(Hash) ? project_info["parent"] : nil
|
|
686
729
|
next unless parent_id
|
package/scripts/install.rb
CHANGED
|
@@ -217,6 +217,31 @@ def bootstrap
|
|
|
217
217
|
puts " \u{2705} Store bootstrapped"
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
+
def bootstrap_project_store(slug)
|
|
221
|
+
project_dir = File.join(PLASTIC_HOME, "projects", slug)
|
|
222
|
+
store_dir = File.join(project_dir, "store")
|
|
223
|
+
|
|
224
|
+
FileUtils.mkdir_p(store_dir)
|
|
225
|
+
|
|
226
|
+
template = File.join(PACKAGE_ROOT, "templates", "project.yml")
|
|
227
|
+
dest = File.join(project_dir, "project.yml")
|
|
228
|
+
write_if_missing(dest, File.read(template)) if File.exist?(template)
|
|
229
|
+
|
|
230
|
+
write_if_missing(File.join(project_dir, "INDEX.md"), <<~MD)
|
|
231
|
+
# Index
|
|
232
|
+
|
|
233
|
+
## Active
|
|
234
|
+
|
|
235
|
+
## Future
|
|
236
|
+
|
|
237
|
+
## Clusters
|
|
238
|
+
|
|
239
|
+
## Abandoned
|
|
240
|
+
|
|
241
|
+
## Completed
|
|
242
|
+
MD
|
|
243
|
+
end
|
|
244
|
+
|
|
220
245
|
# --- Agent adapters ---
|
|
221
246
|
|
|
222
247
|
def install_for_agent(key, force)
|
package/scripts/lib/bridge.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
|
+
require "yaml"
|
|
5
6
|
require "fileutils"
|
|
6
7
|
require "tempfile"
|
|
7
8
|
|
|
@@ -136,4 +137,37 @@ module Bridge
|
|
|
136
137
|
|
|
137
138
|
nil # no gate violation
|
|
138
139
|
end
|
|
140
|
+
|
|
141
|
+
PROJECT_CONFIG_DEFAULTS = {
|
|
142
|
+
"governing_docs" => ["AGENTS.md"],
|
|
143
|
+
"release" => {
|
|
144
|
+
"on_complete" => "commit",
|
|
145
|
+
},
|
|
146
|
+
}.freeze
|
|
147
|
+
|
|
148
|
+
def self.read_project_config(slug)
|
|
149
|
+
path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
|
|
150
|
+
config = if File.exist?(path)
|
|
151
|
+
YAML.safe_load(File.read(path)) || {}
|
|
152
|
+
else
|
|
153
|
+
{}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
deep_merge(PROJECT_CONFIG_DEFAULTS, config)
|
|
157
|
+
rescue => e
|
|
158
|
+
$stderr.puts "Warning: failed to read project config for #{slug}: #{e.message}"
|
|
159
|
+
PROJECT_CONFIG_DEFAULTS.dup
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.deep_merge(base, overlay)
|
|
163
|
+
result = base.dup
|
|
164
|
+
overlay.each do |key, value|
|
|
165
|
+
if value.is_a?(Hash) && result[key].is_a?(Hash)
|
|
166
|
+
result[key] = deep_merge(result[key], value)
|
|
167
|
+
else
|
|
168
|
+
result[key] = value
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
result
|
|
172
|
+
end
|
|
139
173
|
end
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -111,12 +111,26 @@ During initial project creation, all decisions are non-destructive by definition
|
|
|
111
111
|
1. Verify all checklist items are checked
|
|
112
112
|
2. Write `outcome.md` with detailed results
|
|
113
113
|
3. Write `## Outcome` summary in the intent file (1-2 sentences)
|
|
114
|
-
4.
|
|
114
|
+
4. **Release (if configured)**
|
|
115
|
+
1. Detect project — match CWD against paths in `~/.plastic/projects.yml` to find the project slug. If no match, skip to step 5 (default commit-only behavior).
|
|
116
|
+
2. Read `~/.plastic/projects/{slug}/project.yml`. If the file doesn't exist or has no `release` key, skip to step 5.
|
|
117
|
+
3. Based on `release.on_complete`:
|
|
118
|
+
- `commit` — git add + commit (same as default, proceed to step 5)
|
|
119
|
+
- `commit_and_push` — git add + commit + push
|
|
120
|
+
- `manual` — skip auto-commit, notify user: "Release configured as manual — commit when ready."
|
|
121
|
+
4. If `release.verify` is set, run the verify command (e.g. `bundle exec rake test`):
|
|
122
|
+
- **Exit 0 (green):** proceed to sub-step 5
|
|
123
|
+
- **Non-zero (red):** check `release.on_red`:
|
|
124
|
+
- `fix_and_retry` — attempt to fix the failure, re-run verify (max 2 retries)
|
|
125
|
+
- `stop` — write `savepoint.md` with current state, notify user: "Verify failed — savepoint written.", **STOP**
|
|
126
|
+
- `manual` — notify user: "Verify failed: [summary]. Resolve manually."
|
|
127
|
+
5. If `release.on_green` has items, invoke `plastic:releasing` to handle them (tag, changelog, publish, etc.). Do NOT duplicate release logic — delegate entirely.
|
|
128
|
+
5. Review `## Insights` for observations that should spawn future intents. If any:
|
|
115
129
|
- Create them (using `plastic:creating-intent` conventions)
|
|
116
130
|
- Update `chain` in the current intent's frontmatter
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
6. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
|
|
132
|
+
7. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: deliver intent <ID> — <name>"`
|
|
133
|
+
8. Notify user: "Intent [ID] — [name] delivered. [1-2 sentence summary]. See outcome.md for details."
|
|
120
134
|
|
|
121
135
|
## Error Handling
|
|
122
136
|
|
|
@@ -6,25 +6,62 @@ description: Use when merging a feature branch to main and tagging a release, bu
|
|
|
6
6
|
# Releasing
|
|
7
7
|
|
|
8
8
|
Merge, bump, tag, push. Annotated tags with changelogs. Semantic versioning.
|
|
9
|
+
Project configuration drives the workflow — no hardcoded assumptions.
|
|
9
10
|
|
|
10
11
|
## Checklist
|
|
11
12
|
|
|
12
|
-
- [ ]
|
|
13
|
+
- [ ] Read project config
|
|
14
|
+
- [ ] All tests pass (or verification skipped per config)
|
|
13
15
|
- [ ] Merge feature branch to main
|
|
14
|
-
- [ ] Bump version in
|
|
16
|
+
- [ ] Bump version in configured version files
|
|
15
17
|
- [ ] Commit version bump
|
|
16
18
|
- [ ] Create annotated tag
|
|
17
19
|
- [ ] Push to remote with tags
|
|
20
|
+
- [ ] Run post-push actions (GitHub release, npm publish, etc.)
|
|
21
|
+
- [ ] Complete active intent
|
|
18
22
|
|
|
19
23
|
## Workflow
|
|
20
24
|
|
|
25
|
+
### 0. Read Project Config
|
|
26
|
+
|
|
27
|
+
Before anything else, determine which project we are releasing and load its config.
|
|
28
|
+
|
|
29
|
+
1. Read `~/.plastic/projects.yml` — find the project whose `path` matches the current working directory.
|
|
30
|
+
2. Extract the project slug (the key under `projects:`).
|
|
31
|
+
3. Read `~/.plastic/projects/{slug}/project.yml` — this contains the `release:` section.
|
|
32
|
+
|
|
33
|
+
Expected `release:` keys in project.yml:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
release:
|
|
37
|
+
verify: "bin/rails test" # command to run before release
|
|
38
|
+
version_file: package.json # single file containing the version
|
|
39
|
+
version_files: # multiple files (overrides version_file)
|
|
40
|
+
- package.json
|
|
41
|
+
- .claude-plugin/plugin.json
|
|
42
|
+
tag_format: "v{{version}}" # tag naming pattern ({{version}} is replaced)
|
|
43
|
+
on_green: # actions to run after push succeeds
|
|
44
|
+
- github_release
|
|
45
|
+
- npm_publish
|
|
46
|
+
on_complete: commit_and_push # what to do with the version bump commit
|
|
47
|
+
on_red: stop # what to do if verification fails
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Fallback:** If no project.yml exists or it has no `release:` section, fall back to asking the user for each step — verify command, version files, tag format, and post-push actions.
|
|
51
|
+
|
|
21
52
|
### 1. Verify Tests Pass
|
|
22
53
|
|
|
54
|
+
Run the verification command from `release.verify` in project.yml:
|
|
55
|
+
|
|
23
56
|
```bash
|
|
24
|
-
|
|
57
|
+
# Example: release.verify = "ruby -Itest test/*_test.rb"
|
|
58
|
+
<verify-command-from-config>
|
|
25
59
|
```
|
|
26
60
|
|
|
27
|
-
|
|
61
|
+
- If `release.verify` is present: run it. All checks must pass before proceeding.
|
|
62
|
+
- If `release.verify` is absent or empty: skip verification. Log that no verify command is configured.
|
|
63
|
+
- If `release.on_red` is `stop`: abort the release on failure.
|
|
64
|
+
- If `release.on_red` is `fix_and_retry`: ask the user to fix and re-run.
|
|
28
65
|
|
|
29
66
|
### 2. Determine Version Bump
|
|
30
67
|
|
|
@@ -47,18 +84,26 @@ Always `--no-ff` to preserve branch history in the merge commit.
|
|
|
47
84
|
|
|
48
85
|
### 4. Bump Version
|
|
49
86
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
87
|
+
Determine which files to update from project.yml:
|
|
88
|
+
|
|
89
|
+
- If `release.version_files` is set: update ALL listed files (they must stay in sync).
|
|
90
|
+
- Else if `release.version_file` is set: update that single file.
|
|
91
|
+
- Else: ask the user which files contain the version.
|
|
92
|
+
|
|
93
|
+
Update the version string in each file, then commit:
|
|
54
94
|
|
|
55
95
|
```bash
|
|
56
|
-
git add
|
|
96
|
+
git add <version-files>
|
|
57
97
|
git commit -m "chore: bump version to X.Y.Z — [one-line summary]"
|
|
58
98
|
```
|
|
59
99
|
|
|
60
100
|
### 5. Create Annotated Tag
|
|
61
101
|
|
|
102
|
+
Read `release.tag_format` from project.yml to determine the tag name:
|
|
103
|
+
|
|
104
|
+
- If set (e.g. `"v{{version}}"`): replace `{{version}}` with the new version string.
|
|
105
|
+
- If not set: default to `vX.Y.Z`.
|
|
106
|
+
|
|
62
107
|
Generate the changelog from commits since the last tag:
|
|
63
108
|
|
|
64
109
|
```bash
|
|
@@ -68,7 +113,7 @@ git log $(git describe --tags --abbrev=0)..HEAD --oneline --no-merges | grep -E
|
|
|
68
113
|
Create the tag with a multi-line message:
|
|
69
114
|
|
|
70
115
|
```bash
|
|
71
|
-
git tag -a
|
|
116
|
+
git tag -a <tag-name> -m "<tag-name> — [release name]
|
|
72
117
|
|
|
73
118
|
- [changelog bullet points from feat/fix/refactor commits]"
|
|
74
119
|
```
|
|
@@ -79,16 +124,42 @@ git tag -a vX.Y.Z -m "vX.Y.Z — [release name]
|
|
|
79
124
|
git push origin main --tags
|
|
80
125
|
```
|
|
81
126
|
|
|
82
|
-
### 7.
|
|
127
|
+
### 7. Post-Push Actions
|
|
128
|
+
|
|
129
|
+
Read `release.on_green` from project.yml. This is a list of actions to run after a successful push. Execute each in order:
|
|
83
130
|
|
|
84
|
-
|
|
131
|
+
#### `github_release`
|
|
132
|
+
|
|
133
|
+
Create a GitHub release from the tag:
|
|
85
134
|
|
|
86
135
|
```bash
|
|
87
|
-
gh release create
|
|
136
|
+
gh release create <tag-name> --title "<tag-name> — [release name]" --generate-notes --notes-start-tag <previous-tag>
|
|
88
137
|
```
|
|
89
138
|
|
|
90
139
|
For the first release (no previous tag), write notes manually with `--notes "..."` instead.
|
|
91
140
|
|
|
141
|
+
#### `npm_publish`
|
|
142
|
+
|
|
143
|
+
Publish the package to npm:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# For pre-release versions (0.x.y, or version contains -alpha/-beta/-rc):
|
|
147
|
+
npm publish --access public --tag alpha
|
|
148
|
+
|
|
149
|
+
# For stable versions (>= 1.0.0, no pre-release suffix):
|
|
150
|
+
npm publish --access public
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Other values
|
|
154
|
+
|
|
155
|
+
If `on_green` contains an action not listed above, log it:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
[releasing] Action "<action>" is configured but not yet implemented. Skipping.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
If `on_green` is empty or absent: skip post-push actions entirely.
|
|
162
|
+
|
|
92
163
|
### 8. Complete Active Intent
|
|
93
164
|
|
|
94
165
|
A release IS a delivery. The active intent that drove this work must be completed as part of the release process. This is NOT optional.
|
|
@@ -100,17 +171,17 @@ A release IS a delivery. The active intent that drove this work must be complete
|
|
|
100
171
|
c. Update `## Insights` with final observations
|
|
101
172
|
d. Move from `## Active` to `## Completed` in INDEX.md (with today's date)
|
|
102
173
|
e. Update clusters to show `_(completed)_`
|
|
103
|
-
3. Auto-commit: `cd ~/.plastic && git add . && git commit -m "feat: complete intent <ID> — delivered in
|
|
174
|
+
3. Auto-commit: `cd ~/.plastic && git add . && git commit -m "feat: complete intent <ID> — delivered in <tag-name>"`
|
|
104
175
|
|
|
105
176
|
**If no active intent exists for this release**, that itself is a problem — work happened outside the intent system. Log it and move on, but flag it.
|
|
106
177
|
|
|
107
178
|
## Conventions
|
|
108
179
|
|
|
109
180
|
- **Annotated tags only** — `git tag -a`, never lightweight tags
|
|
110
|
-
- **Tag format** — `vX.Y.Z`
|
|
111
|
-
- **Tag message** — first line:
|
|
181
|
+
- **Tag format** — driven by `release.tag_format` in project.yml (default: `vX.Y.Z`)
|
|
182
|
+
- **Tag message** — first line: `<tag> — [short name]`, then blank line, then bullet changelog
|
|
112
183
|
- **Commit prefixes** — `feat:`, `fix:`, `refactor:`, `chore:`, `docs:` (conventional commits)
|
|
113
|
-
- **Version files** —
|
|
184
|
+
- **Version files** — driven by project.yml; all listed files must always match
|
|
114
185
|
- **Branch cleanup** — delete merged feature branches: `git branch -d <branch>`
|
|
115
186
|
|
|
116
187
|
## Retroactive Tagging
|