@zalom/plastic 2.0.0-alpha.1 → 2.0.0-alpha.11

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.
Files changed (55) hide show
  1. package/bin/lib/context_budget.rb +453 -0
  2. package/bin/plastic-bench +78 -0
  3. package/hooks/hooks.json +12 -0
  4. package/hooks/message-display +81 -0
  5. package/hooks/savepoint +5 -5
  6. package/package.json +1 -1
  7. package/scripts/agent-report +8 -2
  8. package/scripts/append-ledger +16 -3
  9. package/scripts/dashboard.rb +39 -10
  10. package/scripts/day-summary +53 -0
  11. package/scripts/doctor.rb +163 -0
  12. package/scripts/end-intent +93 -0
  13. package/scripts/hook-capture +21 -8
  14. package/scripts/hook-close +3 -1
  15. package/scripts/hook-message-display +74 -0
  16. package/scripts/hook-record +12 -4
  17. package/scripts/hook-savepoint +45 -0
  18. package/scripts/hook-session-start +34 -1
  19. package/scripts/intent-screen +77 -0
  20. package/scripts/lib/arm.rb +26 -1
  21. package/scripts/lib/compact_instructions.rb +56 -0
  22. package/scripts/lib/day_summary.rb +211 -0
  23. package/scripts/lib/doctor_core.rb +52 -3
  24. package/scripts/lib/doctor_session_ledger.rb +52 -0
  25. package/scripts/lib/handoff.rb +184 -0
  26. package/scripts/lib/hook_registry.rb +14 -0
  27. package/scripts/lib/installer_core.rb +117 -11
  28. package/scripts/lib/intent_screen.rb +309 -0
  29. package/scripts/lib/intent_screen_ansi.rb +262 -0
  30. package/scripts/lib/message_display.rb +290 -0
  31. package/scripts/lib/report_screen.rb +671 -0
  32. package/scripts/lib/savepoint.rb +14 -0
  33. package/scripts/lib/screen_paint.rb +276 -0
  34. package/scripts/lib/session_close.rb +22 -2
  35. package/scripts/lib/session_git.rb +49 -18
  36. package/scripts/lib/session_ledger.rb +124 -0
  37. package/scripts/plastic-lock +8 -1
  38. package/scripts/read-config +3 -0
  39. package/scripts/report-screen +157 -0
  40. package/scripts/rollback.rb +6 -0
  41. package/scripts/savepoint-note +67 -0
  42. package/scripts/spawn-preamble +9 -2
  43. package/scripts/write-handoff +60 -0
  44. package/skills/auto/SKILL.md +15 -8
  45. package/skills/auto/references/human-report-contract.md +59 -53
  46. package/skills/conventions/references/locks-and-worktrees.md +12 -0
  47. package/skills/intent-continuing/SKILL.md +31 -22
  48. package/skills/intent-continuing/references/boarding-matrix.md +5 -5
  49. package/skills/intent-continuing/references/context-management.md +1 -1
  50. package/skills/intent-ending/SKILL.md +8 -2
  51. package/skills/intent-executing/SKILL.md +6 -0
  52. package/templates/config.yml +5 -0
  53. package/templates/intent-screen.md +17 -0
  54. package/templates/outcome.md +14 -1
  55. package/templates/report-state.md +11 -0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # report-screen - the three delivery-report screens (intent 317): mid-delivery
6
+ # state, post-delivery delivered, and delay. Each fills from the record via
7
+ # scripts/lib/report_screen.rb; no number here is written by eye.
8
+ #
9
+ # Usage:
10
+ # report-screen state <intent_dir> [--changed "<text>"] [--ansi]
11
+ # report-screen state --all <store_root> [--changed "<text>"] [--ansi]
12
+ # report-screen delivered <intent_dir> [--ansi] [--repo <dir>]
13
+ # report-screen delay <intent_dir> [--ansi]
14
+ #
15
+ # --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
16
+ # in the shared TUI core. Selection is by capability, never by harness:
17
+ # NO_COLOR forces plain, a non-TTY stdout stays plain unless
18
+ # PLASTIC_FORCE_COLOR=1 (the test seam), and an unparseable screen falls
19
+ # open to the plain text untouched.
20
+ #
21
+ # Exit codes:
22
+ # 0 - the screen is on stdout
23
+ # 2 - usage error, or the path/store is not what the verb expects (one line
24
+ # on stderr, stdout empty)
25
+
26
+ require "yaml"
27
+ require "shellwords"
28
+ require_relative "lib/report_screen"
29
+ require_relative "lib/intent_screen"
30
+ require_relative "lib/screen_paint"
31
+
32
+ def usage_abort(message)
33
+ warn "report-screen: #{message}"
34
+ exit 2
35
+ end
36
+
37
+ # The one place allowed to shell out (D2/D20): resolves the tag that CONTAINS
38
+ # the intent's merge commit, in the project's own repository, so the version
39
+ # on the delivered screen is the release that merge shipped in. The
40
+ # repository comes from --repo, else from projects.yml beside the store (the
41
+ # installed layout, <home>/projects/<slug>/store/<id>), else the repository
42
+ # this script lives in (the in-repo layout). No merge sha, no repository, or
43
+ # no containing tag all answer nil, and the screen says "not recorded"
44
+ # rather than guessing from HEAD (D14). The pure module never reads git.
45
+ def resolve_repo(intent_dir, explicit_repo, script_dir)
46
+ candidates = []
47
+ candidates << File.expand_path(explicit_repo) if explicit_repo
48
+ if (m = intent_dir.match(%r{\A(.*)/projects/([^/]+)/store/[^/]+\z}))
49
+ home, slug = m[1], m[2]
50
+ projects_yml = File.join(home, "projects.yml")
51
+ if File.exist?(projects_yml)
52
+ data = begin
53
+ YAML.safe_load(File.read(projects_yml))
54
+ rescue StandardError
55
+ nil
56
+ end
57
+ entry = data.is_a?(Hash) && data["projects"].is_a?(Hash) ? data["projects"][slug] : nil
58
+ path = entry.is_a?(Hash) ? entry["path"].to_s : ""
59
+ candidates << File.expand_path(path) unless path.empty?
60
+ end
61
+ end
62
+ candidates << File.expand_path("..", script_dir)
63
+ candidates.find { |c| File.exist?(File.join(c, ".git")) }
64
+ end
65
+
66
+ def git_tag_reader(explicit_repo:, script_dir:)
67
+ lambda do |intent_dir|
68
+ sha = ReportScreen.merge_sha(intent_dir)
69
+ return nil unless sha
70
+ repo = resolve_repo(intent_dir, explicit_repo, script_dir)
71
+ return nil unless repo
72
+ tags = `git -C #{repo.shellescape} tag --contains #{sha.shellescape} 2>/dev/null`
73
+ versions = tags.lines.map(&:strip).reject(&:empty?).filter_map do |tag|
74
+ v = tag.sub(/\Av/, "")
75
+ Gem::Version.correct?(v) ? [Gem::Version.new(v), tag] : nil
76
+ end
77
+ versions.min_by(&:first)&.last
78
+ end
79
+ end
80
+
81
+ args = ARGV.dup
82
+ verb = args.shift
83
+ changed = nil
84
+ ansi = false
85
+ template_path = nil
86
+ repo_flag = nil
87
+ positional = []
88
+
89
+ while (arg = args.shift)
90
+ case arg
91
+ when "--changed"
92
+ usage_abort("--changed needs a value") if args.empty?
93
+ changed = args.shift
94
+ when "--ansi"
95
+ ansi = true
96
+ when "--all"
97
+ positional << "--all"
98
+ when "--repo"
99
+ repo_flag = args.shift or usage_abort("--repo needs a path")
100
+ when "--template"
101
+ template_path = args.shift or usage_abort("--template needs a path")
102
+ else
103
+ usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--") && arg != "--all"
104
+ positional << arg
105
+ end
106
+ end
107
+
108
+ usage_abort("usage: report-screen state|delivered|delay <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
109
+
110
+ all_mode = positional.delete("--all") ? true : false
111
+ target = positional.first
112
+
113
+ ansi_enabled = ansi && ENV["NO_COLOR"].to_s.empty? &&
114
+ ($stdout.tty? || ENV["PLASTIC_FORCE_COLOR"] == "1")
115
+
116
+ def paint(text, ansi_enabled, _renderer_path = nil)
117
+ return text unless ansi_enabled
118
+ ScreenPaint.paint(text, color: true) || text
119
+ end
120
+
121
+ case verb
122
+ when "state"
123
+ if all_mode
124
+ usage_abort("usage: report-screen state --all <store_root>") unless target
125
+ store_root = File.expand_path(target)
126
+ usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
127
+ out = ReportScreen.render_roster(store_root, changed: changed)
128
+ $stdout.write paint(out, ansi_enabled)
129
+ else
130
+ usage_abort("usage: report-screen state <intent_dir> [--changed \"<text>\"]") unless target
131
+ intent_dir = File.expand_path(target)
132
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
133
+ store_root = File.expand_path("../..", intent_dir)
134
+ template_path ||= File.expand_path("../templates/report-state.md", __dir__)
135
+ usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
136
+ out = ReportScreen.render_state(intent_dir: intent_dir, store_root: store_root, changed: changed,
137
+ template: File.read(template_path))
138
+ $stdout.write paint(out, ansi_enabled)
139
+ end
140
+ when "delivered"
141
+ usage_abort("usage: report-screen delivered <intent_dir>") unless target
142
+ intent_dir = File.expand_path(target)
143
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
144
+ out = ReportScreen.render_delivered(intent_dir: intent_dir,
145
+ tag_reader: git_tag_reader(explicit_repo: repo_flag, script_dir: __dir__))
146
+ $stdout.write paint(out, ansi_enabled)
147
+ when "delay"
148
+ usage_abort("usage: report-screen delay <intent_dir>") unless target
149
+ intent_dir = File.expand_path(target)
150
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
151
+ out = ReportScreen.render_delay(intent_dir: intent_dir)
152
+ $stdout.write paint(out, ansi_enabled)
153
+ else
154
+ usage_abort("unknown verb #{verb.inspect} (use state|delivered|delay)")
155
+ end
156
+
157
+ exit 0
@@ -118,6 +118,12 @@ class Rollback < InstallerCore
118
118
  remove_claude_hooks(path)
119
119
  stripped << path
120
120
  end
121
+ # The compact-instructions block (intent 312), for the same reason: no older
122
+ # package knows the section exists, so nothing there would ever replace or
123
+ # remove it. The Codex AGENTS.md section needs no equivalent: every older
124
+ # package knows that one and rewrites it on the downgrade install.
125
+ claude_md = strip_claude_compact_section(File.join(agent[:dir], "CLAUDE.md"))
126
+ stripped << claude_md if claude_md
121
127
  when "codex"
122
128
  path = agent[:home_dir] && File.join(agent[:home_dir], "hooks.json")
123
129
  if path && File.exist?(path)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # savepoint-note - the writer CLI for the two savepoint kinds intent 317 adds:
6
+ # Review (one line per plan-review or post-execution-review verdict) and
7
+ # Commit (one line per commit landing during Exec). D17: there is no automatic
8
+ # seam for either (session-commit writes the day ledger, not the intent
9
+ # ledger; the executor's own commits are plain git), so this is an explicit
10
+ # thin wrapper on Savepoint.append_review_savepoint / append_commit_savepoint.
11
+ #
12
+ # Usage:
13
+ # savepoint-note <intent_dir> --kind Review|Commit --text "<text>"
14
+ #
15
+ # --text is normalized (D21): runs of two or more spaces collapse to one, so a
16
+ # free-text kind that happens to contain a double space cannot key the dedup
17
+ # primitive's split(/\s{2,}/) on the prefix alone and silently drop a later
18
+ # line. A newline in --text is refused outright (exit 2, ledger unchanged)
19
+ # rather than silently splitting one entry into two malformed ledger lines.
20
+ #
21
+ # Exit codes:
22
+ # 0 - the line was appended (or was already recorded; both are success)
23
+ # 2 - usage error, bad --kind, missing --text, a newline in --text, or the
24
+ # path is not an intent directory (one line on stderr, ledger unchanged)
25
+
26
+ require_relative "lib/savepoint"
27
+
28
+ def usage_abort(message)
29
+ warn "savepoint-note: #{message}"
30
+ exit 2
31
+ end
32
+
33
+ args = ARGV.dup
34
+ kind = nil
35
+ text = nil
36
+ positional = []
37
+ while (arg = args.shift)
38
+ case arg
39
+ when "--kind"
40
+ kind = args.shift
41
+ when "--text"
42
+ text = args.shift
43
+ else
44
+ usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
45
+ positional << arg
46
+ end
47
+ end
48
+
49
+ usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit --text \"<text>\"") unless positional.length == 1
50
+
51
+ intent_dir = File.expand_path(positional.first)
52
+ usage_abort("#{intent_dir} is not an intent directory") unless File.exist?(Savepoint.intent_file(intent_dir))
53
+
54
+ KINDS = %w[Review Commit].freeze
55
+ usage_abort("--kind must be one of #{KINDS.join(', ')}, got #{kind.inspect}") unless KINDS.include?(kind)
56
+ usage_abort("--text is required") if text.nil? || text.empty?
57
+ usage_abort("--text must not contain a newline") if text.include?("\n")
58
+
59
+ normalized = text.gsub(/ {2,}/, " ").strip
60
+ usage_abort("--text is empty after normalization") if normalized.empty?
61
+
62
+ case kind
63
+ when "Review" then Savepoint.append_review_savepoint(intent_dir, normalized)
64
+ when "Commit" then Savepoint.append_commit_savepoint(intent_dir, normalized)
65
+ end
66
+
67
+ exit 0
@@ -24,6 +24,7 @@
24
24
 
25
25
  require_relative "lib/savepoint"
26
26
  require_relative "lib/worktree"
27
+ require_relative "lib/intent_screen"
27
28
 
28
29
  # Verbatim honoring instruction. Kept as one constant so the contract doc and the
29
30
  # test assert against the exact same string.
@@ -115,11 +116,17 @@ STAGE_LABELS = {
115
116
  "exec" => "Exec", "done" => "Done"
116
117
  }.freeze
117
118
 
119
+ # Intent 317, D6: a dispatched agent's "Current stage:" line must name the last
120
+ # LIFECYCLE line (What/Why/How/Exec/Done), never a trailing Lock/Review/Commit
121
+ # line - the bug spawn-preamble shared with IntentScreen.savepoint_fields
122
+ # before this guard (plan review finding B1).
118
123
  def current_stage(intent_dir)
119
124
  ledger = File.join(intent_dir, Savepoint::SAVEPOINT_FILE)
120
125
  if File.exist?(ledger)
121
- last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
122
- return last if last
126
+ lines = File.read(ledger).each_line.map(&:strip).reject(&:empty?)
127
+ lifecycle = lines.select { |l| IntentScreen.lifecycle_line?(l) }
128
+ return lifecycle.last if lifecycle.any?
129
+ return lines.last if lines.any?
123
130
  end
124
131
  STAGE_LABELS.fetch(Savepoint.derive_stage(intent_dir), Savepoint.derive_stage(intent_dir))
125
132
  end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # write-handoff (intent 311): render and write one session's hand-off into
6
+ # the day directory (.sessions/<day>/handoff--<session>.md). Derived from the
7
+ # day ledger, so it can be regenerated at any time. Prints the path.
8
+ #
9
+ # Usage:
10
+ # write-handoff --trigger tick|precompact|close [--store <dir>] [--day <YYYYMMDD>]
11
+ # [--session <id>] [--templates <dir>]
12
+ #
13
+ # Defaults match append-ledger: the store is $PLASTIC_HOME/store, the day is
14
+ # today (local wall clock), the session is $CLAUDE_CODE_SESSION_ID, and the
15
+ # templates dir is the one shipped next to this script.
16
+ #
17
+ # Exit codes: 0 done; 2 usage error.
18
+
19
+ require_relative "lib/session_ledger"
20
+ require_relative "lib/handoff"
21
+
22
+ def usage_abort(message)
23
+ warn "write-handoff: #{message}"
24
+ exit 2
25
+ end
26
+
27
+ def expand(path)
28
+ File.expand_path(path.to_s.sub(/\A~/, Dir.home))
29
+ end
30
+
31
+ def parse_args(argv)
32
+ opts = {}
33
+ i = 0
34
+ while i < argv.length
35
+ flag = argv[i]
36
+ usage_abort("unknown argument #{flag.inspect}") unless flag.start_with?("--")
37
+ usage_abort("#{flag} requires a value") if i + 1 >= argv.length
38
+ key = flag.delete_prefix("--").to_sym
39
+ usage_abort("unknown flag #{flag.inspect}") unless %i[store day session trigger templates].include?(key)
40
+ opts[key] = argv[i + 1]
41
+ i += 2
42
+ end
43
+ opts
44
+ end
45
+
46
+ opts = parse_args(ARGV)
47
+ usage_abort("--trigger is required (#{Handoff::TRIGGERS.join(', ')})") unless opts[:trigger]
48
+ usage_abort("--trigger must be one of #{Handoff::TRIGGERS.join(', ')}") unless Handoff::TRIGGERS.include?(opts[:trigger])
49
+
50
+ plastic_home = expand(ENV.fetch("PLASTIC_HOME", "~/.plastic"))
51
+ store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
52
+ templates = opts[:templates] ? expand(opts[:templates]) : File.expand_path("../templates", __dir__)
53
+ usage_abort("templates dir not found: #{templates}") unless Dir.exist?(templates)
54
+
55
+ day = opts[:day] || SessionLedger.day_id
56
+ usage_abort("--day must be eight digits parsing as a real date, got #{day.inspect}") unless SessionLedger.valid_day_id?(day)
57
+ session = SessionLedger.short_session_id(opts[:session], ENV["CLAUDE_CODE_SESSION_ID"])
58
+
59
+ puts Handoff.write(store: store, day: day, session: session, trigger: opts[:trigger], templates: templates)
60
+ exit 0
@@ -67,8 +67,9 @@ Replace `<STORE>` (`~/.plastic/projects/<slug>/store` or `~/.plastic/store`) and
67
67
  nonblank `CLAUDE_CODE_SESSION_ID` as Claude, otherwise passes no identity and the verb keys the
68
68
  lock by a derived session key. Never guess identity from an absent runtime variable; an
69
69
  unknown harness or thread stays unknown. Exit 1 means the lock is held, stale, excluded, or
70
- corrupt; the message names the `plastic-doctor` verb that resolves it. Do not proceed as the
71
- owner after an exit 1.
70
+ corrupt - or `inline_refused`: a conversation session may not arm an intent at all (owner rule
71
+ 2026-08-31); dispatch the delivery team instead. `--allow-inline` exists only for an explicit
72
+ owner override. Do not proceed as the owner after an exit 1.
72
73
 
73
74
  Read `../plastic-conventions/references/locks-and-worktrees.md` for what the lock and the
74
75
  worktree mean and the station table behind them. Code edits happen only inside the worktree.
@@ -200,9 +201,11 @@ Then How.
200
201
  into the spec, the matrix, and the tests; record what was dropped and why in the action
201
202
  file's review fold. A REVISE verdict is folded and not re-reviewed unless a finding changes
202
203
  a decision.
203
- 5. Notify the user (the one mid-flight briefing, per `references/human-report-contract.md`):
204
- State, the plan shape and what it builds; Risk, the riskiest row of the matrix; Call,
205
- proceeding to build. In auto mode this briefing informs; it does not wait.
204
+ 5. Print `ruby ~/.plastic/scripts/report-screen state <intent_dir> --changed "How written, plan review next"`
205
+ (D15; see `references/human-report-contract.md` for the full trigger list). This replaces
206
+ the old prose State/Risk/Call briefing at this boundary. In auto mode the screen informs;
207
+ it does not wait. The screen opens the reply with nothing before it and no code fence: the
208
+ `MessageDisplay` hook paints only a reply whose first characters are the screen marker.
206
209
 
207
210
  Then Exec.
208
211
 
@@ -252,7 +255,8 @@ every choice is non-destructive and the team has full autonomy.
252
255
  Read `../plastic-conventions/references/completion-and-done.md` for what "intent done" means.
253
256
 
254
257
  1. Verify every checklist item is checked and the suite is green once on the branch.
255
- 2. Write `outcome.md` from `~/.plastic/templates/outcome.md` with `disposition: delivered`.
258
+ 2. Write `outcome.md` from `~/.plastic/templates/outcome.md` with `disposition: delivered`,
259
+ `## Delivered` as the labeled table whose row labels match the action-file headings (317a).
256
260
  3. Release, if configured: match the working directory against `~/.plastic/projects.yml`, read
257
261
  `project.yml`'s `release` block, and act on `on_complete` (`commit`, `commit_and_push`,
258
262
  `manual`), `verify` (green proceeds; red follows `on_red`: `fix_and_retry` up to twice,
@@ -272,6 +276,9 @@ Read `../plastic-conventions/references/completion-and-done.md` for what "intent
272
276
  first, or pass `--discard-worktree-changes` deliberately); 3 means the lock survived the
273
277
  disarm (`/plastic-doctor check the lock status`); 6 means the structure check refused. Never
274
278
  leave an orphaned worktree; run `git worktree prune` on a stale reference.
279
+ 6. Print `ruby ~/.plastic/scripts/report-screen delivered <intent_dir>` once (D15): this is the
280
+ owner report at End, replacing the old prose Done briefing. Print it as the first thing in
281
+ the reply, nothing before it and no code fence, or the hook cannot paint it.
275
282
 
276
283
  ## Error Handling
277
284
 
@@ -284,8 +291,8 @@ leaves the project broken.
284
291
 
285
292
  - Read `references/agent-architecture.md` for the team model, the risk list, the headless note,
286
293
  and the solo fallback when dispatching or when a harness has no agent dispatch.
287
- - Read `references/human-report-contract.md` for the State/Risk/Call briefing before sending the
288
- How briefing.
294
+ - Read `references/human-report-contract.md` for the three report screens and the five
295
+ triggers before printing the How or Completion screen above.
289
296
  - Read `references/agent-report-contract.md` for the completion report format when reading a
290
297
  dispatched agent's return or synthesizing one.
291
298
  - Read `references/end-tail.md` for what `Arm.disarm` does at the End tail and why the reindex
@@ -1,72 +1,78 @@
1
- # Human Report Contract (per-stage EM-to-CTO briefing)
2
-
3
- This doc defines how the orchestrator briefs the human at each of the five stage boundaries
4
- (What, Why, How, Exec, Done) in auto mode; for small work only the How boundary fires (see
5
- `## Depth for small work`). It is the outward, human-facing counterpart to the
6
- internal report contract in `references/agent-report-contract.md`. Voice: an engineering
7
- manager briefing a CTO. Lead with impact, name the risk, leave the decision.
8
-
9
- ## The skeleton
10
-
11
- One fixed 3-line shape, reused at every stage:
12
-
13
- 1. **State**: what happened and what it means, impact first, one line.
14
- 2. **Risk**: the one thing that could bite, or "nothing flagged."
15
- 3. **Call**: the decision left to you, or the go-ahead I am taking.
16
-
17
- This is a shape, not a rigid template. Keep the order (State, then Risk, then Call) and keep it
18
- short. The words can flex to fit the stage.
19
-
20
- ## Per-stage content
21
-
22
- - **What**: State = the work I picked up and why it matters now. Risk = scope uncertainty.
23
- Call = confirm this is worth doing, or I proceed.
24
- - **Why**: State = the approach I chose, one line. Risk = the main trade-off. Call = the one
25
- decision I need (approve, or pick an option).
26
- - **How**: State = the plan shape (task count and what it builds). Risk = the riskiest task or
27
- dependency. Call = approve the plan to build.
28
- - **Exec**: State = what got built and the test result. Risk = residual failures or deviations.
29
- Call = go to review, or done.
30
- - **Done**: State = the delivered impact. Risk = residual risk. Call = the decision left to you
31
- (merge, release, accept).
1
+ # Human Report Contract (the three report screens, intent 317)
2
+
3
+ D15: the prose EM-to-CTO briefing this doc used to define is retired. The orchestrator now
4
+ prints one of three report screens, filled from the record by `scripts/report-screen`, never
5
+ written by eye:
6
+
7
+ - **`report-screen state <intent_dir> [--changed "<text>"]`** - the mid-delivery report. One
8
+ intent's field table (Store, Status, Stage, Savepoint, Progress, Next, Insight) plus a
9
+ `Changed` row naming what caused the print, and its Steps table.
10
+ - **`report-screen state --all <store_root>`** - the roster across every in-delivery intent,
11
+ most recently changed first, then one collapsed block (Stage, Next, Changed, first three
12
+ open steps) per intent.
13
+ - **`report-screen delivered <intent_dir>`** - the post-delivery report, printed once at close:
14
+ Asked, Delivered (with a Proven-by column), Evidence, Needs you.
15
+ - **`report-screen delay <intent_dir>`** - printed only on request ("why did X take so long"):
16
+ the delivery as a timeline plus the derived `Where the time went` line.
17
+
18
+ ## The five triggers for `state`
19
+
20
+ Print `state` (one intent, or `--all` for the roster) on any of these; a checklist tick alone,
21
+ an executor's intermediate commit, or an agent going idle is NOT one of them:
22
+
23
+ | Trigger | Scope |
24
+ |---|---|
25
+ | A savepoint line lands (a stage boundary: Why, How, Exec started, outcome written, Done) | that intent |
26
+ | A review verdict returns (plan review or post-execution review), naming what it changed | that intent |
27
+ | A blocker or needs-input is logged | that intent |
28
+ | A merge or a release lands | that intent |
29
+ | The owner asks ("where are we", "state of X", "continue X") | all in delivery, or the one named |
30
+
31
+ `delivered` prints exactly once, at Completion. `delay` prints only when the owner asks why a
32
+ delivery took long.
33
+
34
+ Every verb prints the same plain Markdown on every harness (owner ruling 2026-08-31); where a
35
+ harness can paint it (Claude Code, through 316a's message-display hook), it substitutes a
36
+ painted rendering of that same output, never a different one, and no skill or script branches
37
+ on harness name to decide.
32
38
 
33
39
  ## Depth for small work
34
40
 
35
- For small work in auto mode the mid-flight briefings collapse to one. Only the How briefing fires, and
36
- it folds in what the What and Why briefings would have said: the work picked up and the approach
37
- chosen go into its State line. The Exec briefing folds into the final owner report at End. Larger
38
- work sends all four. The shape does not change: still State, then Risk, then Call, and the per-stage
39
- content above still says what each line covers. This is a depth cut, not a new report. A delivery
40
- still ends with `outcome.md` plus one owner report.
41
+ For small work in auto mode, only the How-boundary `state` screen prints mid-flight (its
42
+ `Changed` row names what the What and Why steps did, since there is no separate briefing per
43
+ stage any more). Larger work prints `state` at every trigger in the table above. This is a
44
+ depth cut, not a different report: the screen's shape never changes, only how often it fires.
45
+ A delivery still ends with `outcome.md` plus one `delivered` screen.
41
46
 
42
47
  ## One report per audience
43
48
 
44
49
  A delivery produces exactly two artifacts: `outcome.md` (authored by `plastic-intent-ending`)
45
- and one EM-to-CTO owner report at the End stage. No stage or skill restates a delivery
46
- already written to `outcome.md`; point at it instead. Skills do not open with a banner that
47
- names the skill or restates the intent id and name the owner just typed. Announce only what
48
- the reader cannot already know: an error, a result, a choice with its reason, or a handoff.
50
+ and one `delivered` screen at the End stage. No stage or skill restates a delivery already
51
+ written to `outcome.md`; point at it instead. Skills do not open with a banner that names the
52
+ skill or restates the intent id and name the owner just typed. Announce only what the reader
53
+ cannot already know: an error, a result, a choice with its reason, or a handoff.
49
54
 
50
55
  ## Boundary vs intent 74
51
56
 
52
57
  Intent 74's report contract (`references/agent-report-contract.md`) is the INTERNAL,
53
58
  machine-checked handoff from a dispatched specialist back to the orchestrator: a structured
54
- envelope plus a per-role payload. This contract is the OUTWARD human briefing, orchestrator to
55
- user, in prose. Different direction, different audience, different form. The orchestrator
56
- CONSUMES the intent 74 report to WRITE the human briefing defined here. The two never merge.
59
+ envelope plus a per-role payload. This contract is the OUTWARD screen shown to the owner.
60
+ Different direction, different audience, different form. The orchestrator reads the intent 74
61
+ report and reflects it into the record (savepoint, outcome.md) that `report-screen` then
62
+ renders. The two never merge.
57
63
 
58
64
  ## Brevity: point, don't repeat
59
65
 
60
- Surface rules are owned by the `writing-style` skill. This contract does not restate them, in full
61
- or in summary. It defines the report's shape only: what to say, in what order, and when to stop.
62
- Apply the `writing-style` skill for the wording.
66
+ Surface rules are owned by the `writing-style` skill. This contract does not restate them. Its
67
+ job is naming which screen prints when, not the wording inside it - `report-screen` derives
68
+ every cell from the record (D14), so there is no prose left to style here.
63
69
 
64
70
  ## Emission: guided vs auto
65
71
 
66
- In guided mode, the briefing lands at each stage boundary and the human acts on the Call line
67
- before the next stage starts.
72
+ In guided mode, `state` prints at each stage boundary and the human decides before the next
73
+ stage starts.
68
74
 
69
- In auto mode, for larger work the orchestrator still emits the briefing at each boundary, as a
70
- running EM-to-CTO account. For small work only the How briefing fires; see `## Depth for small work` above for what it
71
- folds in. The Call line becomes the go-ahead the orchestrator takes itself and moves on, except at
72
- the existing hard stops (destructive action without a safe alternative, project-path confirm).
75
+ In auto mode, `state` prints at every trigger for larger work; for small work only the How
76
+ boundary prints (see `## Depth for small work` above). The orchestrator takes the go-ahead
77
+ itself and moves on, except at the existing hard stops (destructive action without a safe
78
+ alternative, project-path confirm).
@@ -103,3 +103,15 @@ what gets written down.
103
103
  | Exec | code on the intent branch, checklist checked off | heartbeat; code edits confined to the provisioned worktree; delegates write under the owner's lock | checklist boxes; savepoint milestones; the day-ledger line promotes when a project file lands |
104
104
  | 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 pointer is purge-eligible, and the QMD reindex runs LAST (after purge); `end-intent` backfills a placeholder `outcome.md` from the record and its structure check reports (never refuses) | savepoint `Done delivered` (or `abandoned`); takeover audits, if any, remain in savepoint.md |
105
105
  | Maintenance (Future, Terminal, or Active-with-a-stale-or-no-lock) | `revisions.md` move-and-record entries | detects (never acquires) `delivery.lock`; defers and reports while the target's lock is FRESH (`Lock.fresh?`); a stale or absent lock is not-active, maintenance proceeds | append-only, rule-tagged `revisions.md` entry written in the same operation as the change, or the change is refused; lands via a fresh branch off store main merged back as one closed op, never `git add -A` |
106
+
107
+ ## The write guard is not residue
108
+
109
+ `<type>.write.lock` (usually `delivery.write.lock`) is a deliberate sibling
110
+ inode used only for `flock`: no owner, no timestamp, no content, and it is
111
+ NEVER unlinked - deleting it while a writer holds the flock hands the next
112
+ writer a fresh inode at the same path, so two writers hold "the" guard at
113
+ once (see `scripts/lib/lock.rb`, the write-guard comment). A zero-byte
114
+ `*.write.lock` in a completed intent directory is by design; no cleaner may
115
+ sweep it, and it is already inside the store's `*.lock` gitignore rule.
116
+ (Intent 317a, A2: a review misread it as stale residue and nearly shipped
117
+ the sweep.)
@@ -5,7 +5,7 @@ description: >-
5
5
  where we left off", "where was I", "what should I work on", names a specific intent to
6
6
  resume (by id or description, or `--intent {id}`), or names a roadmap or delivery batch to
7
7
  resume (`--roadmap {slug}`, "where is the roadmap", "where did that batch land"). Presents
8
- state and resumes at the last delivered station; it never asks auto or guided, never boots
8
+ state and resumes at the last delivered stage; it never asks auto or guided, never boots
9
9
  (the SessionStart hook owns boot), and never drives work autonomously (plastic-auto does).
10
10
  Absorbs the former continuing, project-continuing, and roadmap-continuing skills and the
11
11
  read half of the former intent-starting skill (intent 304).
@@ -79,40 +79,49 @@ QMD-first when the intent is named by description: run
79
79
  authoritative intent file. The command is a no-op when QMD is absent; fall back to
80
80
  `INDEX.md`.
81
81
 
82
- If the intent is terminal (`## Completed` or `## Abandoned` in `INDEX.md`): report only.
83
- Summarize its `outcome.md` and ask what is next; never reopen it.
82
+ If the intent is terminal (`## Completed` or `## Abandoned` in `INDEX.md`): print the
83
+ intent screen (Status shows the terminal section, Next is empty), summarize its
84
+ `outcome.md`, and ask what is next; never reopen it.
84
85
 
85
86
  For a live intent's directory:
86
87
 
87
88
  1. **Read `savepoint.md` first.** It is a deterministic, append-only ledger, one line per
88
- event, newest at the bottom: `{utc-iso8601} {Stage} {milestone}`. Classify the station
89
+ event, newest at the bottom: `{utc-iso8601} {Stage} {milestone}`. Classify the stage
89
90
  from the last line alone (the table in `references/boarding-matrix.md`, read when
90
91
  classifying), then verify only that line's artifact is real (sentinel-aware:
91
92
  `Savepoint.stage_file_present?`). Do not re-probe every lifecycle file.
92
- 2. **Drift.** When the last line disagrees with the files on disk, rebuild the ledger from
93
+ 2. **Stale ledger.** When the last line disagrees with the files on disk, rebuild the ledger from
93
94
  disk and note the correction. A rebuilt ledger is the file-landing skeleton, which still
94
- pins the station:
95
+ pins the stage:
95
96
  ```bash
96
97
  ruby -r ~/.plastic/scripts/lib/savepoint -e 'Savepoint.rebuild_savepoint("<intent_dir>")'
97
98
  ```
98
- 3. **Read the hand-off.** When `resources/handoff--*.md` exists, its newest file is the prior
99
- session's own account of where things stand; read it after the ledger, never instead of it.
99
+ 3. **Read the hand-off.** The newest `~/.plastic/store/.sessions/<day>/handoff--*.md` (today,
100
+ else the newest prior day) is the prior session's own account of where things stand; read
101
+ it after the ledger, never instead of it.
100
102
  4. **Derive the next step:** the first unchecked item in `checklist.md` when it exists, else
101
- the next thing the station needs (see the matrix). The newest `## Insights` entry supplies
103
+ the next thing the stage needs (see the matrix). The newest `## Insights` entry supplies
102
104
  the human-readable context; an entry marked `(autonomous)` means an auto team was
103
105
  delivering it, so say so and offer to hand back to `plastic-auto`.
104
- 5. **Announce, then continue at that station:**
105
- ```
106
- Resuming intent [ID] - [name]
107
- Store: [global | project:<slug>]
108
- Station: [from the ledger's last line]
109
- Next step: [first unchecked checklist item | what the station needs]
110
- Context: [newest ## Insights entry | hand-off summary]
111
- Drift: [none | ledger rebuilt from disk]
112
- ```
113
- Then continue the work in the session's current mode. In auto mode the running team
114
- already holds the delivery lock; if a lock is held by a session that is gone, the
115
- `plastic-doctor` skill's lock section repairs or reclaims it.
106
+ 5. **Print the report screen as the first thing in the reply, then continue at that stage.**
107
+ The screen must open the message with nothing before it. On Claude Code, a fail-open
108
+ `MessageDisplay` hook recognizes a reply that opens this way and substitutes a styled ANSI
109
+ rendering for it there; the transcript and every other harness keep exactly this plain
110
+ form. For "where are we" on one named intent, run
111
+ `ruby ~/.plastic/scripts/report-screen state <intent_dir>` and print its output as it is:
112
+ the title, the field table, the `Changed` row, and the Steps table come from the record,
113
+ never by eye. For "where are we" with no intent named, run
114
+ `ruby ~/.plastic/scripts/report-screen state --all <store_root>` for the roster across every
115
+ in-delivery intent. Route "why did X take so long" to
116
+ `ruby ~/.plastic/scripts/report-screen delay <intent_dir>` instead - every verb prints the
117
+ same plain screen on any harness, painted only where the harness supports it, with no
118
+ branching on harness name. Under the `state` screen
119
+ write **What this means** as two to four bullets in plain words (what the intent is for,
120
+ what has landed, what is left, any defect named by step), then close with **needs input:**
121
+ naming the first open step. Then continue the work in the
122
+ session's current mode. In auto mode the running team already holds the delivery lock; if a
123
+ lock is held by a session that is gone, the `plastic-doctor` skill's lock section repairs or
124
+ reclaims it.
116
125
 
117
126
  ## Roadmap route: resume the mid-flight roadmap
118
127
 
@@ -140,6 +149,6 @@ For a live intent's directory:
140
149
  | Trigger | Read |
141
150
  |---|---|
142
151
  | Filling the board on the project route | `references/board-fill.md` |
143
- | Classifying the station from the ledger's last line | `references/boarding-matrix.md` |
152
+ | Classifying the stage from the ledger's last line | `references/boarding-matrix.md` |
144
153
  | Explaining why one roadmap ranked above another | `references/liveness-ranking.md` |
145
154
  | Saving or restoring context across a long session, or debugging a resume | `references/context-management.md` |