@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.
@@ -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
- # --- outcome.md guard (D2 step 1a) ------------------------------------------
151
+ # --- structure gate helper (intent 222) -------------------------------------
147
152
  #
148
- # Returns nil when the guard PASSES, or a reason String to refuse with exit 2.
149
- # Read-only: authors nothing regardless of outcome. Refuses a missing file, a
150
- # still-placeholder file (first line is the sentinel), or a disposition that
151
- # does not exactly equal the requested one (this is what naturally refuses a
152
- # freshly scaffolded outcome.md, whose disposition is the literal
153
- # "delivered|abandoned" string, not one of the two real values).
154
- def outcome_guard_reason(intent_dir, disposition)
155
- path = File.join(intent_dir, "outcome.md")
156
- return "outcome.md is missing at #{path}" unless File.exist?(path)
157
-
158
- content = File.read(path)
159
- first_line = content.each_line.first.to_s.chomp
160
- if first_line == Bridge::PLACEHOLDER_SENTINEL
161
- return "outcome.md is still the scaffold placeholder (first line is the sentinel)"
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
- fm = IntentValidator.parse_frontmatter_text(content)
165
- actual = fm.is_a?(Hash) ? fm["disposition"] : nil
166
- unless DISPOSITIONS.include?(actual) && actual == disposition
167
- return "outcome.md frontmatter disposition is #{actual.inspect}, expected #{disposition.inspect}"
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
- nil
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
- reason = outcome_guard_reason(intent_dir, disposition)
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/legacy_bookend_amnesty.rb" => "scripts/lib/legacy_bookend_amnesty.rb",
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, "merge", "--quiet", "--no-ff", "-m", "Merge #{branch_name} into #{base}", branch_name)
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
@@ -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
 
@@ -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`). Only Step 6 (QMD reindex, async,
347
- last) remains a separate action after this call succeeds. Never leave an orphaned
348
- worktree; run `git worktree prune` on a stale reference. If any of this ever needs to
349
- change, change `plastic-intent-ending`, not this skill.
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.
@@ -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, manifest-backed)
19
+ ### `--core` (binary, operational-readiness only)
20
20
 
21
- Verifies that every core file is present and content-matches what the installed
22
- version shipped. It checks two install manifests:
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. The core check also confirms hooks
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
- ### `--store [global|<slug>]`
37
+ **On failure**, the report states this guided route, in order:
33
38
 
34
- Checks store state: intents are well-formed, INDEX sections are present, conventions
35
- are followed, and links are valid. Scope options:
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
- - No argument: checks all stores (global and all projects)
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
- Produces three-state results (pass / warn / fail) and is run per-scope at dashboard
42
- load time: the global board uses `--store global`, a project board uses `--store <slug>`.
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 core plus all store checks plus deprecation checks. This is what `/plastic-doctor`
47
- invokes. It also runs automatically after every `plastic-update` (informational,
48
- does not block or revert the update).
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. If checklist.md still has an unchecked box, stop here. Finish it first
56
- (tick it if the described work is actually done, or do the remaining
57
- work); do not attempt outcome.md and fight the gate's deny.
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 (refuses a missing,
82
- still-placeholder, or wrong-disposition file with exit 2 and authors
83
- nothing), stamps the intent file's `## Outcome` section, moves the INDEX.md
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)
@@ -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