@zalom/plastic 1.14.1 → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC.md +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/close +5 -0
- package/hooks/hooks.json +9 -49
- package/hooks/record +4 -0
- package/package.json +3 -2
- package/scripts/agent-report +8 -9
- package/scripts/append-ledger +195 -0
- package/scripts/codex-hook +78 -90
- package/scripts/dashboard.rb +4 -4
- package/scripts/doctor.rb +106 -43
- package/scripts/end-intent +77 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +242 -0
- package/scripts/hook-close +34 -0
- package/scripts/hook-record +149 -0
- package/scripts/hook-session-start +95 -12
- package/scripts/install.rb +8 -6
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +283 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/doctor_core.rb +17 -88
- package/scripts/lib/doctor_session_ledger.rb +106 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +44 -107
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +19 -30
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +320 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +84 -0
- package/scripts/lib/session_git.rb +469 -0
- package/scripts/lib/session_ledger.rb +497 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +94 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +42 -3
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +5 -5
- package/scripts/update.rb +7 -3
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +218 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +8 -8
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +47 -55
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +131 -89
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +2 -2
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +28 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +12 -49
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/project.yml +7 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
package/scripts/exec-worktree
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
# exec-worktree (intent 213, group 2) - finish an intent's code worktree:
|
|
6
|
-
#
|
|
5
|
+
# exec-worktree (intent 213, group 2) - finish an intent's code worktree: the dirty-worktree
|
|
6
|
+
# guard, then Worktree.finish (delivered merges + removes, abandoned removes only). The order
|
|
7
|
+
# precondition it once ran first was removed in 2.0 with the gates (intent 302).
|
|
7
8
|
#
|
|
8
9
|
# Usage:
|
|
9
10
|
# exec-worktree --store <path> --id <intent_id> --home <plastic_home> \
|
|
@@ -18,26 +19,17 @@
|
|
|
18
19
|
# Steps performed:
|
|
19
20
|
# 0. Resolve inputs: the intent dir, normalize --home, resolve the calling session
|
|
20
21
|
# (--session, else CLAUDE_CODE_SESSION_ID, else the existing delivery.lock's own
|
|
21
|
-
# recorded owner_session when non-blank, else nil;
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# 1. The
|
|
27
|
-
# Bridge.code_gate_decision predicate exactly as it exists. On an auto bridge that
|
|
28
|
-
# has not reached How, this BLOCKS (exit 2). Bridge.code_gate_decision returns nil
|
|
29
|
-
# UNCONDITIONALLY on a guided bridge (build.auto != true), so on a guided bridge
|
|
30
|
-
# this precondition is ADVISORY ONLY: it always passes and the run proceeds. This
|
|
31
|
-
# script is NOT the enforcement point either way; the hook layer
|
|
32
|
-
# (scripts/lib/bridge.rb code_gate_decision, wired through scripts/lib/edit_gates.rb)
|
|
33
|
-
# remains the actual gate. See scripts/lib/exec_worktree.rb's module doc for why the
|
|
34
|
-
# auto-only condition is never relaxed and never duplicated here.
|
|
35
|
-
# 2. The dirty-worktree guard (delivered only): git status --porcelain on the code
|
|
22
|
+
# recorded owner_session when non-blank, else nil; the lock-owner fallback IS
|
|
23
|
+
# used here, matching end-intent's teardown resolution order).
|
|
24
|
+
# Derive the worktree block from projects.yml and the intent id (Arm.worktree_block;
|
|
25
|
+
# the /tmp bridge this once read was removed in 2.0, intent 307). No code worktree on
|
|
26
|
+
# disk means nothing was provisioned (exit 0).
|
|
27
|
+
# 1. The dirty-worktree guard (delivered only): git status --porcelain on the code
|
|
36
28
|
# worktree. Not clean, or the check itself fails, fails CLOSED (exit 3), mirroring
|
|
37
29
|
# end-intent's dirty-worktree handling. abandoned skips this guard entirely: the
|
|
38
30
|
# branch survives and can be reclaimed, so a dirty worktree is not a reason to
|
|
39
31
|
# refuse.
|
|
40
|
-
#
|
|
32
|
+
# 2. Finish: Worktree.finish(bridge_data, home:, runner:, merge: disposition ==
|
|
41
33
|
# "delivered"). Worktree.finish is fail-open and never raises, so failure is
|
|
42
34
|
# detected by checking, for a delivered disposition only, whether the worktree
|
|
43
35
|
# directory still exists on disk afterward (release removes it on success).
|
|
@@ -49,8 +41,7 @@
|
|
|
49
41
|
# 0 finished (merged or removed per disposition), or nothing to finish (no bridge
|
|
50
42
|
# resolved, or no code worktree recorded on the bridge)
|
|
51
43
|
# 1 usage or path-resolution failure
|
|
52
|
-
# 2
|
|
53
|
-
# reached)
|
|
44
|
+
# 2 retired in 2.0 (intent 302): was the order precondition; never emitted, never reused
|
|
54
45
|
# 3 the worktree is not clean, or the merge/removal did not complete (delivered only)
|
|
55
46
|
# 4 lock or session resolution failure
|
|
56
47
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# file-session-intent (intent 301): backfill and close one session day ledger,
|
|
6
|
+
# carrying its open items into a target day.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# file-session-intent --day <YYYYMMDD> [--carry-to <YYYYMMDD>] [--store <dir>]
|
|
10
|
+
# [--templates <dir>] [--author <name>]
|
|
11
|
+
#
|
|
12
|
+
# --store defaults to $PLASTIC_HOME/store ($PLASTIC_HOME defaults to ~/.plastic).
|
|
13
|
+
# Prints one line: `filed <day>` or `skipped <day>: closed`. Exit 0 on success
|
|
14
|
+
# and on any filing error (reported on stderr; the next boot tries again), 2 on
|
|
15
|
+
# a usage error.
|
|
16
|
+
|
|
17
|
+
require_relative "lib/session_ledger"
|
|
18
|
+
require_relative "lib/session_backfill"
|
|
19
|
+
|
|
20
|
+
def usage_abort(message)
|
|
21
|
+
warn "file-session-intent: #{message}"
|
|
22
|
+
exit 2
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def next_value!(argv, i, flag)
|
|
26
|
+
value = argv[i + 1]
|
|
27
|
+
usage_abort("#{flag} needs a value") if value.nil? || value.start_with?("--")
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
opts = { day: nil, carry_to: nil, store: nil, templates: nil, author: "session" }
|
|
32
|
+
i = 0
|
|
33
|
+
while i < ARGV.length
|
|
34
|
+
case ARGV[i]
|
|
35
|
+
when "--day" then opts[:day] = next_value!(ARGV, i, "--day"); i += 1
|
|
36
|
+
when "--carry-to" then opts[:carry_to] = next_value!(ARGV, i, "--carry-to"); i += 1
|
|
37
|
+
when "--store" then opts[:store] = next_value!(ARGV, i, "--store"); i += 1
|
|
38
|
+
when "--templates" then opts[:templates] = next_value!(ARGV, i, "--templates"); i += 1
|
|
39
|
+
when "--author" then opts[:author] = next_value!(ARGV, i, "--author"); i += 1
|
|
40
|
+
else usage_abort("unknown argument #{ARGV[i].inspect}")
|
|
41
|
+
end
|
|
42
|
+
i += 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
usage_abort("--day is required") if opts[:day].nil?
|
|
46
|
+
usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger.valid_day_id?(opts[:day])
|
|
47
|
+
if opts[:carry_to] && !SessionLedger.valid_day_id?(opts[:carry_to])
|
|
48
|
+
usage_abort("--carry-to must be a real calendar date, YYYYMMDD")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
store = opts[:store] || File.join(File.expand_path(ENV.fetch("PLASTIC_HOME", "~/.plastic")), "store")
|
|
52
|
+
templates = opts[:templates] || File.expand_path("../templates", __dir__)
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
result = SessionBackfill.file_day(store: store, day: opts[:day], templates: templates,
|
|
56
|
+
author: opts[:author], carry_to: opts[:carry_to])
|
|
57
|
+
puts(result == :closed ? "skipped #{opts[:day]}: closed" : "filed #{opts[:day]}")
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
warn "file-session-intent: #{opts[:day]} not filed: #{e.class}: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
exit 0
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-capture (reads the UserPromptSubmit stdin JSON payload)
|
|
6
|
+
# Intent 298. One UserPromptSubmit process replacing hook-continue,
|
|
7
|
+
# hook-future-intent-check, and hook-auto-arm: it appends a pending line to
|
|
8
|
+
# the session day ledger, detects "continue" and "auto" prompts, and hints at
|
|
9
|
+
# matching Future intents. Every job runs in its own rescue and the hook
|
|
10
|
+
# always exits 0 (spec D2).
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
require "yaml"
|
|
14
|
+
require "open3"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
require_relative "lib/session_ledger"
|
|
17
|
+
require_relative "lib/dashboard_banner"
|
|
18
|
+
|
|
19
|
+
raw = begin
|
|
20
|
+
$stdin.read
|
|
21
|
+
rescue StandardError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
exit 0 if raw.nil? || raw.strip.empty?
|
|
25
|
+
|
|
26
|
+
payload = begin
|
|
27
|
+
JSON.parse(raw)
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
exit 0 unless payload.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
session_id = payload["session_id"].to_s
|
|
34
|
+
prompt = payload["user_prompt"].to_s
|
|
35
|
+
cwd = payload["cwd"].to_s
|
|
36
|
+
cwd = Dir.pwd if cwd.empty?
|
|
37
|
+
|
|
38
|
+
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
39
|
+
store = File.join(plastic_home, "store")
|
|
40
|
+
templates = File.expand_path("../templates", __dir__)
|
|
41
|
+
sid = SessionLedger.short_session_id(nil, session_id)
|
|
42
|
+
today = SessionLedger.day_id
|
|
43
|
+
|
|
44
|
+
# --- Extracted matching logic, shared by both the global and project store
|
|
45
|
+
# passes of job (f) below (mirrors hook-future-intent-check verbatim). ---
|
|
46
|
+
def future_intent_matches(store_root, message)
|
|
47
|
+
index_path = File.join(store_root, "INDEX.md")
|
|
48
|
+
return [] unless File.exist?(index_path)
|
|
49
|
+
|
|
50
|
+
lines = File.readlines(index_path)
|
|
51
|
+
future_dirs = []
|
|
52
|
+
section = nil
|
|
53
|
+
|
|
54
|
+
lines.each do |line|
|
|
55
|
+
section = :future if line.start_with?("## Future")
|
|
56
|
+
section = nil if line.start_with?("## ") && !line.start_with?("## Future")
|
|
57
|
+
next unless section == :future && line.strip.start_with?("- [")
|
|
58
|
+
|
|
59
|
+
future_dirs << $1 if line =~ /store\/([\w-]+)\//
|
|
60
|
+
end
|
|
61
|
+
return [] if future_dirs.empty?
|
|
62
|
+
|
|
63
|
+
matches = []
|
|
64
|
+
message_words = message.split(/\W+/).reject { |w| w.length < 4 }
|
|
65
|
+
|
|
66
|
+
future_dirs.each do |dir|
|
|
67
|
+
intent_path = File.join(store_root, "store", dir, "#{dir}.md")
|
|
68
|
+
next unless File.exist?(intent_path)
|
|
69
|
+
|
|
70
|
+
content = File.read(intent_path)
|
|
71
|
+
|
|
72
|
+
tags = []
|
|
73
|
+
tags = $1.split(",").map(&:strip).map(&:downcase) if content =~ /^tags:\s*\[([^\]]+)\]/
|
|
74
|
+
|
|
75
|
+
intent_name = ""
|
|
76
|
+
intent_name = $1.downcase if content =~ /^intent:\s*["']?(.+?)["']?\s*$/
|
|
77
|
+
|
|
78
|
+
keywords = (tags + intent_name.split(/\W+/).reject { |w| w.length < 4 }).map(&:downcase).uniq
|
|
79
|
+
|
|
80
|
+
matched_keywords = keywords.select { |kw| message.include?(kw) }
|
|
81
|
+
name_matches = message_words.select { |w| intent_name.include?(w) }
|
|
82
|
+
matched_keywords = (matched_keywords + name_matches).uniq
|
|
83
|
+
|
|
84
|
+
next if matched_keywords.empty?
|
|
85
|
+
|
|
86
|
+
index_line = lines.find { |l| l.include?(dir) }&.strip || "#{dir} - #{intent_name}"
|
|
87
|
+
matches << { "index_line" => index_line, "keywords" => matched_keywords }
|
|
88
|
+
end
|
|
89
|
+
matches
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The store_root that carries the current project's own store, resolved from
|
|
93
|
+
# cwd through projects.yml, mirroring SessionLedger.project_slug's matching
|
|
94
|
+
# but returning nil (not "global") when nothing matches, since there is then
|
|
95
|
+
# no distinct second store to hint against.
|
|
96
|
+
def resolve_project_store_root(cwd, plastic_home)
|
|
97
|
+
projects_path = File.join(plastic_home, "projects.yml")
|
|
98
|
+
return nil unless File.exist?(projects_path)
|
|
99
|
+
|
|
100
|
+
data = begin
|
|
101
|
+
YAML.safe_load(File.read(projects_path))
|
|
102
|
+
rescue StandardError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
data = {} unless data.is_a?(Hash)
|
|
106
|
+
projects = data["projects"].is_a?(Hash) ? data["projects"] : {}
|
|
107
|
+
expanded_cwd = File.expand_path(cwd)
|
|
108
|
+
|
|
109
|
+
matches = projects.filter_map do |slug, info|
|
|
110
|
+
next unless info.is_a?(Hash) && slug.is_a?(String)
|
|
111
|
+
|
|
112
|
+
path = info["path"]
|
|
113
|
+
next unless path
|
|
114
|
+
|
|
115
|
+
root = File.expand_path(path)
|
|
116
|
+
next unless expanded_cwd == root || expanded_cwd.start_with?("#{root}#{File::SEPARATOR}")
|
|
117
|
+
|
|
118
|
+
[root.length, slug]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
best = matches.max_by { |(length, _slug)| length }
|
|
122
|
+
best ? File.join(plastic_home, "projects", best[1]) : nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def truncate(text, max)
|
|
126
|
+
return text if text.length <= max
|
|
127
|
+
|
|
128
|
+
"#{text[0, max - 3]}..."
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# --- (a) tmp root + heartbeat, unconditional -------------------------------
|
|
132
|
+
begin
|
|
133
|
+
SessionLedger.ensure_tmp_root(store)
|
|
134
|
+
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
|
|
135
|
+
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
|
136
|
+
rescue StandardError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# --- (b) read the pointer -----------------------------------------------
|
|
141
|
+
skip_day_ledger = false
|
|
142
|
+
begin
|
|
143
|
+
pointer = SessionLedger.pointer_path(store, sid)
|
|
144
|
+
if File.exist?(pointer)
|
|
145
|
+
current = File.read(pointer).strip
|
|
146
|
+
skip_day_ledger = !current.empty? && current != today
|
|
147
|
+
end
|
|
148
|
+
rescue StandardError
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# --- (c) pending checklist line -------------------------------------------
|
|
153
|
+
unless skip_day_ledger
|
|
154
|
+
begin
|
|
155
|
+
project = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
156
|
+
clean = truncate(SessionLedger.sanitize_summary(prompt), 120)
|
|
157
|
+
if clean.length >= 10
|
|
158
|
+
SessionLedger.open_day(store: store, day: today, templates: templates, author: sid)
|
|
159
|
+
line = SessionLedger.checklist_line(:pending, sid, project, clean)
|
|
160
|
+
SessionLedger.append_line(SessionLedger.checklist_path(store, today), line,
|
|
161
|
+
header: SessionLedger.checklist_header(today))
|
|
162
|
+
end
|
|
163
|
+
rescue StandardError
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context_parts = []
|
|
169
|
+
system_message = nil
|
|
170
|
+
|
|
171
|
+
# --- (d) "continue" cockpit -------------------------------------------------
|
|
172
|
+
begin
|
|
173
|
+
if prompt.match?(/\bcontinue\b/i)
|
|
174
|
+
dashboard = File.expand_path("dashboard.rb", __dir__)
|
|
175
|
+
if File.exist?(dashboard)
|
|
176
|
+
cockpit, _err, status = Open3.capture3({ "RUBYOPT" => nil }, "ruby", dashboard, "continue")
|
|
177
|
+
if status.success? && !cockpit.strip.empty?
|
|
178
|
+
context_parts << (cockpit.rstrip +
|
|
179
|
+
"\n\nFollow the plastic-intent-continuing skill workflow to resume: " \
|
|
180
|
+
"if active intents exist, read their state and resume; otherwise surface the " \
|
|
181
|
+
"most-valuable next work and any stale or triage intents from the board above.")
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
data_json, _data_err, data_status = Open3.capture3({ "RUBYOPT" => nil }, "ruby", dashboard,
|
|
185
|
+
"continue", "--data")
|
|
186
|
+
if data_status.success?
|
|
187
|
+
banner = DashboardBanner.render(JSON.parse(data_json))
|
|
188
|
+
system_message = banner if banner
|
|
189
|
+
end
|
|
190
|
+
rescue StandardError
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
rescue StandardError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# --- (e) auto-trigger steering (hook-auto-arm job 1 only) ------------------
|
|
201
|
+
begin
|
|
202
|
+
message_lower = prompt.downcase
|
|
203
|
+
auto_triggers = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
|
|
204
|
+
triggered = auto_triggers.any? { |p| message_lower.include?(p) } || message_lower.match?(/\bauto\b/)
|
|
205
|
+
if triggered
|
|
206
|
+
context_parts << "Auto-mode request detected. Invoke the plastic-auto skill to deliver the " \
|
|
207
|
+
"active intent autonomously - it takes the intent under a lock and a worktree " \
|
|
208
|
+
"and points this session at it."
|
|
209
|
+
end
|
|
210
|
+
rescue StandardError
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# --- (f) Future-intent hint, global store then the project's own store -----
|
|
215
|
+
begin
|
|
216
|
+
message = prompt.to_s.downcase
|
|
217
|
+
if message.strip.length >= 10 && message.strip != "continue"
|
|
218
|
+
matches = future_intent_matches(plastic_home, message)
|
|
219
|
+
project_root = resolve_project_store_root(cwd, plastic_home)
|
|
220
|
+
matches += future_intent_matches(project_root, message) if project_root
|
|
221
|
+
|
|
222
|
+
if matches.any?
|
|
223
|
+
parts = ["PLASTIC - Future intents related to this message:\n"]
|
|
224
|
+
matches.each { |m| parts << "#{m["index_line"]} (matched: #{m["keywords"].join(", ")})" }
|
|
225
|
+
parts << "\nConsider asking the user if they want to activate any of these, or note the connection."
|
|
226
|
+
context_parts << parts.join("\n")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
rescue StandardError
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
exit 0 if context_parts.empty?
|
|
234
|
+
|
|
235
|
+
payload_out = {
|
|
236
|
+
"hookSpecificOutput" => {
|
|
237
|
+
"hookEventName" => "UserPromptSubmit",
|
|
238
|
+
"additionalContext" => context_parts.join("\n\n")
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
payload_out["systemMessage"] = system_message if system_message
|
|
242
|
+
puts JSON.generate(payload_out)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# hook-close (intent 301): the SessionEnd hook. Reads the harness payload from
|
|
6
|
+
# stdin (session_id, cwd, reason), takes the Plastic home from argv, and runs
|
|
7
|
+
# SessionClose with the detached filer spawner. Always exits 0, prints nothing.
|
|
8
|
+
#
|
|
9
|
+
# Usage: hook-close <plastic_home> (stdin: the hook JSON)
|
|
10
|
+
|
|
11
|
+
require "json"
|
|
12
|
+
require_relative "lib/session_ledger"
|
|
13
|
+
require_relative "lib/session_close"
|
|
14
|
+
|
|
15
|
+
plastic_home = ARGV[0]
|
|
16
|
+
exit 0 if plastic_home.nil? || plastic_home.empty?
|
|
17
|
+
|
|
18
|
+
payload =
|
|
19
|
+
begin
|
|
20
|
+
raw = $stdin.tty? ? "" : $stdin.read
|
|
21
|
+
raw.to_s.strip.empty? ? {} : JSON.parse(raw)
|
|
22
|
+
rescue JSON::ParserError
|
|
23
|
+
{}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
store = File.join(File.expand_path(plastic_home), "store")
|
|
27
|
+
filer = File.expand_path("file-session-intent", __dir__)
|
|
28
|
+
begin
|
|
29
|
+
SessionClose.run(payload: payload, store: store, today: SessionLedger.day_id,
|
|
30
|
+
spawner: SessionClose.default_spawner(filer))
|
|
31
|
+
rescue StandardError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
exit 0
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-record (reads the PostToolUse stdin JSON payload)
|
|
6
|
+
# Intent 298. Replaces hook-gate-check: never blocks, keeps the decoupled
|
|
7
|
+
# intent-dir savepoint append byte for byte, refreshes the delivery-lock lease
|
|
8
|
+
# for the owning session (intent 302, the lock gate's old allow-path job),
|
|
9
|
+
# promotes the session's newest pending day-ledger line when a project file
|
|
10
|
+
# lands, and calls the session-commit seam when that script exists. Always
|
|
11
|
+
# exits 0 (spec D3).
|
|
12
|
+
|
|
13
|
+
require "json"
|
|
14
|
+
require "timeout"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
require_relative "lib/savepoint"
|
|
17
|
+
require_relative "lib/lock"
|
|
18
|
+
require_relative "lib/session_ledger"
|
|
19
|
+
|
|
20
|
+
raw = begin
|
|
21
|
+
$stdin.read
|
|
22
|
+
rescue StandardError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
exit 0 if raw.nil? || raw.strip.empty?
|
|
26
|
+
|
|
27
|
+
payload = begin
|
|
28
|
+
JSON.parse(raw)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
exit 0 unless payload.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
file_path = payload.dig("tool_input", "file_path") || payload.dig("tool_params", "file_path")
|
|
35
|
+
exit 0 if file_path.to_s.empty?
|
|
36
|
+
|
|
37
|
+
session_id = payload["session_id"].to_s
|
|
38
|
+
cwd = payload["cwd"].to_s
|
|
39
|
+
cwd = Dir.pwd if cwd.empty?
|
|
40
|
+
|
|
41
|
+
file_path_abs = File.expand_path(file_path)
|
|
42
|
+
|
|
43
|
+
# --- (a) decoupled savepoint ledger, byte for byte -------------------------
|
|
44
|
+
intent_dir_abs = nil
|
|
45
|
+
begin
|
|
46
|
+
intent_dir_abs = Savepoint.intent_dir_for(file_path_abs)
|
|
47
|
+
if intent_dir_abs
|
|
48
|
+
begin
|
|
49
|
+
Savepoint.append_savepoint(intent_dir_abs, file_path_abs)
|
|
50
|
+
if File.basename(file_path_abs) == "checklist.md" && Savepoint.stage_file_present?(file_path_abs)
|
|
51
|
+
Savepoint.append_exec_started(intent_dir_abs)
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
# The lease heartbeat (intent 302, spec D10): the lock gate's allow path used
|
|
57
|
+
# to refresh delivery.lock on every edit; record does it now. Lock.heartbeat
|
|
58
|
+
# refuses a session that does not hold the lock, so a stranger's write never
|
|
59
|
+
# refreshes the owner's lease.
|
|
60
|
+
begin
|
|
61
|
+
Lock.heartbeat(intent_dir_abs, session: session_id) unless session_id.empty?
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
71
|
+
|
|
72
|
+
# --- (b) stop the day-ledger path when inside ~/.plastic or an intent dir --
|
|
73
|
+
inside_plastic = file_path_abs == plastic_home || file_path_abs.start_with?("#{plastic_home}/")
|
|
74
|
+
skip_day_ledger = inside_plastic || !intent_dir_abs.nil?
|
|
75
|
+
|
|
76
|
+
# --- (c) promote the newest pending line, Item savepoint, seam -------------
|
|
77
|
+
unless skip_day_ledger
|
|
78
|
+
begin
|
|
79
|
+
store = File.join(plastic_home, "store")
|
|
80
|
+
sid = SessionLedger.short_session_id(nil, session_id)
|
|
81
|
+
today = SessionLedger.day_id
|
|
82
|
+
|
|
83
|
+
proceed = true
|
|
84
|
+
pointer = SessionLedger.pointer_path(store, sid)
|
|
85
|
+
if File.exist?(pointer)
|
|
86
|
+
current = File.read(pointer).strip
|
|
87
|
+
proceed = current.empty? || current == today
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if proceed
|
|
91
|
+
checklist = SessionLedger.checklist_path(store, today)
|
|
92
|
+
target_summary = begin
|
|
93
|
+
SessionLedger.set_state(checklist, from: :pending, to: :open, session: sid)
|
|
94
|
+
rescue SessionLedger::LockUnavailableError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if target_summary
|
|
99
|
+
project = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
100
|
+
line = SessionLedger.savepoint_line("Item", sid, project, target_summary, now: Time.now)
|
|
101
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, today), line, header: nil)
|
|
102
|
+
|
|
103
|
+
seam = File.expand_path("session-commit", __dir__)
|
|
104
|
+
if File.executable?(seam)
|
|
105
|
+
begin
|
|
106
|
+
# A bare Timeout.timeout around Open3.capture3 does not reliably bound a
|
|
107
|
+
# genuinely hanging child: capture3's internal wait still blocks on
|
|
108
|
+
# Process.waitpid for the child regardless of the raised Timeout::Error.
|
|
109
|
+
# Spawn directly and kill on timeout instead, so the 10s bound is real.
|
|
110
|
+
pid = Process.spawn({ "RUBYOPT" => nil }, seam, "--cwd", cwd, "--summary", target_summary,
|
|
111
|
+
"--session", sid, "--day", today,
|
|
112
|
+
[:out, :err] => File::NULL)
|
|
113
|
+
begin
|
|
114
|
+
Timeout.timeout(10) { Process.wait(pid) }
|
|
115
|
+
rescue Timeout::Error
|
|
116
|
+
begin
|
|
117
|
+
Process.kill("KILL", pid)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
begin
|
|
122
|
+
Process.wait(pid)
|
|
123
|
+
rescue StandardError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# --- (d) heartbeat, always -------------------------------------------------
|
|
139
|
+
begin
|
|
140
|
+
store ||= File.join(plastic_home, "store")
|
|
141
|
+
sid ||= SessionLedger.short_session_id(nil, session_id)
|
|
142
|
+
SessionLedger.ensure_tmp_root(store)
|
|
143
|
+
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
|
|
144
|
+
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
exit 0
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "date"
|
|
8
8
|
require "yaml"
|
|
9
|
+
require "fileutils"
|
|
9
10
|
require_relative "lib/bridge"
|
|
11
|
+
require_relative "lib/savepoint"
|
|
10
12
|
require_relative "lib/boot_banner"
|
|
11
13
|
require_relative "lib/qmd_sync"
|
|
12
14
|
require_relative "lib/doctor_core"
|
|
15
|
+
require_relative "lib/session_ledger"
|
|
13
16
|
|
|
14
17
|
index_path, plastic_home, mode, plugin_root = ARGV
|
|
15
18
|
exit 0 unless index_path && plastic_home && mode
|
|
@@ -48,16 +51,20 @@ lines.each do |line|
|
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
# ---
|
|
54
|
+
# --- Stage of the single active intent (the /tmp bridge this once derived was removed in 2.0, intent 307) ---
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
stage_line = nil
|
|
54
57
|
if active.length == 1 && active.first =~ /store\/([\w-]+)\//
|
|
55
58
|
dir_name = $1
|
|
56
59
|
intent_dir = "#{store_dir}/#{dir_name}"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
begin
|
|
61
|
+
stage = Savepoint.derive_stage(intent_dir)
|
|
62
|
+
missing = Savepoint.missing_for_stage(stage, intent_dir)
|
|
63
|
+
missing_str = missing.empty? ? "none" : missing.join(", ")
|
|
64
|
+
stage_line = "Stage: #{stage} | Next: #{missing_str}"
|
|
65
|
+
rescue StandardError
|
|
66
|
+
stage_line = nil
|
|
67
|
+
end
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
# --- Detect stale future intents ---
|
|
@@ -251,12 +258,7 @@ if plastic_md
|
|
|
251
258
|
if all_active.any?
|
|
252
259
|
parts << "Active intents:\n"
|
|
253
260
|
all_active.each { |a| parts << a }
|
|
254
|
-
if
|
|
255
|
-
stage = bridge_data["build"]["stage"]
|
|
256
|
-
missing = bridge_data["build"]["missing"]
|
|
257
|
-
missing_str = missing.empty? ? "none" : missing.join(", ")
|
|
258
|
-
parts << "Stage: #{stage} | Next: #{missing_str}"
|
|
259
|
-
end
|
|
261
|
+
parts << stage_line if stage_line
|
|
260
262
|
parts << ""
|
|
261
263
|
else
|
|
262
264
|
parts << "No active intents. #{future.length} future intents available.\n"
|
|
@@ -302,6 +304,87 @@ if update_notice
|
|
|
302
304
|
parts.unshift("! #{update_notice}\n")
|
|
303
305
|
end
|
|
304
306
|
|
|
307
|
+
# --- First-boot sweep (intent 301, spec D9): file every unclosed prior day
|
|
308
|
+
# ledger into today, oldest first, at most 3 per boot within a 5-second
|
|
309
|
+
# budget. Each day runs the sibling file-session-intent in its own rescue;
|
|
310
|
+
# the boot never blocks on it.
|
|
311
|
+
begin
|
|
312
|
+
require "open3"
|
|
313
|
+
require "timeout"
|
|
314
|
+
require_relative "lib/session_backfill"
|
|
315
|
+
|
|
316
|
+
sweep_today = SessionLedger.day_id
|
|
317
|
+
sweep_root = SessionLedger.sessions_root(store_dir)
|
|
318
|
+
sweep_filer = File.expand_path("file-session-intent", __dir__)
|
|
319
|
+
sweep_templates = File.expand_path("../templates", __dir__)
|
|
320
|
+
candidates = []
|
|
321
|
+
if Dir.exist?(sweep_root) && File.exist?(sweep_filer)
|
|
322
|
+
candidates = Dir.children(sweep_root).select do |name|
|
|
323
|
+
File.directory?(File.join(sweep_root, name)) && SessionLedger.valid_day_id?(name) &&
|
|
324
|
+
name < sweep_today && !SessionBackfill.closed?(store_dir, name)
|
|
325
|
+
end.sort
|
|
326
|
+
end
|
|
327
|
+
filed = 0
|
|
328
|
+
budget_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
329
|
+
candidates.first(3).each do |day|
|
|
330
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > budget_end
|
|
331
|
+
|
|
332
|
+
begin
|
|
333
|
+
Timeout.timeout(5) do
|
|
334
|
+
_out, _err, sweep_status = Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, sweep_filer,
|
|
335
|
+
"--day", day, "--carry-to", sweep_today,
|
|
336
|
+
"--store", store_dir, "--templates", sweep_templates)
|
|
337
|
+
filed += 1 if sweep_status.success? && SessionBackfill.closed?(store_dir, day)
|
|
338
|
+
end
|
|
339
|
+
rescue StandardError
|
|
340
|
+
nil
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
if filed.positive?
|
|
344
|
+
line = "PLASTIC: filed #{filed} prior day ledger(s) into #{sweep_today}"
|
|
345
|
+
remaining = candidates.size - filed
|
|
346
|
+
line += ", #{remaining} more wait for the next boot" if remaining.positive?
|
|
347
|
+
parts << line
|
|
348
|
+
end
|
|
349
|
+
rescue StandardError
|
|
350
|
+
nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# --- Session day ledger: open or join today, write the per-session pointer
|
|
354
|
+
# and heartbeat (intent 298, spec D4). Best-effort: any failure here degrades
|
|
355
|
+
# to no ledger context line, never blocks the session.
|
|
356
|
+
begin
|
|
357
|
+
templates = File.expand_path("../templates", __dir__)
|
|
358
|
+
day = SessionLedger.day_id
|
|
359
|
+
author = `"#{read_config}" author`.strip
|
|
360
|
+
author = "session" if author.empty?
|
|
361
|
+
SessionLedger.open_day(store: store_dir, day: day, templates: templates, author: author)
|
|
362
|
+
|
|
363
|
+
session = ENV["CLAUDE_CODE_SESSION_ID"] || Process.pid.to_s
|
|
364
|
+
sid = SessionLedger.short_session_id(nil, session)
|
|
365
|
+
SessionLedger.ensure_tmp_root(store_dir)
|
|
366
|
+
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store_dir, sid))
|
|
367
|
+
File.write(SessionLedger.heartbeat_path(store_dir, sid), "#{Time.now.utc.iso8601}\n")
|
|
368
|
+
pointer = SessionLedger.pointer_path(store_dir, sid)
|
|
369
|
+
File.write(pointer, "#{day}\n") unless File.exist?(pointer)
|
|
370
|
+
|
|
371
|
+
checklist = SessionLedger.checklist_path(store_dir, day)
|
|
372
|
+
open_count = 0
|
|
373
|
+
pending_count = 0
|
|
374
|
+
if File.exist?(checklist)
|
|
375
|
+
File.readlines(checklist).each do |line|
|
|
376
|
+
parsed = SessionLedger.parse_checklist_line(line)
|
|
377
|
+
next unless parsed
|
|
378
|
+
|
|
379
|
+
open_count += 1 if parsed[:state] == :open
|
|
380
|
+
pending_count += 1 if parsed[:state] == :pending
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
parts << "PLASTIC: day ledger #{day} joined (#{open_count} open items, #{pending_count} pending)"
|
|
384
|
+
rescue StandardError
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
|
|
305
388
|
# Emit nothing when there is genuinely nothing to surface (no conventions,
|
|
306
389
|
# no deprecations, no update notice).
|
|
307
390
|
exit 0 if parts.join.strip.empty?
|