@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
package/scripts/doctor.rb
CHANGED
|
@@ -9,9 +9,15 @@
|
|
|
9
9
|
# Exit codes: 0 (all pass), 1 (warnings only), 2 (failures present)
|
|
10
10
|
# Read-only — never modifies files.
|
|
11
11
|
|
|
12
|
+
require_relative "lib/store_layout"
|
|
12
13
|
require "date"
|
|
14
|
+
require "open3"
|
|
15
|
+
require "timeout"
|
|
16
|
+
require "fileutils"
|
|
17
|
+
require "tmpdir"
|
|
13
18
|
|
|
14
19
|
require_relative "lib/doctor_core"
|
|
20
|
+
require_relative "lib/hook_replay"
|
|
15
21
|
|
|
16
22
|
require_relative "lib/doctor_exclusions"
|
|
17
23
|
require_relative "lib/qmd_sync"
|
|
@@ -22,6 +28,10 @@ require_relative "lib/links_projection"
|
|
|
22
28
|
require_relative "lib/links_section"
|
|
23
29
|
require_relative "lib/lock"
|
|
24
30
|
require_relative "lib/savepoint"
|
|
31
|
+
require_relative "lib/node_ledger"
|
|
32
|
+
require_relative "lib/ready_set"
|
|
33
|
+
require_relative "lib/graph_measure_models"
|
|
34
|
+
require_relative "lib/index_projection"
|
|
25
35
|
require_relative "lib/agent_models"
|
|
26
36
|
require_relative "lib/outcome_guard"
|
|
27
37
|
require_relative "lib/skill_lint"
|
|
@@ -30,6 +40,8 @@ require_relative "lib/power_tools"
|
|
|
30
40
|
require_relative "lib/preflight"
|
|
31
41
|
require_relative "lib/ruby_probe"
|
|
32
42
|
require_relative "lib/doctor_session_ledger"
|
|
43
|
+
require_relative "lib/intent_screen"
|
|
44
|
+
require_relative "lib/store_sync"
|
|
33
45
|
|
|
34
46
|
# Diagnostic engine, instantiable with an injected store/agent map so tests can
|
|
35
47
|
# run it hermetically (no eval, no global-constant rewriting).
|
|
@@ -46,6 +58,13 @@ class Doctor
|
|
|
46
58
|
# (intent 60). Alias it here so the two can never drift.
|
|
47
59
|
REQUIRED_FRONTMATTER_FIELDS = IntentValidator::REQUIRED_FIELDS
|
|
48
60
|
|
|
61
|
+
# Repo root, for the three display checks below that read shipped, static
|
|
62
|
+
# package content (the fixture fallback, the harness-adapters doc) rather
|
|
63
|
+
# than a runtime plastic_home path. Same derivation as
|
|
64
|
+
# StoreProvisioning::PACKAGE_ROOT (intent 61): scripts/doctor.rb lives one
|
|
65
|
+
# level under the root.
|
|
66
|
+
PACKAGE_ROOT = File.expand_path("..", __dir__)
|
|
67
|
+
|
|
49
68
|
|
|
50
69
|
|
|
51
70
|
# --- Flag parsing ---
|
|
@@ -122,7 +141,7 @@ class Doctor
|
|
|
122
141
|
--intent ID Per-intent structure gate at intent-end (intent 222): one
|
|
123
142
|
closing intent only, never a store sweep. Pair with
|
|
124
143
|
--store <key> to disambiguate an id that collides across
|
|
125
|
-
stores, and --disposition delivered|abandoned to
|
|
144
|
+
stores, and --disposition delivered|abandoned to include
|
|
126
145
|
the outcome.md disposition check. 3-state pass/warn/fail.
|
|
127
146
|
-h, --help Show this help
|
|
128
147
|
|
|
@@ -199,7 +218,7 @@ class Doctor
|
|
|
199
218
|
def check_global_store
|
|
200
219
|
checks = []
|
|
201
220
|
|
|
202
|
-
index_path = File.join(plastic_home, "INDEX.md")
|
|
221
|
+
index_path = File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md")
|
|
203
222
|
|
|
204
223
|
# index_exists
|
|
205
224
|
if File.exist?(index_path)
|
|
@@ -235,7 +254,7 @@ class Doctor
|
|
|
235
254
|
end
|
|
236
255
|
|
|
237
256
|
# orphaned_intents — directories in store/ not referenced in INDEX.md
|
|
238
|
-
store_dir =
|
|
257
|
+
store_dir = Plastic::StoreLayout.global_store(plastic_home)
|
|
239
258
|
if File.directory?(store_dir)
|
|
240
259
|
intent_dirs = store_intent_dirs(store_dir)
|
|
241
260
|
orphans = intent_dirs.reject { |d| content.include?("store/#{d}") }
|
|
@@ -261,7 +280,7 @@ class Doctor
|
|
|
261
280
|
store_paths = content.scan(%r{store/\S+}).map { |ref| ref.gsub(/[)\]>].*/, "").chomp("/") }.uniq
|
|
262
281
|
|
|
263
282
|
ghosts = store_paths.select do |ref|
|
|
264
|
-
full_path = File.join(plastic_home, ref)
|
|
283
|
+
full_path = File.join(Plastic::StoreLayout.global_root(plastic_home), ref)
|
|
265
284
|
!File.exist?(full_path) && !File.directory?(full_path)
|
|
266
285
|
end
|
|
267
286
|
|
|
@@ -313,7 +332,7 @@ class Doctor
|
|
|
313
332
|
category: "core_files", name: name, status: "warn",
|
|
314
333
|
message: "#{config[:name]}'s agent-side VERSION file not found at #{tilde(agent_version_path)}",
|
|
315
334
|
fixable: true,
|
|
316
|
-
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or plastic
|
|
335
|
+
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or `plastic rollback` to a prior version"
|
|
317
336
|
)
|
|
318
337
|
else
|
|
319
338
|
agent_version = File.read(agent_version_path).strip
|
|
@@ -331,7 +350,7 @@ class Doctor
|
|
|
331
350
|
"#{tilde(agent_version_path)}: #{agent_version}",
|
|
332
351
|
],
|
|
333
352
|
fixable: true,
|
|
334
|
-
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or plastic
|
|
353
|
+
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or `plastic rollback` to a prior version"
|
|
335
354
|
)
|
|
336
355
|
end
|
|
337
356
|
end
|
|
@@ -596,7 +615,7 @@ class Doctor
|
|
|
596
615
|
fixable: true,
|
|
597
616
|
fix_hint: "Relocate each unsanctioned section into the " \
|
|
598
617
|
"intent's revisions.md via move-and-record (a missing required section is restored " \
|
|
599
|
-
"or reprojected instead); see plastic
|
|
618
|
+
"or reprojected instead); see `plastic help maintenance-and-revisions`"
|
|
600
619
|
)
|
|
601
620
|
end
|
|
602
621
|
|
|
@@ -607,6 +626,28 @@ class Doctor
|
|
|
607
626
|
# flagged: validate_graph does not compute it.
|
|
608
627
|
checks.concat(graph_invariant_checks(intent_dirs))
|
|
609
628
|
|
|
629
|
+
# node_graph - ReadySet's own view of every intent carrying a real graph.md
|
|
630
|
+
# (intent 336, n7): dead ends, stale done nodes, and expired running leases,
|
|
631
|
+
# none of which any other doctor rule surfaces. Lives here rather than
|
|
632
|
+
# doctor_core.rb because test/doctor_core_split_test.rb pins that file's
|
|
633
|
+
# boot path to exactly three project files and 781 bytes of headroom;
|
|
634
|
+
# ReadySet's own require chain is 65,648 bytes, far past that budget.
|
|
635
|
+
checks.concat(node_graph_checks(intent_dirs))
|
|
636
|
+
|
|
637
|
+
# model_requalification (D42, intent 343 G10 n7): a recorded model= that no
|
|
638
|
+
# longer matches what RunnerPolicy.model_for resolves for its role today
|
|
639
|
+
# means every measurement taken under the old model measured a different
|
|
640
|
+
# system. Lives here for the same reason node_graph_checks does: pins to
|
|
641
|
+
# test/doctor_core_split_test.rb's exact-set boot-path assertion, never
|
|
642
|
+
# doctor_core.rb (D11) - GraphMeasureModels' own require chain (NodeLedger,
|
|
643
|
+
# NodeFile, RunnerPolicy, AgentModels) is far past that file's byte budget.
|
|
644
|
+
checks.concat(model_requalification_checks(scopes: scopes))
|
|
645
|
+
|
|
646
|
+
# unpromoted_rules (intent 341, G8, C37): an Insights entry tagged
|
|
647
|
+
# `rule:` (via `insight-append --rule`) is a promise the rule will make
|
|
648
|
+
# it into project doctrine. Advisory only.
|
|
649
|
+
checks.concat(unpromoted_rules_checks(intent_dirs))
|
|
650
|
+
|
|
610
651
|
# cross_store_resolution — RESOLVES (not just shape-checks) every cross-store
|
|
611
652
|
# `store:id` ref against the FULL store family via the relocation map
|
|
612
653
|
# (relocation consulted first), closing the shape-only gap i1/i3/i4 leave open.
|
|
@@ -666,14 +707,14 @@ class Doctor
|
|
|
666
707
|
if scopes.nil? || scopes.include?("global")
|
|
667
708
|
stores << {
|
|
668
709
|
scope: "global",
|
|
669
|
-
index: File.join(plastic_home, "INDEX.md"),
|
|
670
|
-
store_dir:
|
|
710
|
+
index: File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md"),
|
|
711
|
+
store_dir: Plastic::StoreLayout.global_store(plastic_home),
|
|
671
712
|
}
|
|
672
713
|
end
|
|
673
714
|
|
|
674
|
-
projects_root =
|
|
715
|
+
projects_root = Plastic::StoreLayout.projects_root(plastic_home)
|
|
675
716
|
if File.directory?(projects_root)
|
|
676
|
-
|
|
717
|
+
Plastic::StoreLayout.project_slugs(plastic_home).each do |project|
|
|
677
718
|
scope = "project:#{project}"
|
|
678
719
|
next unless scopes.nil? || scopes.include?(scope)
|
|
679
720
|
|
|
@@ -771,8 +812,19 @@ def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, activ
|
|
|
771
812
|
# written from the record by `scaffold-intent backfill`, so this is repairable and
|
|
772
813
|
# reported as a fixable warn (backfilled_complete). Its exclusions go to their OWN
|
|
773
814
|
# bucket so savepoint_operational's consumed/dead-row bookkeeping above never sees them.
|
|
774
|
-
|
|
775
|
-
|
|
815
|
+
# A graph intent (real graph.md on disk) never carries spec.md or plan.md by design
|
|
816
|
+
# (D1, no ceremonies, intent 341): the graph IS the spec and the plan, so a missing one
|
|
817
|
+
# is never a gap here.
|
|
818
|
+
is_graph_intent = File.exist?(File.join(dir, "graph.md"))
|
|
819
|
+
backfill_gaps = is_graph_intent ? [] : %w[spec.md plan.md].reject { |f| Savepoint.stage_file_present?(File.join(dir, f)) }
|
|
820
|
+
# Name whichever directory the intent actually used (post-execution
|
|
821
|
+
# review, non-blocking 8), consistent with Savepoint.missing_for_stage:
|
|
822
|
+
# a nodes/ directory on disk means the intent chose the node-graph
|
|
823
|
+
# convention, so a gap here is a missing nodes/, never the literal
|
|
824
|
+
# actions/ has_real_action? no longer implies.
|
|
825
|
+
unless Savepoint.has_real_action?(dir)
|
|
826
|
+
backfill_gaps << (File.directory?(File.join(dir, "nodes")) ? "nodes/" : "actions/")
|
|
827
|
+
end
|
|
776
828
|
if backfill_gaps.any?
|
|
777
829
|
suppressed_backfill = excluded_rules.include?("backfilled_complete")
|
|
778
830
|
target = suppressed_backfill ? findings[:excluded_backfill] : findings[:unbackfilled]
|
|
@@ -807,6 +859,9 @@ def check_done_signals(scopes: nil)
|
|
|
807
859
|
unbackfilled = [] # spec/plan/actions gaps on terminal intents (intent 308) - repairable
|
|
808
860
|
excluded_backfill = [] # backfill gaps knowingly exempted via doctor-exclusions (intent 308)
|
|
809
861
|
dead_rows_by_rule = Hash.new { |h, k| h[k] = [] }
|
|
862
|
+
index_drift = [] # intent 337 (G4): INDEX vs. the intent ledgers, scoped to what
|
|
863
|
+
# savepoint_operational does not already report (row 6.7 - a silent or
|
|
864
|
+
# absent ledger is already IndexProjection's own exclusion, row 5.13)
|
|
810
865
|
|
|
811
866
|
done_signal_stores(scopes).each do |store|
|
|
812
867
|
exclusions = DoctorExclusions.load(store[:index])
|
|
@@ -815,6 +870,14 @@ def check_done_signals(scopes: nil)
|
|
|
815
870
|
exclusion_error_paths << exclusions[:path]
|
|
816
871
|
end
|
|
817
872
|
|
|
873
|
+
if File.exist?(store[:index])
|
|
874
|
+
projection = IndexProjection.analyze(store[:store_dir], index_path: store[:index])
|
|
875
|
+
projection[:drift].each do |row|
|
|
876
|
+
index_drift << "#{store[:scope]}: #{row[:id]} - INDEX says #{row[:index_status]}, " \
|
|
877
|
+
"the ledger's last line says #{row[:ledger_status]}"
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
|
|
818
881
|
consumed = { "savepoint_operational" => [], "backfilled_complete" => [] }
|
|
819
882
|
# `known_ids` (post-review fix): every intent id with a REAL DIRECTORY in this store, scanned
|
|
820
883
|
# directly from disk - independent of INDEX.md. An id can have a directory on disk without
|
|
@@ -931,14 +994,14 @@ def check_done_signals(scopes: nil)
|
|
|
931
994
|
# rebuild-savepoint for most gaps, or knowingly excluded for the ones 219 D6 forbids ever
|
|
932
995
|
# repairing (no real outcome.md to echo a disposition from). Three branches (spec D4/D5):
|
|
933
996
|
# a malformed exclusion file can never report pass (loud), a clean remaining gap set reports
|
|
934
|
-
# pass with the exclusion count
|
|
935
|
-
# before intent 274, with the same count
|
|
997
|
+
# pass with the exclusion count included, and a real remaining gap set stays warn, same as
|
|
998
|
+
# before intent 274, with the same count included when exclusions applied.
|
|
936
999
|
#
|
|
937
|
-
# `dead_suffix` (intent 280)
|
|
1000
|
+
# `dead_suffix` (intent 280) adds a second, independent drift notice: exclusion rows that
|
|
938
1001
|
# suppressed nothing this run. It is purely informational, exactly like `exclusion_suffix` - it
|
|
939
1002
|
# never changes status on any of the three branches below, because a stale governance-record row
|
|
940
1003
|
# is bookkeeping drift, not a store regression (219 D6 is untouched: no disposition is invented).
|
|
941
|
-
# Per rule (intent 308): savepoint_operational and backfilled_complete each
|
|
1004
|
+
# Per rule (intent 308): savepoint_operational and backfilled_complete each include only
|
|
942
1005
|
# their own exclusions and dead rows, so one check never carries the other's counts.
|
|
943
1006
|
suffixes = lambda do |excluded_rows, dead|
|
|
944
1007
|
ex = excluded_rows.empty? ? "" : " (#{excluded_rows.size} excluded via #{exclusion_paths.join(", ")})"
|
|
@@ -989,7 +1052,7 @@ def check_done_signals(scopes: nil)
|
|
|
989
1052
|
# backfilled_complete (intent 308): spec.md, plan.md, or a real action file missing on a
|
|
990
1053
|
# terminal intent. Same three branches as savepoint_operational: a malformed exclusion
|
|
991
1054
|
# file is always loud, a clean set passes with its own exclusion and dead-row counts
|
|
992
|
-
#
|
|
1055
|
+
# included, a real gap set warns with the backfill verb as the fix.
|
|
993
1056
|
backfill_hint = "Write the missing documents from the record via " \
|
|
994
1057
|
"`scaffold-intent backfill --store <store> --id <id> --disposition " \
|
|
995
1058
|
"<delivered|abandoned>` (never touches real content, one intent per invocation)."
|
|
@@ -1055,7 +1118,31 @@ def check_done_signals(scopes: nil)
|
|
|
1055
1118
|
fix_hint: "For a live (Active) intent, rebuild the ledger via " \
|
|
1056
1119
|
"Savepoint.rebuild_savepoint. Terminal (Completed/Abandoned) intents are immutable: " \
|
|
1057
1120
|
"a phantom there stays advisory unless an explicit human grant authorizes the " \
|
|
1058
|
-
"124a manual
|
|
1121
|
+
"124a manual terminal-bookend repair."
|
|
1122
|
+
)
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
# index_ledger_drift (intent 337, G4): a REAL terminal ledger line (Done
|
|
1126
|
+
# delivered or abandoned) that disagrees with the INDEX section an intent
|
|
1127
|
+
# currently sits in. Never fires for a silent or absent ledger - that gap
|
|
1128
|
+
# is savepoint_operational's own concern, and IndexProjection's own drift
|
|
1129
|
+
# computation already excludes it (row 5.13), so no id is ever double
|
|
1130
|
+
# reported across the two checks (row 6.7). Warn+fixable: `index-projection
|
|
1131
|
+
# <store_root> --write` is the repair, never invented here.
|
|
1132
|
+
if index_drift.empty?
|
|
1133
|
+
checks << check(
|
|
1134
|
+
category: "done_signals", name: "index_ledger_drift", status: "pass",
|
|
1135
|
+
message: "No INDEX section disagrees with an intent's own terminal ledger line"
|
|
1136
|
+
)
|
|
1137
|
+
else
|
|
1138
|
+
checks << check(
|
|
1139
|
+
category: "done_signals", name: "index_ledger_drift", status: "warn",
|
|
1140
|
+
message: "#{index_drift.size} intent#{index_drift.size == 1 ? "" : "s"} whose terminal " \
|
|
1141
|
+
"ledger line disagrees with the INDEX section it currently sits in",
|
|
1142
|
+
details: index_drift, fixable: true,
|
|
1143
|
+
fix_hint: "Reconcile via `index-projection <store_root> --write` (the ledger wins; only the " \
|
|
1144
|
+
"conflicting entries move, every other byte of INDEX.md, including ## Clusters and " \
|
|
1145
|
+
"## Relocated, is left untouched)."
|
|
1059
1146
|
)
|
|
1060
1147
|
end
|
|
1061
1148
|
|
|
@@ -1143,7 +1230,7 @@ end
|
|
|
1143
1230
|
"project-links --intent <id> --apply` over running project-links directly: it detects " \
|
|
1144
1231
|
"(never acquires) the target's delivery lock, requires a clean store working tree, and " \
|
|
1145
1232
|
"commits the scoped change plus its revisions.md receipt as one merged operation " \
|
|
1146
|
-
"(plastic
|
|
1233
|
+
"(`plastic help maintenance-and-revisions`)."
|
|
1147
1234
|
)
|
|
1148
1235
|
end
|
|
1149
1236
|
|
|
@@ -1161,9 +1248,12 @@ end
|
|
|
1161
1248
|
#
|
|
1162
1249
|
# Doctor's fourth check scope, invoked by `--intent <id>`. Never a store-wide sweep:
|
|
1163
1250
|
# resolves exactly one intent directory (mirrors scripts/end-intent's resolve_intent_dir /
|
|
1164
|
-
# scripts/project-links's --intent disambiguation) and returns
|
|
1165
|
-
# FAIL-severity,
|
|
1166
|
-
# binding advisory-only ruling
|
|
1251
|
+
# scripts/project-links's --intent disambiguation) and returns seven checks, four
|
|
1252
|
+
# FAIL-severity, three WARN-severity: intent_savepoint_truthful stays WARN per intent 134's
|
|
1253
|
+
# binding advisory-only ruling (see spec.md D2/D8 - do NOT escalate it to FAIL),
|
|
1254
|
+
# intent_ticks_lag is WARN-only per intent 329's ruling that a lagging tick warns rather
|
|
1255
|
+
# than blocks, and intent_reports_printed is WARN-only per intent 331f (never re-litigates
|
|
1256
|
+
# a ledger predating the Report kind).
|
|
1167
1257
|
def check_intent_end(id, store: nil, disposition: nil)
|
|
1168
1258
|
intent_dir, scope = resolve_single_intent_dir(id, store: store)
|
|
1169
1259
|
unless intent_dir
|
|
@@ -1185,6 +1275,8 @@ end
|
|
|
1185
1275
|
intent_structure_check(intent_dir),
|
|
1186
1276
|
intent_lifecycle_artifacts_check(intent_dir, disposition),
|
|
1187
1277
|
intent_checklist_complete_check(intent_dir),
|
|
1278
|
+
intent_ticks_lag_check(intent_dir),
|
|
1279
|
+
intent_reports_printed_check(intent_dir),
|
|
1188
1280
|
intent_links_projection_check_for(id, scope),
|
|
1189
1281
|
intent_savepoint_truthful_check(intent_dir, index_path: index_path),
|
|
1190
1282
|
]
|
|
@@ -1223,9 +1315,13 @@ end
|
|
|
1223
1315
|
end
|
|
1224
1316
|
|
|
1225
1317
|
INTENT_END_LIFECYCLE_FILES = %w[spec.md plan.md checklist.md outcome.md].freeze
|
|
1318
|
+
# A graph intent (D1, no ceremonies, intent 341) never carries spec.md, plan.md, or
|
|
1319
|
+
# checklist.md by design; the graph IS the spec and the plan. outcome.md stays mandatory.
|
|
1320
|
+
GRAPH_INTENT_LIFECYCLE_FILES = %w[outcome.md].freeze
|
|
1226
1321
|
|
|
1227
1322
|
def intent_lifecycle_artifacts_check(intent_dir, disposition)
|
|
1228
|
-
|
|
1323
|
+
files = File.exist?(File.join(intent_dir, "graph.md")) ? GRAPH_INTENT_LIFECYCLE_FILES : INTENT_END_LIFECYCLE_FILES
|
|
1324
|
+
missing = files.select { |f| !Savepoint.stage_file_present?(File.join(intent_dir, f)) }
|
|
1229
1325
|
unless Savepoint.stage_file_present?(Savepoint.intent_file(intent_dir))
|
|
1230
1326
|
missing = [File.basename(Savepoint.intent_file(intent_dir))] + missing
|
|
1231
1327
|
end
|
|
@@ -1263,6 +1359,80 @@ end
|
|
|
1263
1359
|
end
|
|
1264
1360
|
end
|
|
1265
1361
|
|
|
1362
|
+
# Intent 329: the checklist is the only input to the state screen's Progress bar, so an
|
|
1363
|
+
# intent whose commit ledger has entries and whose checklist has no ticked item is
|
|
1364
|
+
# reporting 0 progress on work that already landed. Advisory only (WARN), like
|
|
1365
|
+
# intent_savepoint_truthful: a lagging tick is a lead's review finding, never a machine
|
|
1366
|
+
# refusal. The commit count comes from this intent's own savepoint.md Commit ledger
|
|
1367
|
+
# (intent 317 D17), never from git: git's detected base is the repo default branch, which
|
|
1368
|
+
# is not the base a 2.0 intent branch forks from.
|
|
1369
|
+
def intent_ticks_lag_check(intent_dir)
|
|
1370
|
+
items = IntentScreen.checklist_items(intent_dir)
|
|
1371
|
+
if items.empty?
|
|
1372
|
+
return check(category: "intent_end", name: "intent_ticks_lag", status: "pass",
|
|
1373
|
+
message: "n/a: checklist.md has no items to tick")
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
ticked = items.count { |i| i[:done] }
|
|
1377
|
+
commits = savepoint_commit_count(intent_dir)
|
|
1378
|
+
|
|
1379
|
+
if commits.positive? && ticked.zero?
|
|
1380
|
+
check(category: "intent_end", name: "intent_ticks_lag", status: "warn",
|
|
1381
|
+
message: "ticks lag the branch: #{commits} commit(s) recorded, " \
|
|
1382
|
+
"0 of #{items.size} checklist items ticked")
|
|
1383
|
+
else
|
|
1384
|
+
check(category: "intent_end", name: "intent_ticks_lag", status: "pass",
|
|
1385
|
+
message: "#{ticked} of #{items.size} ticked, #{commits} commit(s) recorded")
|
|
1386
|
+
end
|
|
1387
|
+
end
|
|
1388
|
+
|
|
1389
|
+
# Intent 331f: every skill that shows state during Exec is bound to print a report screen and
|
|
1390
|
+
# log a `Report` savepoint line (`savepoint-note --kind Report`). A commit landed with no
|
|
1391
|
+
# Report line anywhere in the ledger is the exact defect this check exists to catch - the
|
|
1392
|
+
# delivery ran but nothing on disk proves a screen was ever printed. WARN-only, like
|
|
1393
|
+
# intent_ticks_lag: a lead's review finding, never a machine refusal. R6: never re-litigate
|
|
1394
|
+
# history - an intent whose newest Commit line predates the day the Report kind shipped
|
|
1395
|
+
# (Savepoint::REPORT_KIND_SINCE) passes, so every pre-existing ledger keeps passing
|
|
1396
|
+
# `doctor --intent` (which exits 1 on an overall warn).
|
|
1397
|
+
def intent_reports_printed_check(intent_dir)
|
|
1398
|
+
path = File.join(intent_dir, "savepoint.md")
|
|
1399
|
+
lines = File.exist?(path) ? File.readlines(path) : []
|
|
1400
|
+
kinds = lines.filter_map { |l| l.strip.match(IntentScreen::SAVEPOINT_RE) }
|
|
1401
|
+
|
|
1402
|
+
commit_timestamps = kinds.select { |m| m[2] == "Commit" }.map { |m| m[1] }
|
|
1403
|
+
if commit_timestamps.empty?
|
|
1404
|
+
return check(category: "intent_end", name: "intent_reports_printed", status: "pass",
|
|
1405
|
+
message: "n/a: no commits recorded")
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
if kinds.any? { |m| m[2] == "Report" }
|
|
1409
|
+
return check(category: "intent_end", name: "intent_reports_printed", status: "pass",
|
|
1410
|
+
message: "a Report line is recorded")
|
|
1411
|
+
end
|
|
1412
|
+
|
|
1413
|
+
newest_commit_date = commit_timestamps.max[0, 10]
|
|
1414
|
+
if newest_commit_date < Savepoint::REPORT_KIND_SINCE
|
|
1415
|
+
return check(category: "intent_end", name: "intent_reports_printed", status: "pass",
|
|
1416
|
+
message: "n/a: newest commit (#{newest_commit_date}) predates the Report " \
|
|
1417
|
+
"kind (#{Savepoint::REPORT_KIND_SINCE})")
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
check(category: "intent_end", name: "intent_reports_printed", status: "warn",
|
|
1421
|
+
message: "commits are recorded and no Report line exists")
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
# Count `Commit` lines in the intent's savepoint ledger, through the one regex that parses
|
|
1425
|
+
# a savepoint line (IntentScreen::SAVEPOINT_RE; field 2 is the kind).
|
|
1426
|
+
def savepoint_commit_count(intent_dir)
|
|
1427
|
+
path = File.join(intent_dir, "savepoint.md")
|
|
1428
|
+
return 0 unless File.exist?(path)
|
|
1429
|
+
|
|
1430
|
+
File.readlines(path).count do |line|
|
|
1431
|
+
m = line.strip.match(IntentScreen::SAVEPOINT_RE)
|
|
1432
|
+
m && m[2] == "Commit"
|
|
1433
|
+
end
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1266
1436
|
def intent_links_projection_check_for(id, scope)
|
|
1267
1437
|
ctx = build_links_projection_context
|
|
1268
1438
|
node = ctx[:intents].find { |n| n[:id] == id && n[:scope] == scope }
|
|
@@ -1369,20 +1539,37 @@ end
|
|
|
1369
1539
|
expected_pair = Savepoint.savepoint_milestone(intent_dir, File.basename(Savepoint.intent_file(intent_dir)))
|
|
1370
1540
|
|
|
1371
1541
|
phantoms = Savepoint.savepoint_phantom_lines(intent_dir)
|
|
1542
|
+
# Intent 335 (spec C2/C5): a torn or unattributed node/Intent transition
|
|
1543
|
+
# line has no reader anywhere else in the tree, so this is where it
|
|
1544
|
+
# surfaces. NodeLedger.anomalies never touches a stage line (its own
|
|
1545
|
+
# transition-candidate gate excludes them), so this is purely additive
|
|
1546
|
+
# beside the phantom check above.
|
|
1547
|
+
transition_anomalies = NodeLedger.anomalies(savepoint)
|
|
1372
1548
|
problems = []
|
|
1373
1549
|
problems << "born line #{born_pair.inspect} does not match the expected #{expected_pair.inspect}" \
|
|
1374
1550
|
if born_pair != expected_pair
|
|
1375
1551
|
problems << "#{phantoms.size} phantom savepoint line(s): " \
|
|
1376
1552
|
"#{phantoms.map { |l, r| "#{l} (#{r})" }.join("; ")}" if phantoms.any?
|
|
1553
|
+
problems << "#{transition_anomalies.size} torn/unattributed transition line(s): " \
|
|
1554
|
+
"#{transition_anomalies.map { |a| "#{a[:line]} (#{a[:reason]})" }.join("; ")}" \
|
|
1555
|
+
if transition_anomalies.any?
|
|
1377
1556
|
|
|
1378
1557
|
if problems.empty?
|
|
1379
1558
|
check(category: "intent_end", name: "intent_savepoint_truthful", status: "pass",
|
|
1380
1559
|
message: "Savepoint born line and phantom-line state are truthful")
|
|
1381
1560
|
else
|
|
1561
|
+
# A torn or unattributed transition line's CONTENT cannot be repaired by
|
|
1562
|
+
# a rebuild (rebuild_savepoint preserves it verbatim, spec D13), unlike a
|
|
1563
|
+
# born-line mismatch or a stage phantom, which rebuild does fix. Say so
|
|
1564
|
+
# explicitly rather than pointing at a fix that will not fix it.
|
|
1565
|
+
fix_hint = "Rebuild the ledger via Savepoint.rebuild_savepoint"
|
|
1566
|
+
fix_hint += "; note: rebuild preserves a torn or unattributed transition line " \
|
|
1567
|
+
"verbatim and does not repair one - hand-edit it or accept the finding" \
|
|
1568
|
+
if transition_anomalies.any?
|
|
1382
1569
|
check(category: "intent_end", name: "intent_savepoint_truthful", status: "warn",
|
|
1383
1570
|
message: "#{problems.size} savepoint truthfulness issue(s) (advisory, never blocking)",
|
|
1384
1571
|
details: problems,
|
|
1385
|
-
fixable: true, fix_hint:
|
|
1572
|
+
fixable: true, fix_hint: fix_hint)
|
|
1386
1573
|
end
|
|
1387
1574
|
end
|
|
1388
1575
|
|
|
@@ -1507,7 +1694,7 @@ end
|
|
|
1507
1694
|
"Every sources/chain id resolves to a real intent (I4)",
|
|
1508
1695
|
"Record the dangling sources/chain edge as a " \
|
|
1509
1696
|
"broken-source/broken-chain move-and-record entry in the intent's revisions.md (see " \
|
|
1510
|
-
"plastic
|
|
1697
|
+
"`plastic help maintenance-and-revisions`), or restore the missing intent"
|
|
1511
1698
|
)
|
|
1512
1699
|
checks
|
|
1513
1700
|
end
|
|
@@ -1528,6 +1715,210 @@ end
|
|
|
1528
1715
|
end
|
|
1529
1716
|
end
|
|
1530
1717
|
|
|
1718
|
+
# ReadySet over every intent carrying a real graph.md (intent 336, n7):
|
|
1719
|
+
# dead ends, stale done nodes, and expired running leases. An intent with
|
|
1720
|
+
# no real graph.md is skipped entirely (never touches ReadySet); a
|
|
1721
|
+
# malformed or cyclic one is reported by name, never raised out of doctor.
|
|
1722
|
+
def node_graph_checks(intent_dirs)
|
|
1723
|
+
dead_ends = []
|
|
1724
|
+
stale = []
|
|
1725
|
+
expired = []
|
|
1726
|
+
malformed = []
|
|
1727
|
+
|
|
1728
|
+
intent_dirs.each do |d|
|
|
1729
|
+
graph_path = File.join(d[:path], "graph.md")
|
|
1730
|
+
next unless Savepoint.stage_file_present?(graph_path)
|
|
1731
|
+
|
|
1732
|
+
analysis = ReadySet.analyze(d[:path])
|
|
1733
|
+
unless analysis[:ok]
|
|
1734
|
+
malformed << "#{d[:name]}: #{analysis[:errors].join('; ')}"
|
|
1735
|
+
next
|
|
1736
|
+
end
|
|
1737
|
+
|
|
1738
|
+
savepoint_path = File.join(d[:path], "savepoint.md")
|
|
1739
|
+
analysis[:nodes].each do |id, view|
|
|
1740
|
+
dead_ends << "#{d[:name]}/#{id}" if view[:dead_end]
|
|
1741
|
+
stale << "#{d[:name]}/#{id}" if view[:stale]
|
|
1742
|
+
expired << "#{d[:name]}/#{id}" if view[:state] == "running" && expired_running_lease?(savepoint_path, id)
|
|
1743
|
+
end
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
[
|
|
1747
|
+
node_graph_finding_check(
|
|
1748
|
+
"node_graph_dead_ends", dead_ends,
|
|
1749
|
+
"No node waits on a need that resolves to superseded or abandoned",
|
|
1750
|
+
"Re-plan or supersede the dead-end node's need chain, or abandon the node itself"
|
|
1751
|
+
),
|
|
1752
|
+
node_graph_finding_check(
|
|
1753
|
+
"node_graph_stale_done", stale,
|
|
1754
|
+
"No done node rests on a need superseded after it finished",
|
|
1755
|
+
"Re-verify the stale node against the superseding work, or record the staleness in revisions.md"
|
|
1756
|
+
),
|
|
1757
|
+
node_graph_finding_check(
|
|
1758
|
+
"node_graph_expired_running_lease", expired,
|
|
1759
|
+
"No running node's lease has expired unreclaimed",
|
|
1760
|
+
"Run `node-transition <intent_dir> --node <id> --state reclaimed --field holder=<h> " \
|
|
1761
|
+
"--field expired=<iso>` to sweep it"
|
|
1762
|
+
),
|
|
1763
|
+
node_graph_finding_check(
|
|
1764
|
+
"node_graph_malformed", malformed,
|
|
1765
|
+
"Every intent's graph.md parses cleanly",
|
|
1766
|
+
"Fix the malformed graph.md or nodes/ file named in the details"
|
|
1767
|
+
),
|
|
1768
|
+
]
|
|
1769
|
+
end
|
|
1770
|
+
|
|
1771
|
+
# True iff `id`'s last running line in the ledger at `savepoint_path` carries
|
|
1772
|
+
# an `expires=` strictly in the past. An unparseable or absent expires=
|
|
1773
|
+
# never counts as expired (fail milder than the bug).
|
|
1774
|
+
def expired_running_lease?(savepoint_path, id, now: Time.now)
|
|
1775
|
+
entry = NodeLedger.last_running(savepoint_path, id)
|
|
1776
|
+
return false unless entry
|
|
1777
|
+
|
|
1778
|
+
expires_raw = (entry[:fields] || {})["expires"]
|
|
1779
|
+
expiry = begin
|
|
1780
|
+
expires_raw && Time.iso8601(expires_raw)
|
|
1781
|
+
rescue ArgumentError
|
|
1782
|
+
nil
|
|
1783
|
+
end
|
|
1784
|
+
!!(expiry && now > expiry)
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
# One node-graph check: pass when `findings` is empty, otherwise warn
|
|
1788
|
+
# (never fail, matching graph_finding_check's own precedent - an existing
|
|
1789
|
+
# store never turns red on an advisory graph finding).
|
|
1790
|
+
def node_graph_finding_check(name, findings, pass_message, fix_hint)
|
|
1791
|
+
if findings.empty?
|
|
1792
|
+
check(category: "conventions", name: name, status: "pass", message: pass_message)
|
|
1793
|
+
else
|
|
1794
|
+
check(
|
|
1795
|
+
category: "conventions", name: name, status: "warn",
|
|
1796
|
+
message: "#{findings.size} #{name} violation(s)",
|
|
1797
|
+
details: findings, fixable: false, fix_hint: fix_hint
|
|
1798
|
+
)
|
|
1799
|
+
end
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1802
|
+
# --- Check: re-qualification on model change (D42, intent 343 G10 n7) ---
|
|
1803
|
+
#
|
|
1804
|
+
# When the model behind a role changes, every measurement GraphMeasureModels
|
|
1805
|
+
# took under the old model measured a different system, and the numbers
|
|
1806
|
+
# need re-qualifying before anyone acts on them (D42). This warns, never
|
|
1807
|
+
# fails (D10): a model change is routine, and a red doctor over it would
|
|
1808
|
+
# train people to stop reading the report. The comparison itself is
|
|
1809
|
+
# GraphMeasureModels' own (D16, via RunnerPolicy.model_for) - this method
|
|
1810
|
+
# never re-derives it, it only names the drift rows GraphMeasureModels
|
|
1811
|
+
# already computed.
|
|
1812
|
+
#
|
|
1813
|
+
# Runs once per discovered store (StoreDiscovery, shared with every other
|
|
1814
|
+
# store-scoped check here), never per intent directory: GraphMeasureModels
|
|
1815
|
+
# walks a whole store_dir itself in one pass. A store this process cannot
|
|
1816
|
+
# read - or any other exception the store walk raises - is skipped, never
|
|
1817
|
+
# fatal: one bad store must never take every other doctor check down with
|
|
1818
|
+
# it.
|
|
1819
|
+
#
|
|
1820
|
+
# NEW-4 (v2 review, D22): this rule used to pass `project_config: {}` on
|
|
1821
|
+
# purpose, reasoning there was no live project scope at doctor-run time -
|
|
1822
|
+
# false: each discovered store DOES have a real sibling `config.yml`
|
|
1823
|
+
# (`GraphMeasureModels.project_config_path`), the same file the `cohorts`
|
|
1824
|
+
# verb resolves. Loading it here means the doctor and `cohorts` can never
|
|
1825
|
+
# disagree about the same store's project-scope override again.
|
|
1826
|
+
def model_requalification_checks(scopes: nil)
|
|
1827
|
+
global_config = load_yaml_safe(File.join(plastic_home, "config.yml")) || {}
|
|
1828
|
+
findings = []
|
|
1829
|
+
|
|
1830
|
+
store_discovery[:stores].each do |s|
|
|
1831
|
+
next if scopes && !scopes.include?(s[:key])
|
|
1832
|
+
|
|
1833
|
+
project_config = load_yaml_safe(GraphMeasureModels.project_config_path(s[:store])) || {}
|
|
1834
|
+
record = begin
|
|
1835
|
+
GraphMeasureModels.read(s[:store], project_config: project_config, global_config: global_config)
|
|
1836
|
+
rescue StandardError
|
|
1837
|
+
next
|
|
1838
|
+
end
|
|
1839
|
+
|
|
1840
|
+
%i[executor advisor].each do |role|
|
|
1841
|
+
Array(record[:drift][role]).each do |row|
|
|
1842
|
+
findings << "#{s[:key]}/#{row[:intent]}/#{row[:node]} (#{role}): recorded model=#{row[:recorded]}, " \
|
|
1843
|
+
"current config resolves #{row[:expected]} for kind #{row[:kind]} - re-qualify any " \
|
|
1844
|
+
"measurement taken while model=#{row[:recorded]} was in effect"
|
|
1845
|
+
end
|
|
1846
|
+
end
|
|
1847
|
+
end
|
|
1848
|
+
|
|
1849
|
+
if findings.empty?
|
|
1850
|
+
[check(
|
|
1851
|
+
category: "conventions", name: "model_requalification", status: "pass",
|
|
1852
|
+
message: "No recorded model= differs from what config resolves today for its role"
|
|
1853
|
+
)]
|
|
1854
|
+
else
|
|
1855
|
+
[check(
|
|
1856
|
+
category: "conventions", name: "model_requalification", status: "warn",
|
|
1857
|
+
message: "#{findings.size} node(s) recorded a model that no longer matches the role's " \
|
|
1858
|
+
"config-resolved model",
|
|
1859
|
+
details: findings, fixable: false,
|
|
1860
|
+
fix_hint: "The old measurements were taken against a different model; re-qualify them under " \
|
|
1861
|
+
"the current model before acting on their numbers"
|
|
1862
|
+
)]
|
|
1863
|
+
end
|
|
1864
|
+
end
|
|
1865
|
+
|
|
1866
|
+
# --- Check: unpromoted rule: findings (intent 341, G8, C37) ---------------
|
|
1867
|
+
#
|
|
1868
|
+
# `insight-append --rule` tags an entry "... - rule: <text>". A tag is a
|
|
1869
|
+
# promise the rule will make it into project doctrine; until the exact
|
|
1870
|
+
# rule text shows up in some docs/help/*.md chapter, it is only visible to
|
|
1871
|
+
# a session that happens to read this one intent file, and the next
|
|
1872
|
+
# session repeats the mistake the rule names. Advisory only (warn, never
|
|
1873
|
+
# fail): a freshly tagged rule is not yet promoted by design, and nothing
|
|
1874
|
+
# here can auto-promote it (that is an editorial call, not a mechanical
|
|
1875
|
+
# one).
|
|
1876
|
+
RULE_ENTRY_RE = /—\s*rule:\s*(.+?)\s*\z/.freeze
|
|
1877
|
+
|
|
1878
|
+
def unpromoted_rules_checks(intent_dirs, package_root: PACKAGE_ROOT)
|
|
1879
|
+
# docs/help installs straight under PACKAGE_ROOT for every harness alike
|
|
1880
|
+
# (~/.plastic/docs/help once installed, repo docs/help in a checkout), so
|
|
1881
|
+
# a single path covers both the repo and the installed layout; unlike
|
|
1882
|
+
# the old skills-based chapters, help topics are no longer agent-home
|
|
1883
|
+
# specific, so this needs no `home:` split.
|
|
1884
|
+
chapter_dir = File.join(package_root, "docs", "help")
|
|
1885
|
+
|
|
1886
|
+
chapters_text = Dir.glob(File.join(chapter_dir, "*.md"))
|
|
1887
|
+
.map { |f| File.read(f) }
|
|
1888
|
+
.join("\n\n")
|
|
1889
|
+
|
|
1890
|
+
unpromoted = []
|
|
1891
|
+
intent_dirs.each do |d|
|
|
1892
|
+
md_path = File.join(d[:path], "#{d[:name]}.md")
|
|
1893
|
+
next unless File.exist?(md_path)
|
|
1894
|
+
|
|
1895
|
+
File.readlines(md_path).each do |line|
|
|
1896
|
+
m = line.chomp.match(RULE_ENTRY_RE)
|
|
1897
|
+
next unless m
|
|
1898
|
+
|
|
1899
|
+
rule_text = m[1].strip
|
|
1900
|
+
next if rule_text.empty?
|
|
1901
|
+
|
|
1902
|
+
unpromoted << "#{tilde(d[:path])}: #{rule_text}" unless chapters_text.include?(rule_text)
|
|
1903
|
+
end
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
if unpromoted.empty?
|
|
1907
|
+
[check(
|
|
1908
|
+
category: "conventions", name: "unpromoted_rules", status: "pass",
|
|
1909
|
+
message: "Every tagged rule: finding is carried by a conventions chapter"
|
|
1910
|
+
)]
|
|
1911
|
+
else
|
|
1912
|
+
[check(
|
|
1913
|
+
category: "conventions", name: "unpromoted_rules", status: "warn",
|
|
1914
|
+
message: "#{unpromoted.size} tagged rule(s) not yet carried by any conventions chapter",
|
|
1915
|
+
details: unpromoted, fixable: false,
|
|
1916
|
+
fix_hint: "Promote the rule into the right docs/help/*.md chapter, " \
|
|
1917
|
+
"or drop the tag if the finding does not belong in doctrine"
|
|
1918
|
+
)]
|
|
1919
|
+
end
|
|
1920
|
+
end
|
|
1921
|
+
|
|
1531
1922
|
# --- Check category 3: Agent registration ---
|
|
1532
1923
|
|
|
1533
1924
|
|
|
@@ -1539,8 +1930,7 @@ end
|
|
|
1539
1930
|
# skill directory under agent_dir/skills that has NO corresponding entry in the
|
|
1540
1931
|
# current install manifest is a stray (e.g. a leftover old-name copy the
|
|
1541
1932
|
# install/update prune should have removed, or one it never saw because the
|
|
1542
|
-
# manifest predates it).
|
|
1543
|
-
# least one skill exists). Defers to the manifest check when the manifest itself
|
|
1933
|
+
# manifest predates it). Defers to the manifest check when the manifest itself
|
|
1544
1934
|
# is missing or malformed, so the two checks never double-report the same gap.
|
|
1545
1935
|
|
|
1546
1936
|
# Auto-mode role agents install as <dir>/agents/plastic-*.md. Pass if at least
|
|
@@ -1764,6 +2154,8 @@ end
|
|
|
1764
2154
|
|
|
1765
2155
|
global_config = load_yaml_safe(File.join(plastic_home, "config.yml")) || {}
|
|
1766
2156
|
overrides = AgentModels.override_map(project_config: {}, global_config: global_config, harness: "codex")
|
|
2157
|
+
effort_overrides = AgentModels.effort_override_map(project_config: {}, global_config: global_config,
|
|
2158
|
+
harness: "codex")
|
|
1767
2159
|
|
|
1768
2160
|
drifted = []
|
|
1769
2161
|
sanctioned = []
|
|
@@ -1774,13 +2166,13 @@ end
|
|
|
1774
2166
|
fields = codex_agent_toml_model_fields(File.read(path))
|
|
1775
2167
|
override = overrides[basename]
|
|
1776
2168
|
|
|
1777
|
-
if override
|
|
2169
|
+
if override || effort_overrides[basename]
|
|
1778
2170
|
sanctioned << "#{basename}: toml model=#{fields[:model].inspect}, " \
|
|
1779
|
-
"toml effort=#{fields[:effort].inspect}, sanctioned override=#{override.inspect}"
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
expected_model = AgentModels.
|
|
1783
|
-
expected_effort = AgentModels.
|
|
2171
|
+
"toml effort=#{fields[:effort].inspect}, sanctioned model override=#{override.inspect}, " \
|
|
2172
|
+
"effort override=#{effort_overrides[basename].inspect}"
|
|
2173
|
+
elsif AgentModels::SHIPPED_MODEL_DEFAULTS.key?(basename)
|
|
2174
|
+
expected_model = AgentModels.shipped_model_for(basename, harness: "codex")
|
|
2175
|
+
expected_effort = AgentModels.shipped_effort_for(basename)
|
|
1784
2176
|
mismatches = []
|
|
1785
2177
|
if expected_model && fields[:model] != expected_model
|
|
1786
2178
|
mismatches << "model=#{fields[:model].inspect}, resolved default model=#{expected_model.inspect}"
|
|
@@ -1792,7 +2184,7 @@ end
|
|
|
1792
2184
|
else
|
|
1793
2185
|
unclassified << "#{basename}: toml model=#{fields[:model].inspect}, " \
|
|
1794
2186
|
"toml effort=#{fields[:effort].inspect}, no resolved default (basename is in " \
|
|
1795
|
-
"
|
|
2187
|
+
"AgentModels::SHIPPED_MODEL_DEFAULTS in " \
|
|
1796
2188
|
"scripts/lib/agent_models.rb; add it there, or set agents.models.codex.#{basename} " \
|
|
1797
2189
|
"to sanction a model explicitly)"
|
|
1798
2190
|
end
|
|
@@ -1904,7 +2296,7 @@ end
|
|
|
1904
2296
|
# project can be checked in isolation (used by `--store <slug>`).
|
|
1905
2297
|
def check_project_store(slug, project_info)
|
|
1906
2298
|
checks = []
|
|
1907
|
-
project_dir =
|
|
2299
|
+
project_dir = Plastic::StoreLayout.project_root(plastic_home, slug)
|
|
1908
2300
|
|
|
1909
2301
|
# project_dir_exists
|
|
1910
2302
|
if File.directory?(project_dir)
|
|
@@ -1954,7 +2346,7 @@ end
|
|
|
1954
2346
|
end
|
|
1955
2347
|
|
|
1956
2348
|
# project_yml_exists
|
|
1957
|
-
project_yml_path = File.join(plastic_home,
|
|
2349
|
+
project_yml_path = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "project.yml")
|
|
1958
2350
|
project_yml_data = nil
|
|
1959
2351
|
|
|
1960
2352
|
if File.exist?(project_yml_path)
|
|
@@ -2001,7 +2393,7 @@ end
|
|
|
2001
2393
|
return checks unless parent_id
|
|
2002
2394
|
|
|
2003
2395
|
# Find the intent directory for the parent ID
|
|
2004
|
-
store_dir =
|
|
2396
|
+
store_dir = Plastic::StoreLayout.global_store(plastic_home)
|
|
2005
2397
|
parent_dir = nil
|
|
2006
2398
|
if File.directory?(store_dir)
|
|
2007
2399
|
parent_dir = Dir.children(store_dir).find { |d| d.start_with?("#{parent_id}--") }
|
|
@@ -2158,7 +2550,7 @@ end
|
|
|
2158
2550
|
# A config-asks warn in that tier would flip every post-update doctor to
|
|
2159
2551
|
# "fail" on an otherwise healthy install until the question is answered,
|
|
2160
2552
|
# re-noising the post-update surface intent 126 deliberately quieted. The
|
|
2161
|
-
# recoverable-later path for a pending question is a full
|
|
2553
|
+
# recoverable-later path for a pending question is a full `plastic doctor`
|
|
2162
2554
|
# run, the declared maintenance front door; the moment-it-happens path is
|
|
2163
2555
|
# update.rb#announce_pending_config_asks, which already runs on every hop.
|
|
2164
2556
|
#
|
|
@@ -2388,6 +2780,250 @@ end
|
|
|
2388
2780
|
end
|
|
2389
2781
|
end
|
|
2390
2782
|
|
|
2783
|
+
# --- Check category: display (intent 331e, D1) ---
|
|
2784
|
+
#
|
|
2785
|
+
# Three of the four `display` checks live HERE, not in doctor_core.rb: they
|
|
2786
|
+
# need Open3/Timeout to spawn the installed hook as a real subprocess, and
|
|
2787
|
+
# that must never attach to the SessionStart boot path
|
|
2788
|
+
# (test/doctor_core_split_test.rb T2 pins doctor_core.rb's require set
|
|
2789
|
+
# exactly). check_display_registration (the fourth, --core-scoped) lives in
|
|
2790
|
+
# doctor_core.rb instead, alongside display_hook_launcher_name, which this
|
|
2791
|
+
# file's check_display_paints reuses to name the SAME installed launcher.
|
|
2792
|
+
|
|
2793
|
+
# The ambient defeater active for THIS invocation, or nil. `no_color` is
|
|
2794
|
+
# DI'd (default ENV["NO_COLOR"]) rather than read deep inside this method,
|
|
2795
|
+
# so a test can force it on or off without touching the real process
|
|
2796
|
+
# environment. Shared by check_display_paints (a defeater turns a
|
|
2797
|
+
# would-be fail into a pass, R3) and check_display_not_defeated (a
|
|
2798
|
+
# defeater is what it warns about).
|
|
2799
|
+
def active_display_defeater(no_color: ENV["NO_COLOR"])
|
|
2800
|
+
return "NO_COLOR" unless no_color.to_s.empty?
|
|
2801
|
+
|
|
2802
|
+
cfg = load_yaml_safe(File.join(plastic_home, "config.yml"))
|
|
2803
|
+
display_cfg = cfg.is_a?(Hash) ? cfg["display"] : nil
|
|
2804
|
+
return "display.ansi_screen: false in config.yml" if display_cfg.is_a?(Hash) &&
|
|
2805
|
+
display_cfg.fetch("ansi_screen", true) == false
|
|
2806
|
+
|
|
2807
|
+
nil
|
|
2808
|
+
end
|
|
2809
|
+
|
|
2810
|
+
# The shipped fixture's path: plastic_home's own templates/ first (a real
|
|
2811
|
+
# install), the package's own templates/ otherwise (running from a repo
|
|
2812
|
+
# checkout, or an install whose templates/ predates this fixture). Neither
|
|
2813
|
+
# existing means no fixture at all.
|
|
2814
|
+
def display_fixture_path(package_root: PACKAGE_ROOT)
|
|
2815
|
+
[File.join(plastic_home, "templates", "display-fixture.md"),
|
|
2816
|
+
File.join(package_root, "templates", "display-fixture.md")].find { |p| File.file?(p) }
|
|
2817
|
+
end
|
|
2818
|
+
|
|
2819
|
+
# The fixture's replayable text: its header comment (see
|
|
2820
|
+
# templates/display-fixture.md) is for a human reading the file on disk,
|
|
2821
|
+
# never sent through the hook.
|
|
2822
|
+
def display_fixture_text(path)
|
|
2823
|
+
File.read(path).sub(/\A<!--.*?-->\n\n?/m, "")
|
|
2824
|
+
end
|
|
2825
|
+
|
|
2826
|
+
# display_hook_paints (D1, R1/R2/R3): replays the shipped fixture through
|
|
2827
|
+
# the INSTALLED launcher (`<agent_dir>/hooks/plastic-message-display`,
|
|
2828
|
+
# never this package's own hooks/message-display. That is R1's whole point: an
|
|
2829
|
+
# installed launcher can predate the package's, and replaying the wrong
|
|
2830
|
+
# one reports pass while the real stack is stale) and expects a painted
|
|
2831
|
+
# (ANSI) screen back.
|
|
2832
|
+
#
|
|
2833
|
+
# A known defeater active (NO_COLOR, or display.ansi_screen: false) turns
|
|
2834
|
+
# a no-SGR result into a PASS, naming the defeater and noting it reflects
|
|
2835
|
+
# this invocation's own environment (R3). display_not_defeated is what
|
|
2836
|
+
# warns about a defeater; this check never fails because of one.
|
|
2837
|
+
def check_display_paints(agent_key, no_color: ENV["NO_COLOR"],
|
|
2838
|
+
tmp_dir_factory: -> { Dir.mktmpdir("plastic-doctor-display") },
|
|
2839
|
+
timeout_seconds: 10, package_root: PACKAGE_ROOT)
|
|
2840
|
+
config = agents[agent_key]
|
|
2841
|
+
unless agent_key == "claude"
|
|
2842
|
+
return [check(
|
|
2843
|
+
category: "display", name: "display_hook_paints", status: "pass",
|
|
2844
|
+
message: "#{config[:name]} is plain by contract; no display hook to replay"
|
|
2845
|
+
)]
|
|
2846
|
+
end
|
|
2847
|
+
|
|
2848
|
+
fixture_path = display_fixture_path(package_root: package_root)
|
|
2849
|
+
unless fixture_path
|
|
2850
|
+
return [check(
|
|
2851
|
+
category: "display", name: "display_hook_paints", status: "fail",
|
|
2852
|
+
message: "Shipped display fixture is missing (templates/display-fixture.md)",
|
|
2853
|
+
fixable: false
|
|
2854
|
+
)]
|
|
2855
|
+
end
|
|
2856
|
+
|
|
2857
|
+
agent_dir = config[:dir]
|
|
2858
|
+
launcher_path = File.join(agent_dir, "hooks", display_hook_launcher_name)
|
|
2859
|
+
unless File.file?(launcher_path) && File.executable?(launcher_path)
|
|
2860
|
+
return [check(
|
|
2861
|
+
category: "display", name: "display_hook_paints", status: "fail",
|
|
2862
|
+
message: "Installed launcher #{tilde(launcher_path)} is missing or not executable; cannot replay",
|
|
2863
|
+
fixable: true, fix_hint: DISPLAY_HOOK_FIX_HINT
|
|
2864
|
+
)]
|
|
2865
|
+
end
|
|
2866
|
+
|
|
2867
|
+
text = display_fixture_text(fixture_path)
|
|
2868
|
+
tmp_dir = tmp_dir_factory.call
|
|
2869
|
+
outs =
|
|
2870
|
+
begin
|
|
2871
|
+
HookReplay.replay(hook_path: launcher_path, tmp_root: tmp_dir, text: text,
|
|
2872
|
+
env: { "PLASTIC_HOME" => plastic_home }, timeout: timeout_seconds)
|
|
2873
|
+
rescue StandardError => e
|
|
2874
|
+
# Process.spawn (inside HookReplay) can raise before a pid ever
|
|
2875
|
+
# exists: a permissions race, or a launcher that vanishes between the
|
|
2876
|
+
# executable? check above and the spawn, or any other unexpected
|
|
2877
|
+
# error. Unlike this codebase's defensive style elsewhere
|
|
2878
|
+
# (read_json_safe, load_yaml_safe), nothing here degraded that into
|
|
2879
|
+
# a clean check result, so an unlucky replay crashed the entire
|
|
2880
|
+
# doctor run instead of failing just this one check (intent 331e,
|
|
2881
|
+
# F6). `return` still runs the `ensure` below before unwinding.
|
|
2882
|
+
return [check(
|
|
2883
|
+
category: "display", name: "display_hook_paints", status: "fail",
|
|
2884
|
+
message: "Replaying the installed launcher raised #{e.class}: #{e.message}",
|
|
2885
|
+
fixable: false
|
|
2886
|
+
)]
|
|
2887
|
+
ensure
|
|
2888
|
+
FileUtils.remove_entry(tmp_dir) if tmp_dir && File.exist?(tmp_dir)
|
|
2889
|
+
end
|
|
2890
|
+
|
|
2891
|
+
if HookReplay.timed_out?(outs)
|
|
2892
|
+
return [check(
|
|
2893
|
+
category: "display", name: "display_hook_paints", status: "fail",
|
|
2894
|
+
message: "Replaying the installed launcher timed out after #{timeout_seconds}s; painting could not be verified",
|
|
2895
|
+
fixable: false
|
|
2896
|
+
)]
|
|
2897
|
+
end
|
|
2898
|
+
|
|
2899
|
+
defeater = active_display_defeater(no_color: no_color)
|
|
2900
|
+
if defeater
|
|
2901
|
+
return [check(
|
|
2902
|
+
category: "display", name: "display_hook_paints", status: "pass",
|
|
2903
|
+
message: "Painting is defeated by #{defeater} for this invocation; the replay's plain " \
|
|
2904
|
+
"output reflects that setting, not a broken hook"
|
|
2905
|
+
)]
|
|
2906
|
+
end
|
|
2907
|
+
|
|
2908
|
+
content = HookReplay.final_display_content(outs)
|
|
2909
|
+
if content.to_s.include?("\e[")
|
|
2910
|
+
[check(
|
|
2911
|
+
category: "display", name: "display_hook_paints", status: "pass",
|
|
2912
|
+
message: "The installed hook returned a painted (ANSI) screen"
|
|
2913
|
+
)]
|
|
2914
|
+
else
|
|
2915
|
+
[check(
|
|
2916
|
+
category: "display", name: "display_hook_paints", status: "fail",
|
|
2917
|
+
message: "The installed hook returned no ANSI escape sequence; painting may be broken",
|
|
2918
|
+
fixable: true, fix_hint: DISPLAY_HOOK_FIX_HINT
|
|
2919
|
+
)]
|
|
2920
|
+
end
|
|
2921
|
+
end
|
|
2922
|
+
|
|
2923
|
+
# display_not_defeated (D1, R3/R4): warns, never fails, on each active
|
|
2924
|
+
# defeater, naming the setting and its effect. Every result, pass or warn alike,
|
|
2925
|
+
# names the verbose transcript view (Ctrl+O, R4): it redraws every
|
|
2926
|
+
# screen as plain tables too, but it has no on-disk setting doctor can
|
|
2927
|
+
# read, so its absence from these warnings is never proof that view paints.
|
|
2928
|
+
def check_display_not_defeated(agent_key, no_color: ENV["NO_COLOR"])
|
|
2929
|
+
config = agents[agent_key]
|
|
2930
|
+
unless agent_key == "claude"
|
|
2931
|
+
return [check(
|
|
2932
|
+
category: "display", name: "display_not_defeated", status: "pass",
|
|
2933
|
+
message: "#{config[:name]} is plain by contract; no display defeaters apply"
|
|
2934
|
+
)]
|
|
2935
|
+
end
|
|
2936
|
+
|
|
2937
|
+
warnings = []
|
|
2938
|
+
warnings << "NO_COLOR is set: forces every screen to plain text for this invocation" unless no_color.to_s.empty?
|
|
2939
|
+
|
|
2940
|
+
cfg = load_yaml_safe(File.join(plastic_home, "config.yml"))
|
|
2941
|
+
display_cfg = cfg.is_a?(Hash) ? cfg["display"] : nil
|
|
2942
|
+
if display_cfg.is_a?(Hash) && display_cfg.fetch("ansi_screen", true) == false
|
|
2943
|
+
warnings << "config.yml sets display.ansi_screen: false: forces every screen to plain text"
|
|
2944
|
+
end
|
|
2945
|
+
|
|
2946
|
+
settings = read_json_safe(File.join(config[:dir], "settings.json"))
|
|
2947
|
+
if settings.is_a?(Hash) && settings["verbose"] == true
|
|
2948
|
+
warnings << "settings.json sets verbose: true: the verbose transcript view (Ctrl+O) redraws every screen as plain tables"
|
|
2949
|
+
end
|
|
2950
|
+
|
|
2951
|
+
transcript_note = "The Ctrl+O verbose transcript view also redraws every screen as plain " \
|
|
2952
|
+
"tables and has no on-disk setting; its absence above is never proof that view paints."
|
|
2953
|
+
|
|
2954
|
+
if warnings.empty?
|
|
2955
|
+
[check(
|
|
2956
|
+
category: "display", name: "display_not_defeated", status: "pass",
|
|
2957
|
+
message: "No known display defeaters active", details: [transcript_note]
|
|
2958
|
+
)]
|
|
2959
|
+
else
|
|
2960
|
+
[check(
|
|
2961
|
+
category: "display", name: "display_not_defeated", status: "warn",
|
|
2962
|
+
message: "#{warnings.size} display defeater(s) active",
|
|
2963
|
+
details: warnings + [transcript_note]
|
|
2964
|
+
)]
|
|
2965
|
+
end
|
|
2966
|
+
end
|
|
2967
|
+
|
|
2968
|
+
# display_surfaces_documented (D1/D4): the harness-adapters doc names the
|
|
2969
|
+
# three surface classes. Reads the package's own shipped doc: static
|
|
2970
|
+
# content, not a runtime path, the same shape as check_skill_lint reading the
|
|
2971
|
+
# package's own skills/ tree.
|
|
2972
|
+
#
|
|
2973
|
+
# `docs/` ships in NEITHER package.json's `files` list NOR
|
|
2974
|
+
# InstallerCore's manifest (grep confirms zero references), so on every
|
|
2975
|
+
# real install `package_root` resolves to a `~/.plastic` that has no
|
|
2976
|
+
# `docs/` tree at all; only a repo checkout carries it. Absence of the
|
|
2977
|
+
# doc there is therefore not a defect to report; it is this install
|
|
2978
|
+
# having nothing to verify, the same skip-as-pass vocabulary D3 and R3
|
|
2979
|
+
# already use elsewhere in this category. This check fails only when the
|
|
2980
|
+
# doc DOES exist (a repo checkout) but has rotted: no `## Surfaces`
|
|
2981
|
+
# section, or one missing a required literal.
|
|
2982
|
+
def check_display_surfaces_documented(package_root: PACKAGE_ROOT)
|
|
2983
|
+
doc_path = File.join(package_root, "docs", "reference", "harness-adapters.md")
|
|
2984
|
+
|
|
2985
|
+
unless File.file?(doc_path)
|
|
2986
|
+
return [check(
|
|
2987
|
+
category: "display", name: "display_surfaces_documented", status: "pass",
|
|
2988
|
+
message: "Reference docs are not shipped to this install (#{tilde(doc_path)} absent); " \
|
|
2989
|
+
"nothing to verify"
|
|
2990
|
+
)]
|
|
2991
|
+
end
|
|
2992
|
+
|
|
2993
|
+
content = File.read(doc_path)
|
|
2994
|
+
section = content[/^## Surfaces\n(.*?)(?=\n## |\z)/m, 1].to_s
|
|
2995
|
+
|
|
2996
|
+
required = ["Claude Code normal view", "agents view", "Codex", "claude -p", "verbose transcript view"]
|
|
2997
|
+
missing = required.reject { |literal| section.include?(literal) }
|
|
2998
|
+
|
|
2999
|
+
if section.empty?
|
|
3000
|
+
[check(
|
|
3001
|
+
category: "display", name: "display_surfaces_documented", status: "fail",
|
|
3002
|
+
message: "docs/reference/harness-adapters.md has no ## Surfaces section", fixable: false
|
|
3003
|
+
)]
|
|
3004
|
+
elsif missing.empty?
|
|
3005
|
+
[check(
|
|
3006
|
+
category: "display", name: "display_surfaces_documented", status: "pass",
|
|
3007
|
+
message: "harness-adapters.md documents every display surface class"
|
|
3008
|
+
)]
|
|
3009
|
+
else
|
|
3010
|
+
[check(
|
|
3011
|
+
category: "display", name: "display_surfaces_documented", status: "fail",
|
|
3012
|
+
message: "## Surfaces section is missing: #{missing.join(', ')}", details: missing, fixable: false
|
|
3013
|
+
)]
|
|
3014
|
+
end
|
|
3015
|
+
end
|
|
3016
|
+
|
|
3017
|
+
def check_graph_links
|
|
3018
|
+
return [] unless File.exist?(Plastic::WorkGraph.path(plastic_home))
|
|
3019
|
+
|
|
3020
|
+
orphans = Plastic::StoreSync.orphans(plastic_home)
|
|
3021
|
+
return [check(category: "graph_links", name: "graph_links", status: "pass", message: "every stored file belongs to an intent in work_graph.db")] if orphans.empty?
|
|
3022
|
+
|
|
3023
|
+
[check(category: "graph_links", name: "graph_links", status: "warn", details: orphans, fixable: false,
|
|
3024
|
+
message: "#{orphans.size} intent id(s) hold files and have no row in work_graph.db")]
|
|
3025
|
+
end
|
|
3026
|
+
|
|
2391
3027
|
# --- Run all checks ---
|
|
2392
3028
|
|
|
2393
3029
|
def run_checks(agent_key)
|
|
@@ -2406,6 +3042,11 @@ end
|
|
|
2406
3042
|
all_checks += check_session_ledger(scopes: ["global"])
|
|
2407
3043
|
all_checks += check_skill_lint
|
|
2408
3044
|
all_checks += check_install_integrity
|
|
3045
|
+
all_checks += check_graph_links
|
|
3046
|
+
all_checks += check_display_registration(agent_key)
|
|
3047
|
+
all_checks += check_display_paints(agent_key)
|
|
3048
|
+
all_checks += check_display_not_defeated(agent_key)
|
|
3049
|
+
all_checks += check_display_surfaces_documented
|
|
2409
3050
|
|
|
2410
3051
|
summarize(all_checks, agent_key)
|
|
2411
3052
|
end
|