@zalom/plastic 2.0.0-alpha.8 → 2.0.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 +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +20 -15
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +23 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +62 -1
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +238 -22
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +19 -5
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +48 -83
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +99 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +31 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +62 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +71 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +33 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +30 -16
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +229 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +532 -71
- package/scripts/lib/intent_screen.rb +9 -6
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +377 -173
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +913 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1460 -74
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +158 -8
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +525 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +555 -0
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +25 -18
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +42 -33
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +192 -30
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +15 -2
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -295
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -173
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -17,8 +17,9 @@ module SessionClose
|
|
|
17
17
|
NOOP_REASONS = %w[clear resume].freeze
|
|
18
18
|
|
|
19
19
|
# The default hand-off writer (intent 311, spec D6): renders this session's
|
|
20
|
-
# share of
|
|
21
|
-
# goes. The hook script builds it
|
|
20
|
+
# share of its session day (SessionLedger.session_day, graph.md D7) into
|
|
21
|
+
# handoff--<session>.md before the tmp dir goes. The hook script builds it
|
|
22
|
+
# with the shipped templates dir.
|
|
22
23
|
def default_handoff(templates)
|
|
23
24
|
lambda do |store, day, session|
|
|
24
25
|
Handoff.write(store: store, day: day, session: session, trigger: "close", templates: templates)
|
|
@@ -45,26 +46,33 @@ module SessionClose
|
|
|
45
46
|
session = SessionLedger.short_session_id(payload.is_a?(Hash) ? payload["session_id"] : nil, nil)
|
|
46
47
|
return report if session.empty?
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
days = safely { SessionLedger.session_days(store, session, today: today) } || []
|
|
50
|
+
days = [today] if days.empty?
|
|
51
|
+
session_day = days.last
|
|
49
52
|
project = "global"
|
|
50
53
|
|
|
54
|
+
# Intent 344 (G11, D13): a session that crossed midnight carries pending
|
|
55
|
+
# lines on more than one day ledger; every one of them drops, not only
|
|
56
|
+
# the newest, so a close never leaves an earlier day's lines pending.
|
|
51
57
|
report[:dropped] = safely do
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
days.sum do |day|
|
|
59
|
+
checklist = SessionLedger.checklist_path(store, day)
|
|
60
|
+
count = SessionLedger.flip_all(checklist, from: :pending, to: :dropped, session: session)
|
|
61
|
+
if count.positive?
|
|
62
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, day),
|
|
63
|
+
SessionLedger.savepoint_line("Note", session, project,
|
|
64
|
+
"dropped #{count} pending lines at close", now: now))
|
|
65
|
+
end
|
|
66
|
+
count
|
|
58
67
|
end
|
|
59
|
-
count
|
|
60
68
|
end || 0
|
|
61
69
|
|
|
62
70
|
# The hand-off (intent 311, spec D6) is written after the drop, so it
|
|
63
|
-
# reflects it, and before the tmp dir goes
|
|
64
|
-
#
|
|
71
|
+
# reflects it, and before the tmp dir goes. A session with no line in the
|
|
72
|
+
# day ledger's window falls back to today, the same as the drop above.
|
|
65
73
|
if handoff
|
|
66
74
|
report[:handoff] = safely do
|
|
67
|
-
handoff.call(store,
|
|
75
|
+
handoff.call(store, session_day, session)
|
|
68
76
|
true
|
|
69
77
|
end || false
|
|
70
78
|
end
|
|
@@ -75,27 +83,21 @@ module SessionClose
|
|
|
75
83
|
true
|
|
76
84
|
end || false
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
# One carry spawns per day earlier than today, oldest first; a single
|
|
87
|
+
# earlier day keeps reporting its own arg array (not a one-element
|
|
88
|
+
# list) so an existing caller reading report[:spawned] as one array is
|
|
89
|
+
# unaffected.
|
|
90
|
+
earlier_days = days.select { |day| day < today }
|
|
91
|
+
unless earlier_days.empty?
|
|
79
92
|
report[:spawned] = safely do
|
|
80
|
-
|
|
81
|
-
spawner.call(args)
|
|
82
|
-
|
|
93
|
+
arg_lists = earlier_days.map { |day| ["--day", day, "--carry-to", today, "--store", store] }
|
|
94
|
+
arg_lists.each { |args| spawner.call(args) }
|
|
95
|
+
arg_lists.length == 1 ? arg_lists.first : arg_lists
|
|
83
96
|
end
|
|
84
97
|
end
|
|
85
98
|
report
|
|
86
99
|
end
|
|
87
100
|
|
|
88
|
-
# The pointer names the intent this session records into; a day id means
|
|
89
|
-
# the day ledger. Anything else (an intent id) means an auto team owns the
|
|
90
|
-
# record and the close touches no day ledger.
|
|
91
|
-
def read_pointer_day(store, session)
|
|
92
|
-
path = SessionLedger.pointer_path(store, session)
|
|
93
|
-
return nil unless File.exist?(path)
|
|
94
|
-
|
|
95
|
-
value = File.read(path).strip
|
|
96
|
-
SessionLedger.valid_day_id?(value) ? value : nil
|
|
97
|
-
end
|
|
98
|
-
|
|
99
101
|
def safely
|
|
100
102
|
yield
|
|
101
103
|
rescue StandardError
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require_relative "worktree"
|
|
6
7
|
require_relative "scaffold_intent"
|
|
7
8
|
require_relative "session_ledger"
|
|
9
|
+
require_relative "active_delivery"
|
|
8
10
|
|
|
9
11
|
# SessionGit - the branch model and per-repo flow settings behind
|
|
10
12
|
# `scripts/session-commit` (intent 300), which is how a checklist item
|
|
@@ -159,7 +161,7 @@ module SessionGit
|
|
|
159
161
|
def read_project_flow(slug, plastic_home)
|
|
160
162
|
return nil if slug == "global"
|
|
161
163
|
|
|
162
|
-
project_yml = File.join(plastic_home,
|
|
164
|
+
project_yml = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "project.yml")
|
|
163
165
|
return nil unless File.exist?(project_yml)
|
|
164
166
|
|
|
165
167
|
data = begin
|
|
@@ -185,24 +187,29 @@ module SessionGit
|
|
|
185
187
|
text.to_s.downcase.scan(/[a-z0-9]+/).first(max_words).join("-")
|
|
186
188
|
end
|
|
187
189
|
|
|
188
|
-
# The intent
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
# not an intent name, and falls back to the day id here too, which is the
|
|
194
|
-
# same value either way.
|
|
195
|
-
def resolve_ticket(day:, store:, session:, ticket_source:)
|
|
190
|
+
# The delivering intent's id when `ticket_source` is `intent_id`, else the
|
|
191
|
+
# day id (344 n2, D4-D7, D12). `resolver` is injected so tests need no
|
|
192
|
+
# real ~/.plastic; it defaults to ActiveDelivery.resolve_by_short_session
|
|
193
|
+
# against the store's own project roots.
|
|
194
|
+
def resolve_ticket(day:, store:, session:, ticket_source:, resolver: nil)
|
|
196
195
|
return day.to_s unless ticket_source.to_s == "intent_id"
|
|
197
196
|
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
resolver ||= lambda { |short_session|
|
|
198
|
+
ActiveDelivery.resolve_by_short_session(
|
|
199
|
+
global_store: store,
|
|
200
|
+
project_roots: ActiveDelivery.project_roots(Plastic::StoreLayout.locate(store).first),
|
|
201
|
+
short_session: short_session
|
|
202
|
+
)
|
|
203
|
+
}
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
dir = begin
|
|
206
|
+
resolver.call(session)
|
|
207
|
+
rescue StandardError
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
return day.to_s if dir.to_s.empty?
|
|
204
211
|
|
|
205
|
-
|
|
212
|
+
File.basename(dir).split("--", 2).first
|
|
206
213
|
end
|
|
207
214
|
|
|
208
215
|
# --- commit message ------------------------------------------------------------
|
|
@@ -245,7 +252,7 @@ module SessionGit
|
|
|
245
252
|
# The branch HEAD points to, even on an unborn branch (review R3: a fresh
|
|
246
253
|
# `git init`, zero commits). `git rev-parse --abbrev-ref HEAD` FAILS on an
|
|
247
254
|
# unborn branch (there is no commit for it to resolve yet), which the old
|
|
248
|
-
# implementation misread as "cannot determine a branch" and
|
|
255
|
+
# implementation misread as "cannot determine a branch" and merged into
|
|
249
256
|
# the detached-HEAD case. `git symbolic-ref --quiet --short HEAD` succeeds
|
|
250
257
|
# on both a normal AND an unborn branch (HEAD is a symbolic ref to
|
|
251
258
|
# `refs/heads/<name>` in both cases) and only fails when HEAD is
|
|
@@ -327,7 +334,7 @@ module SessionGit
|
|
|
327
334
|
# a detached HEAD. Both modes now share exactly one check of each.
|
|
328
335
|
def commit!(cwd:, summary:, day:, session:, plastic_home:, store: nil,
|
|
329
336
|
runner: Worktree::ShellRunner.new, gh_runner: GhRunner.new)
|
|
330
|
-
effective_store = store ||
|
|
337
|
+
effective_store = store || Plastic::StoreLayout.global_store(plastic_home)
|
|
331
338
|
repo = resolve_repo(cwd, runner: runner)
|
|
332
339
|
return note("no repo") if repo.nil?
|
|
333
340
|
|
|
@@ -354,7 +361,7 @@ module SessionGit
|
|
|
354
361
|
|
|
355
362
|
# An unknown mode/workspace value, or workspace: worktree's degradation
|
|
356
363
|
# to checkout, is itself a degradation (spec D2, BLOCKER 3 ruling), so
|
|
357
|
-
# it always turns the outcome into a Note,
|
|
364
|
+
# it always turns the outcome into a Note, merging both facts into the
|
|
358
365
|
# single savepoint line spec D7 allows.
|
|
359
366
|
Result.new(message: "#{flow_notes.join('; ')}; #{result.message}", event: "Note")
|
|
360
367
|
end
|
|
@@ -29,6 +29,10 @@ module SessionLedger
|
|
|
29
29
|
|
|
30
30
|
SESSIONS_DIR = ".sessions"
|
|
31
31
|
TMP_DIR = ".tmp"
|
|
32
|
+
# Named here, not in a CLI, so a caller can reference the key without the
|
|
33
|
+
# literal harness-branded env var name appearing in its own source (a few
|
|
34
|
+
# scripts are guarded against naming a harness at all).
|
|
35
|
+
SESSION_ID_ENV_KEY = "CLAUDE_CODE_SESSION_ID"
|
|
32
36
|
DAY_ID = /\A\d{8}\z/
|
|
33
37
|
EVENTS = %w[Item Done Note]
|
|
34
38
|
STATES = { pending: "~", open: " ", done: "x", moved: ">", dropped: "-", promoted: "^" }.freeze
|
|
@@ -150,14 +154,54 @@ module SessionLedger
|
|
|
150
154
|
File.join(tmp_root(store), session_id)
|
|
151
155
|
end
|
|
152
156
|
|
|
153
|
-
def pointer_path(store, session_id)
|
|
154
|
-
File.join(session_tmp_dir(store, session_id), "current")
|
|
155
|
-
end
|
|
156
|
-
|
|
157
157
|
def heartbeat_path(store, session_id)
|
|
158
158
|
File.join(session_tmp_dir(store, session_id), "heartbeat")
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
+
# --- Session day (spec D7 replacement for the retired pointer) ------------
|
|
162
|
+
|
|
163
|
+
# Every day, ascending, no later than `today` and no earlier than `today`
|
|
164
|
+
# minus `window_days`, whose checklist carries at least one line tagged to
|
|
165
|
+
# `session` (any state). Each day's checklist is read inside its own
|
|
166
|
+
# rescue, so one unreadable or missing checklist skips that day only and
|
|
167
|
+
# never gives up on a later one. Returns an empty array when no day in the
|
|
168
|
+
# window carries the session's line, and on any error reading the store
|
|
169
|
+
# (never raises).
|
|
170
|
+
def session_days(store, session, today:, window_days: 7)
|
|
171
|
+
root = sessions_root(store)
|
|
172
|
+
return [] unless File.directory?(root)
|
|
173
|
+
|
|
174
|
+
today_date = Date.strptime(today, "%Y%m%d")
|
|
175
|
+
floor_date = today_date - window_days
|
|
176
|
+
|
|
177
|
+
days = Dir.children(root).select { |name| valid_day_id?(name) }.select do |name|
|
|
178
|
+
date = Date.strptime(name, "%Y%m%d")
|
|
179
|
+
date >= floor_date && date <= today_date
|
|
180
|
+
end.sort
|
|
181
|
+
|
|
182
|
+
days.select do |day|
|
|
183
|
+
begin
|
|
184
|
+
read_locked(checklist_path(store, day)).each_line.any? do |line|
|
|
185
|
+
parsed = parse_checklist_line(line)
|
|
186
|
+
parsed && parsed[:session] == session
|
|
187
|
+
end
|
|
188
|
+
rescue StandardError
|
|
189
|
+
false
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
rescue StandardError
|
|
193
|
+
[]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The newest day in `session_days`, the day this session's close and
|
|
197
|
+
# hand-off write belong to (intent 344, G11, D13: a session that crosses
|
|
198
|
+
# midnight carries lines on more than one day, and the newest is the one
|
|
199
|
+
# still open). Falls back to `today` when no day in the window carries the
|
|
200
|
+
# session's line.
|
|
201
|
+
def session_day(store, session, today:, window_days: 7)
|
|
202
|
+
session_days(store, session, today: today, window_days: window_days).last || today
|
|
203
|
+
end
|
|
204
|
+
|
|
161
205
|
# Create `.tmp/` plus a `.gitignore` holding exactly `*`, if that file does
|
|
162
206
|
# not already exist. The store is a local git repo that auto-commits
|
|
163
207
|
# `add -A`, so without this ignore file every heartbeat write would enter
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
# SessionUsage (intent 355, D10): reads the harness transcripts modified since
|
|
8
|
+
# a cutoff, dedupes assistant records by message id (the harness logs one
|
|
9
|
+
# record per content block), and reports per session the model, the calls in
|
|
10
|
+
# the window, the boot and last context, the median step, the steps over 5k,
|
|
11
|
+
# and the cache read. Context is input plus cache read plus cache write.
|
|
12
|
+
# Broken records are counted and named, never averaged over. The transcripts
|
|
13
|
+
# root and the rate-limit cache path are injected; nothing reads ENV.
|
|
14
|
+
class SessionUsage
|
|
15
|
+
WINDOW = 5 * 3600
|
|
16
|
+
BIG_STEP = 5_000
|
|
17
|
+
LABEL_WIDTH = 70
|
|
18
|
+
NO_PROMPT = "(no prompt)"
|
|
19
|
+
SYNTHETIC_MODEL = "<synthetic>"
|
|
20
|
+
CONTEXT_FIELDS = %w[input_tokens cache_read_input_tokens cache_creation_input_tokens].freeze
|
|
21
|
+
TRANSCRIPT_GLOBS = [File.join("*", "*.jsonl"), File.join("*", "*", "subagents", "*.jsonl")].freeze
|
|
22
|
+
HEADERS = ["session", "model", "calls", "boot", "last", "median step", "big steps", "cache read", "broken", "prompt"].freeze
|
|
23
|
+
|
|
24
|
+
def initialize(transcripts_root:, rate_limits_path:, now: Time.now)
|
|
25
|
+
@transcripts_root = transcripts_root
|
|
26
|
+
@rate_limits_path = rate_limits_path
|
|
27
|
+
@now = now
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cutoff(since: nil)
|
|
31
|
+
return [since, "since"] if since
|
|
32
|
+
|
|
33
|
+
reset = reset_time
|
|
34
|
+
return [reset - WINDOW, "rate-limit reset"] if reset && reset > @now
|
|
35
|
+
|
|
36
|
+
[@now - WINDOW, "last 5 hours"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def report(since: nil)
|
|
40
|
+
at, source = cutoff(since: since)
|
|
41
|
+
header = { "status" => "ok", "root" => @transcripts_root, "cutoff" => at.getutc.iso8601, "cutoff_source" => source }
|
|
42
|
+
return header.merge("status" => "unavailable") unless File.directory?(@transcripts_root)
|
|
43
|
+
|
|
44
|
+
broken = []
|
|
45
|
+
sessions = transcripts(at).filter_map { |path| summarize(path, at, broken) }
|
|
46
|
+
header.merge("sessions" => sessions.sort_by { |s| -s["cache_read"] }, "broken" => broken)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.render_text(report)
|
|
50
|
+
return "Session usage: unavailable (no transcripts directory at #{report['root']})\n" if report["status"] == "unavailable"
|
|
51
|
+
|
|
52
|
+
out = +"Session usage since #{report['cutoff']} (#{report['cutoff_source']})\n\n"
|
|
53
|
+
out << (report["sessions"].empty? ? "(no sessions)\n" : table(report["sessions"]))
|
|
54
|
+
return out if report["broken"].empty?
|
|
55
|
+
|
|
56
|
+
out << "\nBroken records (#{report['broken'].size}):\n"
|
|
57
|
+
report["broken"].each { |b| out << " #{b['file']}:#{b['line']} #{b['error']}\n" }
|
|
58
|
+
out
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.table(sessions)
|
|
62
|
+
rows = sessions.map do |s|
|
|
63
|
+
boot = tokens(s["boot"]) + (s["started_before_cutoff"] ? "*" : "")
|
|
64
|
+
[s["id"], s["model"].to_s, s["calls"].to_s, boot, tokens(s["last"]), tokens(s["median_step"]),
|
|
65
|
+
s["big_steps"].to_s, tokens(s["cache_read"]), s["broken"].to_s, s["label"]]
|
|
66
|
+
end
|
|
67
|
+
widths = HEADERS.each_index.map { |i| ([HEADERS] + rows).map { |r| r[i].length }.max }
|
|
68
|
+
lines = ([HEADERS] + rows).map { |r| r.each_with_index.map { |cell, i| cell.ljust(widths[i]) }.join(" ").rstrip }
|
|
69
|
+
note = sessions.any? { |s| s["started_before_cutoff"] } ? "\n* boot predates the cutoff\n" : ""
|
|
70
|
+
lines.join("\n") + "\n" + note
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.tokens(count)
|
|
74
|
+
return "-" if count.nil?
|
|
75
|
+
|
|
76
|
+
count.abs >= 1_000 ? format("%.1fk", count / 1_000.0) : count.to_s
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private_class_method :table, :tokens
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def reset_time
|
|
84
|
+
return unless File.file?(@rate_limits_path)
|
|
85
|
+
|
|
86
|
+
value = JSON.parse(File.read(@rate_limits_path))["resets_at"].to_s
|
|
87
|
+
return if value.empty?
|
|
88
|
+
|
|
89
|
+
value.match?(/\A\d+\z/) ? Time.at(value.to_i).utc : Time.iso8601(value)
|
|
90
|
+
rescue JSON::ParserError, ArgumentError, TypeError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def transcripts(cutoff)
|
|
95
|
+
TRANSCRIPT_GLOBS.flat_map { |glob| Dir.glob(File.join(@transcripts_root, glob)) }
|
|
96
|
+
.select { |path| File.mtime(path) >= cutoff }
|
|
97
|
+
.sort
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def summarize(path, cutoff, broken)
|
|
101
|
+
calls = {}
|
|
102
|
+
label = nil
|
|
103
|
+
torn = 0
|
|
104
|
+
File.foreach(path).with_index(1) do |line, number|
|
|
105
|
+
next if line.strip.empty?
|
|
106
|
+
|
|
107
|
+
record = JSON.parse(line)
|
|
108
|
+
raise TypeError, "record is not a JSON object" unless record.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
label ||= prompt_line(record)
|
|
111
|
+
call = call_from(record)
|
|
112
|
+
next unless call
|
|
113
|
+
|
|
114
|
+
calls[call[:id]] = calls.key?(call[:id]) ? call.merge(at: calls[call[:id]][:at]) : call
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
torn += 1
|
|
117
|
+
broken << { "file" => path, "line" => number, "error" => "unparsable JSON" }
|
|
118
|
+
rescue ArgumentError, TypeError => e
|
|
119
|
+
torn += 1
|
|
120
|
+
broken << { "file" => path, "line" => number, "error" => e.message }
|
|
121
|
+
end
|
|
122
|
+
row(path, calls.values, cutoff, label, torn)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def row(path, calls, cutoff, label, torn)
|
|
126
|
+
window = calls.each_index.select { |i| calls[i][:at] >= cutoff }
|
|
127
|
+
return if window.empty? && torn.zero?
|
|
128
|
+
|
|
129
|
+
steps = window.filter_map { |i| calls[i][:context] - calls[i - 1][:context] if i.positive? }
|
|
130
|
+
{
|
|
131
|
+
"id" => File.basename(path, ".jsonl"),
|
|
132
|
+
"file" => path,
|
|
133
|
+
"label" => label || NO_PROMPT,
|
|
134
|
+
"model" => calls.last && calls.last[:model],
|
|
135
|
+
"calls" => window.size,
|
|
136
|
+
"boot" => calls.first && calls.first[:context],
|
|
137
|
+
"last" => calls.last && calls.last[:context],
|
|
138
|
+
"median_step" => median(steps),
|
|
139
|
+
"big_steps" => steps.count { |step| step > BIG_STEP },
|
|
140
|
+
"cache_read" => window.sum { |i| calls[i][:cache_read] },
|
|
141
|
+
"broken" => torn,
|
|
142
|
+
"started_before_cutoff" => !calls.empty? && calls.first[:at] < cutoff,
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def call_from(record)
|
|
147
|
+
return unless record["type"] == "assistant"
|
|
148
|
+
|
|
149
|
+
message = record["message"]
|
|
150
|
+
raise TypeError, "assistant record without a message" unless message.is_a?(Hash)
|
|
151
|
+
return if message["model"] == SYNTHETIC_MODEL
|
|
152
|
+
|
|
153
|
+
usage = message["usage"]
|
|
154
|
+
raise ArgumentError, "assistant record without message id or usage" unless message["id"] && usage.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
id: message["id"],
|
|
158
|
+
model: message["model"],
|
|
159
|
+
at: Time.iso8601(record["timestamp"].to_s),
|
|
160
|
+
context: CONTEXT_FIELDS.sum { |field| usage[field].to_i },
|
|
161
|
+
cache_read: usage["cache_read_input_tokens"].to_i,
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def prompt_line(record)
|
|
166
|
+
return unless record["type"] == "user" && !record["isMeta"]
|
|
167
|
+
|
|
168
|
+
text = prompt_text(record["message"].is_a?(Hash) ? record["message"]["content"] : nil)
|
|
169
|
+
return if text.nil?
|
|
170
|
+
|
|
171
|
+
first = text.lines.map(&:strip).find { |line| !line.empty? && !line.start_with?("<") }
|
|
172
|
+
first && first[0, LABEL_WIDTH]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def prompt_text(content)
|
|
176
|
+
return content if content.is_a?(String)
|
|
177
|
+
return unless content.is_a?(Array)
|
|
178
|
+
|
|
179
|
+
block = content.find { |b| b.is_a?(Hash) && b["type"] == "text" }
|
|
180
|
+
block && block["text"]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def median(values)
|
|
184
|
+
return if values.empty?
|
|
185
|
+
|
|
186
|
+
sorted = values.sort
|
|
187
|
+
middle = sorted.size / 2
|
|
188
|
+
sorted.size.odd? ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Plastic
|
|
7
|
+
module Sqlite
|
|
8
|
+
Error = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
def self.quote(text)
|
|
11
|
+
"'#{text.gsub("'", "''")}'"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.call(path, sql, readonly: false)
|
|
15
|
+
flags = readonly ? ["-readonly"] : []
|
|
16
|
+
out, err, status = Open3.capture3("sqlite3", "-json", *flags, path, stdin_data: sql)
|
|
17
|
+
raise Error, err.strip unless status.success?
|
|
18
|
+
|
|
19
|
+
out.strip.empty? ? [] : JSON.parse(out)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "active_delivery"
|
|
6
|
+
require_relative "lock"
|
|
7
|
+
require_relative "ready_set"
|
|
8
|
+
|
|
9
|
+
# StopGate (intent 340b, G7c, n4, D5/D7/D8/D9): may this session stop? Blocks
|
|
10
|
+
# only when four conditions hold together: the payload's stop_hook_active is
|
|
11
|
+
# true, runner.stop_hook parses as a real boolean true, this session holds
|
|
12
|
+
# the delivering intent's delivery.lock live and in auto mode, and the last
|
|
13
|
+
# runner step left work a new turn can actually move (some node is ready to
|
|
14
|
+
# dispatch right now - a graph whose only non-terminal node is already
|
|
15
|
+
# running, or that has no ready node at all, permits, because blocking that
|
|
16
|
+
# would trip Claude Code's own eight-block ceiling for nothing).
|
|
17
|
+
#
|
|
18
|
+
# Fail-open is the design, not a fallback: a missing store, a torn lock, an
|
|
19
|
+
# unreadable graph, a raised exception, a payload that is not JSON - every
|
|
20
|
+
# one of those permits. #decide never raises.
|
|
21
|
+
module StopGate
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# decide(payload:, config_stop_hook:, session:, global_store:, project_roots:, ttl:, now:) ->
|
|
25
|
+
# {block: false} or {block: true, reason: "..."}.
|
|
26
|
+
#
|
|
27
|
+
# `payload` is the hook input: already a Hash, or the raw JSON text (row
|
|
28
|
+
# 4.19 - a payload that is not JSON permits, never raises). `config_stop_hook`
|
|
29
|
+
# is the raw string read-config returns for runner.stop_hook: parsed as a
|
|
30
|
+
# boolean here, never trusted as one.
|
|
31
|
+
def decide(payload:, config_stop_hook:, session:, global_store:, project_roots:,
|
|
32
|
+
ttl: Lock::TTL_SECONDS, now: Time.now, caps: nil)
|
|
33
|
+
payload = parse_payload(payload)
|
|
34
|
+
|
|
35
|
+
return permit unless payload["stop_hook_active"] == true
|
|
36
|
+
return permit unless flag_true?(config_stop_hook)
|
|
37
|
+
|
|
38
|
+
intent_dir = ActiveDelivery.resolve(global_store: global_store, project_roots: project_roots,
|
|
39
|
+
session: session, ttl: ttl, now: now)
|
|
40
|
+
return permit unless intent_dir
|
|
41
|
+
|
|
42
|
+
lock = Lock.read(intent_dir)
|
|
43
|
+
return permit unless lock
|
|
44
|
+
return permit unless lock["run_mode"].to_s == "auto"
|
|
45
|
+
|
|
46
|
+
return permit unless work_movable?(intent_dir, caps: caps)
|
|
47
|
+
|
|
48
|
+
block(intent_dir)
|
|
49
|
+
rescue StandardError
|
|
50
|
+
permit
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Accepts a Hash as-is, parses a String as JSON, and falls back to an
|
|
54
|
+
# empty Hash for anything else or anything that fails to parse - a
|
|
55
|
+
# malformed payload must never raise (row 4.19).
|
|
56
|
+
def parse_payload(payload)
|
|
57
|
+
return payload if payload.is_a?(Hash)
|
|
58
|
+
return {} unless payload.is_a?(String)
|
|
59
|
+
|
|
60
|
+
parsed = JSON.parse(payload)
|
|
61
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
{}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A config value counts as on only when it parses as the literal boolean
|
|
67
|
+
# true (row 4.4): read-config emits strings, and the string "false" is
|
|
68
|
+
# truthy in Ruby, so testing the raw value would arm the hook on its own
|
|
69
|
+
# shipped default. Anything else, including an absent key (row 4.5), is off.
|
|
70
|
+
def flag_true?(value)
|
|
71
|
+
value.to_s.strip.downcase == "true"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Some node is ready to dispatch right now. Never raises across this
|
|
75
|
+
# boundary (ReadySet.analyze already never does); an unreadable or absent
|
|
76
|
+
# graph, or an intent with no declared nodes, reads as ok: false or an
|
|
77
|
+
# empty ready set either way, and both permit.
|
|
78
|
+
def work_movable?(intent_dir, caps:)
|
|
79
|
+
analysis = caps ? ReadySet.analyze(intent_dir, caps: caps) : ReadySet.analyze(intent_dir)
|
|
80
|
+
analysis[:ok] && analysis[:ranked_ready].any?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def permit
|
|
84
|
+
{ block: false }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Names the next instruction (row 4.17): a session that is blocked and
|
|
88
|
+
# told nothing stops again immediately.
|
|
89
|
+
def block(intent_dir)
|
|
90
|
+
intent_id = File.basename(intent_dir.to_s)
|
|
91
|
+
reason = "Plastic: intent #{intent_id} still has ready work. Run `runner step` for it " \
|
|
92
|
+
"and dispatch what it prints before stopping."
|
|
93
|
+
{ block: true, reason: reason }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require_relative "store_provisioning"
|
|
5
6
|
|
|
6
7
|
# StoreDiscovery: the single source of truth for "what stores exist" (intent 189).
|
|
@@ -39,20 +40,20 @@ module StoreDiscovery
|
|
|
39
40
|
stores = []
|
|
40
41
|
missing = []
|
|
41
42
|
|
|
42
|
-
global_store =
|
|
43
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
43
44
|
if File.directory?(global_store)
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
global_root = Plastic::StoreLayout.global_root(plastic_home)
|
|
46
|
+
stores << { key: "global", slug: "global", root: global_root,
|
|
47
|
+
store: global_store, index: File.join(global_root, "INDEX.md") }
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
registered = StoreProvisioning.load_projects(plastic_home) # { slug => info }, {} on error/absence
|
|
49
|
-
|
|
50
|
-
on_disk = File.directory?(projects_root) ? Dir.children(projects_root).reject { |e| e.start_with?(".") } : []
|
|
51
|
+
on_disk = Plastic::StoreLayout.project_slugs(plastic_home)
|
|
51
52
|
|
|
52
53
|
all_slugs = (registered.keys + on_disk).uniq.sort
|
|
53
54
|
|
|
54
55
|
all_slugs.each do |slug|
|
|
55
|
-
root =
|
|
56
|
+
root = Plastic::StoreLayout.project_root(plastic_home, slug)
|
|
56
57
|
store_dir = File.join(root, "store")
|
|
57
58
|
if File.directory?(store_dir)
|
|
58
59
|
stores << { key: "project:#{slug}", slug: slug, root: root,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plastic
|
|
4
|
+
module StoreLayout
|
|
5
|
+
GLOBAL = "global"
|
|
6
|
+
PARENTS = %w[stores projects].freeze
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def moved?(home)
|
|
11
|
+
File.directory?(File.join(home, "stores"))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def global_root(home)
|
|
15
|
+
moved?(home) ? File.join(home, "stores", GLOBAL) : home
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def global_store(home)
|
|
19
|
+
File.join(global_root(home), "store")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def projects_root(home)
|
|
23
|
+
File.join(home, moved?(home) ? "stores" : "projects")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def project_root(home, slug)
|
|
27
|
+
File.join(projects_root(home), slug)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def root(home, slug)
|
|
31
|
+
(slug == GLOBAL) ? global_root(home) : project_root(home, slug)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def project_slugs(home)
|
|
35
|
+
parent = projects_root(home)
|
|
36
|
+
return [] unless File.directory?(parent)
|
|
37
|
+
|
|
38
|
+
Dir.children(parent).reject { |entry| entry.start_with?(".") || entry == GLOBAL }.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def locate(store)
|
|
42
|
+
root = File.dirname(File.expand_path(store))
|
|
43
|
+
parent = File.dirname(root)
|
|
44
|
+
return [root, GLOBAL] unless PARENTS.include?(File.basename(parent))
|
|
45
|
+
|
|
46
|
+
[File.dirname(parent), File.basename(root)]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def home_and_scope(store)
|
|
50
|
+
home, slug = locate(store)
|
|
51
|
+
[home, (slug == GLOBAL) ? GLOBAL : "project:#{slug}"]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "fileutils"
|
|
5
6
|
require "yaml"
|
|
6
7
|
|
|
@@ -63,7 +64,7 @@ module StoreProvisioning
|
|
|
63
64
|
}
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
project_dir =
|
|
67
|
+
project_dir = Plastic::StoreLayout.project_root(plastic_home, slug)
|
|
67
68
|
store_dir = File.join(project_dir, "store")
|
|
68
69
|
FileUtils.mkdir_p(store_dir)
|
|
69
70
|
|