@zalom/plastic 2.0.0-alpha.10 → 2.0.0-alpha.12

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.
@@ -31,7 +31,9 @@ deliberately; the auto pipeline never dispatches them.
31
31
  `plastic-intent-executing`'s `plan-reviewer-prompt.md`; fold every finding into the spec, the
32
32
  matrix, and the tests.
33
33
  4. **Dispatch one executor, tests first** - the executor commits the matrix's tests red, then
34
- builds, then drives the full suite green; you verify the checklist against the diff.
34
+ builds, then drives the full suite green; you verify tick-versus-diff at the
35
+ post-execution review and again before the merge. A mismatch is a review finding, not a
36
+ cleanup you perform silently.
35
37
  5. **Review by risk** - dispatch the post-execution reviewer only when the auto skill's risk
36
38
  rule fires; otherwise the green suite is the review.
37
39
  6. **Close** - `outcome.md`, then `plastic-intent-ending`, which releases the worktree, clears
@@ -20,7 +20,11 @@ valid lifecycle artifacts. Honor it as your live state; do not re-derive or cont
20
20
  any other change.
21
21
  2. **Implement the actions** - make the code changes for each action in order, inside the
22
22
  worktree the preamble names.
23
- 3. **Track progress** - check off `checklist.md` items as they complete.
23
+ 3. **Tick with the commit** - commit after each logical unit of work, and in the same step
24
+ tick the checklist item that unit lands: mark its box `[x]` and move the line to
25
+ `## Completed`, then append the savepoint `Commit` line
26
+ (`scripts/savepoint-note <intent_dir> --kind Commit --text "<sha> <what it proves>"`). A
27
+ commit without its tick is incomplete.
24
28
  4. **Record insights** - capture durable discoveries and report them in the `insights:` field;
25
29
  persist each to `## Insights` via the `insight-append` helper
26
30
  (`scripts/insight-append <intent_dir> <text> --stage Exec --author "plastic-executor (autonomous)"`),
@@ -33,7 +37,8 @@ valid lifecycle artifacts. Honor it as your live state; do not re-derive or cont
33
37
  real `ACTION_N.md` with its failure-mode matrix, pasted in by the lead. Execute the action
34
38
  files in order.
35
39
  2. Write the matrix's tests; commit red.
36
- 3. Work one action at a time, preferring safe, non-destructive routes.
40
+ 3. Work one action at a time, ticking its checklist item in the same commit that lands it
41
+ (box marked, line moved); prefer safe, non-destructive routes.
37
42
  4. Run the full suite, iterate to zero failures and zero errors; commit green.
38
43
  5. Produce (output handoff): the code changes, a checked-off `checklist.md`, and `## Insights`.
39
44
  6. Report (see `## Completion Report`); the lead applies the risk rule and may dispatch a
@@ -46,7 +51,8 @@ preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.m
46
51
  finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written,
47
52
  verification, checklist deltas, deviations, blockers, insights) plus the executor payload:
48
53
 
49
- - Actions implemented this turn, mapped to checklist items checked off (checked / total)
54
+ - Actions implemented this turn, mapped to checklist items checked off (checked / total);
55
+ checked / total must equal the items whose commits exist
50
56
  - A summary of the code changed (files and the shape of the change)
51
57
  - Test result: the red commit's failing count, then the full-suite command and its pass / fail
52
58
  counts
package/hooks/statusline CHANGED
@@ -2,8 +2,8 @@
2
2
  # plastic-hook-version: 4.0.0
3
3
  # StatusLine hook - Plastic owns the full statusline (no chaining).
4
4
  # Renders: {model} - ctx {pct}% ({used}/{size}) - 5h {p}% - 7d {p}% - ${cost}
5
- # - claudish {n} rw / {tok} - Plastic {version}[ -> {next}] - {path}
6
- # Every segment but claudish and the Plastic version comes from the stdin payload.
5
+ # - Plastic {version}[ -> {next}] - {path}
6
+ # Every segment but the Plastic version comes from the stdin payload.
7
7
  # Pure bash (macOS 3.2 compatible). No ruby, no jq. Reads stdin once; small file reads
8
8
  # only. The stdin JSON is walked in a single awk pass (below); nothing else in this
9
9
  # script forks a second process per field, which is what keeps a render cheap.
@@ -17,7 +17,6 @@ C_OK='' # default - meter below 70%
17
17
  C_WARN=$'\033[33m' # yellow - meter at 70% and above
18
18
  C_CRIT=$'\033[38;2;231;76;60m' # red - meter at 90% and above
19
19
  C_COST=$'\033[37m' # white - session cost
20
- C_CLAUDISH=$'\033[38;2;249;140;30m' # orange - claudish rewrites
21
20
  C_VER=$'\033[37m' # white - "Plastic {version}" and update arrow
22
21
  C_NEXT=$'\033[33m' # yellow - next version
23
22
  C_PATH=$'\033[38;2;128;134;144m' # gray - path (matches dim UI text)
@@ -309,40 +308,18 @@ if [ -n "$COST_RAW" ]; then
309
308
  [ -n "$COST_FMT" ] && [ "$COST_FMT" != "0.00" ] && COST_SEG="${C_COST}\$${COST_FMT}${RST}"
310
309
  fi
311
310
 
312
- # --- claudish rewrites for THIS session (fork ledger, optional) ---
313
- # Tab separated, one call per line; column 11 is the session id. Rows written
314
- # before that column existed have 9 or 10 fields and never match, so an old
315
- # ledger renders no segment instead of a wrong one.
316
- CL_SEG=""
317
- CL_LOG="${CLAUDISH_LOCAL_DIR:-$HOME/.claude/claudish-local}/usage.log"
318
- if [ -n "$SID" ] && [ -f "$CL_LOG" ]; then
319
- CL=$(awk -F'\t' -v sid="$SID" '
320
- NF >= 11 && $11 == sid { n++; t += $5 + $6 }
321
- END {
322
- if (n > 0) {
323
- if (t >= 1000) {
324
- s = sprintf("%.1f", t / 1000); sub(/\.0$/, "", s); printf "%d %sk", n, s
325
- } else {
326
- printf "%d %d", n, t
327
- }
328
- }
329
- }' "$CL_LOG" 2>/dev/null)
330
- [ -n "$CL" ] && CL_SEG="${C_CLAUDISH}claudish ${CL%% *} rw / ${CL#* }${RST}"
331
- fi
332
-
333
311
  # --- Assemble segments in order, joined by " . " ---
334
312
  PARTS=""
335
313
  add() { [ -z "$1" ] && return; if [ -z "$PARTS" ]; then PARTS="$1"; else PARTS="$PARTS$SEP$1"; fi; }
336
314
 
337
- # Order: identity first (model), then this session's spend (context, meters, cost,
338
- # rewrites), then the volatile-length values last (version, path) so the line does
315
+ # Order: identity first (model), then this session's spend (context, meters, cost),
316
+ # then the volatile-length values last (version, path) so the line does
339
317
  # not visually jump as they change.
340
318
  [ -n "$MODEL" ] && add "${C_MODEL}${MODEL}${RST}"
341
319
  add "$CTX_SEG"
342
320
  add "$M5"
343
321
  add "$M7"
344
322
  add "$COST_SEG"
345
- add "$CL_SEG"
346
323
  if [ -n "$VERSION" ]; then
347
324
  vseg="${C_VER}Plastic ${VERSION}${RST}"
348
325
  [ -n "$NEXT" ] && vseg="${vseg}${C_VER} "$'\342\206\222'" ${RST}${C_NEXT}${NEXT}${RST}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "2.0.0-alpha.10",
3
+ "version": "2.0.0-alpha.12",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/doctor.rb CHANGED
@@ -30,6 +30,7 @@ require_relative "lib/power_tools"
30
30
  require_relative "lib/preflight"
31
31
  require_relative "lib/ruby_probe"
32
32
  require_relative "lib/doctor_session_ledger"
33
+ require_relative "lib/intent_screen"
33
34
 
34
35
  # Diagnostic engine, instantiable with an injected store/agent map so tests can
35
36
  # run it hermetically (no eval, no global-constant rewriting).
@@ -1161,9 +1162,11 @@ end
1161
1162
  #
1162
1163
  # Doctor's fourth check scope, invoked by `--intent <id>`. Never a store-wide sweep:
1163
1164
  # resolves exactly one intent directory (mirrors scripts/end-intent's resolve_intent_dir /
1164
- # scripts/project-links's --intent disambiguation) and returns five checks, four
1165
- # FAIL-severity, one WARN-severity (intent_savepoint_truthful stays WARN per intent 134's
1166
- # binding advisory-only ruling; see spec.md D2/D8 - do NOT escalate it to FAIL).
1165
+ # scripts/project-links's --intent disambiguation) and returns six checks, four
1166
+ # FAIL-severity, two WARN-severity: intent_savepoint_truthful stays WARN per intent 134's
1167
+ # binding advisory-only ruling (see spec.md D2/D8 - do NOT escalate it to FAIL), and
1168
+ # intent_ticks_lag is WARN-only per intent 329's ruling that a lagging tick warns rather
1169
+ # than blocks.
1167
1170
  def check_intent_end(id, store: nil, disposition: nil)
1168
1171
  intent_dir, scope = resolve_single_intent_dir(id, store: store)
1169
1172
  unless intent_dir
@@ -1185,6 +1188,7 @@ end
1185
1188
  intent_structure_check(intent_dir),
1186
1189
  intent_lifecycle_artifacts_check(intent_dir, disposition),
1187
1190
  intent_checklist_complete_check(intent_dir),
1191
+ intent_ticks_lag_check(intent_dir),
1188
1192
  intent_links_projection_check_for(id, scope),
1189
1193
  intent_savepoint_truthful_check(intent_dir, index_path: index_path),
1190
1194
  ]
@@ -1263,6 +1267,45 @@ end
1263
1267
  end
1264
1268
  end
1265
1269
 
1270
+ # Intent 329: the checklist is the only input to the state screen's Progress bar, so an
1271
+ # intent whose commit ledger has entries and whose checklist has no ticked item is
1272
+ # reporting 0 progress on work that already landed. Advisory only (WARN), like
1273
+ # intent_savepoint_truthful: a lagging tick is a lead's review finding, never a machine
1274
+ # refusal. The commit count comes from this intent's own savepoint.md Commit ledger
1275
+ # (intent 317 D17), never from git: git's detected base is the repo default branch, which
1276
+ # is not the base a 2.0 intent branch forks from.
1277
+ def intent_ticks_lag_check(intent_dir)
1278
+ items = IntentScreen.checklist_items(intent_dir)
1279
+ if items.empty?
1280
+ return check(category: "intent_end", name: "intent_ticks_lag", status: "pass",
1281
+ message: "n/a: checklist.md has no items to tick")
1282
+ end
1283
+
1284
+ ticked = items.count { |i| i[:done] }
1285
+ commits = savepoint_commit_count(intent_dir)
1286
+
1287
+ if commits.positive? && ticked.zero?
1288
+ check(category: "intent_end", name: "intent_ticks_lag", status: "warn",
1289
+ message: "ticks lag the branch: #{commits} commit(s) recorded, " \
1290
+ "0 of #{items.size} checklist items ticked")
1291
+ else
1292
+ check(category: "intent_end", name: "intent_ticks_lag", status: "pass",
1293
+ message: "#{ticked} of #{items.size} ticked, #{commits} commit(s) recorded")
1294
+ end
1295
+ end
1296
+
1297
+ # Count `Commit` lines in the intent's savepoint ledger, through the one regex that parses
1298
+ # a savepoint line (IntentScreen::SAVEPOINT_RE; field 2 is the kind).
1299
+ def savepoint_commit_count(intent_dir)
1300
+ path = File.join(intent_dir, "savepoint.md")
1301
+ return 0 unless File.exist?(path)
1302
+
1303
+ File.readlines(path).count do |line|
1304
+ m = line.strip.match(IntentScreen::SAVEPOINT_RE)
1305
+ m && m[2] == "Commit"
1306
+ end
1307
+ end
1308
+
1266
1309
  def intent_links_projection_check_for(id, scope)
1267
1310
  ctx = build_links_projection_context
1268
1311
  node = ctx[:intents].find { |n| n[:id] == id && n[:scope] == scope }
@@ -303,10 +303,33 @@ module ReportScreen
303
303
  { kind: "red", what: "#{sha} proven test-only and red", source: "outcome.md ## Verification" }
304
304
  end
305
305
 
306
- def self.ship_row(text, intent_dir, tag_reader)
307
- line = text.to_s.lines.find { |l| l =~ /\bmerge(d)?\b/i && l =~ /\b[0-9a-f]{7,40}\b/ }
308
- sha = line && line.match(/\b([0-9a-f]{7,40})\b/)[1]
309
- version = tag_reader.call(intent_dir)
306
+ # Fix 2026-09-01: the record is the truth of delivery (D14: never a guess).
307
+ # The shipped version comes from outcome.md's own ship line first ("Shipped
308
+ # as `v2.0.0-alpha.10`", "released as **v2.0.0-alpha.5**", "released
309
+ # v2.0.0-alpha.9", "Tagged v1.14.1", "Delivered in", "Release v"); the injected tag reader (git) is the
310
+ # fallback when the record is silent. A bare version with no ship verb
311
+ # ("from 1.14.1") is not a shipped version.
312
+ SHIP_VERSION_RE = /\b(?:shipped|released?|delivered|tagged)\b(?:\s+(?:as|in))?[\s`*]*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?)/i.freeze
313
+
314
+ def self.shipped_version(intent_dir)
315
+ text = outcome_text(intent_dir)
316
+ return nil unless text
317
+ m = text.match(SHIP_VERSION_RE)
318
+ m && m[1]
319
+ end
320
+
321
+ # The merge commit named on outcome.md's merge line, or nil. The CLI's tag
322
+ # reader asks git which tag contains it; the ship row prints it.
323
+ def self.merge_sha(intent_dir)
324
+ text = outcome_text(intent_dir)
325
+ return nil unless text
326
+ line = text.lines.find { |l| l =~ /\bmerge(d)?\b/i && l =~ /\b[0-9a-f]{7,40}\b/ }
327
+ line && line.match(/\b([0-9a-f]{7,40})\b/)[1]
328
+ end
329
+
330
+ def self.ship_row(_text, intent_dir, tag_reader)
331
+ sha = merge_sha(intent_dir)
332
+ version = shipped_version(intent_dir) || tag_reader.call(intent_dir)
310
333
  return nil if sha.nil? && (version.nil? || version.to_s.empty?)
311
334
  ver_text = version && !version.to_s.empty? ? "v#{version.to_s.sub(/\Av/, '')}" : NOT_RECORDED
312
335
  sha_text = sha || NOT_RECORDED
@@ -524,7 +547,7 @@ module ReportScreen
524
547
  ts = delivered_timestamp(intent_dir)
525
548
  m = mode(intent_dir)
526
549
  dur = duration(intent_dir)
527
- version = tag_reader.call(intent_dir)
550
+ version = shipped_version(intent_dir) || tag_reader.call(intent_dir)
528
551
  ver_text = version && !version.to_s.empty? ? "v#{version.to_s.sub(/\Av/, '')}" : NOT_RECORDED
529
552
 
530
553
  lines = []
@@ -9,7 +9,7 @@
9
9
  # Usage:
10
10
  # report-screen state <intent_dir> [--changed "<text>"] [--ansi]
11
11
  # report-screen state --all <store_root> [--changed "<text>"] [--ansi]
12
- # report-screen delivered <intent_dir> [--ansi]
12
+ # report-screen delivered <intent_dir> [--ansi] [--repo <dir>]
13
13
  # report-screen delay <intent_dir> [--ansi]
14
14
  #
15
15
  # --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
@@ -23,6 +23,8 @@
23
23
  # 2 - usage error, or the path/store is not what the verb expects (one line
24
24
  # on stderr, stdout empty)
25
25
 
26
+ require "yaml"
27
+ require "shellwords"
26
28
  require_relative "lib/report_screen"
27
29
  require_relative "lib/intent_screen"
28
30
  require_relative "lib/screen_paint"
@@ -32,15 +34,47 @@ def usage_abort(message)
32
34
  exit 2
33
35
  end
34
36
 
35
- # The one place allowed to shell out (D2/D20): resolves the nearest git tag
36
- # reachable from the repo this script lives in, so `render_delivered`'s
37
- # version field and the `ship` evidence row are never invented. The pure
38
- # module never reads git itself; this is injected as `tag_reader:`.
39
- def git_tag_reader(repo_root)
40
- lambda do |_intent_dir|
41
- return nil unless File.exist?(File.join(repo_root, ".git"))
42
- tag = `git -C #{repo_root} describe --tags --abbrev=0 2>/dev/null`.strip
43
- tag.empty? ? nil : tag
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
44
78
  end
45
79
  end
46
80
 
@@ -49,6 +83,7 @@ verb = args.shift
49
83
  changed = nil
50
84
  ansi = false
51
85
  template_path = nil
86
+ repo_flag = nil
52
87
  positional = []
53
88
 
54
89
  while (arg = args.shift)
@@ -60,6 +95,8 @@ while (arg = args.shift)
60
95
  ansi = true
61
96
  when "--all"
62
97
  positional << "--all"
98
+ when "--repo"
99
+ repo_flag = args.shift or usage_abort("--repo needs a path")
63
100
  when "--template"
64
101
  template_path = args.shift or usage_abort("--template needs a path")
65
102
  else
@@ -104,8 +141,8 @@ when "delivered"
104
141
  usage_abort("usage: report-screen delivered <intent_dir>") unless target
105
142
  intent_dir = File.expand_path(target)
106
143
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
107
- repo_root = File.expand_path("..", __dir__)
108
- out = ReportScreen.render_delivered(intent_dir: intent_dir, tag_reader: git_tag_reader(repo_root))
144
+ out = ReportScreen.render_delivered(intent_dir: intent_dir,
145
+ tag_reader: git_tag_reader(explicit_repo: repo_flag, script_dir: __dir__))
109
146
  $stdout.write paint(out, ansi_enabled)
110
147
  when "delay"
111
148
  usage_abort("usage: report-screen delay <intent_dir>") unless target
@@ -15,7 +15,8 @@
15
15
  # other flags exist.
16
16
  #
17
17
  # Checks, all run every invocation (the fourth only when --suite is given):
18
- # 1. doctor - Doctor#run_intent_check(id, store: scope), fail-open on a crash
18
+ # 1. doctor - Doctor#run_intent_check(id, store: scope), fail-open on a crash; the doctor
19
+ # scan includes the intent_ticks_lag warning (intent 329)
19
20
  # 2. em-dash - added lines only of `git diff <base>...HEAD`, never the whole tree
20
21
  # 3. diffstat - `git diff --stat <base>...HEAD`, never a failure on its own
21
22
  # 4. suite - the supplied --suite command, run with RUBYOPT cleared
@@ -204,7 +204,8 @@ Then How.
204
204
  5. Print `ruby ~/.plastic/scripts/report-screen state <intent_dir> --changed "How written, plan review next"`
205
205
  (D15; see `references/human-report-contract.md` for the full trigger list). This replaces
206
206
  the old prose State/Risk/Call briefing at this boundary. In auto mode the screen informs;
207
- it does not wait.
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.
208
209
 
209
210
  Then Exec.
210
211
 
@@ -216,7 +217,7 @@ Then Exec.
216
217
  then builds, then drives the full suite green.
217
218
  2. Read its return by code: DONE or DONE_WITH_CONCERNS proceeds; NEEDS_CONTEXT re-dispatches
218
219
  with the missing context; BLOCKED stops under the error procedure.
219
- 3. Tick the checklist as items land (the executor does this; verify it).
220
+ 3. Tick the checklist as items land (the executor does this); verify tick-versus-diff against the diff. A mismatch is a review finding, not a lead cleanup.
220
221
 
221
222
  ## Review by risk (boot 3, only when a rule fires)
222
223
 
@@ -253,7 +254,7 @@ every choice is non-destructive and the team has full autonomy.
253
254
 
254
255
  Read `../plastic-conventions/references/completion-and-done.md` for what "intent done" means.
255
256
 
256
- 1. Verify every checklist item is checked and the suite is green once on the branch.
257
+ 1. This is the merge gate: verify every checklist item is checked, verify tick-versus-diff against the diff, and confirm the suite is green once on the branch.
257
258
  2. Write `outcome.md` from `~/.plastic/templates/outcome.md` with `disposition: delivered`,
258
259
  `## Delivered` as the labeled table whose row labels match the action-file headings (317a).
259
260
  3. Release, if configured: match the working directory against `~/.plastic/projects.yml`, read
@@ -276,7 +277,8 @@ Read `../plastic-conventions/references/completion-and-done.md` for what "intent
276
277
  disarm (`/plastic-doctor check the lock status`); 6 means the structure check refused. Never
277
278
  leave an orphaned worktree; run `git worktree prune` on a stale reference.
278
279
  6. Print `ruby ~/.plastic/scripts/report-screen delivered <intent_dir>` once (D15): this is the
279
- owner report at End, replacing the old prose Done briefing.
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.
280
282
 
281
283
  ## Error Handling
282
284
 
@@ -108,11 +108,15 @@ Capture observations in `## Insights`. When ALL checklist items are checked:
108
108
 
109
109
  ## Tick-as-you-land
110
110
 
111
- As each task lands, in the same edit: move its checklist item from `## In
112
- Progress` to `## Completed` in `checklist.md`, and add one `## Session Log`
113
- row (Date, Items Completed, Notes). Do not batch several tasks' worth of
114
- checklist updates into one later edit; tick the moment the task is verified,
115
- before moving to the next task.
111
+ A tick is two edits, made together: mark the item's box `[x]`, and move its
112
+ checklist item from `## In Progress` to `## Completed` in `checklist.md`;
113
+ then add one `## Session Log` row (Date, Items Completed, Notes). The box is
114
+ the half the state screen's Progress bar reads: `IntentScreen::ITEM_RE` and
115
+ `progress_fields` count `[x]`, not which section the line sits in, so a line
116
+ moved to `## Completed` with its box left unmarked still reads as zero
117
+ progress. Do not batch several tasks' worth of checklist updates into one
118
+ later edit; tick the moment the task is verified, before moving to the next
119
+ task.
116
120
 
117
121
  ## Verify before every owner review
118
122
 
@@ -20,12 +20,17 @@ You are implementing a specific task from a plan. You have been given the full t
20
20
  2. Implement exactly what the task specifies — nothing more, nothing less.
21
21
  3. Write tests first when the task includes test steps (TDD).
22
22
  4. Follow the file paths specified in the task exactly.
23
- 5. Commit after each logical unit of work.
23
+ 5. Commit after each logical unit of work, and in the same step tick the checklist item that
24
+ unit lands: mark its box `[x]` and move the line to `## Completed`, then append the
25
+ savepoint `Commit` line
26
+ (`scripts/savepoint-note <intent_dir> --kind Commit --text "<sha> <what it proves>"`). A
27
+ commit without its tick is incomplete.
24
28
  6. When done, self-review against this checklist:
25
29
  - [ ] All steps in the task are completed
26
30
  - [ ] Tests pass
27
31
  - [ ] Code is clean and follows project conventions
28
32
  - [ ] No unrelated changes
33
+ - [ ] Every landed unit's checklist item is ticked
29
34
 
30
35
  ## Report Format
31
36