@zalom/plastic 2.0.0-alpha.26 → 2.0.0-alpha.28
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 -139
- package/README.md +337 -133
- package/agents/plastic-enforcer.md +12 -12
- package/agents/plastic-executor.md +2 -1
- package/agents/plastic-node-research.md +4 -2
- package/agents/plastic-node-verify.md +1 -0
- package/agents/plastic-node-work.md +1 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +6 -8
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +35 -1
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- 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 +7 -7
- package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
- package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/package.json +3 -2
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +10 -9
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +57 -49
- package/scripts/end-intent +2 -8
- package/scripts/file-session-intent +2 -1
- package/scripts/hook-capture +4 -3
- package/scripts/hook-close +2 -1
- package/scripts/hook-record +3 -2
- package/scripts/hook-savepoint +3 -2
- package/scripts/hook-session-start +5 -4
- package/scripts/hook-stop +2 -1
- package/scripts/insight-append +1 -2
- package/scripts/install.rb +7 -5
- package/scripts/lib/action_graph_shim.rb +3 -1
- package/scripts/lib/active_delivery.rb +1 -1
- package/scripts/lib/agent_models.rb +44 -26
- package/scripts/lib/arm.rb +2 -1
- package/scripts/lib/backup.rb +63 -0
- package/scripts/lib/cli/command.rb +85 -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 +60 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +25 -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 +20 -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 +61 -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 +25 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +43 -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 +49 -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 +25 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +34 -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 +84 -0
- package/scripts/lib/cli/legacy.rb +50 -0
- package/scripts/lib/cli/output.rb +102 -0
- package/scripts/lib/cli/scope.rb +127 -0
- package/scripts/lib/cli/table.rb +64 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +5 -4
- package/scripts/lib/compact_instructions.rb +8 -0
- package/scripts/lib/day_summary.rb +4 -3
- package/scripts/lib/doctor_core.rb +7 -32
- package/scripts/lib/doctor_session_ledger.rb +2 -1
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_measure_models.rb +4 -2
- package/scripts/lib/index_entry.rb +9 -0
- package/scripts/lib/installer_core.rb +175 -56
- package/scripts/lib/intent_screen.rb +3 -3
- package/scripts/lib/lock.rb +2 -2
- package/scripts/lib/node_file.rb +22 -2
- package/scripts/lib/node_input.rb +11 -6
- package/scripts/lib/node_return.rb +8 -3
- package/scripts/lib/preflight.rb +2 -4
- package/scripts/lib/project_config.rb +2 -1
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +2 -0
- package/scripts/lib/report_screen.rb +4 -3
- 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_queue.rb +2 -2
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +59 -2
- package/scripts/lib/runner_dispatch.rb +12 -8
- package/scripts/lib/runner_policy.rb +28 -10
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_git.rb +4 -3
- package/scripts/lib/sqlite.rb +22 -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 +2 -7
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/worktree.rb +3 -8
- package/scripts/lib/worktree_sweep.rb +3 -2
- package/scripts/link-suggest +2 -1
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +3 -12
- package/scripts/node-run +2 -1
- package/scripts/plastic-lock +3 -2
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +44 -6
- package/scripts/release-check +10 -5
- package/scripts/report-screen +1 -1
- package/scripts/session-commit +2 -1
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +6 -6
- package/templates/config.yml +13 -6
- package/templates/node-research.md +4 -2
- package/templates/render.css +10 -0
- package/bin/plastic.js +0 -70
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -297
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -64
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -180
- 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 -305
- 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 -156
- package/skills/intent-continuing/references/board-fill.md +0 -52
- package/skills/intent-continuing/references/boarding-matrix.md +0 -35
- 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 -89
- 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 -182
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -87
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -47
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -136
- 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 -376
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -70
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -90
- package/skills/roadmap/references/file-format.md +0 -134
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -66
- 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}/knowledge-graph.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
- /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "json"
|
|
5
6
|
require "yaml"
|
|
6
7
|
require "fileutils"
|
|
@@ -12,6 +13,7 @@ require_relative "agent_models"
|
|
|
12
13
|
require_relative "harness_text"
|
|
13
14
|
require_relative "compact_instructions"
|
|
14
15
|
require_relative "engine_permissions"
|
|
16
|
+
require_relative "qmd_sync"
|
|
15
17
|
|
|
16
18
|
# Shared installer machinery, instantiable with injected package root / store / agent
|
|
17
19
|
# map so the verb scripts (install/update/uninstall/rollback) and their tests can run
|
|
@@ -45,23 +47,21 @@ class InstallerCore
|
|
|
45
47
|
CLAUDE_SECTION_END = "<!-- END PLASTIC COMPACT -->"
|
|
46
48
|
CLAUDE_SECTION_RE = /^<!-- BEGIN PLASTIC COMPACT.*?-->\n.*?\n<!-- END PLASTIC COMPACT -->\n?/m
|
|
47
49
|
|
|
48
|
-
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the
|
|
49
|
-
#
|
|
50
|
-
# a shared file Codex merges from multiple sources,
|
|
51
|
-
# hand-curated pointer rather than embedding the core
|
|
52
|
-
# because it only ever points, never duplicates.
|
|
50
|
+
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the help topics
|
|
51
|
+
# `plastic help TOPIC` prints, injected into ~/.codex/AGENTS.md. Not a slice of
|
|
52
|
+
# PLASTIC.md itself: AGENTS.md is a shared file Codex merges from multiple sources,
|
|
53
|
+
# so this block stays a small, hand-curated pointer rather than embedding the core
|
|
54
|
+
# wholesale, and it never drifts because it only ever points, never duplicates.
|
|
53
55
|
CODEX_AGENTS_MD_BODY = <<~MD.freeze
|
|
54
56
|
Plastic is installed for this agent. Plastic is intent-driven state management: work runs
|
|
55
57
|
in one of three modes, direct, thinking, or auto (a team drives the runner loop:
|
|
56
58
|
`runner step`, `status`, `answer`). Do not jump straight to code.
|
|
57
59
|
|
|
58
60
|
Standing rules:
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- Operational procedures are
|
|
63
|
-
plastic-<name>/SKILL.md). Invoke one explicitly as $plastic-<name> (for example
|
|
64
|
-
$plastic-doctor), or let Codex pick one implicitly by matching its description.
|
|
61
|
+
- The command line is ~/.plastic/PLASTIC.md. Read it and follow it exactly. The
|
|
62
|
+
conventions are chapters `plastic help TOPIC` prints on demand. Both are
|
|
63
|
+
generated and overwritten on Plastic updates, so never edit them.
|
|
64
|
+
- Operational procedures are the `plastic` command line itself, run directly.
|
|
65
65
|
- Intents, specs, plans, checklists, and outcomes live under ~/.plastic/, never in
|
|
66
66
|
the project tree.
|
|
67
67
|
|
|
@@ -352,12 +352,36 @@ class InstallerCore
|
|
|
352
352
|
end
|
|
353
353
|
end
|
|
354
354
|
|
|
355
|
+
# The command line (intent 363). Glob-derived for the reason screen_files is:
|
|
356
|
+
# a command is one row in scripts/lib/cli/table.rb plus one file, and a
|
|
357
|
+
# hand-written manifest would make it two files and a diff. The launcher
|
|
358
|
+
# bin/plastic requires scripts/lib/cli.rb by a relative path, so the whole
|
|
359
|
+
# subtree travels together or an installed copy LoadErrors on the first call.
|
|
360
|
+
def cli_files
|
|
361
|
+
Dir.glob(File.join(package_root, "scripts", "lib", "{cli,rlm}", "**", "*.rb")).each_with_object({}) do |path, acc|
|
|
362
|
+
rel = path.sub("#{package_root}/", "")
|
|
363
|
+
acc[rel] = rel
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# `plastic help TOPIC` (intent 372, family 4) reads docs/help/TOPIC.md from
|
|
368
|
+
# package_root the same way it reads a command's file, so the chapter has to
|
|
369
|
+
# ship the same way a template does: glob-derived, one file added and it
|
|
370
|
+
# installs with no diff here.
|
|
371
|
+
def help_files
|
|
372
|
+
Dir.glob(File.join(package_root, "docs", "help", "*.md")).each_with_object({}) do |path, acc|
|
|
373
|
+
rel = File.join("docs", "help", File.basename(path))
|
|
374
|
+
acc[rel] = rel
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
355
378
|
# Files copied into ~/.plastic on install/update. Every verb script + the shared lib
|
|
356
379
|
# must be here so the installed ~/.plastic/scripts copy is self-complete (sync-guarded
|
|
357
380
|
# by install_sync_test). The templates and screen-kind halves are glob-derived
|
|
358
381
|
# (template_files, screen_files above); the rest stays a hand-written literal.
|
|
359
382
|
def core_files
|
|
360
|
-
hand_registered_files.merge(template_files).merge(hook_files).merge(screen_files)
|
|
383
|
+
hand_registered_files.merge(template_files).merge(hook_files).merge(screen_files).merge(cli_files)
|
|
384
|
+
.merge(help_files)
|
|
361
385
|
end
|
|
362
386
|
|
|
363
387
|
def hand_registered_files
|
|
@@ -391,6 +415,9 @@ class InstallerCore
|
|
|
391
415
|
"scripts/plastic-lock" => "scripts/plastic-lock",
|
|
392
416
|
"scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
|
|
393
417
|
"scripts/lib/compact_instructions.rb" => "scripts/lib/compact_instructions.rb",
|
|
418
|
+
"scripts/lib/version_number.rb" => "scripts/lib/version_number.rb",
|
|
419
|
+
"scripts/lib/cli.rb" => "scripts/lib/cli.rb",
|
|
420
|
+
"bin/plastic" => "bin/plastic",
|
|
394
421
|
"scripts/agent-report" => "scripts/agent-report",
|
|
395
422
|
"scripts/lib/insights.rb" => "scripts/lib/insights.rb",
|
|
396
423
|
"scripts/insight-append" => "scripts/insight-append",
|
|
@@ -406,6 +433,14 @@ class InstallerCore
|
|
|
406
433
|
"scripts/lib/intent_validator.rb" => "scripts/lib/intent_validator.rb",
|
|
407
434
|
"scripts/lib/graph_rebuild.rb" => "scripts/lib/graph_rebuild.rb",
|
|
408
435
|
"scripts/lib/store_discovery.rb" => "scripts/lib/store_discovery.rb",
|
|
436
|
+
"scripts/lib/store_layout.rb" => "scripts/lib/store_layout.rb",
|
|
437
|
+
"scripts/lib/stores_move.rb" => "scripts/lib/stores_move.rb",
|
|
438
|
+
"scripts/lib/sqlite.rb" => "scripts/lib/sqlite.rb",
|
|
439
|
+
"scripts/lib/search_index.rb" => "scripts/lib/search_index.rb",
|
|
440
|
+
"scripts/lib/store_sync.rb" => "scripts/lib/store_sync.rb",
|
|
441
|
+
"scripts/lib/work_graph.rb" => "scripts/lib/work_graph.rb",
|
|
442
|
+
"scripts/lib/reference_archive.rb" => "scripts/lib/reference_archive.rb",
|
|
443
|
+
"scripts/lib/backup.rb" => "scripts/lib/backup.rb",
|
|
409
444
|
"scripts/lib/frontmatter_writer.rb" => "scripts/lib/frontmatter_writer.rb",
|
|
410
445
|
"scripts/lib/links_projection.rb" => "scripts/lib/links_projection.rb",
|
|
411
446
|
"scripts/lib/links_section.rb" => "scripts/lib/links_section.rb",
|
|
@@ -624,7 +659,7 @@ class InstallerCore
|
|
|
624
659
|
def bootstrap
|
|
625
660
|
puts " \u{1f331} First install \u{2014} bootstrapping store..."
|
|
626
661
|
|
|
627
|
-
FileUtils.mkdir_p(
|
|
662
|
+
FileUtils.mkdir_p(Plastic::StoreLayout.global_store(plastic_home))
|
|
628
663
|
FileUtils.mkdir_p(File.join(plastic_home, "projects"))
|
|
629
664
|
|
|
630
665
|
write_if_missing(File.join(plastic_home, "config.yml"), <<~YAML)
|
|
@@ -643,7 +678,7 @@ class InstallerCore
|
|
|
643
678
|
|
|
644
679
|
write_if_missing(File.join(plastic_home, "projects.yml"), "---\nprojects: {}\n")
|
|
645
680
|
|
|
646
|
-
write_if_missing(File.join(plastic_home, "INDEX.md"), <<~MD)
|
|
681
|
+
write_if_missing(File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md"), <<~MD)
|
|
647
682
|
# Index
|
|
648
683
|
|
|
649
684
|
## Active
|
|
@@ -661,7 +696,7 @@ class InstallerCore
|
|
|
661
696
|
# Plastic: Agent Instructions
|
|
662
697
|
|
|
663
698
|
Read `PLASTIC.md` in this directory for the core conventions; deeper doctrine lives
|
|
664
|
-
in the `plastic
|
|
699
|
+
in the chapters `plastic help TOPIC` prints. Follow it exactly. Never modify it:
|
|
665
700
|
it is overwritten on plugin updates.
|
|
666
701
|
|
|
667
702
|
This file (`AGENTS.md`) is where project-specific rules live.
|
|
@@ -672,6 +707,25 @@ class InstallerCore
|
|
|
672
707
|
puts " \u{2705} Store bootstrapped"
|
|
673
708
|
end
|
|
674
709
|
|
|
710
|
+
# Intent 372 (former install skill, lines 119-127): `~/.plastic` becomes its own
|
|
711
|
+
# git repository the first time it exists, so `update` can later commit the core
|
|
712
|
+
# files it re-syncs. Leaves an existing repository alone.
|
|
713
|
+
def git_init_if_absent(runner: ->(cmd) { system(*cmd) })
|
|
714
|
+
FileUtils.mkdir_p(plastic_home)
|
|
715
|
+
return if File.directory?(File.join(plastic_home, ".git"))
|
|
716
|
+
runner.call(["git", "-C", plastic_home, "init", "-q"])
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# Intent 372 (former install skill, lines 167-179): register every Plastic store as
|
|
720
|
+
# a QMD collection. QmdSync.register already no-ops per store when QMD is absent;
|
|
721
|
+
# the detector is injected too so a run stays hermetic on a machine that happens to
|
|
722
|
+
# have the real `qmd` binary on PATH.
|
|
723
|
+
def register_with_qmd(runner: QmdSync.default_runner, detector: QmdSync.method(:detect))
|
|
724
|
+
QmdSync.enumerate_stores(plastic_home: plastic_home).each do |store|
|
|
725
|
+
QmdSync.register(collection: store[:collection], dir: store[:dir], runner: runner, detector: detector)
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
675
729
|
# --- Agent adapters ---
|
|
676
730
|
|
|
677
731
|
# Uniform per-agent install record dir: <agent-dir>/plastic/ holds VERSION and
|
|
@@ -943,9 +997,9 @@ class InstallerCore
|
|
|
943
997
|
# Every store root (the global store, plus each project store) whose real
|
|
944
998
|
# path is reached with no symlink between it and plastic_home.
|
|
945
999
|
def real_store_roots(plastic_home)
|
|
946
|
-
roots = [
|
|
947
|
-
Dir.glob(File.join(plastic_home, "
|
|
948
|
-
roots.select { |root| real_store_root?(plastic_home, root) }
|
|
1000
|
+
roots = [Plastic::StoreLayout.global_store(plastic_home)] +
|
|
1001
|
+
Dir.glob(File.join(Plastic::StoreLayout.projects_root(plastic_home), "*", "store"))
|
|
1002
|
+
roots.uniq.select { |root| real_store_root?(plastic_home, root) }
|
|
949
1003
|
end
|
|
950
1004
|
|
|
951
1005
|
def real_store_root?(plastic_home, store_root)
|
|
@@ -1020,7 +1074,7 @@ class InstallerCore
|
|
|
1020
1074
|
# fails milder than the bug it prevents, independent of what any caller's `root:` is or
|
|
1021
1075
|
# what a hand-edited manifest claims.
|
|
1022
1076
|
def store_roots
|
|
1023
|
-
[
|
|
1077
|
+
%w[store projects stores].map { |name| File.expand_path(File.join(plastic_home, name)) }
|
|
1024
1078
|
end
|
|
1025
1079
|
|
|
1026
1080
|
# `install_skills_flat` relocates any top-level underscore-prefixed markdown
|
|
@@ -1132,7 +1186,12 @@ class InstallerCore
|
|
|
1132
1186
|
# Codex-scoped overrides only (agents.models.codex.*): a literal Claude
|
|
1133
1187
|
# model id set under agents.models.claude.* (or the legacy flat form,
|
|
1134
1188
|
# which resolves as claude) must never reach a Codex TOML.
|
|
1135
|
-
installed += generate_codex_agents(
|
|
1189
|
+
installed += generate_codex_agents(
|
|
1190
|
+
File.join(config[:home_dir], "agents"),
|
|
1191
|
+
models: agent_model_overrides(harness: "codex"),
|
|
1192
|
+
efforts: agent_effort_overrides(harness: "codex"),
|
|
1193
|
+
advisor_enabled: advisor_enabled?
|
|
1194
|
+
)
|
|
1136
1195
|
|
|
1137
1196
|
# Instruction injection (L1): Plastic standing conventions into ~/.codex/AGENTS.md.
|
|
1138
1197
|
# Partial-ownership file, so it is NOT manifest-tracked (stripped surgically on uninstall).
|
|
@@ -1162,24 +1221,17 @@ class InstallerCore
|
|
|
1162
1221
|
# a whole-file, Plastic-owned .toml per repo agents/*.md instead of copying markdown.
|
|
1163
1222
|
# The returned paths append to `installed`, so they are manifest-tracked and pruned on
|
|
1164
1223
|
# uninstall by the manifest whole-file-delete path, exactly like ~/.claude/agents/*.md.
|
|
1165
|
-
def generate_codex_agents(agents_root, models: {})
|
|
1224
|
+
def generate_codex_agents(agents_root, models: {}, efforts: {}, advisor_enabled: true)
|
|
1166
1225
|
sources = Dir.glob(File.join(package_root, "agents", "*.md"))
|
|
1167
1226
|
return [] if sources.empty?
|
|
1168
1227
|
|
|
1169
1228
|
FileUtils.mkdir_p(agents_root)
|
|
1170
1229
|
sources.filter_map do |src|
|
|
1171
1230
|
basename = File.basename(src, ".md")
|
|
1172
|
-
|
|
1173
|
-
# owner has not evaluated the Codex reasoning-model ecosystem long enough to
|
|
1174
|
-
# judge it. Skip every AgentModels::CONSULTATION_AGENTS file (both
|
|
1175
|
-
# plastic-advisor and plastic-faux-advisor) by name, a deliberate and
|
|
1176
|
-
# mechanical scope cut tracked at intent 186 (Codex advisor evaluation), not
|
|
1177
|
-
# a permanent exclusion and not conditioned on any frontmatter or override
|
|
1178
|
-
# value.
|
|
1179
|
-
next if AgentModels::CONSULTATION_AGENTS.include?(basename)
|
|
1231
|
+
next if !advisor_enabled && AgentModels::CONSULTATION_AGENTS.include?(basename)
|
|
1180
1232
|
|
|
1181
1233
|
dest = File.join(agents_root, "#{basename}.toml")
|
|
1182
|
-
write_text_atomic(dest, render_codex_agent_toml(src, models[basename]))
|
|
1234
|
+
write_text_atomic(dest, render_codex_agent_toml(src, models[basename], efforts[basename]))
|
|
1183
1235
|
dest
|
|
1184
1236
|
end
|
|
1185
1237
|
end
|
|
@@ -1187,16 +1239,25 @@ class InstallerCore
|
|
|
1187
1239
|
# Render one repo agents/*.md into a deterministic Codex agent TOML document. Fixed field
|
|
1188
1240
|
# order (name, description, the model field(s) from codex_model_fields, developer_instructions)
|
|
1189
1241
|
# so regenerate is byte-identical (idempotency).
|
|
1190
|
-
def render_codex_agent_toml(source_path, override)
|
|
1242
|
+
def render_codex_agent_toml(source_path, override, effort_override = nil)
|
|
1191
1243
|
front, body = split_frontmatter(File.read(source_path))
|
|
1192
1244
|
name = (front["name"] || File.basename(source_path, ".md")).to_s
|
|
1193
1245
|
description = (front["description"] || "").to_s
|
|
1194
|
-
effective =
|
|
1246
|
+
effective = if override && !override.to_s.empty?
|
|
1247
|
+
override
|
|
1248
|
+
else
|
|
1249
|
+
AgentModels.shipped_model_for(name, harness: "codex") || front["model"]
|
|
1250
|
+
end
|
|
1251
|
+
effort = if effort_override && !effort_override.to_s.empty?
|
|
1252
|
+
effort_override
|
|
1253
|
+
else
|
|
1254
|
+
front["effort"] || AgentModels.shipped_effort_for(name)
|
|
1255
|
+
end
|
|
1195
1256
|
|
|
1196
1257
|
parts = []
|
|
1197
1258
|
parts << %(name = "#{toml_inline_escape(name)}")
|
|
1198
1259
|
parts << %(description = "#{toml_inline_escape(description)}")
|
|
1199
|
-
parts << codex_model_fields(effective)
|
|
1260
|
+
parts << codex_model_fields(effective, effort: effort)
|
|
1200
1261
|
parts << "developer_instructions = \"\"\"\n#{toml_ml_escape(body.strip)}\n\"\"\""
|
|
1201
1262
|
parts.reject(&:empty?).join("\n") + "\n"
|
|
1202
1263
|
end
|
|
@@ -1216,20 +1277,20 @@ class InstallerCore
|
|
|
1216
1277
|
# The model-selection line(s). A known tier alias (opus/sonnet/haiku) emits BOTH a `model` line
|
|
1217
1278
|
# (from AgentModels.codex_model_for, the intent-186 per-role Codex identity) and a
|
|
1218
1279
|
# model_reasoning_effort line, model first for deterministic byte-identical regenerate. Any other
|
|
1219
|
-
# non-empty value is a literal Codex model id
|
|
1220
|
-
#
|
|
1221
|
-
|
|
1222
|
-
def codex_model_fields(effective)
|
|
1280
|
+
# non-empty value is a literal Codex model id. Every non-empty model also emits the resolved effort,
|
|
1281
|
+
# medium by default or the harness-scoped override. Empty emits no model fields.
|
|
1282
|
+
def codex_model_fields(effective, effort: AgentModels::DEFAULT_EFFORT)
|
|
1223
1283
|
return "" if effective.nil? || effective.to_s.empty?
|
|
1224
|
-
|
|
1225
|
-
if effort
|
|
1284
|
+
if AgentModels.effort_for(effective)
|
|
1226
1285
|
lines = []
|
|
1227
1286
|
model = AgentModels.codex_model_for(effective)
|
|
1228
1287
|
lines << %(model = "#{toml_inline_escape(model)}") if model && !model.to_s.empty?
|
|
1229
|
-
lines << %(model_reasoning_effort = "#{effort}")
|
|
1288
|
+
lines << %(model_reasoning_effort = "#{toml_inline_escape(effort)}") unless effort.to_s.empty?
|
|
1230
1289
|
lines.join("\n")
|
|
1231
1290
|
else
|
|
1232
|
-
%(model = "#{toml_inline_escape(effective.to_s)}")
|
|
1291
|
+
lines = [%(model = "#{toml_inline_escape(effective.to_s)}")]
|
|
1292
|
+
lines << %(model_reasoning_effort = "#{toml_inline_escape(effort)}") unless effort.to_s.empty?
|
|
1293
|
+
lines.join("\n")
|
|
1233
1294
|
end
|
|
1234
1295
|
end
|
|
1235
1296
|
|
|
@@ -1362,8 +1423,7 @@ class InstallerCore
|
|
|
1362
1423
|
# them to `installed` before write_manifest (manifest + prune are then automatic).
|
|
1363
1424
|
# No-op safe: returns [] when the package has no agents dir or it is empty.
|
|
1364
1425
|
# advisor_enabled: false (advisor.enabled config key) skips every
|
|
1365
|
-
# AgentModels::CONSULTATION_AGENTS file entirely
|
|
1366
|
-
# plastic-faux-advisor), so a user who declined the advisor never gets either
|
|
1426
|
+
# AgentModels::CONSULTATION_AGENTS file entirely, so a user who declined the advisor never gets either
|
|
1367
1427
|
# agent installed.
|
|
1368
1428
|
def install_agents(agents_root, models: {}, efforts: {}, advisor_enabled: true)
|
|
1369
1429
|
sources = Dir.glob(File.join(package_root, "agents", "*.md"))
|
|
@@ -1408,18 +1468,21 @@ class InstallerCore
|
|
|
1408
1468
|
# ("claude" or "codex"). Defaults are NOT included, so unconfigured agents
|
|
1409
1469
|
# keep their shipped frontmatter.
|
|
1410
1470
|
#
|
|
1411
|
-
# Both advisor agents
|
|
1471
|
+
# Both advisor agents resolve through
|
|
1412
1472
|
# this SAME generic map, like any other agent: a config author sets
|
|
1413
|
-
# agents.models.claude
|
|
1414
|
-
# agents.models.plastic-advisor, read as claude) to point either agent at a
|
|
1473
|
+
# agents.models.claude.<agent> (or the legacy flat form, read as Claude) to point either agent at a
|
|
1415
1474
|
# different literal model. There is no separate advisor-specific model key;
|
|
1416
1475
|
# which agent the advisor SKILL routes to by default is a routing decision
|
|
1417
1476
|
# (advisor.claude.default), never a model-selection one.
|
|
1418
1477
|
def agent_model_overrides(project_dir = nil, harness: "claude")
|
|
1419
|
-
|
|
1478
|
+
global_path = File.join(plastic_home, "config.yml")
|
|
1479
|
+
migrate_advisor_config_file(global_path)
|
|
1480
|
+
global_config = load_config_yaml(global_path)
|
|
1420
1481
|
project_config =
|
|
1421
1482
|
if project_dir
|
|
1422
|
-
|
|
1483
|
+
project_path = File.join(project_dir, ".plastic_store", "config.yml")
|
|
1484
|
+
migrate_advisor_config_file(project_path)
|
|
1485
|
+
load_config_yaml(project_path)
|
|
1423
1486
|
else
|
|
1424
1487
|
{}
|
|
1425
1488
|
end
|
|
@@ -1427,8 +1490,12 @@ class InstallerCore
|
|
|
1427
1490
|
end
|
|
1428
1491
|
|
|
1429
1492
|
def agent_effort_overrides(project_dir = nil, harness: "claude")
|
|
1430
|
-
|
|
1431
|
-
|
|
1493
|
+
global_path = File.join(plastic_home, "config.yml")
|
|
1494
|
+
migrate_advisor_config_file(global_path)
|
|
1495
|
+
global_config = load_config_yaml(global_path)
|
|
1496
|
+
project_path = project_dir && File.join(project_dir, ".plastic_store", "config.yml")
|
|
1497
|
+
migrate_advisor_config_file(project_path) if project_path
|
|
1498
|
+
project_config = project_path ? load_config_yaml(project_path) : {}
|
|
1432
1499
|
AgentModels.effort_override_map(project_config: project_config, global_config: global_config, harness: harness)
|
|
1433
1500
|
end
|
|
1434
1501
|
|
|
@@ -1443,10 +1510,14 @@ class InstallerCore
|
|
|
1443
1510
|
value != false
|
|
1444
1511
|
end
|
|
1445
1512
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1513
|
+
ADVISOR_NAME_MIGRATIONS = {
|
|
1514
|
+
"plastic-advisor" => "plastic-primary-advisor",
|
|
1515
|
+
"plastic-faux-advisor" => "plastic-secondary-advisor"
|
|
1516
|
+
}.freeze
|
|
1517
|
+
ADVISOR_SHORTHANDS = {
|
|
1518
|
+
"primary" => "plastic-primary-advisor", "secondary" => "plastic-secondary-advisor",
|
|
1519
|
+
"real" => "plastic-primary-advisor", "faux" => "plastic-secondary-advisor"
|
|
1520
|
+
}.freeze
|
|
1450
1521
|
|
|
1451
1522
|
# Write advisor.enabled / advisor.claude.default into the global config.yml
|
|
1452
1523
|
# from install-time flags. Absent flags change nothing: advisor.enabled
|
|
@@ -1454,7 +1525,7 @@ class InstallerCore
|
|
|
1454
1525
|
# (the skill's own fallback chain applies) when missing.
|
|
1455
1526
|
# --no-advisor -> advisor.enabled: false
|
|
1456
1527
|
# --advisor VALUE -> advisor.claude.default: VALUE (an agent name, or the
|
|
1457
|
-
# shorthand "
|
|
1528
|
+
# shorthand "primary"/"secondary")
|
|
1458
1529
|
def apply_config_flags(argv)
|
|
1459
1530
|
no_advisor = argv.include?("--no-advisor")
|
|
1460
1531
|
advisor_idx = argv.index("--advisor")
|
|
@@ -1462,7 +1533,13 @@ class InstallerCore
|
|
|
1462
1533
|
return unless no_advisor || advisor_value
|
|
1463
1534
|
|
|
1464
1535
|
config_path = File.join(plastic_home, "config.yml")
|
|
1465
|
-
config =
|
|
1536
|
+
config = if File.exist?(config_path)
|
|
1537
|
+
parsed = YAML.safe_load(File.read(config_path))
|
|
1538
|
+
return false unless parsed.nil? || parsed.is_a?(Hash)
|
|
1539
|
+
migrate_advisor_config(parsed || {})
|
|
1540
|
+
else
|
|
1541
|
+
{}
|
|
1542
|
+
end
|
|
1466
1543
|
|
|
1467
1544
|
if no_advisor
|
|
1468
1545
|
config["advisor"] ||= {}
|
|
@@ -1477,6 +1554,9 @@ class InstallerCore
|
|
|
1477
1554
|
|
|
1478
1555
|
FileUtils.mkdir_p(plastic_home)
|
|
1479
1556
|
File.write(config_path, YAML.dump(config))
|
|
1557
|
+
true
|
|
1558
|
+
rescue StandardError
|
|
1559
|
+
false
|
|
1480
1560
|
end
|
|
1481
1561
|
|
|
1482
1562
|
def load_config_yaml(path)
|
|
@@ -1486,6 +1566,45 @@ class InstallerCore
|
|
|
1486
1566
|
{}
|
|
1487
1567
|
end
|
|
1488
1568
|
|
|
1569
|
+
# Renames retired advisor keys without discarding a current key. It accepts
|
|
1570
|
+
# malformed config sections and leaves unrelated values untouched.
|
|
1571
|
+
def migrate_advisor_config(config)
|
|
1572
|
+
return {} unless config.is_a?(Hash)
|
|
1573
|
+
config = Marshal.load(Marshal.dump(config))
|
|
1574
|
+
advisor = config["advisor"]
|
|
1575
|
+
if advisor.is_a?(Hash) && advisor["claude"].is_a?(Hash)
|
|
1576
|
+
default = advisor["claude"]["default"]
|
|
1577
|
+
advisor["claude"]["default"] = ADVISOR_NAME_MIGRATIONS.fetch(default, default)
|
|
1578
|
+
end
|
|
1579
|
+
agents = config["agents"]
|
|
1580
|
+
%w[models efforts].each do |section_name|
|
|
1581
|
+
section = agents.is_a?(Hash) ? agents[section_name] : nil
|
|
1582
|
+
next unless section.is_a?(Hash)
|
|
1583
|
+
migrate_advisor_keys!(section)
|
|
1584
|
+
%w[claude codex].each { |harness| migrate_advisor_keys!(section[harness]) if section[harness].is_a?(Hash) }
|
|
1585
|
+
end
|
|
1586
|
+
config
|
|
1587
|
+
end
|
|
1588
|
+
|
|
1589
|
+
def migrate_advisor_config_file(path)
|
|
1590
|
+
return false unless path && File.file?(path)
|
|
1591
|
+
parsed = YAML.safe_load(File.read(path))
|
|
1592
|
+
return false unless parsed.is_a?(Hash)
|
|
1593
|
+
migrated = migrate_advisor_config(parsed)
|
|
1594
|
+
return false if migrated == parsed
|
|
1595
|
+
File.write(path, YAML.dump(migrated))
|
|
1596
|
+
true
|
|
1597
|
+
rescue StandardError
|
|
1598
|
+
false
|
|
1599
|
+
end
|
|
1600
|
+
|
|
1601
|
+
def migrate_advisor_keys!(section)
|
|
1602
|
+
ADVISOR_NAME_MIGRATIONS.each do |legacy, current|
|
|
1603
|
+
section[current] = section[legacy] if !section.key?(current) && section.key?(legacy)
|
|
1604
|
+
section.delete(legacy)
|
|
1605
|
+
end
|
|
1606
|
+
end
|
|
1607
|
+
|
|
1489
1608
|
# --- Legacy plugin migration ---
|
|
1490
1609
|
|
|
1491
1610
|
# Earlier versions registered Plastic as a local marketplace plugin
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
|
+
require_relative "store_layout"
|
|
2
3
|
# frozen_string_literal: true
|
|
3
4
|
# IntentScreen (intent 316) - fills templates/intent-screen.md from one intent's
|
|
4
5
|
# record: the intent file, the tier's INDEX.md, savepoint.md and checklist.md.
|
|
@@ -101,9 +102,8 @@ module IntentScreen
|
|
|
101
102
|
# --- store ---------------------------------------------------------------------
|
|
102
103
|
|
|
103
104
|
def self.store_fields(store_root)
|
|
104
|
-
|
|
105
|
-
if
|
|
106
|
-
slug = File.basename(store_root)
|
|
105
|
+
slug = Plastic::StoreLayout.locate(File.join(store_root, "store")).last
|
|
106
|
+
if slug != Plastic::StoreLayout::GLOBAL
|
|
107
107
|
{ "store" => "project:#{slug}", "store.note" => "the #{slug} project store" }
|
|
108
108
|
else
|
|
109
109
|
{ "store" => "global", "store.note" => "the global store" }
|
package/scripts/lib/lock.rb
CHANGED
|
@@ -368,8 +368,8 @@ module Lock
|
|
|
368
368
|
# file is inert: nothing reads it, it stays zero bytes, and
|
|
369
369
|
# scripts/end-intent:558 keys its exit contract on Lock.path alone. This is
|
|
370
370
|
# a requirement, not a guarantee the code enforces: the curator stray-file
|
|
371
|
-
# rule (
|
|
372
|
-
#
|
|
371
|
+
# rule (docs/help/maintenance-and-revisions.md:155) has no *.lock
|
|
372
|
+
# carve-out today, so a wrongful delete there degrades one
|
|
373
373
|
# write window to the pre-fix (unguarded) behavior, not a hard failure.
|
|
374
374
|
def with_write_guard(intent_dir, type: "delivery",
|
|
375
375
|
guard_timeout: WRITE_GUARD_TIMEOUT_SECONDS,
|
package/scripts/lib/node_file.rb
CHANGED
|
@@ -5,7 +5,8 @@ require "yaml"
|
|
|
5
5
|
require "date"
|
|
6
6
|
|
|
7
7
|
# NodeFile (intent 334, n3): one node file's YAML envelope (`node`, `kind`,
|
|
8
|
-
# `files`, `budget`) over a Markdown body,
|
|
8
|
+
# `files`, `budget`, and the optional research `report`) over a Markdown body,
|
|
9
|
+
# plus the deterministic id minter
|
|
9
10
|
# (327 D1r, D5r, D9r-D12r, D16r). The kind-prefix rule lives here, not in
|
|
10
11
|
# GraphEdges, which stays loose about id grammar so a numeric roadmap id
|
|
11
12
|
# parses the same way (D12r).
|
|
@@ -83,6 +84,8 @@ module NodeFile
|
|
|
83
84
|
|
|
84
85
|
budget, budget_errors = normalize_budget(fm["budget"])
|
|
85
86
|
errors.concat(budget_errors)
|
|
87
|
+
report, report_errors = normalize_report(fm["report"], kind)
|
|
88
|
+
errors.concat(report_errors)
|
|
86
89
|
|
|
87
90
|
{
|
|
88
91
|
ok: errors.empty?,
|
|
@@ -90,15 +93,32 @@ module NodeFile
|
|
|
90
93
|
kind: kind.empty? ? nil : kind,
|
|
91
94
|
files: files.is_a?(Array) ? files : nil,
|
|
92
95
|
budget: budget,
|
|
96
|
+
report: report,
|
|
93
97
|
body: body,
|
|
94
98
|
errors: errors,
|
|
95
99
|
}
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
def failure(errors)
|
|
99
|
-
{ ok: false, node: nil, kind: nil, files: nil, budget: nil, body: nil, errors: errors }
|
|
103
|
+
{ ok: false, node: nil, kind: nil, files: nil, budget: nil, report: nil, body: nil, errors: errors }
|
|
100
104
|
end
|
|
101
105
|
|
|
106
|
+
def normalize_report(raw, kind)
|
|
107
|
+
return [nil, []] if raw.nil?
|
|
108
|
+
return [nil, ["report: is allowed only on research nodes"]] unless kind == "research"
|
|
109
|
+
return [nil, ["report: must be a non-empty relative Markdown path under resources/"]] unless raw.is_a?(String)
|
|
110
|
+
|
|
111
|
+
path = raw
|
|
112
|
+
parts = path.split("/", -1)
|
|
113
|
+
valid = path == path.strip && parts.length >= 2 && parts.first == "resources" &&
|
|
114
|
+
parts.none? { |part| part.empty? || %w[. ..].include?(part) } &&
|
|
115
|
+
!path.include?("\\") && path.end_with?(".md")
|
|
116
|
+
return [nil, ["report: must be a relative .md path under resources/ with no traversal"]] unless valid
|
|
117
|
+
|
|
118
|
+
[path, []]
|
|
119
|
+
end
|
|
120
|
+
private_class_method :normalize_report
|
|
121
|
+
|
|
102
122
|
# <id>.md or <id>--<slug>.md, exactly - a longer id's file (n11--x.md) must
|
|
103
123
|
# never satisfy a shorter id (n1), so the id is matched as the whole prefix
|
|
104
124
|
# up to end-of-string or the literal "--" separator, never as a substring
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require "date"
|
|
6
7
|
require "digest"
|
|
@@ -70,7 +71,7 @@ module NodeInput
|
|
|
70
71
|
status.success? ? truncate_landed_commits(out) : nil
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
PROJECT_LAYOUT_RE = %r{\A(.*)/projects/([^/]+)/store/[^/]+\z}.freeze
|
|
74
|
+
PROJECT_LAYOUT_RE = %r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}.freeze
|
|
74
75
|
|
|
75
76
|
# --- paths ---------------------------------------------------------------
|
|
76
77
|
|
|
@@ -118,22 +119,26 @@ module NodeInput
|
|
|
118
119
|
end
|
|
119
120
|
|
|
120
121
|
text = render_node_block(node: node, kind: parsed[:kind], files: parsed[:files], budget: parsed[:budget],
|
|
121
|
-
body: parsed[:body])
|
|
122
|
+
report: parsed[:report], body: parsed[:body])
|
|
122
123
|
{ ok: true, error_kind: nil, text: text, errors: [], kind: parsed[:kind], files: parsed[:files] || [],
|
|
123
|
-
budget: parsed[:budget] }
|
|
124
|
+
budget: parsed[:budget], report: parsed[:report] }
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
def failure_block(kind, errors)
|
|
127
|
-
{ ok: false, error_kind: kind, text: nil, errors: errors, kind: nil, files: nil, budget: nil }
|
|
128
|
+
{ ok: false, error_kind: kind, text: nil, errors: errors, kind: nil, files: nil, budget: nil, report: nil }
|
|
128
129
|
end
|
|
129
130
|
private_class_method :failure_block
|
|
130
131
|
|
|
131
|
-
def render_node_block(node:, kind:, files:, budget:, body:)
|
|
132
|
+
def render_node_block(node:, kind:, files:, budget:, body:, report: nil)
|
|
132
133
|
lines = []
|
|
133
134
|
lines << "# Node #{node}"
|
|
134
135
|
lines << "kind: #{kind}"
|
|
135
136
|
lines << "files: #{Array(files).join(', ')}"
|
|
136
137
|
lines << "budget: #{budget}"
|
|
138
|
+
if report
|
|
139
|
+
lines << "report: #{report}"
|
|
140
|
+
lines << "report delivery: return the complete Markdown body in the YAML report field; the runner writes it"
|
|
141
|
+
end
|
|
137
142
|
lines << ""
|
|
138
143
|
lines << body.to_s.strip
|
|
139
144
|
"#{lines.join("\n")}\n"
|
|
@@ -395,7 +400,7 @@ module NodeInput
|
|
|
395
400
|
return nil unless m
|
|
396
401
|
|
|
397
402
|
home, slug = m[1], m[2]
|
|
398
|
-
path = File.join(home,
|
|
403
|
+
path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
|
|
399
404
|
return nil unless File.exist?(path)
|
|
400
405
|
|
|
401
406
|
# `permitted_classes` (post-execution review finding B3): the same bug
|
|
@@ -33,7 +33,7 @@ module NodeReturn
|
|
|
33
33
|
STATUSES = %w[done failed_verification needs_decision blocked].freeze
|
|
34
34
|
|
|
35
35
|
ALLOWED_KEYS = %w[
|
|
36
|
-
node status commit summary findings proposed_nodes proposed_edges question reason
|
|
36
|
+
node status commit summary findings report proposed_nodes proposed_edges question reason
|
|
37
37
|
].freeze
|
|
38
38
|
|
|
39
39
|
REQUIRED_FIELDS = {
|
|
@@ -51,7 +51,7 @@ module NodeReturn
|
|
|
51
51
|
|
|
52
52
|
Result = Struct.new(
|
|
53
53
|
:ok, :node, :status, :commit, :summary, :findings, :proposed_nodes, :proposed_edges,
|
|
54
|
-
:question, :reason, :errors,
|
|
54
|
+
:report, :question, :reason, :errors,
|
|
55
55
|
keyword_init: true
|
|
56
56
|
)
|
|
57
57
|
|
|
@@ -86,6 +86,10 @@ module NodeReturn
|
|
|
86
86
|
proposed_edges, edge_errors = normalize_proposed_edges(doc["proposed_edges"])
|
|
87
87
|
return failure(edge_errors) if edge_errors.any?
|
|
88
88
|
|
|
89
|
+
if !doc["report"].nil? && !doc["report"].is_a?(String)
|
|
90
|
+
return failure(["report: must be a Markdown string"])
|
|
91
|
+
end
|
|
92
|
+
|
|
89
93
|
Result.new(
|
|
90
94
|
ok: true,
|
|
91
95
|
node: doc["node"].to_s,
|
|
@@ -95,6 +99,7 @@ module NodeReturn
|
|
|
95
99
|
findings: normalize_findings(doc["findings"]),
|
|
96
100
|
proposed_nodes: proposed_nodes,
|
|
97
101
|
proposed_edges: proposed_edges,
|
|
102
|
+
report: doc["report"],
|
|
98
103
|
question: doc["question"],
|
|
99
104
|
reason: doc["reason"],
|
|
100
105
|
errors: [],
|
|
@@ -111,7 +116,7 @@ module NodeReturn
|
|
|
111
116
|
def failure(errors)
|
|
112
117
|
Result.new(
|
|
113
118
|
ok: false, node: nil, status: nil, commit: nil, summary: nil, findings: [],
|
|
114
|
-
proposed_nodes: [], proposed_edges: [], question: nil, reason: nil,
|
|
119
|
+
proposed_nodes: [], proposed_edges: [], report: nil, question: nil, reason: nil,
|
|
115
120
|
errors: Array(errors)
|
|
116
121
|
)
|
|
117
122
|
end
|
package/scripts/lib/preflight.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require_relative "version_number"
|
|
5
5
|
|
|
6
6
|
# Pure, dependency-injected pre-flight checks for Plastic's runtime dependencies
|
|
7
7
|
# (intent 38). Takes injected probes (ruby version, node version, git presence,
|
|
@@ -63,9 +63,7 @@ module Preflight
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def safe_version(str)
|
|
66
|
-
|
|
67
|
-
rescue ArgumentError
|
|
68
|
-
nil
|
|
66
|
+
VersionNumber.parse(str)
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
def strip_leading_v(str)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
|
|
6
7
|
# ProjectConfig - reads a project's project.yml merged over the shipped defaults
|
|
@@ -18,7 +19,7 @@ module ProjectConfig
|
|
|
18
19
|
}.freeze
|
|
19
20
|
|
|
20
21
|
def read(slug)
|
|
21
|
-
path = File.join(Dir.home, ".plastic",
|
|
22
|
+
path = File.join(Plastic::StoreLayout.project_root(File.join(Dir.home, ".plastic"), slug), "project.yml")
|
|
22
23
|
config = if File.exist?(path)
|
|
23
24
|
YAML.safe_load(File.read(path)) || {}
|
|
24
25
|
else
|