@zalom/plastic 1.0.0-beta.25 → 1.0.0-beta.27

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 CHANGED
@@ -362,9 +362,64 @@ each station.
362
362
  | Why | `spec.md` | owner writes refresh the lease (lock file mtime heartbeat) | gate-check requires the intent file with `## Intent` before spec.md; lock-gate admits only the owner or a delegate | savepoint `Why started`, `Why spec.md created` |
363
363
  | How | `plan.md`, `actions/`, `checklist.md` | heartbeat on writes; the code gate stays closed until plan.md plus checklist.md exist | gate-check requires spec.md before plan.md, and plan.md plus actions/ before checklist.md | savepoint `How started`, `How plan.md created`, `How checklist.md created`, `Exec started` |
364
364
  | Exec | code on the intent branch, checklist checked off | heartbeat; code edits confined to the provisioned worktree; delegates write under the owner's lock; bash, interpreter, and MCP writes gated the same way | code-gate, worktree-gate, bash-gate, lock-gate | checklist boxes; savepoint milestones |
365
- | End (done) | `outcome.md`, INDEX moves to Completed | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`, only then is the bridge purge-eligible | gate-check blocks outcome.md while checklist items are unchecked | savepoint `Done delivered` (or `abandoned`); takeover audits, if any, remain in savepoint.md |
365
+ | End (done) | mandatory `outcome.md` (`disposition: delivered\|abandoned`), INDEX moves to Completed or Abandoned | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`, then the bridge is purge-eligible, and the QMD reindex runs LAST (after purge) | gate-check blocks outcome.md while checklist items are unchecked | savepoint `Done delivered` (or `abandoned`); takeover audits, if any, remain in savepoint.md |
366
366
  | Maintenance (any stage) | `revisions.md` move-and-record entries | future `maintenance.lock` (short TTL), mutually exclusive with `delivery.lock` in either direction; 108 ships the schema seam only, the implementation follows intent 93 in a chained intent | acquisition refuses while the other lock type is fresh; a terminal intent with no lock held is read-only | dated, rule-tagged `revisions.md` entry; savepoint untouched |
367
367
 
368
+ ### What "intent done" means (intent 93)
369
+
370
+ Done is one law with three signals, and they must agree. INDEX `## Completed` /
371
+ `## Abandoned` is the single canonical terminal marker: it is the store-wide ledger a fresh
372
+ session reads first, so it wins on any conflict. `outcome.md` is the "deliverable exists"
373
+ signal, and the savepoint `Done delivered|abandoned` line is the audit echo. All three must
374
+ agree; when they disagree, INDEX is authoritative and `doctor` flags the mismatch (the
375
+ `done_signals` check: `outcome.md` real but still under `## Active`, or terminal without a
376
+ real `outcome.md`, or a terminal intent whose savepoint carries no `Done` line).
377
+
378
+ `outcome.md` is mandatory at every terminal transition, delivered and abandoned alike. It
379
+ self-declares its disposition through a `disposition: delivered|abandoned` frontmatter
380
+ header. The delivered path authors it with the result; the abandoned path authors it with
381
+ the abandonment reason and no longer leaves the scaffolded placeholder sentinel in place.
382
+
383
+ The canonical End tail runs in this order, and the QMD reindex is always LAST, after the
384
+ purge: `outcome.md -> INDEX terminal -> savepoint Done -> commit -> disarm (Worktree.release
385
+ -> Lock.release -> purge) -> QMD reindex`. Running the reindex last keeps the index from
386
+ ever referencing a bridge or lock that disarm is about to remove.
387
+
388
+ The post-done access window is lock-bounded: `[INDEX terminal -> Lock.release]`. Through it
389
+ the completing session keeps full read and write access to the terminal directory and no
390
+ purge can fire (108's lock-held keep-guard keeps the bridge while `delivery.lock` exists).
391
+ Once the lock is released the window closes: the bridge becomes purge-eligible and the
392
+ directory is frozen. A crash mid-tail is recovered by stale-lock reclaim plus finishing the
393
+ tail; `doctor` surfaces this as a "stalled completion" (terminal in INDEX but the lock is
394
+ still present or stale). Finishing the tail is FINISHING a completion, never a reactivation:
395
+ a done intent is never moved back to `## Active`.
396
+
397
+ Terminal immutability (the contract intent 112 enforces): a terminal directory is writable
398
+ ONLY while a lock is held. The delivery lock covers the completing session's End tail up to
399
+ `Lock.release`; the maintenance lock covers sanctioned structural move-and-record edits
400
+ after. Terminal with no lock held is frozen. There are only two locks in the system,
401
+ delivery and maintenance (108 D11). This governs WRITES only: reads of a terminal intent are
402
+ always allowed and unbounded (curator reindex, dashboards, and future intents that reference
403
+ its id or chain), so a done intent stays fully readable forever. Intent 93 states this rule;
404
+ intent 112 builds the gate that enforces it.
405
+
406
+ Fail-safe lock doctrine (the contract intent 111 implements): the lock system never traps a
407
+ session or burns credits. When a gate cannot verify lock integrity it fails open, degrading
408
+ to advisory (warn) rather than hard-blocking. Repair is orchestrator-driven: on a lock-issue
409
+ signal the orchestrator inspects and repairs the lock automatically, and the human
410
+ `plastic-lock` command is a fallback path, not the trigger. Intent 93 states this doctrine;
411
+ intent 111 builds the fail-open behavior, the lock-liveness surface, the lock-issue message,
412
+ and the auto-repair.
413
+
414
+ Scope split. Intent 93 ships doctrine plus the low-risk reconciliation that needs no new
415
+ lock: the canonical done-marker and three-signal reconciliation, the mandatory `outcome.md`
416
+ plus `disposition` header at both terminals, the End tail with the reindex moved last, the
417
+ `done_signals` doctor check (three-signal agreement plus stalled-completion detection), and
418
+ the lock-bounded post-done window with its keep-guard test. Intent 111 owns the lock
419
+ liveness surface, the lock-issue message, orchestrator auto-repair, and the fail-open
420
+ behavior itself. Intent 112 owns the maintenance lock and the immutability gate (it inherits
421
+ fail-open from 111). Intent 4a1b1 owns deep agent stuck-detection and is not superseded.
422
+
368
423
  ## Deprecation Process
369
424
 
370
425
  Deprecations live in `deprecations.yml` and are shown at SessionStart. While Plastic is
@@ -32,7 +32,10 @@ You are the Plastic Intent Curator. Your role is to maintain the health and navi
32
32
  2. Read `~/.plastic/INDEX.md` (or project INDEX.md) to understand current organization
33
33
  3. Compare: are there intents not in any cluster? Missing from Active/Completed/Abandoned? Status mismatches?
34
34
  4. Make targeted edits to INDEX.md and intent frontmatter/links
35
- 5. On a terminal-state transition: whenever you move an intent to Completed OR Abandoned, refresh the QMD index for that store so the new outcome (or abandonment rationale) is searchable. This is mandatory on any terminal-state move and a no-op when QMD is absent, and it runs in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`
35
+ 5. On a terminal-state transition (Completed OR Abandoned), run the closing acts in the canonical End-tail order (see PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`):
36
+ 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).
37
+ b. Stamp the terminal savepoint bookend (intent 81) recording the disposition. Idempotent.
38
+ c. Refresh the QMD index for that store LAST, after the terminal move, outcome.md, and savepoint, so the new outcome (or abandonment rationale) is searchable. This is mandatory on any terminal-state move and a no-op when QMD is absent, and it runs in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`
36
39
  6. Structural maintenance is move-and-record: 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`). 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.md.
37
40
  7. Report what you changed
38
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-beta.25",
3
+ "version": "1.0.0-beta.27",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/doctor.rb CHANGED
@@ -21,6 +21,8 @@ require_relative "lib/graph_rebuild"
21
21
  require_relative "lib/links_projection"
22
22
  require_relative "lib/links_section"
23
23
  require_relative "lib/hook_registry"
24
+ require_relative "lib/lock"
25
+ require_relative "lib/bridge"
24
26
 
25
27
  # Diagnostic engine, instantiable with an injected store/agent map so tests can
26
28
  # run it hermetically (no eval, no global-constant rewriting).
@@ -505,6 +507,184 @@ class Doctor
505
507
  checks
506
508
  end
507
509
 
510
+ # --- Check: the three done-signals agree + stalled completion (intent 93) ---
511
+ #
512
+ # "Done" is one law with three signals that MUST agree: INDEX
513
+ # `## Completed`/`## Abandoned` is the single canonical terminal marker;
514
+ # `outcome.md` is the "deliverable exists" signal (real, non-placeholder, with a
515
+ # `disposition:` header); the savepoint `Done` line is the audit echo. INDEX
516
+ # wins on conflict. This check flags any disagreement, and separately surfaces a
517
+ # "stalled completion": an intent that is terminal in INDEX but whose End tail
518
+ # never released its `delivery.lock` (the post-done window never closed). Read
519
+ # only, dependency-light: it uses INDEX parsing, file presence, the placeholder
520
+ # sentinel, and `Lock.fresh?` — no new lock, no 111 lock-liveness surface.
521
+
522
+ # For one store's INDEX.md, map each referenced intent directory name to the
523
+ # `## ...` section(s) it appears under: { "<id>--<slug>" => ["Active", ...] }.
524
+ def index_sections_by_dir(index_path)
525
+ sections = Hash.new { |h, k| h[k] = [] }
526
+ return sections unless File.exist?(index_path)
527
+
528
+ current = nil
529
+ File.foreach(index_path) do |line|
530
+ if (m = line.match(/^##\s+(.+?)\s*$/))
531
+ current = m[1]
532
+ next
533
+ end
534
+ next unless current
535
+
536
+ line.scan(%r{store/([\w][\w.-]*?)(?:/|\))}) do |(dirname)|
537
+ sections[dirname] << current unless sections[dirname].include?(current)
538
+ end
539
+ end
540
+ sections
541
+ end
542
+
543
+ # Stores to reconcile: the global store plus each project store, filtered by
544
+ # `scopes` (nil = all) exactly like check_conventions.
545
+ def done_signal_stores(scopes)
546
+ stores = []
547
+
548
+ if scopes.nil? || scopes.include?("global")
549
+ stores << {
550
+ scope: "global",
551
+ index: File.join(plastic_home, "INDEX.md"),
552
+ store_dir: File.join(plastic_home, "store"),
553
+ }
554
+ end
555
+
556
+ projects_root = File.join(plastic_home, "projects")
557
+ if File.directory?(projects_root)
558
+ Dir.children(projects_root).sort.each do |project|
559
+ scope = "project:#{project}"
560
+ next unless scopes.nil? || scopes.include?(scope)
561
+
562
+ store_dir = File.join(projects_root, project, "store")
563
+ next unless File.directory?(store_dir)
564
+
565
+ stores << {
566
+ scope: scope,
567
+ index: File.join(projects_root, project, "INDEX.md"),
568
+ store_dir: store_dir,
569
+ }
570
+ end
571
+ end
572
+
573
+ stores
574
+ end
575
+
576
+ def check_done_signals(scopes: nil)
577
+ conflicts = [] # canonical INDEX-wins disagreement (fail)
578
+ gaps = [] # terminal completeness gaps on immutable/legacy history (warn)
579
+ stalled = [] # terminal but End tail unfinished (warn)
580
+
581
+ done_signal_stores(scopes).each do |store|
582
+ index_sections_by_dir(store[:index]).each do |dirname, in_sections|
583
+ dir = File.join(store[:store_dir], dirname)
584
+ next unless File.directory?(dir)
585
+
586
+ terminal = (in_sections & ["Completed", "Abandoned"]).any?
587
+ active = in_sections.include?("Active") && !terminal
588
+ outcome = File.join(dir, "outcome.md")
589
+ outcome_real = Bridge.stage_file_present?(outcome)
590
+ label = "#{store[:scope]} store/#{dirname}"
591
+
592
+ # HARD conflict: the deliverable exists but INDEX still says Active. This
593
+ # is the one true INDEX-wins disagreement, so it stays a fail.
594
+ if active && outcome_real
595
+ conflicts << "#{label}: outcome.md is real but the intent is still under ## Active " \
596
+ "(INDEX is canonical — move it to its terminal section or revert outcome.md)"
597
+ end
598
+
599
+ # Completeness gap (warn, not fail): a terminal intent whose outcome.md is
600
+ # missing/placeholder. Legacy terminal intents predate this convention and
601
+ # are immutable, so this is advisory, never breakage.
602
+ if terminal && !outcome_real
603
+ state = File.exist?(outcome) ? "still a placeholder" : "missing"
604
+ gaps << "#{label}: terminal in INDEX but outcome.md is #{state} " \
605
+ "(author outcome.md with the disposition header)"
606
+ end
607
+
608
+ next unless terminal
609
+
610
+ # Audit echo (weakest signal, D1): the savepoint should carry a
611
+ # `Done delivered|abandoned` line. Missing on legacy history -> advisory gap.
612
+ savepoint = File.join(dir, "savepoint.md")
613
+ if File.exist?(savepoint) && File.read(savepoint) !~ /\bDone\b.*\b(delivered|abandoned)\b/
614
+ gaps << "#{label}: terminal in INDEX but savepoint.md has no " \
615
+ "`Done delivered|abandoned` line (audit echo missing)"
616
+ end
617
+
618
+ # Stalled completion: the End tail never released the delivery lock, so the
619
+ # post-done window `[INDEX terminal -> Lock.release]` never closed.
620
+ if File.exist?(Lock.path(dir))
621
+ note = Lock.fresh?(dir) ? "delivery.lock still present (post-done window not closed)"
622
+ : "delivery.lock is present and STALE"
623
+ stalled << "#{label}: #{note} — the End tail did not finish"
624
+ end
625
+ end
626
+ end
627
+
628
+ checks = []
629
+
630
+ # signals_agree: only the canonical INDEX-wins conflict is a hard fail, so
631
+ # `doctor` never goes red on immutable legacy terminal intents.
632
+ if conflicts.empty?
633
+ checks << check(
634
+ category: "done_signals", name: "signals_agree", status: "pass",
635
+ message: "No done-signal conflicts (no intent has a real outcome.md while still Active)"
636
+ )
637
+ else
638
+ checks << check(
639
+ category: "done_signals", name: "signals_agree", status: "fail",
640
+ message: "#{conflicts.size} done-signal conflict#{conflicts.size == 1 ? "" : "s"} " \
641
+ "(INDEX ## Completed/## Abandoned is canonical; a real outcome.md must not stay Active)",
642
+ details: conflicts, fixable: true,
643
+ fix_hint: "Reconcile to INDEX (canonical): move the intent to its terminal section, or revert " \
644
+ "outcome.md to a placeholder. Deliverable-exists but still-Active is the one done-signal " \
645
+ "state that must never persist."
646
+ )
647
+ end
648
+
649
+ # signals_complete: terminal completeness gaps are advisory (warn), because
650
+ # completed intents are immutable and legacy history predates this convention.
651
+ if gaps.empty?
652
+ checks << check(
653
+ category: "done_signals", name: "signals_complete", status: "pass",
654
+ message: "Every terminal intent carries a real outcome.md and a Done savepoint echo"
655
+ )
656
+ else
657
+ checks << check(
658
+ category: "done_signals", name: "signals_complete", status: "warn",
659
+ message: "#{gaps.size} terminal completeness gap#{gaps.size == 1 ? "" : "s"} " \
660
+ "(outcome.md or the savepoint Done echo is missing; advisory on immutable history)",
661
+ details: gaps, fixable: true,
662
+ fix_hint: "For live terminals, author a real outcome.md with the `disposition:` header via the " \
663
+ "completion/abandon path (plastic-auto or plastic-intent-curator) and stamp the terminal " \
664
+ "savepoint. Legacy terminal intents are immutable, so pre-convention gaps stay advisory."
665
+ )
666
+ end
667
+
668
+ if stalled.empty?
669
+ checks << check(
670
+ category: "done_signals", name: "stalled_completion", status: "pass",
671
+ message: "No stalled completions (every terminal intent released its delivery lock)"
672
+ )
673
+ else
674
+ checks << check(
675
+ category: "done_signals", name: "stalled_completion", status: "warn",
676
+ message: "#{stalled.size} stalled completion#{stalled.size == 1 ? "" : "s"} " \
677
+ "(terminal in INDEX but the End tail did not finish)",
678
+ details: stalled, fixable: true,
679
+ fix_hint: "Finish the End tail via stale-lock reclaim: `plastic-lock reclaim`, then complete the " \
680
+ "tail (Worktree.release -> Lock.release -> purge -> QMD reindex last). This FINISHES a " \
681
+ "completion; it is NOT a reactivation of a done intent."
682
+ )
683
+ end
684
+
685
+ checks
686
+ end
687
+
508
688
  # Build the cross-store node maps (basename + label per store) + relocation map
509
689
  # from ALL stores, then for every intent compute its canonical `## Links`
510
690
  # projection and flag any whose ACTUAL `## Links` section differs (membership or
@@ -839,9 +1019,9 @@ class Doctor
839
1019
  groups = group.is_a?(Array) ? group : [group]
840
1020
  live = settings.dig("hooks", event) || []
841
1021
  groups.each do |g|
842
- match = live.find { |h| h.is_a?(Hash) && h["matcher"] == g["matcher"] }
1022
+ matches = live.select { |h| h.is_a?(Hash) && h["matcher"] == g["matcher"] }
843
1023
  wanted = g["hooks"].map { |h| h["command"] }
844
- got = match ? Array(match["hooks"]).map { |h| h["command"] } : []
1024
+ got = matches.flat_map { |m| Array(m["hooks"]).map { |h| h["command"] } }
845
1025
  missing = wanted - got
846
1026
  diffs << "#{event}[#{g['matcher']}] missing: #{missing.join(', ')}" unless missing.empty?
847
1027
  end
@@ -1457,6 +1637,7 @@ class Doctor
1457
1637
  all_checks += check_project_stores
1458
1638
  all_checks += check_deprecations
1459
1639
  all_checks += check_qmd
1640
+ all_checks += check_done_signals
1460
1641
 
1461
1642
  summarize(all_checks, agent_key)
1462
1643
  end
@@ -1483,9 +1664,9 @@ class Doctor
1483
1664
  all_checks =
1484
1665
  case store
1485
1666
  when :all
1486
- check_global_store + check_project_stores + check_conventions
1667
+ check_global_store + check_project_stores + check_conventions + check_done_signals
1487
1668
  when :global
1488
- check_global_store + check_conventions(scopes: ["global"])
1669
+ check_global_store + check_conventions(scopes: ["global"]) + check_done_signals(scopes: ["global"])
1489
1670
  else
1490
1671
  all_checks_for_project_slug(store)
1491
1672
  end
@@ -1508,7 +1689,8 @@ class Doctor
1508
1689
  end
1509
1690
 
1510
1691
  check_project_store(slug, projects[slug]) +
1511
- check_conventions(scopes: ["project:#{slug}"])
1692
+ check_conventions(scopes: ["project:#{slug}"]) +
1693
+ check_done_signals(scopes: ["project:#{slug}"])
1512
1694
  end
1513
1695
 
1514
1696
  # Roll a list of checks up into the standard result envelope.
@@ -209,7 +209,10 @@ During initial project creation, all decisions are non-destructive by definition
209
209
  ## Completion
210
210
 
211
211
  1. Verify all checklist items are checked
212
- 2. Write `outcome.md` with detailed results
212
+ 2. Write `outcome.md` with detailed results, from `${CLAUDE_PLUGIN_ROOT}/templates/outcome.md`.
213
+ Set the frontmatter `disposition: delivered` (this is the delivered terminal). `outcome.md`
214
+ is mandatory at every terminal and self-declares its disposition (see the canonical done-marker
215
+ and End tail in PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`).
213
216
  3. Write `## Outcome` summary in the intent file (1-2 sentences)
214
217
  4. **Release (if configured)**
215
218
  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).
@@ -236,14 +239,7 @@ During initial project creation, all decisions are non-destructive by definition
236
239
  ```
237
240
  (Use `"abandoned"` instead when the intent is being moved to `## Abandoned`.) Idempotent.
238
241
  7. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: deliver intent <ID> — <name>"`
239
- 8. On completion, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent).
240
- It runs in the background so it never blocks the turn:
241
- ```bash
242
- ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async
243
- ```
244
- Completion is the lifecycle event that keeps the search index fresh. `<store-root>` is the
245
- store that holds this intent (the global store or the project store).
246
- 9. Disarm the lifecycle gate (auto delivery is finished):
242
+ 8. Disarm the lifecycle gate (auto delivery is finished):
247
243
  ```bash
248
244
  ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.disarm_auto(ENV["CLAUDE_CODE_SESSION_ID"])'
249
245
  ```
@@ -266,6 +262,16 @@ During initial project creation, all decisions are non-destructive by definition
266
262
  removed, so the integrated work is not lost. It does this with `Worktree.finish(bridge_data,
267
263
  merge: true)` (merge-then-remove). Never leave an orphaned worktree, and run `git worktree
268
264
  prune` if you hit a stale reference.
265
+ 9. QMD reindex LAST (canonical End tail). AFTER disarm has released the worktrees, cleared the
266
+ `delivery.lock`, and purged the bridge, refresh the QMD search index for this store (no-op when
267
+ QMD is absent). It runs in the background so it never blocks the turn:
268
+ ```bash
269
+ ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async
270
+ ```
271
+ Completion is the lifecycle event that keeps the search index fresh. `<store-root>` is the store
272
+ that holds this intent (the global store or the project store). The reindex is the LAST End-tail
273
+ step, run after purge, so the index never references a bridge or lock that is about to disappear
274
+ (see PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`).
269
275
  10. Notify user: "Intent [ID] — [name] delivered. [1-2 sentence summary]. See outcome.md for details."
270
276
 
271
277
  ## Error Handling
@@ -70,12 +70,12 @@ After all tasks complete, dispatch a final reviewer for the entire implementatio
70
70
  ### Step 4: Update Intent and Complete
71
71
  Capture observations in `## Insights`. When ALL checklist items are checked:
72
72
 
73
- 1. Write `outcome.md` with detailed results in the intent directory, using the `${CLAUDE_PLUGIN_ROOT}/templates/outcome.md` form
73
+ 1. Write `outcome.md` with detailed results in the intent directory, using the `${CLAUDE_PLUGIN_ROOT}/templates/outcome.md` form. Set the frontmatter `disposition: delivered` (the delivered terminal). `outcome.md` is mandatory at every terminal and self-declares its disposition (canonical done-marker in PLASTIC.md).
74
74
  2. Write `## Outcome` summary in the intent file (1-2 sentences)
75
75
  3. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
76
76
  4. Update cluster entries to show `_(completed)_`
77
77
  5. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: complete intent <ID> — <name>"`
78
- 6. On completion, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent), running in the background so it never blocks the turn: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`. Completion is the lifecycle event that keeps the search index fresh.
78
+ 6. QMD reindex LAST (canonical End tail). As the final End-tail step, after the terminal move and any disarm, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent), running in the background so it never blocks the turn: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`. Completion is the lifecycle event that keeps the search index fresh, and the reindex runs last so the index never references a bridge or lock that is about to disappear (see PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`).
79
79
 
80
80
  **This is NOT optional.** An intent with all checklist items done but no Outcome is a broken state. Complete the intent immediately — do not leave it for later.
81
81
 
@@ -96,12 +96,12 @@ For each task:
96
96
  ### Step 3: Update Intent and Complete
97
97
  Capture observations in `## Insights`. When ALL checklist items are checked:
98
98
 
99
- 1. Write `outcome.md` with detailed results in the intent directory, using the `${CLAUDE_PLUGIN_ROOT}/templates/outcome.md` form
99
+ 1. Write `outcome.md` with detailed results in the intent directory, using the `${CLAUDE_PLUGIN_ROOT}/templates/outcome.md` form. Set the frontmatter `disposition: delivered` (the delivered terminal). `outcome.md` is mandatory at every terminal and self-declares its disposition (canonical done-marker in PLASTIC.md).
100
100
  2. Write `## Outcome` summary in the intent file (1-2 sentences)
101
101
  3. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
102
102
  4. Update cluster entries to show `_(completed)_`
103
103
  5. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: complete intent <ID> — <name>"`
104
- 6. On completion, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent), running in the background so it never blocks the turn: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`. Completion is the lifecycle event that keeps the search index fresh.
104
+ 6. QMD reindex LAST (canonical End tail). As the final End-tail step, after the terminal move and any disarm, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent), running in the background so it never blocks the turn: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`. Completion is the lifecycle event that keeps the search index fresh, and the reindex runs last so the index never references a bridge or lock that is about to disappear (see PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`).
105
105
 
106
106
  **This is NOT optional.** Complete the intent immediately when work is done.
107
107
 
@@ -38,9 +38,10 @@ The agent handles:
38
38
  - Cluster management (create, merge, rename)
39
39
  - Orphan detection
40
40
 
41
- When an intent reaches a terminal state — moved to Completed OR Abandoned — do two things as the closing act of the transfer:
41
+ When an intent reaches a terminal state — moved to Completed OR Abandoned — do these things as the closing act of the transfer, in the canonical End-tail order (see PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`):
42
42
 
43
- 1. Stamp the terminal savepoint bookend (intent 81), so the ledger's last line records the disposition: `ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.append_terminal_savepoint("<intent_dir>", "delivered")'` (use `"abandoned"` for an abandoned intent). Idempotent.
44
- 2. Refresh the QMD index for the affected store (no-op when QMD absent), running in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`.
43
+ 1. 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).
44
+ 2. Stamp the terminal savepoint bookend (intent 81), so the ledger's last line records the disposition: `ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.append_terminal_savepoint("<intent_dir>", "delivered")'` (use `"abandoned"` for an abandoned intent). Idempotent.
45
+ 3. Refresh the QMD index for the affected store LAST, after the terminal move and savepoint (no-op when QMD absent), running in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`.
45
46
 
46
47
  After the agent completes, report what changed.
@@ -41,7 +41,11 @@ Topic-based groupings. Manually curated. Create a new cluster when 3+ intents sh
41
41
  ### Completed
42
42
  All completed intents with dates. Links preserved, never deleted.
43
43
 
44
- When you move an intent INTO Completed or Abandoned, stamp the terminal savepoint bookend as the closing act of the transfer (intent 81), so the ledger's last line records the disposition: `ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.append_terminal_savepoint("<intent_dir>", "delivered")'` (use `"abandoned"` for an abandoned intent). Idempotent.
44
+ When you move an intent INTO Completed or Abandoned, run the closing acts of the transfer in the canonical End-tail order (see PLASTIC.md `## Delivery Isolation and the Single-Owner Lock`):
45
+
46
+ 1. 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).
47
+ 2. Stamp the terminal savepoint bookend (intent 81), so the ledger's last line records the disposition: `ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.append_terminal_savepoint("<intent_dir>", "delivered")'` (use `"abandoned"` for an abandoned intent). Idempotent.
48
+ 3. Refresh the QMD index for that store LAST, after the terminal move and savepoint (no-op when QMD is absent), in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`.
45
49
 
46
50
  ## Workflow
47
51
 
@@ -1,3 +1,6 @@
1
+ ---
2
+ disposition: delivered|abandoned
3
+ ---
1
4
  # Outcome: <intent name>
2
5
 
3
6
  ## Summary