@zalom/plastic 1.0.0-beta.14 → 1.0.0-beta.16

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
@@ -79,10 +79,29 @@ The connection: an intent's `## Insights` feeds the Coordinator's Observe phase.
79
79
  | **How** | Planning | `plan.md` + `actions/` + `checklist.md` | `plastic-writing-plans` |
80
80
  | **Exec** | Execution | `outcome.md` | `plastic-executing-plan` |
81
81
 
82
- `## Insights` append-only work log captured throughout ALL stages. **Append-only means
83
- newest entry at the bottom; never prepend.** This ordering is a hard convention: Insights
84
- are the semantic trace of an intent, and a consistent newest-last order keeps that trace
85
- readable across every intent.
82
+ `## Insights` is the append-only log of durable discoveries captured throughout ALL stages.
83
+ An insight is a discovery worth keeping for later reads: novel, or old but newly relevant,
84
+ surfaced at any stage (What, Why, How, Exec). It is the most interesting residue of an
85
+ intent, the part a future reader most wants. **Append-only means newest entry at the bottom;
86
+ never prepend.** This ordering is a hard convention: Insights are the semantic trace of an
87
+ intent, and a consistent newest-last order keeps that trace readable across every intent.
88
+
89
+ Every entry leads with a fixed, machine-parseable prefix `{utc-iso8601} · {stage} · {author}`,
90
+ for example `2026-06-24T08:13:05Z · Why · plastic-brainstorming (autonomous)`. The UTC ISO8601
91
+ timestamp (to the second, trailing `Z`) is the same convention the savepoint ledger uses, so
92
+ the store has one timestamp convention. This per-entry prefix is not prepending the entry:
93
+ entries stay append-only, newest at the bottom; the prefix only stamps each line with when,
94
+ which stage, and who.
95
+
96
+ The blessed write path is the `insight-append` helper
97
+ (`scripts/insight-append <intent_dir> <text> --stage S --author A`), which formats the prefix,
98
+ validates it, and appends at the bottom. Hand-editing `## Insights` is an escape hatch; the
99
+ helper is the default so the format cannot drift.
100
+
101
+ Background sessions and dispatched sub-agents do not write the insight themselves. They carry
102
+ each nugget home in the completion report's `insights:` field, and the orchestrator (or any
103
+ agent that can write the file) persists it via the helper. A session that cannot write the
104
+ intent file still returns its report, so the insight survives.
86
105
  For full lifecycle detail, the skills in the Detail column have references/.
87
106
 
88
107
  `savepoint.md` — a deterministic, append-only ledger of cycle-step milestones (one line per
@@ -31,7 +31,7 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
31
31
 
32
32
  ## Completion Report
33
33
 
34
- END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the brainstorming payload:
34
+ END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers, insights) plus the brainstorming payload:
35
35
 
36
36
  - Decisions recorded in `### Decisions`, each with its one-line rationale
37
37
  - Context enriched: what was researched and the key findings
@@ -18,7 +18,7 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
18
18
 
19
19
  1. **Implement the actions** — make the code changes for each action in order
20
20
  2. **Track progress** — check off `checklist.md` items as they complete
21
- 3. **Record insights** — append observations to `## Insights` with the `(autonomous)` marker
21
+ 3. **Record insights** — capture durable discoveries and report them in the `insights:` field; persist each to `## Insights` via the `insight-append` helper (`scripts/insight-append <intent_dir> <text> --stage Exec --author "plastic-executor (autonomous)"`), the blessed write path that stamps the `{utc-iso8601} · {stage} · {author}` prefix
22
22
  4. **Prove it green** — run the full test suite and reach zero failures before reporting done
23
23
 
24
24
  ## How You Work
@@ -32,7 +32,7 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
32
32
 
33
33
  ## Completion Report
34
34
 
35
- END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the executor payload:
35
+ END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers, insights) plus the executor payload:
36
36
 
37
37
  - Actions implemented this turn, mapped to checklist items checked off (checked / total)
38
38
  - A summary of the code changed (files and the shape of the change)
@@ -31,7 +31,7 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
31
31
 
32
32
  ## Completion Report
33
33
 
34
- END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the planner payload, which EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR:
34
+ END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers, insights) plus the planner payload, which EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR:
35
35
 
36
36
  - The ordered actions, one line each: what the action does and how it is verified
37
37
  - Decomposition rationale: why this order, and why the actions are independent
@@ -31,7 +31,7 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
31
31
 
32
32
  ## Completion Report
33
33
 
34
- END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the spec payload:
34
+ END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers, insights) plus the spec payload:
35
35
 
36
36
  - The spec sections produced (Problem, Goals, Non-Goals, Approach, Decisions, Acceptance Criteria)
37
37
  - How the recorded decisions resolved into the chosen approach
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.16",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -108,6 +108,26 @@ def outcome_line(intent_dir)
108
108
  "(no outcome yet)"
109
109
  end
110
110
 
111
+ # The insights line: the LAST line of the intent file's `## Insights` section
112
+ # (the newest nugget, since entries are append-only newest-at-bottom), else
113
+ # `(none)`. Pure read of the intent dir, no clock.
114
+ def insights_line(intent_dir)
115
+ ifile = Bridge.intent_file(intent_dir)
116
+ return "(none)" unless File.exist?(ifile)
117
+
118
+ lines = File.read(ifile).split("\n", -1)
119
+ idx = lines.index { |l| l.strip == "## Insights" }
120
+ return "(none)" if idx.nil?
121
+
122
+ last = nil
123
+ (idx + 1).upto(lines.length - 1) do |i|
124
+ break if lines[i].start_with?("## ")
125
+
126
+ last = lines[i] unless lines[i].strip.empty?
127
+ end
128
+ last || "(none)"
129
+ end
130
+
111
131
  intent_dir_arg, role = parse_args(ARGV)
112
132
 
113
133
  if intent_dir_arg.nil? || intent_dir_arg.empty?
@@ -137,6 +157,7 @@ lines << "Status: synthesized (filesystem-derived; no agent-authored report)"
137
157
  lines << "Artifacts present: #{artifacts_str}"
138
158
  lines << "Checklist: #{checklist_progress(intent_dir)}"
139
159
  lines << "Outcome: #{outcome_line(intent_dir)}"
160
+ lines << "Insights: #{insights_line(intent_dir)}"
140
161
  lines << "=== end report ==="
141
162
 
142
163
  puts lines.join("\n")
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # insight-append - the blessed write path for an intent's `## Insights` section
6
+ # (intent 82). A thin CLI wrapper over Insights.append_insight: it formats the
7
+ # `{utc-iso8601} · {stage} · {author}` prefix, validates it, and appends one
8
+ # entry at the bottom of the section (creating the section or file if absent).
9
+ #
10
+ # Sibling to scripts/spawn-preamble and scripts/agent-report. The library's
11
+ # `now:` seam is the test seam; the CLI uses the default Time.now, which is fine
12
+ # because determinism is covered at the library level (test/insights_test.rb).
13
+ #
14
+ # Usage:
15
+ # insight-append <intent_dir> <text> --stage S --author A
16
+ #
17
+ # Exit codes: 0 (entry appended), 2 (usage).
18
+
19
+ require_relative "lib/insights"
20
+
21
+ def parse_args(argv)
22
+ stage = nil
23
+ author = nil
24
+ positional = []
25
+ i = 0
26
+ while i < argv.length
27
+ case argv[i]
28
+ when "--stage"
29
+ stage = argv[i + 1]
30
+ i += 2
31
+ when "--author"
32
+ author = argv[i + 1]
33
+ i += 2
34
+ else
35
+ positional << argv[i]
36
+ i += 1
37
+ end
38
+ end
39
+ [positional[0], positional[1], stage, author]
40
+ end
41
+
42
+ intent_dir, text, stage, author = parse_args(ARGV)
43
+
44
+ if [intent_dir, text, stage, author].any? { |v| v.nil? || v.to_s.empty? }
45
+ warn "usage: insight-append <intent_dir> <text> --stage S --author A"
46
+ exit 2
47
+ end
48
+
49
+ entry = Insights.append_insight(File.expand_path(intent_dir), text,
50
+ stage: stage, author: author)
51
+ puts "appended: #{entry}"
@@ -116,6 +116,22 @@ module Bridge
116
116
  end
117
117
  return nil if parsed.empty?
118
118
 
119
+ has_session = !blank?(session)
120
+
121
+ # Strict per-session ownership (intent 90): when the caller HAS a session, a foreign
122
+ # session's bridge is NEVER a valid resolution. Own-session and the derived-key case both
123
+ # reduce to candidate["session"] == session (the derived key IS the session that armed the
124
+ # bridge). A caller that owns no bridge resolves to nil, so its gates fail open instead of
125
+ # inheriting another session's armed intent.
126
+ #
127
+ # When the caller has NO session (truly headless, intent 52), keep the legacy degraded
128
+ # selection below so a single armed derived-key bridge is still discoverable - the hook
129
+ # cannot know the session there, and a lone armed intent must still gate.
130
+ if has_session
131
+ parsed = parsed.select { |c| c[:data]["session"].to_s == session.to_s }
132
+ return nil if parsed.empty?
133
+ end
134
+
119
135
  auto = parsed.select { |c| c[:data].dig("build", "auto") == true }
120
136
  pool = auto.empty? ? parsed : auto
121
137
 
@@ -129,7 +145,10 @@ module Bridge
129
145
  cwd_abs.start_with?("#{store_abs}/") ||
130
146
  store_abs.start_with?("#{cwd_abs}/")
131
147
  end
132
- pool = matching unless matching.empty?
148
+ # Hard cwd filter when the caller has a session (intent 90): a non-matching store
149
+ # excludes the candidate outright. Without a session, keep the best-effort revert
150
+ # (intent 52) so a lone armed bridge is still found when cwd does not overlap its store.
151
+ pool = has_session ? matching : (matching.empty? ? pool : matching)
133
152
  end
134
153
 
135
154
  pool.max_by { |c| c[:mtime] }&.fetch(:data)
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # insights.rb - the blessed write path for an intent's `## Insights` section
5
+ # (intent 82). A sibling library to bridge.rb so the insight write path stays
6
+ # independent of the savepoint ledger (the spec forbids touching the ledger).
7
+ #
8
+ # Every entry leads with a fixed, machine-parseable prefix
9
+ # `{utc-iso8601} · {stage} · {author}` where the timestamp is `now.utc.iso8601`,
10
+ # the SAME convention `Bridge.append_savepoint_line` uses, so the store has one
11
+ # timestamp convention across both ledgers. Entries are append-only, newest at
12
+ # the BOTTOM (the [[34]] ordering law): the per-entry prefix is not prepending
13
+ # the entry, and existing entries are never reordered.
14
+
15
+ require "time"
16
+ require_relative "bridge"
17
+
18
+ module Insights
19
+ HEADING = "## Insights"
20
+ # The middle dot (U+00B7) with a single space on each side separates the
21
+ # timestamp, stage, and author fields.
22
+ SEPARATOR = " · "
23
+ # A well-formed prefix: UTC ISO8601 timestamp to the second with a trailing
24
+ # `Z`, then a non-empty stage, then a non-empty author, each ` · `-separated.
25
+ PREFIX_RE = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z · [^·]+ · [^·]+/.freeze
26
+
27
+ # Append one insight entry at the BOTTOM of the `## Insights` section of the
28
+ # intent file, creating the section (or the file) if absent. `now:` is the DI
29
+ # seam: defaults to Time.now, tests inject Time.utc(...). Raises ArgumentError
30
+ # if the assembled prefix is not well-formed (guard on write).
31
+ def self.append_insight(intent_dir, text, stage:, author:, now: Time.now)
32
+ prefix = "#{now.utc.iso8601}#{SEPARATOR}#{stage}#{SEPARATOR}#{author}"
33
+ unless valid_insight_prefix?(prefix)
34
+ raise ArgumentError, "malformed insight prefix: #{prefix.inspect}"
35
+ end
36
+
37
+ entry = "#{prefix} — #{text}"
38
+ path = Bridge.intent_file(intent_dir)
39
+ File.write(path, with_entry(File.exist?(path) ? File.read(path) : "", entry))
40
+ entry
41
+ end
42
+
43
+ # Pure validator: true when `line` begins with a well-formed prefix. No IO,
44
+ # no clock. Rejects a date-only prefix, a missing separator, a missing `Z`,
45
+ # and sub-second precision.
46
+ def self.valid_insight_prefix?(line)
47
+ line.is_a?(String) && line.match?(PREFIX_RE)
48
+ end
49
+
50
+ # Insert `entry` at the bottom of the `## Insights` section of `content`,
51
+ # preserving every existing line and their order. Pure string transform.
52
+ def self.with_entry(content, entry)
53
+ lines = content.empty? ? [] : content.split("\n", -1)
54
+ heading_idx = lines.index { |l| l.strip == HEADING }
55
+ return append_new_section(lines, entry) if heading_idx.nil?
56
+
57
+ insert_at = section_end(lines, heading_idx)
58
+ lines.insert(insert_at, entry)
59
+ lines.join("\n")
60
+ end
61
+
62
+ # The insertion index for a new entry: just after the last non-empty content
63
+ # line that belongs to the `## Insights` section (before the next `## `
64
+ # heading or EOF).
65
+ def self.section_end(lines, heading_idx)
66
+ last_content = heading_idx
67
+ idx = heading_idx + 1
68
+ while idx < lines.length
69
+ break if lines[idx].start_with?("## ")
70
+
71
+ last_content = idx unless lines[idx].strip.empty?
72
+ idx += 1
73
+ end
74
+ last_content + 1
75
+ end
76
+
77
+ # Append a fresh `## Insights` section (blank line, heading, entry) to the end
78
+ # of the existing content.
79
+ def self.append_new_section(lines, entry)
80
+ body = lines.join("\n")
81
+ body = body.sub(/\n+\z/, "") unless body.empty?
82
+ pieces = body.empty? ? [] : [body, ""]
83
+ pieces << HEADING << entry
84
+ "#{pieces.join("\n")}\n"
85
+ end
86
+ end
@@ -213,6 +213,7 @@ class InstallerCore
213
213
  "scripts/lib/retrieval_gate.rb" => "scripts/lib/retrieval_gate.rb",
214
214
  "scripts/hook-auto-arm" => "scripts/hook-auto-arm",
215
215
  "scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
216
+ "scripts/lib/insights.rb" => "scripts/lib/insights.rb",
216
217
  "scripts/lib/worktree.rb" => "scripts/lib/worktree.rb",
217
218
  "scripts/lib/boot_banner.rb" => "scripts/lib/boot_banner.rb",
218
219
  "scripts/lib/qmd_sync.rb" => "scripts/lib/qmd_sync.rb",
@@ -41,10 +41,16 @@ REPORT_CONTRACT =
41
41
  "FINAL MESSAGE (your return value), not a side-channel file. Do not go idle or " \
42
42
  "finish silently. The report carries a common envelope: role, intent id, stage, " \
43
43
  "status (delivered or blocked), artifacts written, verification or tests run, " \
44
- "checklist deltas, deviations from spec, and blockers or handoff notes; plus a " \
44
+ "checklist deltas, deviations from spec, blockers or handoff notes, and an " \
45
+ "insights field carrying 0..N durable nuggets (what I discovered worth keeping, " \
46
+ "the most interesting residue of this turn; none if there were none); plus a " \
45
47
  "role-specific payload that fulfils your place in the What, Why, How, Exec cycle " \
46
- "(for example the planner explains the plan back to the orchestrator). Keep the " \
47
- "report prose-stripped: the envelope and payload only, no greeting, no preamble, " \
48
+ "(for example the planner explains the plan back to the orchestrator). Populate " \
49
+ "the insights field even when you cannot write the intent file yourself: a " \
50
+ "background or dispatched agent carries each nugget home in the report and the " \
51
+ "orchestrator persists it via scripts/insight-append, so an insight never " \
52
+ "depends on the discovering session's file-write access. Keep the report " \
53
+ "prose-stripped: the envelope and payload only, no greeting, no preamble, " \
48
54
  "no end-recap, no restating of the task; reasoning stays in the thinking channel. " \
49
55
  "See skills/auto/references/agent-report-contract.md for the per-role format."
50
56
 
@@ -39,6 +39,11 @@ Every role report, whatever the stage, carries these fields:
39
39
  - **Checklist deltas**: which checklist items this turn checked off (executor), or `n/a`.
40
40
  - **Deviations from spec**: anything done differently from the spec or plan, and why, or `none`.
41
41
  - **Blockers / handoff notes**: what the next stage must watch for, or `none`.
42
+ - **Insights**: 0..N durable nuggets discovered this turn (the most interesting residue),
43
+ each one a `## Insights`-worthy line; `none` if there were none. Background and dispatched
44
+ agents MUST populate this: they carry each nugget home in the report and the orchestrator
45
+ persists it (see Insights delivery below), so an insight never depends on the discovering
46
+ session having file-write access.
42
47
 
43
48
  ## Per-role payload
44
49
 
@@ -49,11 +54,13 @@ D2). The payload is what makes the report useful to the orchestrator beyond the
49
54
  - Decisions recorded in `### Decisions`, each with its one-line rationale.
50
55
  - Context enriched: what was researched and the key findings.
51
56
  - Open questions resolved, and any deliberately left for the spec.
57
+ - Insights: durable discoveries from the Why exploration, reported in the `insights:` field.
52
58
 
53
59
  ### spec-specialist (Why to How boundary)
54
60
  - Spec sections produced (Problem, Goals, Non-Goals, Approach, Decisions, Acceptance Criteria).
55
61
  - How the recorded decisions resolved into the chosen approach.
56
62
  - Acceptance-criteria count, so the planner knows the surface to cover.
63
+ - Insights: durable discoveries from consolidating the spec, reported in the `insights:` field.
57
64
 
58
65
  ### planner (How): worked exemplar
59
66
  The planner report EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR. It carries:
@@ -62,17 +69,20 @@ The planner report EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR. It carries:
62
69
  - Checklist coverage: item count and that every action plus suite-green is covered.
63
70
  This is the exemplar because the plan is an argument, and the orchestrator gates on whether that
64
71
  argument is sound before any code is written.
72
+ - Insights: durable discoveries from planning, reported in the `insights:` field.
65
73
 
66
74
  ### executor (Exec)
67
75
  - Actions implemented this turn, mapped to checklist items checked off (checked / total).
68
76
  - A summary of the code changed (files and the shape of the change).
69
77
  - Test result: the full-suite command and its pass / fail counts.
70
- - Insights appended, with the `(autonomous)` marker.
78
+ - Insights reported in the `insights:` field (each with the `(autonomous)` marker); the
79
+ executor or the orchestrator persists them to `## Insights` via the `insight-append` helper.
71
80
 
72
81
  ### final reviewer (final gate)
73
82
  - Verdict: `pass` or `blockers found`.
74
83
  - Each acceptance criterion checked, with the evidence that confirms or refutes it.
75
84
  - Gaps or risks found, ranked, with a recommended disposition.
85
+ - Insights: durable discoveries from the review, reported in the `insights:` field.
76
86
 
77
87
  ## Fallback: always a report
78
88
 
@@ -92,3 +102,20 @@ ledger, the lifecycle artifacts present, the checklist checked / total, and the
92
102
  line, and emits a filesystem-derived report labelled `synthesized`. So a handoff account always
93
103
  exists: authored by the agent when possible, reconstructed deterministically when not. This
94
104
  formalizes the by-hand reconstruction the orchestrator did while delivering intent 68.
105
+
106
+ ## Insights delivery
107
+
108
+ Insights ride home in the completion report. Every agent reports its durable nuggets in the
109
+ `insights:` field; the orchestrator (or any agent that can write the intent file) then persists
110
+ each one via the helper:
111
+
112
+ ```
113
+ scripts/insight-append <intent_dir> <text> --stage S --author A
114
+ ```
115
+
116
+ The helper formats the `{utc-iso8601} · {stage} · {author}` prefix (the same timestamp
117
+ convention as the savepoint ledger), validates it, and appends the entry at the bottom of the
118
+ `## Insights` section, newest last. This is the fix for dropped background and sub-agent
119
+ insights: a session that cannot write the intent file still returns its report, so the insight
120
+ survives and the orchestrator writes it on receipt. Hand-editing `## Insights` is an escape
121
+ hatch; the helper is the default so the prefix format cannot drift.