@zalom/plastic 1.14.1 → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC.md +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/bin/lib/context_budget.rb +453 -0
- package/bin/plastic-bench +78 -0
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/close +5 -0
- package/hooks/hooks.json +14 -42
- package/hooks/message-display +81 -0
- package/hooks/record +4 -0
- package/hooks/savepoint +5 -5
- package/package.json +3 -2
- package/scripts/agent-report +15 -10
- package/scripts/append-ledger +208 -0
- package/scripts/codex-hook +78 -90
- package/scripts/dashboard.rb +43 -14
- package/scripts/day-summary +53 -0
- package/scripts/doctor.rb +269 -43
- package/scripts/end-intent +170 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +255 -0
- package/scripts/hook-close +36 -0
- package/scripts/hook-message-display +74 -0
- package/scripts/hook-record +157 -0
- package/scripts/hook-savepoint +45 -0
- package/scripts/hook-session-start +128 -12
- package/scripts/install.rb +8 -6
- package/scripts/intent-screen +77 -0
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +308 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/compact_instructions.rb +56 -0
- package/scripts/lib/day_summary.rb +211 -0
- package/scripts/lib/doctor_core.rb +66 -88
- package/scripts/lib/doctor_session_ledger.rb +158 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/handoff.rb +184 -0
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +57 -106
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +136 -41
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/message_display.rb +290 -0
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/report_screen.rb +648 -0
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +334 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/screen_paint.rb +276 -0
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +104 -0
- package/scripts/lib/session_git.rb +500 -0
- package/scripts/lib/session_ledger.rb +621 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +101 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/read-config +3 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/report-screen +120 -0
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +48 -3
- package/scripts/savepoint-note +67 -0
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +14 -7
- package/scripts/update.rb +7 -3
- package/scripts/write-handoff +60 -0
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +223 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +62 -56
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +62 -58
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +142 -91
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +3 -3
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +34 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +15 -46
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/config.yml +5 -0
- package/templates/intent-screen.md +17 -0
- package/templates/outcome.md +14 -1
- package/templates/project.yml +7 -0
- package/templates/report-state.md +11 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
|
@@ -9,6 +9,7 @@ require "time"
|
|
|
9
9
|
require_relative "hook_registry"
|
|
10
10
|
require_relative "agent_models"
|
|
11
11
|
require_relative "harness_text"
|
|
12
|
+
require_relative "compact_instructions"
|
|
12
13
|
|
|
13
14
|
# Shared installer machinery, instantiable with injected package root / store / agent
|
|
14
15
|
# map so the verb scripts (install/update/uninstall/rollback) and their tests can run
|
|
@@ -33,6 +34,15 @@ class InstallerCore
|
|
|
33
34
|
# Regex matching exactly one managed section (BEGIN line .. END line), non-greedy.
|
|
34
35
|
CODEX_SECTION_RE = /^<!-- BEGIN PLASTIC INTEGRATION.*?-->\n.*?\n<!-- END PLASTIC INTEGRATION -->\n?/m
|
|
35
36
|
|
|
37
|
+
# Claude CLAUDE.md marked-section markers (intent 312). A pair of its own, not the
|
|
38
|
+
# Codex literals: the two managed files can be one file (a user who symlinks
|
|
39
|
+
# ~/.claude/CLAUDE.md at ~/.codex/AGENTS.md, or the reverse), and a shared literal
|
|
40
|
+
# would let one body silently replace the other and an uninstall of one strip both.
|
|
41
|
+
# doctor_core.rb matches these literals structurally, so keep the two in sync by hand.
|
|
42
|
+
CLAUDE_SECTION_BEGIN_PREFIX = "<!-- BEGIN PLASTIC COMPACT"
|
|
43
|
+
CLAUDE_SECTION_END = "<!-- END PLASTIC COMPACT -->"
|
|
44
|
+
CLAUDE_SECTION_RE = /^<!-- BEGIN PLASTIC COMPACT.*?-->\n.*?\n<!-- END PLASTIC COMPACT -->\n?/m
|
|
45
|
+
|
|
36
46
|
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the plastic-conventions
|
|
37
47
|
# skill, injected into ~/.codex/AGENTS.md. Not a slice of PLASTIC.md itself: AGENTS.md is
|
|
38
48
|
# a shared file Codex merges from multiple sources, so this block stays a small,
|
|
@@ -66,8 +76,25 @@ class InstallerCore
|
|
|
66
76
|
@agents = agents
|
|
67
77
|
end
|
|
68
78
|
|
|
79
|
+
# Reads the package version from `package.json` when present (the source
|
|
80
|
+
# tree, and any dev checkout), falling back to the bare `VERSION` file the
|
|
81
|
+
# installer copies to `~/.plastic` (row B, spec 315b): an installed system
|
|
82
|
+
# carries VERSION but never package.json, so `rollback.rb` and `update.rb`
|
|
83
|
+
# constructing an InstallerCore with `package_root: ~/.plastic` crashed with
|
|
84
|
+
# a raw Errno::ENOENT before this fallback existed. Raises a named error,
|
|
85
|
+
# not a bare ENOENT, when neither file exists, so the caller is not
|
|
86
|
+
# misdirected toward the wrong missing file.
|
|
69
87
|
def read_package_version(root)
|
|
70
|
-
File.
|
|
88
|
+
package_json = File.join(root, "package.json")
|
|
89
|
+
version_file = File.join(root, "VERSION")
|
|
90
|
+
|
|
91
|
+
if File.exist?(package_json)
|
|
92
|
+
JSON.parse(File.read(package_json))["version"]
|
|
93
|
+
elsif File.exist?(version_file)
|
|
94
|
+
File.read(version_file).strip
|
|
95
|
+
else
|
|
96
|
+
raise "cannot determine the package version: neither #{package_json} nor #{version_file} exists"
|
|
97
|
+
end
|
|
71
98
|
end
|
|
72
99
|
|
|
73
100
|
# --- Channel derivation (the channel is encoded in the version string) ---
|
|
@@ -174,13 +201,6 @@ class InstallerCore
|
|
|
174
201
|
keys.uniq
|
|
175
202
|
end
|
|
176
203
|
|
|
177
|
-
def channel_from(argv, default: "latest")
|
|
178
|
-
return "alpha" if argv.include?("--alpha")
|
|
179
|
-
return "beta" if argv.include?("--beta")
|
|
180
|
-
return "latest" if argv.include?("--latest")
|
|
181
|
-
default
|
|
182
|
-
end
|
|
183
|
-
|
|
184
204
|
def prompt_agents(input: $stdin)
|
|
185
205
|
return ["claude"] unless input.tty?
|
|
186
206
|
|
|
@@ -329,25 +349,21 @@ class InstallerCore
|
|
|
329
349
|
"scripts/write-config" => "scripts/write-config",
|
|
330
350
|
"scripts/select-update-target" => "scripts/select-update-target",
|
|
331
351
|
"scripts/hook-session-start" => "scripts/hook-session-start",
|
|
332
|
-
"scripts/hook-
|
|
333
|
-
"scripts/hook-
|
|
334
|
-
"scripts/hook-
|
|
335
|
-
"scripts/hook-savepoint-pre" => "scripts/hook-savepoint-pre",
|
|
336
|
-
"scripts/hook-power-tools" => "scripts/hook-power-tools",
|
|
337
|
-
"scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
|
|
352
|
+
"scripts/hook-capture" => "scripts/hook-capture",
|
|
353
|
+
"scripts/hook-record" => "scripts/hook-record",
|
|
354
|
+
"scripts/hook-close" => "scripts/hook-close",
|
|
338
355
|
"scripts/lib/power_tools.rb" => "scripts/lib/power_tools.rb",
|
|
339
356
|
"scripts/lib/ruby_probe.rb" => "scripts/lib/ruby_probe.rb",
|
|
340
357
|
"scripts/lib/agent_models.rb" => "scripts/lib/agent_models.rb",
|
|
341
358
|
"scripts/lib/config_asks.rb" => "scripts/lib/config_asks.rb",
|
|
342
359
|
"scripts/lib/release_guard.rb" => "scripts/lib/release_guard.rb",
|
|
343
|
-
"scripts/hook-code-gate" => "scripts/hook-code-gate",
|
|
344
|
-
"scripts/hook-lock-gate" => "scripts/hook-lock-gate",
|
|
345
|
-
"scripts/hook-bash-gate" => "scripts/hook-bash-gate",
|
|
346
|
-
"scripts/hook-auto-arm" => "scripts/hook-auto-arm",
|
|
347
360
|
"scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
|
|
361
|
+
"scripts/lib/savepoint.rb" => "scripts/lib/savepoint.rb",
|
|
362
|
+
"scripts/lib/arm.rb" => "scripts/lib/arm.rb",
|
|
348
363
|
"scripts/lib/lock.rb" => "scripts/lib/lock.rb",
|
|
349
364
|
"scripts/plastic-lock" => "scripts/plastic-lock",
|
|
350
365
|
"scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
|
|
366
|
+
"scripts/lib/compact_instructions.rb" => "scripts/lib/compact_instructions.rb",
|
|
351
367
|
"scripts/agent-report" => "scripts/agent-report",
|
|
352
368
|
"scripts/lib/insights.rb" => "scripts/lib/insights.rb",
|
|
353
369
|
"scripts/insight-append" => "scripts/insight-append",
|
|
@@ -380,16 +396,13 @@ class InstallerCore
|
|
|
380
396
|
"scripts/validate-intent" => "scripts/validate-intent",
|
|
381
397
|
"scripts/new-intent" => "scripts/new-intent",
|
|
382
398
|
"scripts/end-intent" => "scripts/end-intent",
|
|
383
|
-
"scripts/hook-create-gate" => "scripts/hook-create-gate",
|
|
384
|
-
"scripts/hook-links-gate" => "scripts/hook-links-gate",
|
|
385
|
-
"scripts/lib/links_gate.rb" => "scripts/lib/links_gate.rb",
|
|
386
|
-
"scripts/lib/edit_gates.rb" => "scripts/lib/edit_gates.rb",
|
|
387
|
-
"scripts/hook-edit-gates" => "scripts/hook-edit-gates",
|
|
388
399
|
"scripts/lib/apply_patch_envelope.rb" => "scripts/lib/apply_patch_envelope.rb",
|
|
389
|
-
"scripts/lib/codex_edit_gates.rb" => "scripts/lib/codex_edit_gates.rb",
|
|
390
400
|
"scripts/lib/harness_text.rb" => "scripts/lib/harness_text.rb",
|
|
391
401
|
"scripts/codex-hook" => "scripts/codex-hook",
|
|
392
402
|
"scripts/spawn-preamble" => "scripts/spawn-preamble",
|
|
403
|
+
"scripts/lib/report_screen.rb" => "scripts/lib/report_screen.rb",
|
|
404
|
+
"scripts/report-screen" => "scripts/report-screen",
|
|
405
|
+
"scripts/savepoint-note" => "scripts/savepoint-note",
|
|
393
406
|
"scripts/lib/store_provisioning.rb" => "scripts/lib/store_provisioning.rb",
|
|
394
407
|
"scripts/provision-project-store" => "scripts/provision-project-store",
|
|
395
408
|
"scripts/lib/project_validator.rb" => "scripts/lib/project_validator.rb",
|
|
@@ -401,24 +414,47 @@ class InstallerCore
|
|
|
401
414
|
"scripts/uninstall.rb" => "scripts/uninstall.rb",
|
|
402
415
|
"scripts/rollback.rb" => "scripts/rollback.rb",
|
|
403
416
|
"scripts/lib/outcome_guard.rb" => "scripts/lib/outcome_guard.rb",
|
|
404
|
-
"scripts/lib/spec_header.rb" => "scripts/lib/spec_header.rb",
|
|
405
417
|
"scripts/lib/scaffold_intent.rb" => "scripts/lib/scaffold_intent.rb",
|
|
418
|
+
"scripts/lib/backfill_intent.rb" => "scripts/lib/backfill_intent.rb",
|
|
406
419
|
"scripts/scaffold-intent" => "scripts/scaffold-intent",
|
|
407
420
|
"scripts/lib/verify_intent.rb" => "scripts/lib/verify_intent.rb",
|
|
408
421
|
"scripts/verify-intent" => "scripts/verify-intent",
|
|
409
|
-
"scripts/lib/start_intent.rb" => "scripts/lib/start_intent.rb",
|
|
410
|
-
"scripts/start-intent" => "scripts/start-intent",
|
|
411
422
|
"scripts/lib/exec_worktree.rb" => "scripts/lib/exec_worktree.rb",
|
|
412
423
|
"scripts/exec-worktree" => "scripts/exec-worktree",
|
|
413
424
|
"scripts/doctor.rb" => "scripts/doctor.rb",
|
|
414
425
|
"scripts/lib/doctor_core.rb" => "scripts/lib/doctor_core.rb",
|
|
415
426
|
"scripts/lib/rule_catalog.rb" => "scripts/lib/rule_catalog.rb",
|
|
416
427
|
"scripts/lib/doctor_exclusions.rb" => "scripts/lib/doctor_exclusions.rb",
|
|
428
|
+
"scripts/lib/doctor_session_ledger.rb" => "scripts/lib/doctor_session_ledger.rb",
|
|
417
429
|
"scripts/dashboard.rb" => "scripts/dashboard.rb",
|
|
418
430
|
"scripts/skill-lint" => "scripts/skill-lint",
|
|
419
431
|
"scripts/lib/skill_lint.rb" => "scripts/lib/skill_lint.rb",
|
|
420
432
|
"scripts/feedback-report" => "scripts/feedback-report",
|
|
421
433
|
"scripts/lib/feedback_report.rb" => "scripts/lib/feedback_report.rb",
|
|
434
|
+
"scripts/append-ledger" => "scripts/append-ledger",
|
|
435
|
+
"scripts/lib/session_ledger.rb" => "scripts/lib/session_ledger.rb",
|
|
436
|
+
"scripts/session-commit" => "scripts/session-commit",
|
|
437
|
+
"scripts/file-session-intent" => "scripts/file-session-intent",
|
|
438
|
+
"scripts/promote-session-item" => "scripts/promote-session-item",
|
|
439
|
+
"scripts/lib/session_backfill.rb" => "scripts/lib/session_backfill.rb",
|
|
440
|
+
"scripts/lib/session_close.rb" => "scripts/lib/session_close.rb",
|
|
441
|
+
"scripts/lib/session_git.rb" => "scripts/lib/session_git.rb",
|
|
442
|
+
"scripts/lib/handoff.rb" => "scripts/lib/handoff.rb",
|
|
443
|
+
"scripts/lib/day_summary.rb" => "scripts/lib/day_summary.rb",
|
|
444
|
+
"scripts/write-handoff" => "scripts/write-handoff",
|
|
445
|
+
"scripts/day-summary" => "scripts/day-summary",
|
|
446
|
+
"scripts/lib/intent_screen.rb" => "scripts/lib/intent_screen.rb",
|
|
447
|
+
"scripts/intent-screen" => "scripts/intent-screen",
|
|
448
|
+
"scripts/hook-savepoint" => "scripts/hook-savepoint",
|
|
449
|
+
# Intent 316a: hooks/* only glob-copies scripts/*, never scripts/lib/*
|
|
450
|
+
# (see hook_files above), so the two lib files a require_relative
|
|
451
|
+
# between themselves are unguarded there — these three literal
|
|
452
|
+
# entries are their only protection (test/install_sync_test.rb:23-29
|
|
453
|
+
# greps installer_core.rb's own source text for "scripts/<name>").
|
|
454
|
+
"scripts/lib/intent_screen_ansi.rb" => "scripts/lib/intent_screen_ansi.rb",
|
|
455
|
+
"scripts/lib/screen_paint.rb" => "scripts/lib/screen_paint.rb",
|
|
456
|
+
"scripts/lib/message_display.rb" => "scripts/lib/message_display.rb",
|
|
457
|
+
"scripts/hook-message-display" => "scripts/hook-message-display",
|
|
422
458
|
}
|
|
423
459
|
end
|
|
424
460
|
|
|
@@ -432,6 +468,8 @@ class InstallerCore
|
|
|
432
468
|
version: 3
|
|
433
469
|
execution_mode: subagent-driven
|
|
434
470
|
stale_threshold_days: 3
|
|
471
|
+
context_offer_tokens: 350000
|
|
472
|
+
context_insist_tokens: 500000
|
|
435
473
|
hash_length: 6
|
|
436
474
|
hash_algorithm: sha256-base36
|
|
437
475
|
max_slug_words: 5
|
|
@@ -718,7 +756,7 @@ class InstallerCore
|
|
|
718
756
|
end
|
|
719
757
|
|
|
720
758
|
# `install_skills_flat` relocates any top-level underscore-prefixed markdown
|
|
721
|
-
# fragment (today: `
|
|
759
|
+
# fragment (today: `_decision-tables.md`) out of the
|
|
722
760
|
# per-agent skills tree and into `plastic_home` directly, then lists them in
|
|
723
761
|
# `installed`, so they are manifest-tracked and eligible for prune. But
|
|
724
762
|
# `install_for_agent`'s prune roots are `[config[:dir], config[:home_dir]]`,
|
|
@@ -790,6 +828,11 @@ class InstallerCore
|
|
|
790
828
|
choice = statusline_choice(settings_path, argv: argv, input: input, reinstall: reinstall)
|
|
791
829
|
merge_claude_hooks(settings_path, choice: choice)
|
|
792
830
|
|
|
831
|
+
# Instruction injection (intent 312): the compact-instructions block into
|
|
832
|
+
# ~/.claude/CLAUDE.md. A partial-ownership user file, so it is NOT manifest-tracked
|
|
833
|
+
# (stripped surgically on uninstall), the same treatment ~/.codex/AGENTS.md gets.
|
|
834
|
+
inject_claude_compact_md(File.join(config[:dir], "CLAUDE.md"))
|
|
835
|
+
|
|
793
836
|
# Write manifest
|
|
794
837
|
manifest_path = File.join(plastic_dir, "manifest.json")
|
|
795
838
|
write_manifest(installed, manifest_path)
|
|
@@ -1012,7 +1055,7 @@ class InstallerCore
|
|
|
1012
1055
|
|
|
1013
1056
|
# Copy each skills/<name>/ to <skills_root>/plastic-<name>/ (flat, namespaced by
|
|
1014
1057
|
# directory name -- the only personal-skill namespacing Claude Code supports).
|
|
1015
|
-
# Any top-level underscore-prefixed markdown fragment (e.g. `
|
|
1058
|
+
# Any top-level underscore-prefixed markdown fragment (e.g. `_decision-tables.md`,
|
|
1016
1059
|
# `_decision-tables.md`) is a shared non-skill fragment and relocates to ~/.plastic/
|
|
1017
1060
|
# instead, so every skill can read it from one shared location. `exclude` skips
|
|
1018
1061
|
# named top-level skill directories entirely (intent 185: the agent-advisor skill
|
|
@@ -1273,8 +1316,8 @@ class InstallerCore
|
|
|
1273
1316
|
plastic_hooks.each do |event, group|
|
|
1274
1317
|
hooks[event] ||= []
|
|
1275
1318
|
|
|
1276
|
-
# An event may map to a LIST of plastic groups (
|
|
1277
|
-
#
|
|
1319
|
+
# An event may map to a LIST of plastic groups (none does since the edit-path
|
|
1320
|
+
# gates left in 2.0, intent 302; the shape stays). The purge pass above already removed all
|
|
1278
1321
|
# prior plastic groups, so appending each desired group fresh is idempotent
|
|
1279
1322
|
# across re-runs and never collapses two matchers into one group.
|
|
1280
1323
|
groups = group.is_a?(Array) ? group : [group]
|
|
@@ -1355,14 +1398,28 @@ class InstallerCore
|
|
|
1355
1398
|
raise e
|
|
1356
1399
|
end
|
|
1357
1400
|
|
|
1358
|
-
|
|
1401
|
+
# A managed instruction file may be a symlink into a dotfiles repo. write_text_atomic
|
|
1402
|
+
# renames a temp file over its argument, which would replace the link with a regular
|
|
1403
|
+
# file and silently detach it, so every read and write resolves the link first and the
|
|
1404
|
+
# change lands on its target (intent 312).
|
|
1405
|
+
def resolve_managed_path(path)
|
|
1406
|
+
File.symlink?(path) ? File.realpath(path) : path
|
|
1407
|
+
rescue Errno::ENOENT
|
|
1408
|
+
path
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def marked_section(body: CODEX_AGENTS_MD_BODY, begin_prefix: CODEX_SECTION_BEGIN_PREFIX,
|
|
1412
|
+
end_marker: CODEX_SECTION_END)
|
|
1359
1413
|
hash = Digest::SHA256.hexdigest(body)[0, 12]
|
|
1360
|
-
"#{
|
|
1414
|
+
"#{begin_prefix} hash:#{hash} -->\n#{body.strip}\n#{end_marker}\n"
|
|
1361
1415
|
end
|
|
1362
1416
|
|
|
1363
1417
|
# Returns :created / :appended / :replaced / :refused. Never raises on a normal user file.
|
|
1364
|
-
def
|
|
1365
|
-
|
|
1418
|
+
def inject_marked_section(path, body: CODEX_AGENTS_MD_BODY,
|
|
1419
|
+
begin_prefix: CODEX_SECTION_BEGIN_PREFIX,
|
|
1420
|
+
end_marker: CODEX_SECTION_END, section_re: CODEX_SECTION_RE)
|
|
1421
|
+
section = marked_section(body: body, begin_prefix: begin_prefix, end_marker: end_marker)
|
|
1422
|
+
path = resolve_managed_path(path)
|
|
1366
1423
|
|
|
1367
1424
|
unless File.exist?(path)
|
|
1368
1425
|
FileUtils.mkdir_p(File.dirname(path))
|
|
@@ -1371,14 +1428,14 @@ class InstallerCore
|
|
|
1371
1428
|
end
|
|
1372
1429
|
|
|
1373
1430
|
content = File.read(path)
|
|
1374
|
-
has_begin = content.include?(
|
|
1375
|
-
has_end = content.include?(
|
|
1431
|
+
has_begin = content.include?(begin_prefix)
|
|
1432
|
+
has_end = content.include?(end_marker)
|
|
1376
1433
|
|
|
1377
1434
|
# 22a safety rule: never write if the existing section cannot be parsed.
|
|
1378
1435
|
return :refused if has_begin && !has_end
|
|
1379
1436
|
|
|
1380
1437
|
if has_begin
|
|
1381
|
-
write_text_atomic(path, content.sub(
|
|
1438
|
+
write_text_atomic(path, content.sub(section_re, section))
|
|
1382
1439
|
:replaced
|
|
1383
1440
|
else
|
|
1384
1441
|
base = content.end_with?("\n") ? content : content + "\n"
|
|
@@ -1387,18 +1444,41 @@ class InstallerCore
|
|
|
1387
1444
|
end
|
|
1388
1445
|
end
|
|
1389
1446
|
|
|
1447
|
+
# --- The two blocks Plastic ships, each with its own marker pair ---
|
|
1448
|
+
|
|
1449
|
+
def codex_section(body: CODEX_AGENTS_MD_BODY)
|
|
1450
|
+
marked_section(body: body)
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
def inject_codex_agents_md(path, body: CODEX_AGENTS_MD_BODY)
|
|
1454
|
+
inject_marked_section(path, body: body)
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
# The compact-instructions block for ~/.claude/CLAUDE.md (intent 312).
|
|
1458
|
+
def claude_compact_section(body: CompactInstructions::BODY)
|
|
1459
|
+
marked_section(body: body, begin_prefix: CLAUDE_SECTION_BEGIN_PREFIX,
|
|
1460
|
+
end_marker: CLAUDE_SECTION_END)
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1463
|
+
def inject_claude_compact_md(path, body: CompactInstructions::BODY)
|
|
1464
|
+
inject_marked_section(path, body: body, begin_prefix: CLAUDE_SECTION_BEGIN_PREFIX,
|
|
1465
|
+
end_marker: CLAUDE_SECTION_END, section_re: CLAUDE_SECTION_RE)
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1390
1468
|
# Remove exactly Plastic's managed section from a user-owned AGENTS.md. Preserve all other
|
|
1391
1469
|
# content. Delete the file only if Plastic created it and nothing else remains. Returns the
|
|
1392
1470
|
# path when it acted, nil on no-op. Mirrors remove_claude_hooks: dedicated surgical strip,
|
|
1393
1471
|
# never the manifest whole-file-delete path.
|
|
1394
|
-
def
|
|
1472
|
+
def strip_marked_section(path, begin_prefix: CODEX_SECTION_BEGIN_PREFIX,
|
|
1473
|
+
section_re: CODEX_SECTION_RE)
|
|
1474
|
+
path = resolve_managed_path(path)
|
|
1395
1475
|
return nil unless File.exist?(path)
|
|
1396
1476
|
content = File.read(path)
|
|
1397
|
-
return nil unless content.include?(
|
|
1477
|
+
return nil unless content.include?(begin_prefix)
|
|
1398
1478
|
|
|
1399
1479
|
# Remove the section plus the single separator newline the append introduced, so a
|
|
1400
1480
|
# standard user file round-trips byte-identical.
|
|
1401
|
-
stripped = content.sub(/\n?#{
|
|
1481
|
+
stripped = content.sub(/\n?#{section_re}/, "")
|
|
1402
1482
|
|
|
1403
1483
|
if stripped.strip.empty?
|
|
1404
1484
|
File.delete(path) # Plastic-created file: nothing else left
|
|
@@ -1409,6 +1489,15 @@ class InstallerCore
|
|
|
1409
1489
|
path
|
|
1410
1490
|
end
|
|
1411
1491
|
|
|
1492
|
+
def strip_codex_section(path)
|
|
1493
|
+
strip_marked_section(path)
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1496
|
+
def strip_claude_compact_section(path)
|
|
1497
|
+
strip_marked_section(path, begin_prefix: CLAUDE_SECTION_BEGIN_PREFIX,
|
|
1498
|
+
section_re: CLAUDE_SECTION_RE)
|
|
1499
|
+
end
|
|
1500
|
+
|
|
1412
1501
|
# --- Uninstall ---
|
|
1413
1502
|
|
|
1414
1503
|
def handle_uninstall(uninstall_agents)
|
|
@@ -1435,6 +1524,7 @@ class InstallerCore
|
|
|
1435
1524
|
puts " ls ~/.claude/skills | grep '^plastic-' # → no output"
|
|
1436
1525
|
puts " ls ~/.claude/hooks | grep '^plastic-' # → no output"
|
|
1437
1526
|
puts " grep -c plastic ~/.claude/settings.json # → only hook refs gone"
|
|
1527
|
+
puts " grep 'PLASTIC COMPACT' ~/.claude/CLAUDE.md # → no output"
|
|
1438
1528
|
puts "\n To also delete your intent store: rm -rf #{tilde(plastic_home)}\n\n"
|
|
1439
1529
|
end
|
|
1440
1530
|
|
|
@@ -1481,6 +1571,11 @@ class InstallerCore
|
|
|
1481
1571
|
settings_path = File.join(config[:dir], "settings.json")
|
|
1482
1572
|
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
1483
1573
|
removed.concat(migrate_legacy_plugin(config[:dir]))
|
|
1574
|
+
|
|
1575
|
+
# The compact-instructions block in the user-owned CLAUDE.md (intent 312): a
|
|
1576
|
+
# surgical strip, never the manifest whole-file-delete path above.
|
|
1577
|
+
stripped = strip_claude_compact_section(File.join(config[:dir], "CLAUDE.md"))
|
|
1578
|
+
removed << stripped if stripped
|
|
1484
1579
|
end
|
|
1485
1580
|
|
|
1486
1581
|
# Codex: surgically strip Plastic's marked section from the user-owned AGENTS.md
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# IntentScreen (intent 316) - fills templates/intent-screen.md from one intent's
|
|
4
|
+
# record: the intent file, the tier's INDEX.md, savepoint.md and checklist.md.
|
|
5
|
+
# Every number on the screen comes from here so the session never writes one by
|
|
6
|
+
# eye. Pure: explicit paths in, a Markdown string out; no ENV, no Dir.pwd.
|
|
7
|
+
#
|
|
8
|
+
# Intent 316a fixed three defects the field code inherited into both the plain
|
|
9
|
+
# renderer and the ANSI renderer (scripts/lib/intent_screen_ansi.rb): the
|
|
10
|
+
# Insight row dumping a multi-clause remainder into the note column, an empty
|
|
11
|
+
# "What this means" heading rendering bold with nothing under it, and step
|
|
12
|
+
# text cut mid-sentence. `step_text`, `insight_fields` and `next_fields` are
|
|
13
|
+
# public so the ANSI renderer reuses the exact same trims (D3) rather than
|
|
14
|
+
# re-deriving them and drifting.
|
|
15
|
+
#
|
|
16
|
+
# Harness-agnostic core: no harness assumption lives here.
|
|
17
|
+
module IntentScreen
|
|
18
|
+
BAR_WIDTH = 20
|
|
19
|
+
ON = "█"
|
|
20
|
+
OFF = "░"
|
|
21
|
+
PLACEHOLDER_SENTINEL = "<!-- plastic:placeholder -->"
|
|
22
|
+
SECTIONS = %w[Active Future Completed Abandoned].freeze
|
|
23
|
+
ITEM_RE = /^\s*- \[([ xX])\]\s+(.*)$/
|
|
24
|
+
# Em dash and en dash added (intent 316a O1e): a checklist item written
|
|
25
|
+
# "S1 — text" (the em dash every checklist this intent writes, and the one a
|
|
26
|
+
# reviewer reads, uses) kept its prefix under the old character class and
|
|
27
|
+
# rendered "S1 [ open ] S1 — text" on screen. Intent 317a (A1) made the
|
|
28
|
+
# separator optional with required whitespace: "S1 text" (the shape real
|
|
29
|
+
# checklists use) kept its label and doubled in next_fields; a bare "S1"
|
|
30
|
+
# has no following text and stays prose.
|
|
31
|
+
STEP_PREFIX_RE = /\A(?:Step|S)\s*\d+\s*(?:[-:·—–]\s*|\s+)(?=\S)/i
|
|
32
|
+
INSIGHT_RE = /\A(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s+·\s+\S+\s+·\s+.+?\s+—\s+(.+)\z/
|
|
33
|
+
SAVEPOINT_RE = /\A(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s{2,}(\S+)\s{2,}(.+?)\s*\z/
|
|
34
|
+
# Intent 317, D6: field-2 tokens that are genuine lifecycle stages. A ledger
|
|
35
|
+
# can also carry non-lifecycle lines (`Lock takeover: ...`, and 317's own
|
|
36
|
+
# `Review`/`Commit`); those must never be mistaken for the current stage.
|
|
37
|
+
# Placed here, clear of STEP_PREFIX_RE above (316a edits that one).
|
|
38
|
+
LIFECYCLE_STAGES = %w[What Why How Exec Done].freeze
|
|
39
|
+
|
|
40
|
+
# True iff a raw savepoint ledger line's field 2 is a genuine lifecycle
|
|
41
|
+
# stage (post-execution-review finding 5: centralizes what spawn-preamble
|
|
42
|
+
# and agent-report used to each copy-paste beside the constant above).
|
|
43
|
+
def self.lifecycle_line?(line)
|
|
44
|
+
parts = line.to_s.split(/\s{2,}/)
|
|
45
|
+
parts.length >= 2 && LIFECYCLE_STAGES.include?(parts[1])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Word-boundary truncation caps (intent 316a D3/O1a/O1c). Never a clause
|
|
49
|
+
# trim: a clause trim on step text destroys a pinned `OPEN:` row
|
|
50
|
+
# (test/intent_screen_test.rb:171-178) that a mid-sentence cut would eat.
|
|
51
|
+
INSIGHT_VALUE_MAX = 72
|
|
52
|
+
INSIGHT_NOTE_MAX = 96
|
|
53
|
+
NEXT_VALUE_MAX = 72
|
|
54
|
+
STEP_TEXT_MAX = 110
|
|
55
|
+
|
|
56
|
+
# Where a resume lands, from the ledger's last line (the boarding matrix).
|
|
57
|
+
def self.landing_stage(stage, milestone)
|
|
58
|
+
case stage
|
|
59
|
+
when "Done" then "Done"
|
|
60
|
+
when "What" then "Why"
|
|
61
|
+
when "Why" then milestone.to_s.include?("spec.md") ? "How" : "Why"
|
|
62
|
+
when "How" then milestone.to_s.include?("checklist.md") ? "Exec" : "How"
|
|
63
|
+
when "Exec" then milestone.to_s.include?("outcome.md") ? "ready to complete" : "Exec"
|
|
64
|
+
else "Why"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.intent_dir?(dir)
|
|
69
|
+
return false unless dir && File.directory?(dir)
|
|
70
|
+
|
|
71
|
+
base = File.basename(dir)
|
|
72
|
+
return false unless base.match?(/\A[0-9][0-9a-z]*--[\w-]+\z/)
|
|
73
|
+
|
|
74
|
+
File.exist?(File.join(dir, "#{base}.md"))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.render(intent_dir:, store_root:, template:)
|
|
78
|
+
base = File.basename(intent_dir)
|
|
79
|
+
id = base.split("--", 2).first
|
|
80
|
+
intent_text = File.read(File.join(intent_dir, "#{base}.md"))
|
|
81
|
+
|
|
82
|
+
fields = {}
|
|
83
|
+
fields.merge!(store_fields(store_root))
|
|
84
|
+
status, title = index_fields(store_root, id)
|
|
85
|
+
fields["status"] = status
|
|
86
|
+
fields["status.note"] = status == "unlisted" ? "no INDEX.md line names this id" : "listed under ## #{status} in INDEX.md"
|
|
87
|
+
fields["id"] = id
|
|
88
|
+
fields["name"] = title || fallback_name(intent_text)
|
|
89
|
+
fields.merge!(savepoint_fields(intent_dir, intent_text))
|
|
90
|
+
items = checklist_items(intent_dir)
|
|
91
|
+
fields.merge!(progress_fields(items))
|
|
92
|
+
fields.merge!(next_fields(items, status, checklist_present: items_present?(intent_dir)))
|
|
93
|
+
fields.merge!(insight_fields(intent_text))
|
|
94
|
+
fields["steps.rows"] = steps_rows(items)
|
|
95
|
+
|
|
96
|
+
out = template.dup
|
|
97
|
+
fields.each { |k, v| out = out.gsub("{{#{k}}}", v.to_s) }
|
|
98
|
+
out.gsub(/\n{3,}/, "\n\n")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- store ---------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def self.store_fields(store_root)
|
|
104
|
+
parent = File.basename(File.dirname(store_root))
|
|
105
|
+
if parent == "projects"
|
|
106
|
+
slug = File.basename(store_root)
|
|
107
|
+
{ "store" => "project:#{slug}", "store.note" => "the #{slug} project store" }
|
|
108
|
+
else
|
|
109
|
+
{ "store" => "global", "store.note" => "the global store" }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# --- INDEX.md -----------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def self.index_fields(store_root, id)
|
|
116
|
+
path = File.join(store_root, "INDEX.md")
|
|
117
|
+
return ["unlisted", nil] unless File.exist?(path)
|
|
118
|
+
|
|
119
|
+
section = nil
|
|
120
|
+
File.foreach(path) do |line|
|
|
121
|
+
if line.start_with?("## ")
|
|
122
|
+
name = line[3..].strip
|
|
123
|
+
section = SECTIONS.include?(name) ? name : nil
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
next unless section && line.strip.start_with?("- [")
|
|
127
|
+
|
|
128
|
+
m = line.match(/\A\s*- \[#{Regexp.escape(id)}\s+[-—]\s+(.+?)\]\(/)
|
|
129
|
+
return [section, m[1].strip] if m
|
|
130
|
+
end
|
|
131
|
+
["unlisted", nil]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.fallback_name(intent_text)
|
|
135
|
+
m = intent_text.match(/^intent:\s*["']?(.+?)["']?\s*$/)
|
|
136
|
+
text = m ? m[1] : ""
|
|
137
|
+
text.length > 60 ? "#{text[0, 57]}..." : text
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# --- savepoint.md ---------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def self.savepoint_fields(intent_dir, intent_text)
|
|
143
|
+
path = File.join(intent_dir, "savepoint.md")
|
|
144
|
+
lines = File.exist?(path) ? File.readlines(path).map(&:strip).reject(&:empty?) : []
|
|
145
|
+
matched = lines.reverse.map { |l| l.match(SAVEPOINT_RE) }.compact
|
|
146
|
+
last = matched.first
|
|
147
|
+
unless last
|
|
148
|
+
return { "stage" => "Why", "stage.note" => "no savepoint line yet",
|
|
149
|
+
"savepoint" => "none", "savepoint.note" => "" }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# D6: the STAGE PICK is guarded to the last LIFECYCLE line (What/Why/How/
|
|
153
|
+
# Exec/Done), so a trailing Lock/Review/Commit line cannot be mistaken for
|
|
154
|
+
# the stage. The Savepoint field below still shows the TRUE last line,
|
|
155
|
+
# whatever its kind - that is what a savepoint is.
|
|
156
|
+
lifecycle_last = matched.find { |m| LIFECYCLE_STAGES.include?(m[2]) }
|
|
157
|
+
stage_source = lifecycle_last || last
|
|
158
|
+
|
|
159
|
+
ts, stage, milestone = last[1], last[2], last[3]
|
|
160
|
+
landing = landing_stage(stage_source[2], stage_source[3])
|
|
161
|
+
delivered = lines.map { |l| l.match(SAVEPOINT_RE) }.compact.map { |m| m[2] }.uniq
|
|
162
|
+
delivered &= %w[What Why How Exec]
|
|
163
|
+
note = if landing == "Done"
|
|
164
|
+
"delivered; the record is immutable"
|
|
165
|
+
elsif landing == "ready to complete"
|
|
166
|
+
"outcome.md is real; run the ending procedure"
|
|
167
|
+
else
|
|
168
|
+
"#{delivered.join(', ')} delivered; the work is open"
|
|
169
|
+
end
|
|
170
|
+
{ "stage" => landing, "stage.note" => note,
|
|
171
|
+
"savepoint" => "#{stage} · #{milestone}", "savepoint.note" => human_time(ts) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.human_time(ts)
|
|
175
|
+
m = ts.match(/\A(\d{4}-\d\d-\d\d)T(\d\d:\d\d)/)
|
|
176
|
+
m ? "#{m[1]} #{m[2]} UTC" : ts
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# --- checklist.md --------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def self.items_present?(intent_dir)
|
|
182
|
+
path = File.join(intent_dir, "checklist.md")
|
|
183
|
+
return false unless File.exist?(path)
|
|
184
|
+
|
|
185
|
+
!File.read(path).lstrip.start_with?(PLACEHOLDER_SENTINEL)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def self.checklist_items(intent_dir)
|
|
189
|
+
return [] unless items_present?(intent_dir)
|
|
190
|
+
|
|
191
|
+
File.readlines(File.join(intent_dir, "checklist.md")).filter_map do |line|
|
|
192
|
+
m = line.match(ITEM_RE)
|
|
193
|
+
next unless m
|
|
194
|
+
|
|
195
|
+
text = m[2].strip
|
|
196
|
+
next if text == "..."
|
|
197
|
+
|
|
198
|
+
{ done: m[1] != " ", text: text.sub(STEP_PREFIX_RE, "") }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.progress_fields(items)
|
|
203
|
+
total = items.length
|
|
204
|
+
done = items.count { |i| i[:done] }
|
|
205
|
+
on = total.zero? ? 0 : (done * BAR_WIDTH) / total
|
|
206
|
+
bar = (ON * on) + (OFF * (BAR_WIDTH - on))
|
|
207
|
+
note = if total.zero?
|
|
208
|
+
"no checklist yet"
|
|
209
|
+
elsif done == total
|
|
210
|
+
"all steps done"
|
|
211
|
+
else
|
|
212
|
+
"#{total - done} steps open"
|
|
213
|
+
end
|
|
214
|
+
{ "progress.bar" => bar, "progress.done" => done.to_s, "progress.total" => total.to_s,
|
|
215
|
+
"progress.note" => note }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# `escape_pipes:` (intent 316a O1d, default true) keeps the plain Markdown
|
|
219
|
+
# table's pipe-escaping; the ANSI renderer, which never emits a table,
|
|
220
|
+
# passes `escape_pipes: false` to get the raw value instead of a literal
|
|
221
|
+
# `\|`. Named to not shadow the module's own `escape` method.
|
|
222
|
+
def self.next_fields(items, status, checklist_present:, escape_pipes: true)
|
|
223
|
+
return { "next" => "", "next.note" => "" } if %w[Completed Abandoned].include?(status)
|
|
224
|
+
return { "next" => "write checklist.md", "next.note" => "How" } unless checklist_present
|
|
225
|
+
|
|
226
|
+
idx = items.index { |i| !i[:done] }
|
|
227
|
+
return { "next" => "", "next.note" => "all steps done" } unless idx
|
|
228
|
+
|
|
229
|
+
head, = split_first_clause(items[idx][:text])
|
|
230
|
+
head = truncate_words(head, NEXT_VALUE_MAX)
|
|
231
|
+
head = escape(head) if escape_pipes
|
|
232
|
+
{ "next" => "S#{idx + 1} · #{head}", "next.note" => "first open step" }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def self.steps_rows(items)
|
|
236
|
+
return "| | | no steps yet |" if items.empty?
|
|
237
|
+
|
|
238
|
+
items.each_with_index.map do |item, i|
|
|
239
|
+
"| S#{i + 1} | #{item[:done] ? 'done' : 'open'} | #{escape(step_text(item[:text]))} |"
|
|
240
|
+
end.join("\n")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Public (intent 316a O1c) so the ANSI renderer trims step text identically:
|
|
244
|
+
# word-boundary truncation only, never a clause trim, at STEP_TEXT_MAX.
|
|
245
|
+
def self.step_text(text)
|
|
246
|
+
truncate_words(text, STEP_TEXT_MAX)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def self.escape(text)
|
|
250
|
+
text.gsub("|", "\\|")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# --- ## Insights ----------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def self.insight_fields(intent_text, escape_pipes: true)
|
|
256
|
+
section = intent_text.split(/^## Insights\s*$/, 2)[1].to_s.split(/^## /, 2)[0].to_s
|
|
257
|
+
entry = section.lines.map(&:strip).reverse.map { |l| l.match(INSIGHT_RE) }.compact.first
|
|
258
|
+
return { "insight" => "none yet", "insight.note" => "" } unless entry
|
|
259
|
+
|
|
260
|
+
ts, text = entry[1], entry[2].strip
|
|
261
|
+
head, tail = split_first_clause(text)
|
|
262
|
+
value = truncate_words(head, INSIGHT_VALUE_MAX)
|
|
263
|
+
tail = tail.empty? ? "" : truncate_words(tail, INSIGHT_NOTE_MAX)
|
|
264
|
+
note = tail.empty? ? human_time(ts) : "#{human_time(ts)} · #{tail}"
|
|
265
|
+
if escape_pipes
|
|
266
|
+
{ "insight" => escape(value), "insight.note" => escape(note) }
|
|
267
|
+
else
|
|
268
|
+
{ "insight" => value, "insight.note" => note }
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# First clause of `text`, and at most one following clause as the tail.
|
|
273
|
+
# Anything past the second clause is discarded (intent 316a O1a): the old
|
|
274
|
+
# behavior dumped the ENTIRE remainder into the note (an 800-character
|
|
275
|
+
# real-world tail starting mid-list). Boundary is a `.` or `;` immediately
|
|
276
|
+
# followed by whitespace-then-more or end of string, so "alpha.2" and "2.0"
|
|
277
|
+
# are never mistaken for clause ends.
|
|
278
|
+
def self.split_first_clause(text)
|
|
279
|
+
head, rest = clause_and_rest(text)
|
|
280
|
+
return [head, ""] unless rest
|
|
281
|
+
|
|
282
|
+
second, more = clause_and_rest(rest)
|
|
283
|
+
tail = more ? second : rest
|
|
284
|
+
[head, tail]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Returns [clause_without_terminal_punctuation, remainder_or_nil]. `nil` for
|
|
288
|
+
# the remainder means either no boundary exists at all, or the boundary
|
|
289
|
+
# sits at the absolute end of `text` (a single trailing clause with nothing
|
|
290
|
+
# after it) — both cases where there is no SECOND clause to fold in.
|
|
291
|
+
def self.clause_and_rest(text)
|
|
292
|
+
m = text.match(/\A(.+?)[.;](\s+(.*)|\z)/m)
|
|
293
|
+
return [text, nil] unless m
|
|
294
|
+
|
|
295
|
+
remainder = m[2].to_s.strip
|
|
296
|
+
remainder.empty? ? [m[1], nil] : [m[1], remainder]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Word-boundary truncation with a trailing "…" when cut, never mid-word and
|
|
300
|
+
# never a clause trim (intent 316a D3).
|
|
301
|
+
def self.truncate_words(text, max)
|
|
302
|
+
return text if text.length <= max
|
|
303
|
+
return "…" if max <= 1
|
|
304
|
+
|
|
305
|
+
cut = text[0, max - 1].rindex(" ")
|
|
306
|
+
cut = max - 1 if cut.nil? || cut.zero?
|
|
307
|
+
"#{text[0, cut].rstrip}…"
|
|
308
|
+
end
|
|
309
|
+
end
|