@zalom/plastic 1.7.0 → 1.8.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 +20 -3
- package/package.json +1 -1
- package/scripts/doctor.rb +603 -183
- package/scripts/end-intent +65 -22
- package/scripts/lib/installer_core.rb +1 -1
- package/scripts/lib/maintenance_git.rb +2 -1
- package/scripts/lib/outcome_guard.rb +38 -0
- package/scripts/maintenance-run +56 -2
- package/skills/auto/SKILL.md +5 -4
- package/skills/doctor/SKILL.md +50 -18
- package/skills/intent-ending/SKILL.md +16 -6
- package/templates/agents.md +8 -0
- package/scripts/lib/legacy_bookend_amnesty.rb +0 -35
package/scripts/end-intent
CHANGED
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
# (git status itself failed: corrupt index, git missing, not a repo);
|
|
64
64
|
# refused before removal; pass --discard-worktree-changes to override
|
|
65
65
|
# deliberately.
|
|
66
|
+
# 6 the doctor per-intent structure gate refused (checklist incomplete,
|
|
67
|
+
# links out of projection, intent-file malformed, or a lifecycle artifact
|
|
68
|
+
# missing/placeholder); named reasons on stderr; authors nothing
|
|
69
|
+
# (intent 222).
|
|
66
70
|
|
|
67
71
|
require "fileutils"
|
|
68
72
|
require "date"
|
|
@@ -71,6 +75,7 @@ require "pathname"
|
|
|
71
75
|
require_relative "lib/bridge"
|
|
72
76
|
require_relative "lib/lock"
|
|
73
77
|
require_relative "lib/intent_validator"
|
|
78
|
+
require_relative "lib/outcome_guard"
|
|
74
79
|
|
|
75
80
|
DISPOSITIONS = %w[delivered abandoned].freeze
|
|
76
81
|
|
|
@@ -143,33 +148,69 @@ def resolve_intent_dir(store, id)
|
|
|
143
148
|
matches.first
|
|
144
149
|
end
|
|
145
150
|
|
|
146
|
-
# ---
|
|
151
|
+
# --- structure gate helper (intent 222) -------------------------------------
|
|
147
152
|
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
# Derive the TRUE plastic_home and this intent's store scope key from the --store
|
|
154
|
+
# directory end-intent was given, so the per-intent doctor gate (below) sees the SAME
|
|
155
|
+
# multi-store universe doctor.rb itself would (needed for its links-projection sub-check,
|
|
156
|
+
# which resolves cross-store refs). --store is either the global store
|
|
157
|
+
# (<plastic_home>/store) or a project store (<plastic_home>/projects/<slug>/store); the two
|
|
158
|
+
# shapes are told apart by whether store's grandparent directory is literally named
|
|
159
|
+
# "projects" (the StoreDiscovery convention, scripts/lib/store_discovery.rb), never by
|
|
160
|
+
# hardcoding ".plastic" as a name.
|
|
161
|
+
def resolve_plastic_home_and_scope(store)
|
|
162
|
+
parent = File.dirname(store)
|
|
163
|
+
grandparent = File.dirname(parent)
|
|
164
|
+
if File.basename(grandparent) == "projects"
|
|
165
|
+
slug = File.basename(parent)
|
|
166
|
+
[File.dirname(grandparent), "project:#{slug}"]
|
|
167
|
+
else
|
|
168
|
+
[parent, "global"]
|
|
162
169
|
end
|
|
170
|
+
end
|
|
163
171
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
# Doctor's per-intent structure gate (intent 222), a pre-write step: FAIL refuses the close
|
|
173
|
+
# (exit 6, authors nothing), WARN prints an advisory note and proceeds, PASS proceeds
|
|
174
|
+
# silently, and an unexpected exception from the gate call itself is rescued and treated as
|
|
175
|
+
# a skipped gate (fail-open on a crash: a broken gate must never wedge a close).
|
|
176
|
+
#
|
|
177
|
+
# `gate:` is an injected seam (mirrors run_disarm's own bridge_reader:/disarm: pattern
|
|
178
|
+
# below) defaulting to the real Doctor.new(...).run_intent_check(...) call; tests load this
|
|
179
|
+
# script in-process (as test/end_intent_disarm_toctou_test.rb already does for run_disarm)
|
|
180
|
+
# and inject a raising stub to prove the fail-open contract, with no eval/ENV/global seam.
|
|
181
|
+
#
|
|
182
|
+
# Disposition is deliberately NOT passed to the real gate call (intent 222 executor
|
|
183
|
+
# decision): the existing outcome-only guard immediately following this call already owns
|
|
184
|
+
# disposition-matching (exit 2), and intent_lifecycle_artifacts unconditionally checks
|
|
185
|
+
# outcome.md PRESENCE via Bridge.stage_file_present? regardless of disposition, so passing
|
|
186
|
+
# it here would make this gate re-check the SAME disposition match the old guard already
|
|
187
|
+
# owns, needlessly widening which exit-2 fixtures become exit-6. doctor.rb --intent called
|
|
188
|
+
# directly (with --disposition) still exercises the fold-in per D2/acceptance criteria.
|
|
189
|
+
def run_structure_gate(id, store:, gate: ->(gate_home, gate_scope) {
|
|
190
|
+
require_relative "doctor"
|
|
191
|
+
Doctor.new(plastic_home: gate_home).run_intent_check(id, store: gate_scope)
|
|
192
|
+
})
|
|
193
|
+
gate_home, gate_scope = resolve_plastic_home_and_scope(store)
|
|
194
|
+
gate_verdict = gate.call(gate_home, gate_scope)
|
|
195
|
+
case gate_verdict[:status]
|
|
196
|
+
when "fail"
|
|
197
|
+
gate_verdict[:checks].select { |c| c[:status] == "fail" }.each do |c|
|
|
198
|
+
detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
|
|
199
|
+
warn "end-intent: structure gate refused: #{c[:name]}: #{c[:message]}#{detail}"
|
|
200
|
+
end
|
|
201
|
+
exit 6
|
|
202
|
+
when "warn"
|
|
203
|
+
gate_verdict[:checks].select { |c| c[:status] == "warn" }.each do |c|
|
|
204
|
+
detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
|
|
205
|
+
warn "end-intent: structure gate warning (proceeding): #{c[:name]}: #{c[:message]}#{detail}"
|
|
206
|
+
end
|
|
168
207
|
end
|
|
169
|
-
|
|
170
|
-
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
warn "end-intent: structure gate crashed (#{e.message}); proceeding without it"
|
|
171
210
|
end
|
|
172
211
|
|
|
212
|
+
# outcome.md guard: see OutcomeGuard.reason (scripts/lib/outcome_guard.rb, extracted intent 222)
|
|
213
|
+
|
|
173
214
|
# --- intent-file `## Outcome` summary stamp (D2 step 1b) --------------------
|
|
174
215
|
#
|
|
175
216
|
# Replace the body of the intent file's `## Outcome` section with `summary`,
|
|
@@ -580,7 +621,9 @@ def main(argv)
|
|
|
580
621
|
exit 4
|
|
581
622
|
end
|
|
582
623
|
|
|
583
|
-
|
|
624
|
+
run_structure_gate(id, store: store)
|
|
625
|
+
|
|
626
|
+
reason = OutcomeGuard.reason(intent_dir, disposition)
|
|
584
627
|
if reason
|
|
585
628
|
warn "end-intent: #{reason}"
|
|
586
629
|
exit 2
|
|
@@ -358,7 +358,7 @@ class InstallerCore
|
|
|
358
358
|
"scripts/update.rb" => "scripts/update.rb",
|
|
359
359
|
"scripts/uninstall.rb" => "scripts/uninstall.rb",
|
|
360
360
|
"scripts/rollback.rb" => "scripts/rollback.rb",
|
|
361
|
-
"scripts/lib/
|
|
361
|
+
"scripts/lib/outcome_guard.rb" => "scripts/lib/outcome_guard.rb",
|
|
362
362
|
"scripts/doctor.rb" => "scripts/doctor.rb",
|
|
363
363
|
"scripts/dashboard.rb" => "scripts/dashboard.rb",
|
|
364
364
|
"scripts/skill-lint" => "scripts/skill-lint",
|
|
@@ -73,7 +73,8 @@ module MaintenanceGit
|
|
|
73
73
|
run_git!(root, "-c", "user.name=Plastic", "-c", "user.email=plastic@localhost",
|
|
74
74
|
"commit", "--quiet", "-m", commit_message)
|
|
75
75
|
checkout!(root, base)
|
|
76
|
-
run_git!(root, "
|
|
76
|
+
run_git!(root, "-c", "user.name=Plastic", "-c", "user.email=plastic@localhost",
|
|
77
|
+
"merge", "--quiet", "--no-ff", "-m", "Merge #{branch_name} into #{base}", branch_name)
|
|
77
78
|
delete_branch(root, branch_name)
|
|
78
79
|
|
|
79
80
|
{ changed: changed, committed: true, merged: true, branch: branch_name }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "bridge"
|
|
5
|
+
require_relative "intent_validator"
|
|
6
|
+
|
|
7
|
+
# OutcomeGuard - the single definition of "is outcome.md real and disposition-matched for
|
|
8
|
+
# THIS close" (intent 222). Extracted verbatim from scripts/end-intent's own
|
|
9
|
+
# outcome_guard_reason (intent 161/188) so end-intent's existing exit-2 path and doctor's new
|
|
10
|
+
# per-intent check (intent_lifecycle_artifacts) can never independently drift on what counts
|
|
11
|
+
# as a valid outcome.md. Pure, read-only: authors nothing regardless of outcome.
|
|
12
|
+
module OutcomeGuard
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
DISPOSITIONS = %w[delivered abandoned].freeze
|
|
16
|
+
|
|
17
|
+
# Returns nil when the guard PASSES, or a reason String to refuse with. Refuses a missing
|
|
18
|
+
# file, a still-placeholder file (first line is the sentinel), or a disposition that does
|
|
19
|
+
# not exactly equal the requested one.
|
|
20
|
+
def reason(intent_dir, disposition)
|
|
21
|
+
path = File.join(intent_dir, "outcome.md")
|
|
22
|
+
return "outcome.md is missing at #{path}" unless File.exist?(path)
|
|
23
|
+
|
|
24
|
+
content = File.read(path)
|
|
25
|
+
first_line = content.each_line.first.to_s.chomp
|
|
26
|
+
if first_line == Bridge::PLACEHOLDER_SENTINEL
|
|
27
|
+
return "outcome.md is still the scaffold placeholder (first line is the sentinel)"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
fm = IntentValidator.parse_frontmatter_text(content)
|
|
31
|
+
actual = fm.is_a?(Hash) ? fm["disposition"] : nil
|
|
32
|
+
unless DISPOSITIONS.include?(actual) && actual == disposition
|
|
33
|
+
return "outcome.md frontmatter disposition is #{actual.inspect}, expected #{disposition.inspect}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
package/scripts/maintenance-run
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# maintenance-run --tool project-links --intent <id> [--store <key>] [--plastic-home PATH] [--apply]
|
|
15
15
|
# maintenance-run --tool rebuild-graph [--plastic-home PATH] [--apply]
|
|
16
16
|
# maintenance-run --tool restore-intent-v1 <id> --at <ref> [--plastic-home PATH] [--apply] [--skip-links]
|
|
17
|
+
# maintenance-run --tool rebuild-savepoint --intent <id> [--store <key>] [--plastic-home PATH] [--apply]
|
|
17
18
|
#
|
|
18
19
|
# project-links here is ALWAYS single-intent: --intent is required. A store-wide
|
|
19
20
|
# project-links sweep is the rare, owner-approved batch exception (D2) and is run directly
|
|
@@ -35,6 +36,8 @@ require "time"
|
|
|
35
36
|
require_relative "lib/store_discovery"
|
|
36
37
|
require_relative "lib/lock"
|
|
37
38
|
require_relative "lib/maintenance_git"
|
|
39
|
+
require_relative "lib/bridge"
|
|
40
|
+
require_relative "lib/revisions_writer"
|
|
38
41
|
|
|
39
42
|
DEFAULT_HOME = File.join(Dir.home, ".plastic")
|
|
40
43
|
|
|
@@ -217,9 +220,58 @@ def run_restore_intent_v1(home, id, at, store, apply, skip_links)
|
|
|
217
220
|
report_result(result)
|
|
218
221
|
end
|
|
219
222
|
|
|
223
|
+
def run_rebuild_savepoint(home, intent, store, apply)
|
|
224
|
+
abort_loud("--tool rebuild-savepoint requires --intent <id>") unless intent
|
|
225
|
+
|
|
226
|
+
discovery = StoreDiscovery.discover(home)
|
|
227
|
+
dir = resolve_dir_for_id(discovery, intent, store: store)
|
|
228
|
+
abort_loud("intent #{intent} not found under #{home}#{store ? " (--store #{store})" : ""}") unless dir
|
|
229
|
+
check_not_fresh!(dir, intent)
|
|
230
|
+
|
|
231
|
+
outcome_path = File.join(dir, "outcome.md")
|
|
232
|
+
unless Bridge.stage_file_present?(outcome_path)
|
|
233
|
+
abort_loud("intent #{intent}'s outcome.md is missing or a placeholder; the 124a recipe " \
|
|
234
|
+
"requires a real disposition to echo, and 219 D6 forbids ever inventing one", 1)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
frontmatter = File.read(outcome_path)[/\A---\n(.*?)\n---/m, 1].to_s
|
|
238
|
+
disposition = frontmatter[/^disposition:\s*(\S+)/, 1]
|
|
239
|
+
abort_loud("intent #{intent}'s outcome.md has no disposition: frontmatter field", 1) unless disposition
|
|
240
|
+
|
|
241
|
+
unless apply
|
|
242
|
+
puts "maintenance-run: DRY RUN, would reconstruct savepoint.md for #{intent} " \
|
|
243
|
+
"(Bridge.rebuild_savepoint + Done #{disposition} echo)"
|
|
244
|
+
exit 0
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
begin
|
|
248
|
+
result = MaintenanceGit.run_scoped(
|
|
249
|
+
repo_dir: home, branch_name: "maintenance/rebuild-savepoint-#{intent}-#{stamp}",
|
|
250
|
+
commit_message: "chore: maintenance - rebuild-savepoint #{intent}"
|
|
251
|
+
) do
|
|
252
|
+
Bridge.rebuild_savepoint(dir)
|
|
253
|
+
Bridge.append_terminal_savepoint(dir, disposition)
|
|
254
|
+
RevisionsWriter.append!(
|
|
255
|
+
dir,
|
|
256
|
+
why: "reconstruct missing operational savepoint.md (generic operational-gap predicate)",
|
|
257
|
+
rule: "savepoint-operational-reconstruction",
|
|
258
|
+
prior_location: "#{intent}/savepoint.md",
|
|
259
|
+
change: "savepoint.md rebuilt from disk (Bridge.rebuild_savepoint) plus Done " \
|
|
260
|
+
"#{disposition} echo appended (Bridge.append_terminal_savepoint); disposition " \
|
|
261
|
+
"read from outcome.md's own frontmatter, never invented"
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
rescue MaintenanceGit::DirtyWorkingTree, MaintenanceGit::NotAGitRepo => e
|
|
265
|
+
abort_loud(e.message, 4)
|
|
266
|
+
rescue RuntimeError => e
|
|
267
|
+
abort_loud(e.message, 3)
|
|
268
|
+
end
|
|
269
|
+
report_result(result)
|
|
270
|
+
end
|
|
271
|
+
|
|
220
272
|
def main(argv)
|
|
221
273
|
opts = parse_argv(argv)
|
|
222
|
-
abort_loud("--tool is required (project-links|rebuild-graph|restore-intent-v1)") unless opts[:tool]
|
|
274
|
+
abort_loud("--tool is required (project-links|rebuild-graph|restore-intent-v1|rebuild-savepoint)") unless opts[:tool]
|
|
223
275
|
|
|
224
276
|
case opts[:tool]
|
|
225
277
|
when "project-links"
|
|
@@ -227,9 +279,11 @@ def main(argv)
|
|
|
227
279
|
when "rebuild-graph" then run_rebuild_graph(opts[:plastic_home], opts[:apply])
|
|
228
280
|
when "restore-intent-v1"
|
|
229
281
|
run_restore_intent_v1(opts[:plastic_home], opts[:id], opts[:at], opts[:store], opts[:apply], opts[:skip_links])
|
|
282
|
+
when "rebuild-savepoint"
|
|
283
|
+
run_rebuild_savepoint(opts[:plastic_home], opts[:intent], opts[:store], opts[:apply])
|
|
230
284
|
else
|
|
231
285
|
abort_loud("unknown --tool #{opts[:tool].inspect} " \
|
|
232
|
-
"(expected project-links|rebuild-graph|restore-intent-v1)")
|
|
286
|
+
"(expected project-links|rebuild-graph|restore-intent-v1|rebuild-savepoint)")
|
|
233
287
|
end
|
|
234
288
|
end
|
|
235
289
|
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -343,10 +343,11 @@ During initial project creation, all decisions are non-destructive by definition
|
|
|
343
343
|
non-zero exit needs attention before moving on: 4 means a live foreign session holds the
|
|
344
344
|
lock (back off), 5 means the code worktree is dirty (commit/stash first, or pass
|
|
345
345
|
`--discard-worktree-changes` deliberately), 3 means disarm ran but the lock is still
|
|
346
|
-
present (run `/plastic-doctor check the lock status`)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
346
|
+
present (run `/plastic-doctor check the lock status`), 6 means the structure gate refused
|
|
347
|
+
(see the named reason on stderr; fix via the owning tool named above, then re-run). Only
|
|
348
|
+
Step 6 (QMD reindex, async, last) remains a separate action after this call succeeds.
|
|
349
|
+
Never leave an orphaned worktree; run `git worktree prune` on a stale reference. If any of
|
|
350
|
+
this ever needs to change, change `plastic-intent-ending`, not this skill.
|
|
350
351
|
7. Notify user (Done briefing): brief per `references/human-report-contract.md`
|
|
351
352
|
(State: the delivered impact; Risk: residual risk; Call: the decision left to you, merge,
|
|
352
353
|
release, or accept). See `outcome.md` for details.
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -16,36 +16,67 @@ Doctor has three scopes. Pick the right one for the situation:
|
|
|
16
16
|
| Store check | `--store [global\|<slug>]` | Dashboard load, `plastic-project-continuing` | Three-state: pass / warn / fail |
|
|
17
17
|
| Full check | (no flag) | After every update (automatic), or `/plastic-doctor` | Three-state: pass / warn / fail |
|
|
18
18
|
|
|
19
|
-
### `--core` (binary,
|
|
19
|
+
### `--core` (binary, operational-readiness only)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Checks ONLY that Plastic is loaded and ready for work: agent registration (skills,
|
|
22
|
+
subagents, hooks/harnesses present and registered), core files (manifest-backed
|
|
23
|
+
presence/hash checks, excluding `agent_model_drift`, which is a non-boot
|
|
24
|
+
config-honoring check, never run at core), manifest sync (global + agent manifest
|
|
25
|
+
SHA256), every registered project's path resolving to a real, existing directory,
|
|
26
|
+
and the global store being reachable (`INDEX.md` present; no orphan/ghost content
|
|
27
|
+
scanning). Result is binary: exit 0 on pass, non-zero on error. It never produces
|
|
28
|
+
warnings, and it never scans store content.
|
|
29
|
+
|
|
30
|
+
It checks two install manifests as part of core files and manifest sync:
|
|
23
31
|
|
|
24
32
|
- `~/.plastic/manifest.json` (global manifest, covers PLASTIC.md and global scripts)
|
|
25
33
|
- `~/.claude/plastic/manifest.json` (agent-side manifest, covers agent scripts and hooks)
|
|
26
34
|
|
|
27
|
-
Each manifest maps a file path to its SHA256.
|
|
28
|
-
are registered, scripts are present and executable, and the installed version
|
|
29
|
-
matches. Result is binary: exit 0 on pass, non-zero on error. It never produces
|
|
30
|
-
warnings.
|
|
35
|
+
Each manifest maps a file path to its SHA256.
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
**On failure**, the report states this guided route, in order:
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
1. Run `plastic doctor --fix` (the Fix all / Select individually / Skip router from
|
|
40
|
+
Step 4-5 below).
|
|
41
|
+
2. If that does not resolve it, roll back to the last known-good version via
|
|
42
|
+
`plastic-rollback` (restores from the local, append-only `versions.json` ledger of
|
|
43
|
+
versions actually run).
|
|
44
|
+
3. Optionally report the issue via the feedback command (`scripts/feedback-report`,
|
|
45
|
+
backing the `plastic-feedback` skill), which composes a local report plus a
|
|
46
|
+
prefilled GitHub issue URL and never holds a credential or contacts GitHub
|
|
47
|
+
directly.
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
- `global`: checks only the global store
|
|
39
|
-
- A project slug (e.g. `--store plastic`): checks only that project's store
|
|
49
|
+
### `--store [global|<slug>]`
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
Checks the operations Plastic itself depends on in one store: QMD search reachability (scoped
|
|
52
|
+
to that store's own collection; global uses `plastic-global`, a project slug uses
|
|
53
|
+
`plastic-<slug>`), sources/chain resolution, cross-store resolution, INDEX parsing, and links
|
|
54
|
+
projection. A project slug also checks tool readiness (Serena, Enola): each is a pass whether
|
|
55
|
+
present or absent, present naming it available, absent noting it as an optional integration
|
|
56
|
+
never installed by doctor. Scope options:
|
|
57
|
+
|
|
58
|
+
- No argument (`:all`): checks all stores (global and all project stores), QMD reachability
|
|
59
|
+
unscoped across every collection.
|
|
60
|
+
- `global`: checks only the global store, QMD scoped to `plastic-global`, no tool-readiness
|
|
61
|
+
checks (code-navigation tools have no meaning against the global store).
|
|
62
|
+
- A project slug (e.g. `--store plastic`): checks only that project's store, QMD scoped to
|
|
63
|
+
`plastic-<slug>`, plus Serena/Enola readiness for that project.
|
|
64
|
+
|
|
65
|
+
Produces three-state results (pass / warn / fail) and is run per-scope at dashboard load time:
|
|
66
|
+
the global board uses `--store global`, a project board uses `--store <slug>`. **This IS the
|
|
67
|
+
load-time full project check** named by 219's doctrine: no separate mechanism exists or is
|
|
68
|
+
needed, since a project slug's scan already carries every per-project finding scoped to that
|
|
69
|
+
project alone.
|
|
43
70
|
|
|
44
71
|
### Full doctor (no flag)
|
|
45
72
|
|
|
46
|
-
Runs
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
Runs the install-wide surface: agent registration, core files (including config-honoring
|
|
74
|
+
drift), manifest sync is core-only and not part of this run, deprecation checks, config-ask
|
|
75
|
+
checks, install-integrity checks, skill-lint (advisory), QMD reachability (unscoped, every
|
|
76
|
+
collection), and the global store's own conventions/done-signals content. **Never carries a
|
|
77
|
+
per-project finding**; that is `--store <slug>`'s job (see above). This is what
|
|
78
|
+
`/plastic-doctor` invokes, and it also runs automatically after every `plastic-update`
|
|
79
|
+
(informational, does not block or revert the update).
|
|
49
80
|
|
|
50
81
|
## When to Use
|
|
51
82
|
|
|
@@ -125,6 +156,7 @@ Use the `fix_hint` value to determine the correct action:
|
|
|
125
156
|
| "Inject the missing required frontmatter field(s)" | Edit the intent's `{ID}--{slug}.md` frontmatter to add the missing key (e.g. `chain: []`) without touching other keys |
|
|
126
157
|
| "Run: provision-project-store {slug}" | Run `provision-project-store <slug>` (or invoke the `plastic-store-provisioning` skill) to create the missing store |
|
|
127
158
|
| "Re-run installer" | Run `npx -y @zalom/plastic@<channel> install --agent <agent>` (channel: -alpha->@alpha, -beta->@beta, else @latest) |
|
|
159
|
+
| "Run the Plastic installer to bootstrap the store" | Run `npx -y @zalom/plastic@<channel> install --agent <agent>` (channel: -alpha->@alpha, -beta->@beta, else @latest) to restore the global store's plastic_home directory or INDEX.md |
|
|
128
160
|
| "Dispatch plastic-store-curating ... revisions.md ..." | Invoke the `plastic-store-curating` (or the agent) to relocate the flagged section or ref into the intent's `revisions.md` via move-and-record (one dated, `[rule: <tag>]`-tagged entry per item), per PLASTIC.md > Structural maintenance and revisions.md. For a missing required section, restore or reproject it instead. |
|
|
129
161
|
| "Run scripts/project-links ... PRESERVES ... --drop-unbacked-links" | Run `ruby ~/.plastic/scripts/maintenance-run --tool project-links --intent <id> --apply` for the one flagged id (never run bare `project-links` against a real store outside the rare owner-approved batch exception, D2) |
|
|
130
162
|
|
|
@@ -52,9 +52,18 @@ for orchestrator-owned or completion-tracking items.
|
|
|
52
52
|
the gate.
|
|
53
53
|
2. Confirm every acceptance criterion in spec.md is verifiable (tests pass,
|
|
54
54
|
or the manual check described in its HOW line was actually run).
|
|
55
|
-
3.
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
3. The structure gate (intent 222) now enforces this: `scripts/end-intent`
|
|
56
|
+
refuses with exit 6 and names the exact unchecked box (or any other
|
|
57
|
+
structural gap: intent-file validity, lifecycle-artifact presence,
|
|
58
|
+
`## Links` projection). On a refusal, fix via the OWNING tool, never a
|
|
59
|
+
hand edit of the check's own output:
|
|
60
|
+
- checklist/outcome content - finish it yourself, the same as before.
|
|
61
|
+
- links projection - `scripts/project-links --intent <id> --apply` via
|
|
62
|
+
`maintenance-run`.
|
|
63
|
+
- a savepoint issue - advisory only (WARN, never blocks): run
|
|
64
|
+
`plastic-intent-savepoint` to rebuild via `Bridge.rebuild_savepoint` if
|
|
65
|
+
you want it clean, but it never refuses the close on its own.
|
|
66
|
+
Then re-run `scripts/end-intent`.
|
|
58
67
|
|
|
59
68
|
### Step 1-5. Run `scripts/end-intent`
|
|
60
69
|
|
|
@@ -78,9 +87,10 @@ ruby ~/.plastic/scripts/end-intent \
|
|
|
78
87
|
--index-note "<rich Completed/Abandoned entry description>"
|
|
79
88
|
```
|
|
80
89
|
|
|
81
|
-
This does all of steps 1-5 in order: guards outcome.md (
|
|
82
|
-
still-placeholder
|
|
83
|
-
|
|
90
|
+
This does all of steps 1-5 in order: guards outcome.md (a missing or
|
|
91
|
+
still-placeholder file is caught by the structure gate first and exits 6; a
|
|
92
|
+
wrong-disposition file exits 2; either way it authors nothing), stamps the
|
|
93
|
+
intent file's `## Outcome` section, moves the INDEX.md
|
|
84
94
|
line from `## Active` to `## Completed` or `## Abandoned` (dated today,
|
|
85
95
|
idempotent, accepting either a real em dash or a plain hyphen as the id/
|
|
86
96
|
title separator on read while always emitting the real em dash on write)
|
package/templates/agents.md
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
You are working on this project as part of a strategic intent. Your governing intent is tracked in the global Plastic store at `~/.plastic/store/`.
|
|
9
9
|
|
|
10
|
+
## Tools
|
|
11
|
+
|
|
12
|
+
Plastic detects the tools it can use to work faster on this project: QMD (search this
|
|
13
|
+
project's intent store instead of grep), and Serena or Enola (symbolic code navigation
|
|
14
|
+
instead of grep). None is required. Whichever are present, their readiness is checked every
|
|
15
|
+
time this project loads (`doctor --store <slug>`), so working through Plastic on this project
|
|
16
|
+
means preferring a detected tool's own search or navigation over a generic file scan.
|
|
17
|
+
|
|
10
18
|
## How to Work
|
|
11
19
|
|
|
12
20
|
1. **Check active tactical intents** in `.plastic/store/` — these are your current tasks
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# encoding: UTF-8
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Intent 170a - A2 cutoff amnesty for the legacy savepoint Done-bookend gap.
|
|
5
|
-
#
|
|
6
|
-
# Frozen 2026-07-10. Pre-161 terminal intents predate
|
|
7
|
-
# Bridge.append_terminal_savepoint, so their savepoint.md never got a
|
|
8
|
-
# `Done delivered|abandoned` line. This list grandfathers exactly those,
|
|
9
|
-
# keyed by store scope plus intent id, so doctor's signals_complete check
|
|
10
|
-
# stops counting them as gaps. Every intent NOT on this list still warns if
|
|
11
|
-
# its savepoint lacks the Done bookend, including any new terminal intent
|
|
12
|
-
# going forward. This is a frozen historical snapshot: it must never grow.
|
|
13
|
-
# Regenerating it requires re-running the exact predicate below against the
|
|
14
|
-
# real store and reviewing the diff, never appending ad hoc.
|
|
15
|
-
#
|
|
16
|
-
# Predicate used to build this list (2026-07-10): for each store in
|
|
17
|
-
# Doctor#done_signal_stores(nil), each dir in index_sections_by_dir(index),
|
|
18
|
-
# terminal = the dir's INDEX section is Completed or Abandoned, gap =
|
|
19
|
-
# savepoint.md exists AND does not match
|
|
20
|
-
# /\bDone\b.*\b(delivered|abandoned)\b/.
|
|
21
|
-
#
|
|
22
|
-
# This does NOT grandfather the separate outcome.md completeness gap
|
|
23
|
-
# (checked independently at scripts/doctor.rb:603-607); some of these ids
|
|
24
|
-
# may still warn on that axis.
|
|
25
|
-
module LegacyBookendAmnesty
|
|
26
|
-
LIST = {
|
|
27
|
-
"global" => %w[1a 1a2 23].freeze,
|
|
28
|
-
"project:plastic" => %w[
|
|
29
|
-
1 11 121a 124 128 13 13b 15 158 158a 159 160 163 1a 1b1a3 22 30a1a 34
|
|
30
|
-
36a 36a1 37 38 39 45 45a 49 4a 4a1 4a1c1 50 52 54 55 56 58 59 60b 65
|
|
31
|
-
66 66a 66b 66c 66c1 67 68 71 72 73b 73c 73c1 73c2 73c3 74 77 79 80 83
|
|
32
|
-
84 85a 9
|
|
33
|
-
].freeze,
|
|
34
|
-
}.freeze
|
|
35
|
-
end
|