@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.
- package/bin/lib/context_budget.rb +453 -0
- package/bin/plastic-bench +78 -0
- package/hooks/hooks.json +12 -0
- package/hooks/message-display +81 -0
- package/hooks/savepoint +5 -5
- package/package.json +1 -1
- package/scripts/agent-report +8 -2
- package/scripts/append-ledger +16 -3
- package/scripts/dashboard.rb +39 -10
- package/scripts/day-summary +53 -0
- package/scripts/doctor.rb +163 -0
- package/scripts/end-intent +93 -0
- package/scripts/hook-capture +21 -8
- package/scripts/hook-close +3 -1
- package/scripts/hook-message-display +74 -0
- package/scripts/hook-record +12 -4
- package/scripts/hook-savepoint +45 -0
- package/scripts/hook-session-start +34 -1
- package/scripts/intent-screen +77 -0
- package/scripts/lib/arm.rb +26 -1
- package/scripts/lib/compact_instructions.rb +56 -0
- package/scripts/lib/day_summary.rb +211 -0
- package/scripts/lib/doctor_core.rb +52 -3
- package/scripts/lib/doctor_session_ledger.rb +52 -0
- package/scripts/lib/handoff.rb +184 -0
- package/scripts/lib/hook_registry.rb +14 -0
- package/scripts/lib/installer_core.rb +117 -11
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/message_display.rb +290 -0
- package/scripts/lib/report_screen.rb +671 -0
- package/scripts/lib/savepoint.rb +14 -0
- package/scripts/lib/screen_paint.rb +276 -0
- package/scripts/lib/session_close.rb +22 -2
- package/scripts/lib/session_git.rb +49 -18
- package/scripts/lib/session_ledger.rb +124 -0
- package/scripts/plastic-lock +8 -1
- package/scripts/read-config +3 -0
- package/scripts/report-screen +157 -0
- package/scripts/rollback.rb +6 -0
- package/scripts/savepoint-note +67 -0
- package/scripts/spawn-preamble +9 -2
- package/scripts/write-handoff +60 -0
- package/skills/auto/SKILL.md +15 -8
- package/skills/auto/references/human-report-contract.md +59 -53
- package/skills/conventions/references/locks-and-worktrees.md +12 -0
- package/skills/intent-continuing/SKILL.md +31 -22
- package/skills/intent-continuing/references/boarding-matrix.md +5 -5
- package/skills/intent-continuing/references/context-management.md +1 -1
- package/skills/intent-ending/SKILL.md +8 -2
- package/skills/intent-executing/SKILL.md +6 -0
- package/templates/config.yml +5 -0
- package/templates/intent-screen.md +17 -0
- package/templates/outcome.md +14 -1
- 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
|
package/scripts/rollback.rb
CHANGED
|
@@ -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
|
package/scripts/spawn-preamble
CHANGED
|
@@ -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
|
-
|
|
122
|
-
|
|
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
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -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
|
|
71
|
-
|
|
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.
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
288
|
-
How
|
|
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 (
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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,
|
|
67
|
-
|
|
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,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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`):
|
|
83
|
-
|
|
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
|
|
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. **
|
|
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
|
|
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.**
|
|
99
|
-
session's own account of where things stand; read
|
|
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
|
|
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. **
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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` |
|