@zalom/plastic 1.0.0-alpha.19 → 1.0.0-alpha.20

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
@@ -19,7 +19,7 @@ store/
19
19
  outcome.md # optional — detailed result (Exec deliverable)
20
20
  actions/ # optional — individual work items
21
21
  resources/ # optional — research, references, screenshots, diagrams
22
- savepoint.md # optional — session state for resume
22
+ savepoint.md # optional — deterministic cycle-step ledger (auto-written)
23
23
  ```
24
24
 
25
25
  Lifecycle files (`spec.md`, `plan.md`, `checklist.md`, `outcome.md`) have defined
@@ -66,9 +66,18 @@ The connection: an intent's `## Insights` feeds the Coordinator's Observe phase.
66
66
  | **How** | Planning | `plan.md` + `actions/` + `checklist.md` | `plastic-writing-plans` |
67
67
  | **Exec** | Execution | `outcome.md` | `plastic-executing-plan` |
68
68
 
69
- `## Insights` — append-only work log captured throughout ALL stages.
69
+ `## Insights` — append-only work log captured throughout ALL stages. **Append-only means
70
+ newest entry at the bottom; never prepend.** This ordering is a hard convention: Insights
71
+ are the semantic trace of an intent, and a consistent newest-last order keeps that trace
72
+ readable across every intent.
70
73
  For full lifecycle detail, the skills in the Detail column have references/.
71
74
 
75
+ `savepoint.md` — a deterministic, append-only ledger of cycle-step milestones (one line per
76
+ lifecycle boundary, newest at the bottom), written automatically by the gate hook. It is
77
+ sugar on top of the conventions, not a source of truth: state is always derivable from
78
+ files-on-disk, and the ledger is rebuildable. It exists so a resuming agent reads the cycle's
79
+ succession at a glance (last line = where we are).
80
+
72
81
  ## Gotchas
73
82
 
74
83
  - **Artifacts go in the intent directory.** Never create `docs/plans/`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-alpha.19",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -98,6 +98,14 @@ if is_stage_file || is_action_file
98
98
 
99
99
  Bridge.write(session, bridge_data)
100
100
 
101
+ # Cycle-step savepoint ledger (intent 34) — record this stage-file milestone.
102
+ # Best-effort: the ledger is derived sugar, so a failure must never block.
103
+ begin
104
+ Bridge.append_savepoint(intent_dir_abs, file_path_abs)
105
+ rescue StandardError
106
+ # ignore — rebuildable from disk
107
+ end
108
+
101
109
  # Build transition context
102
110
  stage_labels = { "what" => "What", "why" => "Why", "how" => "How", "exec" => "Exec", "done" => "Done" }
103
111
  next_hints = {
@@ -129,7 +137,14 @@ if is_stage_file || is_action_file
129
137
  puts JSON.generate(payload)
130
138
  exit 0
131
139
  else
132
- # Regular file in intent dir — just update last_activity
140
+ # Regular file in intent dir — just update last_activity. The intent file
141
+ # (What milestone) lands here, so still offer it to the savepoint ledger;
142
+ # non-milestone files self-filter to a no-op (intent 34).
143
+ begin
144
+ Bridge.append_savepoint(intent_dir_abs, file_path_abs)
145
+ rescue StandardError
146
+ # ignore — rebuildable from disk
147
+ end
133
148
  bridge_data["build"]["last_activity"] = Time.now.utc.iso8601
134
149
  Bridge.write(session, bridge_data)
135
150
  exit 0
@@ -70,6 +70,68 @@ module Bridge
70
70
  end
71
71
  end
72
72
 
73
+ # --- Cycle-step savepoint ledger (intent 34) ------------------------------
74
+ #
75
+ # savepoint.md is a deterministic, append-only, one-line-per-milestone ledger
76
+ # (newest at the bottom). It is sugar on top of the conventions: derived from
77
+ # files-on-disk, rebuildable, never a source of truth. Milestones are
78
+ # file-event boundaries only; action/resource files record nothing.
79
+
80
+ SAVEPOINT_FILE = "savepoint.md"
81
+
82
+ # Map a written filename to [stage_label, milestone_text], or nil if the file
83
+ # is not a lifecycle milestone.
84
+ def self.savepoint_milestone(intent_dir, basename)
85
+ return ["What", basename] if basename == File.basename(intent_file(intent_dir))
86
+
87
+ case basename
88
+ when "spec.md" then ["Why", "spec.md created"]
89
+ when "plan.md" then ["How", "plan.md created"]
90
+ when "checklist.md" then ["How", "checklist.md created"]
91
+ when "outcome.md" then ["Exec", "outcome.md created"]
92
+ end
93
+ end
94
+
95
+ # Milestones already recorded in the ledger (field 3 of each line).
96
+ def self.savepoint_recorded_milestones(intent_dir)
97
+ f = File.join(intent_dir, SAVEPOINT_FILE)
98
+ return [] unless File.exist?(f)
99
+ File.read(f).each_line.map do |line|
100
+ parts = line.strip.split(/\s{2,}/)
101
+ parts.length >= 3 ? parts[2] : nil
102
+ end.compact
103
+ end
104
+
105
+ # Append a milestone line for file_path if (and only if) it is a milestone
106
+ # not already recorded. Returns true when a line was written, false otherwise.
107
+ def self.append_savepoint(intent_dir, file_path, now: Time.now)
108
+ stage, milestone = savepoint_milestone(intent_dir, File.basename(file_path))
109
+ return false unless milestone
110
+ return false if savepoint_recorded_milestones(intent_dir).include?(milestone)
111
+
112
+ line = "#{now.utc.iso8601} #{stage} #{milestone}\n"
113
+ File.open(File.join(intent_dir, SAVEPOINT_FILE), "a") { |io| io.write(line) }
114
+ true
115
+ end
116
+
117
+ # Reconstruct the ledger from files on disk (timestamps from mtimes), in
118
+ # stage order, overwriting savepoint.md. Returns the number of lines written.
119
+ def self.rebuild_savepoint(intent_dir)
120
+ ordered = [
121
+ File.basename(intent_file(intent_dir)),
122
+ "spec.md", "plan.md", "checklist.md", "outcome.md",
123
+ ]
124
+ lines = ordered.filter_map do |basename|
125
+ path = File.join(intent_dir, basename)
126
+ next unless File.exist?(path)
127
+ stage, milestone = savepoint_milestone(intent_dir, basename)
128
+ next unless milestone
129
+ "#{File.mtime(path).utc.iso8601} #{stage} #{milestone}\n"
130
+ end
131
+ File.write(File.join(intent_dir, SAVEPOINT_FILE), lines.join)
132
+ lines.length
133
+ end
134
+
73
135
  def self.derive(session, intent_id:, intent_dir:, store:, name:)
74
136
  stage = derive_stage(intent_dir)
75
137
  has = has_files(intent_dir)
@@ -1,61 +1,66 @@
1
1
  ---
2
2
  name: plastic-savepoint
3
- description: Use when context is being compacted (PreCompact hook), user says "save" or "savepoint", or before ending a session. Saves the active intent's state so work can resume after /clear.
3
+ description: Use when verifying or repairing an intent's savepoint ledger, when the user says "save" or "savepoint", or when a PreCompact hook fires. The ledger is written automatically by the gate hook at each lifecycle boundary; this skill only reads, verifies, and rebuilds it.
4
4
  ---
5
5
 
6
6
  # Savepoint
7
7
 
8
- ## When to Use
9
- - PreCompact hook fires (automatic)
10
- - User says "save", "savepoint", or "save progress"
11
- - Before ending a long session
12
- - Before switching to a different intent
8
+ `savepoint.md` is a deterministic, append-only ledger of an intent's cycle steps, one line
9
+ per lifecycle milestone, newest at the bottom:
13
10
 
14
- ## Workflow
11
+ ```
12
+ 2026-06-16T14:02Z What 34--cycle-step-savepoints.md
13
+ 2026-06-16T14:20Z Why spec.md created
14
+ 2026-06-16T15:10Z How plan.md created
15
+ 2026-06-16T15:11Z How checklist.md created
16
+ 2026-06-16T16:40Z Exec outcome.md created
17
+ ```
15
18
 
16
- ### 1. Find Active Intent(s)
17
- Read `~/.plastic/INDEX.md` (or the project INDEX.md) and extract all intents listed under `## Active`.
19
+ It is **sugar on top of the conventions**, not a source of truth. The gate hook
20
+ (`hook-gate-check`) writes each line automatically when a stage file is created, so there is
21
+ nothing to save by hand. State is always derivable from files-on-disk; the ledger just lets
22
+ a resuming agent read the cycle's succession from one glance (last line = where we are).
18
23
 
19
- ### 2. For Each Active Intent
20
- Read the intent directory at `~/.plastic/store/ID--slug/`:
24
+ ## When to Use
25
+ - A PreCompact hook fires, or the user says "save" / "savepoint": verify the ledger is current.
26
+ - Resuming an intent: read the ledger to learn the cycle's succession quickly.
27
+ - The ledger looks stale, empty, or missing: rebuild it from the filesystem.
21
28
 
22
- **a. Update checklist.md** (if exists):
23
- - Check off completed items
24
- - Add any new items discovered during the session
29
+ ## What this skill does NOT do
30
+ - It does not write prose, "in progress", "next step", or "blockers". The old 50%-context
31
+ prose savepoint is retired. The semantic trace lives in `## Insights` (append-only,
32
+ newest at the bottom); the byte history lives in git.
25
33
 
26
- **b. Create/update savepoint.md:**
27
- ```markdown
28
- # Savepoint
34
+ ## Workflow
29
35
 
30
- ## Last Updated
31
- {{DATE}} Session #{{N}}
36
+ ### 1. Find Active Intent(s)
37
+ Read the active store's `INDEX.md` and extract intents under `## Active`.
32
38
 
33
- ## In Progress
34
- - (what was being worked on when savepoint triggered)
35
- - Next: (immediate next step)
39
+ ### 2. For Each Active Intent — verify the ledger
40
+ - Read `savepoint.md`. Confirm it exists and is non-empty.
41
+ - Confirm the **last line's stage** matches the stage derived from files-on-disk
42
+ (`Bridge.derive_stage`). If they disagree, or the file is missing/empty, the ledger has
43
+ drifted.
36
44
 
37
- ## Blockers
38
- (any blockers or open questions)
45
+ ### 3. Rebuild on drift
46
+ Reconstruct from the filesystem rather than hand-editing:
39
47
 
40
- ## Key Discoveries This Session
41
- - (important things learned)
48
+ ```bash
49
+ ruby -r ~/.plastic/scripts/lib/bridge -e \
50
+ 'Bridge.rebuild_savepoint("<intent_dir>")'
42
51
  ```
43
52
 
44
- **c. Update `{ID}--{slug}.md`:**
45
- - Add observations to `## Insights` section
46
-
47
- ### 3. Update INDEX.md
48
- Verify the `## Active` section is accurate.
53
+ This rewrites `savepoint.md` from the milestone files present (timestamps from mtimes),
54
+ in stage order. Safe to run anytime: the ledger is derived.
49
55
 
50
- ### 4. Commit
56
+ ### 4. Commit (store only)
57
+ If a rebuild changed the ledger, commit it in the store repo:
51
58
  ```bash
52
- git add .plastic/
53
- git commit -m "chore: savepoint — [active intent name]"
59
+ cd <store-root> && git add . && git commit -m "chore: rebuild savepoint ledger — [intent]"
54
60
  ```
55
-
56
- ### 5. Notify User
57
- Tell the user: "Context is getting large. I've saved progress to intent [ID] — [name]. Please run `/clear` and say `continue` to resume."
61
+ Never push `~/.plastic/`.
58
62
 
59
63
  ## References
60
64
 
61
- - Read `references/context-management.md` for the full save/continue protocol when the save flow needs debugging or you need to understand the full resume sequence
65
+ - Read `references/context-management.md` for the full save/continue protocol and how the
66
+ resume flow consumes the ledger.
@@ -1,13 +1,14 @@
1
- # Savepoint
2
-
3
- ## Last Updated
4
- {{DATE}} — Session #{{N}}
5
-
6
- ## In Progress
7
- - ...
8
-
9
- ## Blockers
10
- None
11
-
12
- ## Key Discoveries This Session
13
- - ...
1
+ # Deterministic cycle-step ledger, written automatically by the gate hook.
2
+ # One line per lifecycle milestone, append-only, newest at the bottom:
3
+ #
4
+ # {UTC-iso8601} {Stage} {milestone}
5
+ #
6
+ # Example:
7
+ # 2026-06-16T14:02:00Z What ID--slug.md
8
+ # 2026-06-16T14:20:00Z Why spec.md created
9
+ # 2026-06-16T15:10:00Z How plan.md created
10
+ # 2026-06-16T15:11:00Z How checklist.md created
11
+ # 2026-06-16T16:40:00Z Exec outcome.md created
12
+ #
13
+ # This file is sugar on top of the conventions, not a source of truth. It is
14
+ # rebuildable from files-on-disk via Bridge.rebuild_savepoint. Do not hand-edit.