@zalom/plastic 1.1.1 → 1.1.2
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 +8 -7
- package/agents/plastic-enforcer.md +3 -2
- package/agents/plastic-executor.md +5 -5
- package/agents/plastic-planner.md +15 -11
- package/package.json +1 -1
- package/scripts/doctor.rb +44 -0
- package/scripts/lib/bridge.rb +33 -11
- package/scripts/lib/installer_core.rb +2 -0
- package/scripts/lib/skill_lint.rb +304 -0
- package/scripts/skill-lint +50 -0
- package/skills/auto/SKILL.md +10 -7
- package/skills/auto/references/tiers.md +4 -3
- package/skills/intent-planning/SKILL.md +11 -11
- package/skills/intent-planning/evals/evals.json +20 -5
- package/skills/intent-planning/references/plan-format.md +9 -5
- package/skills/tutorial/references/track-1-guided.md +5 -4
package/PLASTIC.md
CHANGED
|
@@ -19,7 +19,7 @@ store/
|
|
|
19
19
|
plan.md # optional - implementation plan (How deliverable)
|
|
20
20
|
checklist.md # optional - execution registry (How deliverable)
|
|
21
21
|
outcome.md # optional - detailed result (Exec deliverable)
|
|
22
|
-
actions/ # optional -
|
|
22
|
+
actions/ # optional - real action files (How deliverable, at least one required)
|
|
23
23
|
resources/ # optional - research, references, screenshots, diagrams
|
|
24
24
|
savepoint.md # optional - deterministic cycle-step ledger (auto-written)
|
|
25
25
|
revisions.md # optional - append-only structural-maintenance audit trail
|
|
@@ -95,7 +95,7 @@ tags: [plastic, architecture]
|
|
|
95
95
|
|-------|---------|-------------|--------|
|
|
96
96
|
| **What** | `## Intent` | `{ID}--{slug}.md` | `plastic-intent-creating` |
|
|
97
97
|
| **Why** | `## Context` + Decisions | `spec.md` | `plastic-intent-brainstorming` |
|
|
98
|
-
| **How** | Planning | `plan.md` + `actions
|
|
98
|
+
| **How** | Planning | `plan.md` + `actions/ACTION_N.md` (at least one) + `checklist.md` | `plastic-intent-planning` |
|
|
99
99
|
| **Exec** | Execution | `outcome.md` | `plastic-intent-executing` |
|
|
100
100
|
|
|
101
101
|
`## Insights` is the append-only log of durable discoveries captured throughout ALL stages.
|
|
@@ -133,8 +133,9 @@ same-structure invariant holds: same file set, stage order, gates, and savepoint
|
|
|
133
133
|
every tier and in both modes.
|
|
134
134
|
|
|
135
135
|
S/M collapse the topology (one thinker agent writes spec.md then plan.md plus
|
|
136
|
-
checklist.md
|
|
137
|
-
|
|
136
|
+
checklist.md plus at least one real action file in one context, consolidated into a single
|
|
137
|
+
actions/ACTION_1.md at S/M and one file per task at L; a sonnet executor implements). L
|
|
138
|
+
keeps the full team.
|
|
138
139
|
|
|
139
140
|
Never cut at any tier: the independent reviewer, outcome.md as truth of delivery, the
|
|
140
141
|
delivery lock, worktree isolation, intent creation via skill, INDEX as status truth, the
|
|
@@ -425,8 +426,8 @@ purge. See `docs/internals.md` for depth.
|
|
|
425
426
|
| Transition | Trigger | Gate |
|
|
426
427
|
|---|---|---|
|
|
427
428
|
| What → Why | `spec.md` written | (none) |
|
|
428
|
-
| Why → How | `plan.md` + `actions
|
|
429
|
-
| How → Exec | Checklist has items |
|
|
429
|
+
| Why → How | `plan.md` + `actions/ACTION_N.md` (at least one) + `checklist.md` | `spec.md` must exist |
|
|
430
|
+
| How → Exec | Checklist has items | plan.md, checklist.md, and at least one real actions/ACTION_N.md must exist |
|
|
430
431
|
| Exec → Done | `outcome.md` written | All checklist items checked |
|
|
431
432
|
|
|
432
433
|
Hard blocking: hooks exit code 2 on gate failure.
|
|
@@ -530,7 +531,7 @@ each station.
|
|
|
530
531
|
| Start (board) | none (a procedure, not a stage) | `plastic-lock fix` self-heals stale, corrupt, or legacy state; arm acquires `delivery.lock` (O_EXCL, session-keyed), provisions the code worktree, writes the bridge cache | lock-gate denies any write into an active intent dir without this intent's lock; every deny names the resolving command | savepoint confirms the boarding station |
|
|
531
532
|
| What (create) | `<id>--<slug>.md`, born complete | no lock yet; no bridge | create-gate validates the proposed intent content (Write, Edit, and MCP edits) | savepoint `What` line; intent listed in INDEX `## Active` |
|
|
532
533
|
| Why | `spec.md` | owner writes refresh the lease (lock file mtime heartbeat) | gate-check requires the intent file with `## Intent` before spec.md; lock-gate admits only the owner or a delegate | savepoint `Why started`, `Why spec.md created` |
|
|
533
|
-
| How | `plan.md`, `actions
|
|
534
|
+
| How | `plan.md`, `actions/ACTION_N.md` (at least one), `checklist.md` | heartbeat on writes; the code gate stays closed until plan.md, checklist.md, and a real action file all exist | gate-check requires spec.md before plan.md, and plan.md plus a real actions/ACTION_N.md before checklist.md | savepoint `How started`, `How plan.md created`, `How checklist.md created`, `Exec started` |
|
|
534
535
|
| Exec | code on the intent branch, checklist checked off | heartbeat; code edits confined to the provisioned worktree; delegates write under the owner's lock; bash, interpreter, and MCP writes gated the same way | code-gate, worktree-gate, bash-gate, lock-gate | checklist boxes; savepoint milestones |
|
|
535
536
|
| End (done) | mandatory `outcome.md` (`disposition: delivered\|abandoned`), INDEX moves to Completed or Abandoned | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`, then the bridge is purge-eligible, and the QMD reindex runs LAST (after purge) | gate-check blocks outcome.md while checklist items are unchecked | savepoint `Done delivered` (or `abandoned`); takeover audits, if any, remain in savepoint.md |
|
|
536
537
|
| Maintenance (any stage) | `revisions.md` move-and-record entries | future `maintenance.lock` (short TTL), mutually exclusive with `delivery.lock` in either direction; 108 ships the schema seam only, the implementation follows intent 93 in a chained intent | acquisition refuses while the other lock type is fresh; a terminal intent with no lock held is read-only | dated, rule-tagged `revisions.md` entry; savepoint untouched |
|
|
@@ -31,8 +31,9 @@ is honored as written.
|
|
|
31
31
|
4. **Sequence the team** — dispatch specialists per the chosen topology with a constructed
|
|
32
32
|
context bundle:
|
|
33
33
|
- S/M: ONE thinker agent, one boot, two stations — it writes spec.md, then plan.md +
|
|
34
|
-
checklist.md, in a single context. Sections may be one
|
|
35
|
-
|
|
34
|
+
checklist.md + at least one real action file, in a single context. Sections may be one
|
|
35
|
+
line each; the thinker writes one consolidated `actions/ACTION_1.md` (never an empty
|
|
36
|
+
`actions/`), while L writes one `actions/ACTION_N.md` per task. S may skip the QMD
|
|
36
37
|
discovery deposit when chain and sources are both empty. A sonnet executor implements.
|
|
37
38
|
- L: today's full team, one specialist per stage (brainstorming, spec-specialist, planner,
|
|
38
39
|
executor), each in a fresh context.
|
|
@@ -19,11 +19,11 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
19
19
|
|
|
20
20
|
## How You Work
|
|
21
21
|
|
|
22
|
-
1. Receive (input handoff): `plan.md
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
1. Receive (input handoff): `plan.md`, `checklist.md`, and at least one real `ACTION_N.md`
|
|
23
|
+
inside `actions/` from the planner (S/M hand you one consolidated `actions/ACTION_1.md`;
|
|
24
|
+
L hands you one `actions/ACTION_N.md` per task). Execute the action files in order. For
|
|
25
|
+
S/M intents you run on the sonnet default, the collapsed topology's implementer; behavior
|
|
26
|
+
is otherwise unchanged.
|
|
27
27
|
2. Work one action at a time, preferring safe, non-destructive routes
|
|
28
28
|
3. Edit project code (the gate is open now that plan and checklist exist)
|
|
29
29
|
4. Run the full suite, iterate to zero failures and zero errors
|
|
@@ -14,14 +14,17 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
14
14
|
|
|
15
15
|
1. **Decompose the spec** — break the approach into ordered, independent actions
|
|
16
16
|
2. **Write the plan** — produce `plan.md` with numbered tasks and verification
|
|
17
|
-
3. **Write
|
|
18
|
-
- S/M: `
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
3. **Write at least one real action file at every tier, tier-forked**:
|
|
18
|
+
- S/M: write ONE self-contained `actions/ACTION_1.md` that consolidates the whole
|
|
19
|
+
ordered delivery (the steps plus the exact changes). You MAY split into a few files
|
|
20
|
+
when that reads cleaner, but one real action file is the floor.
|
|
21
|
+
- L: one self-contained `actions/ACTION_N.md` per task. A `.gitkeep` never counts as an
|
|
22
|
+
action, and an empty `actions/` fails the How gate.
|
|
21
23
|
4. **Write the checklist** — `checklist.md` as the execution registry covering every action.
|
|
22
|
-
`checklist.md`
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
`checklist.md` and at least one real action file exist at every tier: the file set does not
|
|
25
|
+
change by tier, only action DEPTH (one consolidated action for S/M vs one-per-task for L)
|
|
26
|
+
and agent topology do. plan.md, checklist.md, AND a real `actions/ACTION_N.md` are what open
|
|
27
|
+
the code gate at every tier.
|
|
25
28
|
|
|
26
29
|
## How You Work
|
|
27
30
|
|
|
@@ -38,7 +41,7 @@ END your turn with a structured completion report as your final message, per the
|
|
|
38
41
|
- The ordered actions, one line each: what the action does and how it is verified
|
|
39
42
|
- Decomposition rationale: why this order, and why the actions are independent
|
|
40
43
|
- Checklist coverage: the item count, and that every action plus suite-green is covered
|
|
41
|
-
- Which tier shape was produced:
|
|
44
|
+
- Which tier shape was produced: one consolidated `actions/ACTION_1.md` (S/M) or one `actions/ACTION_N.md` per task (L)
|
|
42
45
|
|
|
43
46
|
The plan is an argument; the orchestrator gates on whether that argument is sound before any code is written, so make the report make that case.
|
|
44
47
|
|
|
@@ -46,7 +49,8 @@ The plan is an argument; the orchestrator gates on whether that argument is soun
|
|
|
46
49
|
|
|
47
50
|
- You are dispatched by the plastic-enforcer and your deliverable is gated before Exec begins
|
|
48
51
|
- You write only intent-store files (`plan.md`, `actions/`, `checklist.md`); no project code
|
|
49
|
-
- The code gate stays closed until `plan.md` and `
|
|
52
|
+
- The code gate stays closed until `plan.md`, `checklist.md`, and at least one real `actions/ACTION_N.md` exist, so produce all three
|
|
50
53
|
- Keep each action self-contained so the executor can run them one at a time
|
|
51
|
-
-
|
|
52
|
-
|
|
54
|
+
- Write real `actions/ACTION_N.md` files, never a `.gitkeep`: keeping a freshly-scaffolded
|
|
55
|
+
empty `actions/` under git is `scripts/new-intent`'s job at intent birth, not the planner's,
|
|
56
|
+
and a `.gitkeep` never counts as an action
|
package/package.json
CHANGED
package/scripts/doctor.rb
CHANGED
|
@@ -25,6 +25,7 @@ require_relative "lib/lock"
|
|
|
25
25
|
require_relative "lib/bridge"
|
|
26
26
|
require_relative "lib/agent_models"
|
|
27
27
|
require_relative "lib/legacy_bookend_amnesty"
|
|
28
|
+
require_relative "lib/skill_lint"
|
|
28
29
|
|
|
29
30
|
# Diagnostic engine, instantiable with an injected store/agent map so tests can
|
|
30
31
|
# run it hermetically (no eval, no global-constant rewriting).
|
|
@@ -1805,6 +1806,48 @@ class Doctor
|
|
|
1805
1806
|
)
|
|
1806
1807
|
end
|
|
1807
1808
|
|
|
1809
|
+
# --- Check category: skill-lint (advisory only; intent 85b) ---
|
|
1810
|
+
#
|
|
1811
|
+
# Reports SkillLint's five structural checks over the skills/ tree as a
|
|
1812
|
+
# single ADVISORY finding. Always status: "pass", so it can never contribute
|
|
1813
|
+
# to doctor's warn/fail exit code (summarize buckets purely by status; the
|
|
1814
|
+
# lint verdict belongs to a future ship gate, intent 100, not to doctor).
|
|
1815
|
+
# Resolves the same repo skills/ directory `scripts/skill-lint` defaults to,
|
|
1816
|
+
# relative to this script's own location. Skills install flat and renamed
|
|
1817
|
+
# (plastic-<name>) under each agent's own skills/ dir, a different directory
|
|
1818
|
+
# shape SkillLint's frontmatter name-check does not target, so an installed
|
|
1819
|
+
# ~/.plastic/scripts/doctor.rb with no sibling skills/ tree has nothing in
|
|
1820
|
+
# scope here and reports a silent pass rather than guessing at a mismatched
|
|
1821
|
+
# layout.
|
|
1822
|
+
def check_skill_lint
|
|
1823
|
+
dir = File.expand_path("../skills", __dir__)
|
|
1824
|
+
unless File.directory?(dir)
|
|
1825
|
+
return [check(
|
|
1826
|
+
category: "skill_lint", name: "skill_lint", status: "pass",
|
|
1827
|
+
message: "No skills/ directory found at #{tilde(dir)} to lint (nothing in scope for this advisory check)"
|
|
1828
|
+
)]
|
|
1829
|
+
end
|
|
1830
|
+
|
|
1831
|
+
result = SkillLint.new(skills_dir: dir).run
|
|
1832
|
+
|
|
1833
|
+
if result.ok?
|
|
1834
|
+
[check(
|
|
1835
|
+
category: "skill_lint", name: "skill_lint", status: "pass",
|
|
1836
|
+
message: "skill-lint: clean (0 violations across the skills/ tree)"
|
|
1837
|
+
)]
|
|
1838
|
+
else
|
|
1839
|
+
by_check = result.violations.group_by { |v| v[:check] }.transform_values(&:size)
|
|
1840
|
+
counts = by_check.map { |check_id, n| "#{check_id}: #{n}" }.join(", ")
|
|
1841
|
+
details = result.violations.map { |v| "#{v[:check]} #{v[:skill]} #{v[:file]}:#{v[:line]}: #{v[:message]}" }
|
|
1842
|
+
[check(
|
|
1843
|
+
category: "skill_lint", name: "skill_lint", status: "pass",
|
|
1844
|
+
message: "skill-lint: #{result.violations.size} advisory violation(s) found (#{counts}); " \
|
|
1845
|
+
"informational only, never affects doctor's exit code",
|
|
1846
|
+
details: details
|
|
1847
|
+
)]
|
|
1848
|
+
end
|
|
1849
|
+
end
|
|
1850
|
+
|
|
1808
1851
|
# --- Run all checks ---
|
|
1809
1852
|
|
|
1810
1853
|
def run_checks(agent_key)
|
|
@@ -1817,6 +1860,7 @@ class Doctor
|
|
|
1817
1860
|
all_checks += check_deprecations
|
|
1818
1861
|
all_checks += check_qmd
|
|
1819
1862
|
all_checks += check_done_signals
|
|
1863
|
+
all_checks += check_skill_lint
|
|
1820
1864
|
|
|
1821
1865
|
summarize(all_checks, agent_key)
|
|
1822
1866
|
end
|
package/scripts/lib/bridge.rb
CHANGED
|
@@ -395,10 +395,24 @@ module Bridge
|
|
|
395
395
|
File.exist?(path)
|
|
396
396
|
end
|
|
397
397
|
|
|
398
|
+
# True iff actions/ holds AT LEAST ONE real action file: a non-empty *.md whose
|
|
399
|
+
# first line is not the placeholder sentinel. A `.gitkeep` (no .md extension)
|
|
400
|
+
# never counts, an empty *.md never counts, and a sentinel-only *.md never
|
|
401
|
+
# counts. Pure and side-effect-free so the gate stays unit-testable. Fail-open:
|
|
402
|
+
# a missing actions/ dir globs to nothing and returns false (the gate then
|
|
403
|
+
# reports it needs a real action file); it never raises.
|
|
404
|
+
def self.has_real_action?(intent_dir)
|
|
405
|
+
Dir.glob("#{intent_dir}/actions/*.md").any? do |f|
|
|
406
|
+
File.file?(f) && File.size(f) > 0 && stage_file_present?(f)
|
|
407
|
+
end
|
|
408
|
+
rescue StandardError
|
|
409
|
+
false
|
|
410
|
+
end
|
|
411
|
+
|
|
398
412
|
def self.derive_stage(intent_dir)
|
|
399
413
|
return "done" if stage_file_present?("#{intent_dir}/outcome.md")
|
|
400
414
|
if stage_file_present?("#{intent_dir}/plan.md") &&
|
|
401
|
-
|
|
415
|
+
has_real_action?(intent_dir) &&
|
|
402
416
|
stage_file_present?("#{intent_dir}/checklist.md")
|
|
403
417
|
return "exec"
|
|
404
418
|
end
|
|
@@ -414,7 +428,7 @@ module Bridge
|
|
|
414
428
|
["spec.md", "plan.md", "checklist.md", "outcome.md"].each do |f|
|
|
415
429
|
files << f if stage_file_present?("#{intent_dir}/#{f}")
|
|
416
430
|
end
|
|
417
|
-
files << "actions/" if
|
|
431
|
+
files << "actions/" if has_real_action?(intent_dir)
|
|
418
432
|
files
|
|
419
433
|
end
|
|
420
434
|
|
|
@@ -779,8 +793,13 @@ module Bridge
|
|
|
779
793
|
return "Cannot start How — Why is incomplete (spec.md missing)"
|
|
780
794
|
end
|
|
781
795
|
when "checklist.md"
|
|
782
|
-
unless stage_file_present?("#{intent_dir}/plan.md")
|
|
783
|
-
return "Cannot complete How — plan.md
|
|
796
|
+
unless stage_file_present?("#{intent_dir}/plan.md")
|
|
797
|
+
return "Cannot complete How — plan.md missing"
|
|
798
|
+
end
|
|
799
|
+
unless has_real_action?(intent_dir)
|
|
800
|
+
return "Cannot complete How — actions/ has no real action file (only .gitkeep or empty). " \
|
|
801
|
+
"The planner must write at least one actions/ACTION_N.md before checklist.md. " \
|
|
802
|
+
"See skills/intent-planning."
|
|
784
803
|
end
|
|
785
804
|
when "outcome.md"
|
|
786
805
|
checklist = "#{intent_dir}/checklist.md"
|
|
@@ -1017,11 +1036,14 @@ module Bridge
|
|
|
1017
1036
|
return nil unless store && dir
|
|
1018
1037
|
intent_dir_abs = File.expand_path("#{store}/#{dir}")
|
|
1019
1038
|
|
|
1020
|
-
# "How reached" =
|
|
1021
|
-
#
|
|
1022
|
-
#
|
|
1039
|
+
# "How reached" = plan.md + checklist.md are both present AND actions/ holds at
|
|
1040
|
+
# least one real action file. Gate by artifact presence, not the stage label
|
|
1041
|
+
# (derive_stage returns "how" as soon as spec.md exists, before any plan). Code
|
|
1042
|
+
# edits stay blocked until the planner has written a real action file, so an
|
|
1043
|
+
# empty or .gitkeep-only actions/ never opens the code gate.
|
|
1023
1044
|
reached_how = stage_file_present?("#{intent_dir_abs}/plan.md") &&
|
|
1024
|
-
stage_file_present?("#{intent_dir_abs}/checklist.md")
|
|
1045
|
+
stage_file_present?("#{intent_dir_abs}/checklist.md") &&
|
|
1046
|
+
has_real_action?(intent_dir_abs)
|
|
1025
1047
|
return nil if reached_how
|
|
1026
1048
|
|
|
1027
1049
|
file_abs = File.expand_path(file_path.to_s)
|
|
@@ -1030,9 +1052,9 @@ module Bridge
|
|
|
1030
1052
|
return nil if file_abs == intent_dir_abs || file_abs.start_with?("#{intent_dir_abs}/")
|
|
1031
1053
|
|
|
1032
1054
|
id = intent_info["id"]
|
|
1033
|
-
"intent #{id} has not reached How — write plan.md + checklist.md
|
|
1034
|
-
"editing project code. Run plastic-auto or
|
|
1035
|
-
"(blocked edit: #{file_abs})"
|
|
1055
|
+
"intent #{id} has not reached How — write plan.md + checklist.md and at least " \
|
|
1056
|
+
"one real actions/ACTION_N.md before editing project code. Run plastic-auto or " \
|
|
1057
|
+
"plastic-intent-planning first. (blocked edit: #{file_abs})"
|
|
1036
1058
|
end
|
|
1037
1059
|
|
|
1038
1060
|
# --- Solo-mode detection (intent 128) ---------------------------------------
|
|
@@ -296,6 +296,8 @@ class InstallerCore
|
|
|
296
296
|
"scripts/lib/legacy_bookend_amnesty.rb" => "scripts/lib/legacy_bookend_amnesty.rb",
|
|
297
297
|
"scripts/doctor.rb" => "scripts/doctor.rb",
|
|
298
298
|
"scripts/dashboard.rb" => "scripts/dashboard.rb",
|
|
299
|
+
"scripts/skill-lint" => "scripts/skill-lint",
|
|
300
|
+
"scripts/lib/skill_lint.rb" => "scripts/lib/skill_lint.rb",
|
|
299
301
|
}
|
|
300
302
|
end
|
|
301
303
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
# SkillLint: deterministic, dependency-injected engine that runs five
|
|
7
|
+
# structural checks over a directory of Agent Skills (intent 85b).
|
|
8
|
+
#
|
|
9
|
+
# Pure and DI: constructed with an injected `skills_dir`, performs no writes,
|
|
10
|
+
# no `eval`, and reads no ambient config beyond the injected directory. Mirrors
|
|
11
|
+
# the CLI-over-lib shape of `scripts/lib/intent_validator.rb`: the `skill-lint`
|
|
12
|
+
# CLI (ACTION_2) is a thin wrapper, `scripts/doctor.rb` (ACTION_4) consumes the
|
|
13
|
+
# same engine for an advisory finding, and `test/skill_lint_test.rb` (ACTION_3)
|
|
14
|
+
# proves every check red-and-green plus a no-skip-list live-tree guard.
|
|
15
|
+
#
|
|
16
|
+
# Each violation is a Hash:
|
|
17
|
+
# { check:, skill:, file:, line:, rule:, message: }
|
|
18
|
+
# `check` is one of the five check-ids below, `skill` is the skill directory
|
|
19
|
+
# basename, `file` is the offending path, `line` is an Integer where locatable
|
|
20
|
+
# else nil, `rule` cites the standard being enforced, `message` is an
|
|
21
|
+
# actionable fix instruction.
|
|
22
|
+
class SkillLint
|
|
23
|
+
# A binding keyword: a mention's paragraph carries an observable trigger
|
|
24
|
+
# condition when it names one of these (word-boundary, case-insensitive).
|
|
25
|
+
BINDING_KEYWORD_RE = /\b(when|if|before|after|while)\b/i
|
|
26
|
+
|
|
27
|
+
# A `to <verb>` or `for <noun-phrase>` purpose clause.
|
|
28
|
+
PURPOSE_RE = /\b(to|for)\s+\S/i
|
|
29
|
+
|
|
30
|
+
Result = Struct.new(:violations) do
|
|
31
|
+
def ok?
|
|
32
|
+
violations.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Slice violations down to one check-id, e.g. for a doctor finding or a
|
|
36
|
+
# red-proof assertion.
|
|
37
|
+
def violations_for(check_id)
|
|
38
|
+
violations.select { |v| v[:check] == check_id }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(skills_dir:)
|
|
43
|
+
@skills_dir = skills_dir
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def run
|
|
47
|
+
violations = []
|
|
48
|
+
|
|
49
|
+
skill_md_paths.each do |skill_md|
|
|
50
|
+
skill_dir = File.dirname(skill_md)
|
|
51
|
+
content = File.read(skill_md)
|
|
52
|
+
|
|
53
|
+
violations.concat(check_body_budget(skill_dir, skill_md, content))
|
|
54
|
+
violations.concat(check_frontmatter_validity(skill_dir, skill_md, content))
|
|
55
|
+
violations.concat(check_bare_pointer(skill_dir, skill_md, content))
|
|
56
|
+
violations.concat(check_orphan_files(skill_dir))
|
|
57
|
+
violations.concat(check_references_depth(skill_dir))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Result.new(violations)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def skill_md_paths
|
|
66
|
+
Dir.glob(File.join(@skills_dir, "*", "SKILL.md")).sort
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def skill_name(skill_dir)
|
|
70
|
+
File.basename(skill_dir)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def violation(check:, skill:, file:, line:, rule:, message:)
|
|
74
|
+
{ check: check, skill: skill, file: file, line: line, rule: rule, message: message }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Frontmatter split uses the shared convention (scripts/lib/intent_validator.rb):
|
|
78
|
+
# `content.split("---", 3)`; parts[1] is the frontmatter text, parts[2] is the
|
|
79
|
+
# body. Also computes the body's starting line number (0-based line count of
|
|
80
|
+
# everything before the body) so callers can report absolute file line
|
|
81
|
+
# numbers, not just body-relative ones.
|
|
82
|
+
def frontmatter_and_body(content)
|
|
83
|
+
parts = content.split("---", 3)
|
|
84
|
+
return { frontmatter: nil, body: content, body_offset_lines: 0 } if parts.length < 3
|
|
85
|
+
|
|
86
|
+
prefix_len = parts[0].length + 3 + parts[1].length + 3
|
|
87
|
+
prefix = content[0...prefix_len]
|
|
88
|
+
{ frontmatter: parts[1], body: parts[2], body_offset_lines: prefix.count("\n") }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# --- 1. body-budget ---
|
|
92
|
+
|
|
93
|
+
def check_body_budget(skill_dir, skill_md, content)
|
|
94
|
+
violations = []
|
|
95
|
+
body = frontmatter_and_body(content)[:body]
|
|
96
|
+
name = skill_name(skill_dir)
|
|
97
|
+
|
|
98
|
+
line_count = body.lines.count
|
|
99
|
+
if line_count >= 500
|
|
100
|
+
violations << violation(
|
|
101
|
+
check: "body-budget", skill: name, file: skill_md, line: nil,
|
|
102
|
+
rule: "body under 500 lines",
|
|
103
|
+
message: "SKILL.md body is #{line_count} lines; move detail into references/*.md to get under 500"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
tokens = (body.split(/\s+/).reject(&:empty?).length * 1.3).round
|
|
108
|
+
if tokens >= 5000
|
|
109
|
+
violations << violation(
|
|
110
|
+
check: "body-budget", skill: name, file: skill_md, line: nil,
|
|
111
|
+
rule: "body under about 5000 tokens (word-based estimate)",
|
|
112
|
+
message: "SKILL.md body is about #{tokens} tokens (word-based estimate); move detail into references/*.md to get under 5000"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
violations
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# --- 2. frontmatter-validity ---
|
|
120
|
+
|
|
121
|
+
def check_frontmatter_validity(skill_dir, skill_md, content)
|
|
122
|
+
violations = []
|
|
123
|
+
name = skill_name(skill_dir)
|
|
124
|
+
frontmatter_text = frontmatter_and_body(content)[:frontmatter]
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
fm = YAML.safe_load(frontmatter_text.to_s)
|
|
128
|
+
rescue Psych::SyntaxError
|
|
129
|
+
violations << violation(
|
|
130
|
+
check: "frontmatter-validity", skill: name, file: skill_md, line: nil,
|
|
131
|
+
rule: "frontmatter must survive strict YAML.safe_load",
|
|
132
|
+
message: "SKILL.md frontmatter fails YAML.safe_load; quote scalar values that contain an unquoted \": \" sequence"
|
|
133
|
+
)
|
|
134
|
+
return violations
|
|
135
|
+
end
|
|
136
|
+
fm = {} unless fm.is_a?(Hash)
|
|
137
|
+
|
|
138
|
+
expected_name = "plastic-#{name}"
|
|
139
|
+
if fm["name"] != expected_name
|
|
140
|
+
violations << violation(
|
|
141
|
+
check: "frontmatter-validity", skill: name, file: skill_md, line: nil,
|
|
142
|
+
rule: "name: must equal plastic-<directory>",
|
|
143
|
+
message: "frontmatter name: is #{fm["name"].inspect}; expected #{expected_name.inspect}"
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
user_invocable = fm["user-invocable"]
|
|
148
|
+
unless user_invocable == true || user_invocable == false
|
|
149
|
+
violations << violation(
|
|
150
|
+
check: "frontmatter-validity", skill: name, file: skill_md, line: nil,
|
|
151
|
+
rule: "user-invocable: must be present and boolean",
|
|
152
|
+
message: "frontmatter user-invocable: is #{user_invocable.inspect}; must be present and true or false"
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
violations
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# --- 3. bare-pointer ---
|
|
160
|
+
|
|
161
|
+
def check_bare_pointer(skill_dir, skill_md, content)
|
|
162
|
+
violations = []
|
|
163
|
+
ref_files = Dir.glob(File.join(skill_dir, "references", "*.md")).sort
|
|
164
|
+
return violations if ref_files.empty?
|
|
165
|
+
|
|
166
|
+
parsed = frontmatter_and_body(content)
|
|
167
|
+
body_lines = parsed[:body].lines
|
|
168
|
+
offset = parsed[:body_offset_lines]
|
|
169
|
+
blocks = paragraph_blocks(body_lines)
|
|
170
|
+
name = skill_name(skill_dir)
|
|
171
|
+
|
|
172
|
+
ref_files.each do |ref_file|
|
|
173
|
+
base = File.basename(ref_file)
|
|
174
|
+
mention_indices = (0...body_lines.length).select { |i| body_lines[i].include?(base) }
|
|
175
|
+
next if mention_indices.empty? # zero mentions is an orphan (check 4), not a bare pointer
|
|
176
|
+
|
|
177
|
+
bound = mention_indices.any? { |i| bound_mention?(body_lines[i], i, blocks, base) }
|
|
178
|
+
next if bound
|
|
179
|
+
|
|
180
|
+
first = mention_indices.first
|
|
181
|
+
violations << violation(
|
|
182
|
+
check: "bare-pointer", skill: name, file: skill_md, line: offset + first + 1,
|
|
183
|
+
rule: "every reference link must bind to an observable trigger condition, never a bare pointer",
|
|
184
|
+
message: "#{base} is only ever a bare pointer; add a when/if/before/after/while clause or a " \
|
|
185
|
+
"to/for purpose so the trigger is observable"
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
violations
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def bound_mention?(line, index, blocks, base)
|
|
193
|
+
stripped = line.strip
|
|
194
|
+
return table_row_bound?(stripped, base) if stripped.start_with?("|")
|
|
195
|
+
|
|
196
|
+
block = blocks.find { |b| index.between?(b[:start], b[:end]) }
|
|
197
|
+
block_text = block ? block[:text] : line
|
|
198
|
+
|
|
199
|
+
# Narrow to the unit(s) that actually mention the reference, so an
|
|
200
|
+
# unrelated unit elsewhere in the same paragraph cannot launder a
|
|
201
|
+
# genuinely bare pointer through an incidental "to"/"for" (an ordinary
|
|
202
|
+
# preposition, not a binding purpose clause). Split on BOTH sentence
|
|
203
|
+
# boundaries (.!?) AND list-item starts, so a bullet list (which has no
|
|
204
|
+
# terminal punctuation between items) does not collapse into one unit
|
|
205
|
+
# whose bound siblings launder a bare bullet -- the trailing "References"
|
|
206
|
+
# list pattern (most bullets bound, one forgotten) this linter exists to
|
|
207
|
+
# catch. Keep only units that mention `base`. Falls back to the whole
|
|
208
|
+
# block when no unit boundary contains the mention (e.g. a mid-sentence
|
|
209
|
+
# line wrap with no terminal punctuation nearby), so the legitimate
|
|
210
|
+
# multi-line-wrapped case still binds.
|
|
211
|
+
#
|
|
212
|
+
# Known conservative limitation (documented, not closed): a bare mention
|
|
213
|
+
# with NO unit boundary of its own (no leading list marker, no preceding
|
|
214
|
+
# terminal punctuation) immediately followed by an unrelated sentence
|
|
215
|
+
# carrying "to"/"for" can still borrow that neighbor's binding via the
|
|
216
|
+
# empty-fallback path. Closing this structurally risks re-breaking the
|
|
217
|
+
# legitimate wrapped-sentence case (which also relies on the fallback),
|
|
218
|
+
# so it stays open; see intent 85b insights.
|
|
219
|
+
units = block_text.split(/(?<=[.!?])\s+|\n(?=\s*(?:[-*+]|\d+[.)])\s)/)
|
|
220
|
+
mentioning = units.select { |s| s.include?(base) }
|
|
221
|
+
scoped_text = mentioning.empty? ? block_text : mentioning.join(" ")
|
|
222
|
+
|
|
223
|
+
scoped_text.match?(BINDING_KEYWORD_RE) || scoped_text.match?(PURPOSE_RE)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# A table row is bound when some OTHER cell (not the one carrying the
|
|
227
|
+
# reference path) has visible text (the trigger-condition column).
|
|
228
|
+
def table_row_bound?(stripped_line, base)
|
|
229
|
+
cells = stripped_line.split("|").map(&:strip).reject(&:empty?)
|
|
230
|
+
other_cells = cells.reject { |c| c.include?(base) }
|
|
231
|
+
!other_cells.empty?
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Group body lines into blank-line-delimited paragraph blocks so a mention
|
|
235
|
+
# that trails a wrapped sentence (the binding keyword on the line above) is
|
|
236
|
+
# still evaluated against its full paragraph, not just its own physical line.
|
|
237
|
+
def paragraph_blocks(lines)
|
|
238
|
+
blocks = []
|
|
239
|
+
start = nil
|
|
240
|
+
lines.each_with_index do |line, i|
|
|
241
|
+
if line.strip.empty?
|
|
242
|
+
blocks << { start: start, end: i - 1, text: lines[start..(i - 1)].join } if start
|
|
243
|
+
start = nil
|
|
244
|
+
else
|
|
245
|
+
start ||= i
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
blocks << { start: start, end: lines.length - 1, text: lines[start..-1].join } if start
|
|
249
|
+
blocks
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# --- 4. orphan-files ---
|
|
253
|
+
|
|
254
|
+
def check_orphan_files(skill_dir)
|
|
255
|
+
violations = []
|
|
256
|
+
ref_files = Dir.glob(File.join(skill_dir, "references", "*.md")).sort
|
|
257
|
+
return violations if ref_files.empty?
|
|
258
|
+
|
|
259
|
+
other_files = Dir.glob(File.join(skill_dir, "**", "*")).select { |p| File.file?(p) }
|
|
260
|
+
name = skill_name(skill_dir)
|
|
261
|
+
|
|
262
|
+
ref_files.each do |ref_file|
|
|
263
|
+
base = File.basename(ref_file)
|
|
264
|
+
mentioned = other_files.any? do |other|
|
|
265
|
+
next false if other == ref_file
|
|
266
|
+
|
|
267
|
+
File.read(other).include?(base)
|
|
268
|
+
end
|
|
269
|
+
next if mentioned
|
|
270
|
+
|
|
271
|
+
violations << violation(
|
|
272
|
+
check: "orphan-files", skill: name, file: ref_file, line: nil,
|
|
273
|
+
rule: "every references/ file must be routed from the skill",
|
|
274
|
+
message: "#{base} is never mentioned anywhere in the skill directory; route it from SKILL.md or delete it"
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
violations
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# --- 5. references-depth ---
|
|
282
|
+
|
|
283
|
+
def check_references_depth(skill_dir)
|
|
284
|
+
violations = []
|
|
285
|
+
refs_root = File.join(skill_dir, "references")
|
|
286
|
+
return violations unless File.directory?(refs_root)
|
|
287
|
+
|
|
288
|
+
name = skill_name(skill_dir)
|
|
289
|
+
Dir.glob(File.join(refs_root, "**", "*")).each do |path|
|
|
290
|
+
next unless File.file?(path)
|
|
291
|
+
|
|
292
|
+
rel = path.sub("#{refs_root}/", "")
|
|
293
|
+
next unless rel.include?("/")
|
|
294
|
+
|
|
295
|
+
violations << violation(
|
|
296
|
+
check: "references-depth", skill: name, file: path, line: nil,
|
|
297
|
+
rule: "references stay one level deep",
|
|
298
|
+
message: "#{rel} is nested under references/; move it to a flat references/*.md file"
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
violations
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# skill-lint: deterministic CLI over SkillLint (intent 85b).
|
|
6
|
+
#
|
|
7
|
+
# Runs the five structural skill checks (body-budget, frontmatter-validity,
|
|
8
|
+
# bare-pointer, orphan-files, references-depth) over a directory of Agent
|
|
9
|
+
# Skills and reports every violation. Mirrors `scripts/validate-intent`'s
|
|
10
|
+
# CLI-over-lib shape and exit-code contract.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# skill-lint [--skills-dir <path>]
|
|
14
|
+
#
|
|
15
|
+
# Exit codes: 0 (clean), 1 (violations found; reported on stderr), 2 (usage).
|
|
16
|
+
# --skills-dir defaults to the repo skills/ directory next to this script.
|
|
17
|
+
|
|
18
|
+
require_relative "lib/skill_lint"
|
|
19
|
+
|
|
20
|
+
def resolve_skills_dir(args)
|
|
21
|
+
if (i = args.index("--skills-dir"))
|
|
22
|
+
args[i + 1]
|
|
23
|
+
else
|
|
24
|
+
File.expand_path("../skills", __dir__)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if ARGV.any? { |a| a.start_with?("--") && a != "--skills-dir" }
|
|
29
|
+
warn "usage: skill-lint [--skills-dir <path>]"
|
|
30
|
+
exit 2
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if ARGV.include?("--skills-dir") && ARGV[ARGV.index("--skills-dir") + 1].nil?
|
|
34
|
+
warn "usage: skill-lint [--skills-dir <path>]"
|
|
35
|
+
exit 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
dir = File.expand_path(resolve_skills_dir(ARGV))
|
|
39
|
+
result = SkillLint.new(skills_dir: dir).run
|
|
40
|
+
|
|
41
|
+
if result.ok?
|
|
42
|
+
puts "OK: #{dir}"
|
|
43
|
+
exit 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
warn "VIOLATIONS: #{dir}"
|
|
47
|
+
result.violations.each do |v|
|
|
48
|
+
warn "#{v[:check]} #{v[:skill]} #{v[:file]}:#{v[:line].nil? ? "-" : v[:line]} #{v[:message]}"
|
|
49
|
+
end
|
|
50
|
+
exit 1
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -70,8 +70,10 @@ and artifact depth to that size. Extended walkthrough: `references/tiers.md`.
|
|
|
70
70
|
every tier and in both modes. A three-line spec.md is still a spec.md, in the same
|
|
71
71
|
place, under the same gate.
|
|
72
72
|
3. **Per-tier topology.** S/M: one thinker agent, one boot, two stations, sonnet
|
|
73
|
-
executor
|
|
74
|
-
|
|
73
|
+
executor; the thinker writes at least one real action file (one consolidated
|
|
74
|
+
`actions/ACTION_1.md`), never an empty `actions/`. S may also skip the QMD discovery
|
|
75
|
+
deposit when chain and sources are both empty. L: today's full team (`## Team Spin-Up`
|
|
76
|
+
below), one `actions/ACTION_N.md` per task.
|
|
75
77
|
4. **Never-cut list**, any tier or mode: the independent reviewer (separate agent, fresh
|
|
76
78
|
context, never the maker), `outcome.md` as truth of delivery, the delivery lock,
|
|
77
79
|
worktree isolation, intent creation via skill, INDEX as status truth, the QMD reindex
|
|
@@ -203,14 +205,15 @@ Then proceed to How.
|
|
|
203
205
|
|
|
204
206
|
## How Phase
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
Every tier runs all four steps below (see `## Tiers` above). The `actions/` directory is
|
|
209
|
+
scaffolded (with a `.gitkeep`) at intent birth; the planner then writes at least one REAL
|
|
210
|
+
`ACTION_N.md` into it at every tier. The tier only changes step 3's granularity: S/M write
|
|
211
|
+
one consolidated `actions/ACTION_1.md`, L writes one `actions/ACTION_N.md` per task. A
|
|
212
|
+
`.gitkeep`-only or empty `actions/` fails the How gate.
|
|
210
213
|
|
|
211
214
|
1. If `superpowers:writing-plans` is available as a skill, delegate plan creation to it. Tell it the plan saves to the active intent's directory (not `docs/superpowers/plans/`).
|
|
212
215
|
2. Otherwise, write `plan.md` directly - implementation plan with numbered tasks
|
|
213
|
-
3. Write `ACTION_N.md`
|
|
216
|
+
3. Write at least one real `ACTION_N.md` into the existing `actions/` directory, self-contained (S/M: one consolidated `ACTION_1.md`; L: one per task)
|
|
214
217
|
4. Write `checklist.md` - execution registry with checkboxes covering all actions
|
|
215
218
|
5. Notify user (How briefing): brief per `references/human-report-contract.md`
|
|
216
219
|
(State: the plan shape, task count and what it builds; Risk: the riskiest task or
|
|
@@ -30,9 +30,10 @@ the savepoint ledger.
|
|
|
30
30
|
One thinker agent boots ONCE and stays in a single context for two stations:
|
|
31
31
|
|
|
32
32
|
1. Station 1 — writes `spec.md` (collapsed sections allowed, one line each is valid).
|
|
33
|
-
2. Station 2 — writes `plan.md` + `checklist.md`
|
|
34
|
-
|
|
35
|
-
`actions/` is
|
|
33
|
+
2. Station 2 — writes `plan.md` + `checklist.md` + at least one real action file in the
|
|
34
|
+
SAME context (no reboot). At S/M the thinker consolidates the whole delivery into one
|
|
35
|
+
`actions/ACTION_1.md` (rather than one file per task); `actions/` is populated at every
|
|
36
|
+
tier, and a `.gitkeep`-only or empty `actions/` fails the How gate.
|
|
36
37
|
|
|
37
38
|
Then a sonnet executor (a fresh dispatch, this is the one topology split that always
|
|
38
39
|
happens) implements from plan.md + checklist.md, checks off items, appends `## Insights`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plastic-intent-planning
|
|
3
|
-
description: "Write implementation plans from a spec. Produces plan.md, checklist.md, and
|
|
3
|
+
description: "Write implementation plans from a spec. Produces plan.md, checklist.md, and at least one real actions/ACTION_N.md (every tier) in the active intent directory."
|
|
4
4
|
user-invocable: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -31,12 +31,12 @@ If the spec covers multiple independent subsystems, it should have been broken i
|
|
|
31
31
|
|
|
32
32
|
## Tier shapes
|
|
33
33
|
|
|
34
|
-
Read the spec's stamped `Tier:` line (written by intent-speccing) and pick the
|
|
34
|
+
Read the spec's stamped `Tier:` line (written by intent-speccing) and pick the action shape it calls for. Every tier produces at least one REAL action file in `actions/`; the tier only changes how many:
|
|
35
35
|
|
|
36
|
-
- **S or M (default):**
|
|
36
|
+
- **S or M (default):** write ONE consolidated `actions/ACTION_1.md` that carries the whole ordered delivery (the steps plus the exact changes). `plan.md` still holds the overall map and `checklist.md` still mirrors the task list. You may split into a few action files when that reads cleaner, but one real action file is the floor.
|
|
37
37
|
- **L (many independent tasks, dispatched in parallel):** self-contained `actions/ACTION_N.md`, one per task, each readable without the plan (see `references/plan-format.md`).
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
A `.gitkeep` never counts as an action, and an empty `actions/` fails the How gate at every tier. At S/M, keep it to a single consolidated action file rather than one-per-task; over-splitting a small intent is the S/M failure mode, an empty `actions/` is the tier-wide one.
|
|
40
40
|
|
|
41
41
|
## File Structure
|
|
42
42
|
|
|
@@ -97,11 +97,11 @@ If you find issues, fix them inline. No need to re-review, just fix and move on.
|
|
|
97
97
|
## Plastic Artifacts
|
|
98
98
|
|
|
99
99
|
After writing `plan.md`, create `checklist.md` (execution registry following the
|
|
100
|
-
FORM: `## In Progress`, `## Completed`, `## Session Log`)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
FORM: `## In Progress`, `## Completed`, `## Session Log`) and at least one real
|
|
101
|
+
`actions/ACTION_N.md` (self-contained, in an `actions/` directory inside the intent
|
|
102
|
+
directory). At S/M write one consolidated `actions/ACTION_1.md`; at L write one
|
|
103
|
+
`actions/ACTION_N.md` per task (see Tier shapes above). For the exact format of
|
|
104
|
+
both, read `references/plan-format.md`.
|
|
105
105
|
|
|
106
106
|
## Owner-decision hard-gate items
|
|
107
107
|
|
|
@@ -118,12 +118,12 @@ When collecting owner rulings for `[ORCHESTRATOR]` hard-gate items, read
|
|
|
118
118
|
## Gate position
|
|
119
119
|
|
|
120
120
|
- **Before:** `spec.md` exists.
|
|
121
|
-
- **Produces:** `plan.md` and `
|
|
121
|
+
- **Produces:** `plan.md`, `checklist.md`, and at least one real `actions/ACTION_N.md` (every tier; one consolidated file at S/M, one per task at L).
|
|
122
122
|
- **Next:** /plastic-intent-executing.
|
|
123
123
|
|
|
124
124
|
## Git Commit
|
|
125
125
|
|
|
126
|
-
After writing all artifacts (plan.md, checklist.md, actions/
|
|
126
|
+
After writing all artifacts (plan.md, checklist.md, and the actions/ACTION_N.md files), commit to the store:
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
129
|
cd {store_root} && git add . && git commit -m "docs: plan for intent {id}: {name}"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"skill_name": "plastic-intent-planning",
|
|
3
|
-
"notes": "Intent 164
|
|
3
|
+
"notes": "Intent 164, updated by 133a. Evals for the writing-plans skill: action-file enforcement at every tier (S/M consolidate the delivery into one real actions/ACTION_1.md, L writes one actions/ACTION_N.md per task) and the checklist FORM plus [ORCHESTRATOR] hard-gate item convention. All assertions are pending a real observed run per the plastic-evaluating-skills convention.",
|
|
4
4
|
"evals": [
|
|
5
5
|
{
|
|
6
6
|
"id": 1,
|
|
7
7
|
"scope": "behavior",
|
|
8
8
|
"set": "train",
|
|
9
9
|
"prompt": "spec.md is stamped Tier: M and describes a single subsystem change touching four files across two tasks. Write the plan.",
|
|
10
|
-
"expected_output": "Produces
|
|
10
|
+
"expected_output": "Produces plan.md plus checklist.md plus at least one real actions/ACTION_N.md. At M the planner consolidates the whole delivery into a single actions/ACTION_1.md (it may split into a few files if that reads cleaner); the actions/ directory is never left empty or .gitkeep-only. checklist.md mirrors plan.md's task list.",
|
|
11
11
|
"files": [],
|
|
12
12
|
"assertions": [
|
|
13
13
|
{
|
|
14
14
|
"type": "code",
|
|
15
|
-
"check": "
|
|
15
|
+
"check": "at least one real (non-empty, non-.gitkeep) file exists under actions/ for this intent; checklist.md exists and mirrors plan.md's task list",
|
|
16
16
|
"result": "expect-pass"
|
|
17
17
|
}
|
|
18
18
|
]
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"scope": "behavior",
|
|
23
23
|
"set": "train",
|
|
24
24
|
"prompt": "spec.md is stamped Tier: S and describes a single-file config change. Write the plan.",
|
|
25
|
-
"expected_output": "Produces
|
|
25
|
+
"expected_output": "Produces plan.md plus checklist.md plus one consolidated actions/ACTION_1.md carrying the whole ordered delivery (the steps plus the exact changes). The actions/ directory is never left empty or .gitkeep-only, matching the every-tier action-file rule.",
|
|
26
26
|
"files": [],
|
|
27
27
|
"assertions": [
|
|
28
28
|
{
|
|
29
29
|
"type": "code",
|
|
30
|
-
"check": "
|
|
30
|
+
"check": "at least one real (non-empty, non-.gitkeep) actions/ACTION_N.md exists under actions/ for this intent; no empty or .gitkeep-only actions/ dir remains",
|
|
31
31
|
"result": "expect-pass"
|
|
32
32
|
}
|
|
33
33
|
]
|
|
@@ -61,6 +61,21 @@
|
|
|
61
61
|
"result": "expect-pass"
|
|
62
62
|
}
|
|
63
63
|
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": 5,
|
|
67
|
+
"scope": "behavior",
|
|
68
|
+
"set": "validation",
|
|
69
|
+
"prompt": "spec.md is stamped Tier: S. A prior draft left actions/ with only a .gitkeep. Write the plan and its actions.",
|
|
70
|
+
"expected_output": "Writes at least one real actions/ACTION_1.md (non-empty, not the placeholder sentinel, not a .gitkeep) that consolidates the whole S-tier delivery. Leaving actions/ empty or .gitkeep-only is a plan failure that would block the How gate; the planner never does this at any tier.",
|
|
71
|
+
"files": [],
|
|
72
|
+
"assertions": [
|
|
73
|
+
{
|
|
74
|
+
"type": "code",
|
|
75
|
+
"check": "actions/ contains at least one real action file (non-empty *.md, first line not the placeholder sentinel); no .gitkeep-only or empty actions/ dir remains",
|
|
76
|
+
"result": "expect-pass"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
64
79
|
}
|
|
65
80
|
]
|
|
66
81
|
}
|
|
@@ -28,8 +28,9 @@ plan.md so the shape matches on the first pass.
|
|
|
28
28
|
|
|
29
29
|
**Intent:** {id}: {name}
|
|
30
30
|
|
|
31
|
-
**Tier:** S|M|L, copied from the spec's stamped `Tier:` line.
|
|
32
|
-
|
|
31
|
+
**Tier:** S|M|L, copied from the spec's stamped `Tier:` line. Every tier produces at
|
|
32
|
+
least one real `actions/ACTION_N.md`; S and M consolidate the delivery into one action
|
|
33
|
+
file, L uses one `actions/ACTION_N.md` per task.
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
```
|
|
@@ -112,10 +113,13 @@ of tier (S, M, or L):
|
|
|
112
113
|
|
|
113
114
|
## actions/ACTION_N.md format
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
Every tier writes at least one real action file here (see the parent SKILL.md's
|
|
117
|
+
`## Tier shapes`): S and M consolidate the whole delivery into a single
|
|
118
|
+
`actions/ACTION_1.md`, L writes one file per task.
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
Each action is self-contained: a subagent (or a solo executor) can execute it without
|
|
121
|
+
reading the plan. It carries the ordered steps to deliver the task plus the exact changes
|
|
122
|
+
to make (for code, the anchor or `file:line` and the old/new text).
|
|
119
123
|
|
|
120
124
|
```markdown
|
|
121
125
|
# Action {N}: {task title}
|
|
@@ -71,10 +71,11 @@ an answer given in station 3.
|
|
|
71
71
|
|
|
72
72
|
Type `/plastic-intent-planning`.
|
|
73
73
|
|
|
74
|
-
Artifact: `plan.md
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
Artifact: `plan.md`, `checklist.md`, and at least one real `actions/ACTION_N.md`. At the S
|
|
75
|
+
tier used here, the planner consolidates the whole delivery into a single
|
|
76
|
+
`actions/ACTION_1.md` (the ordered steps plus the exact changes); the L tier (many
|
|
77
|
+
independent tasks, dispatched in parallel) instead gets one `actions/ACTION_N.md` file per
|
|
78
|
+
task. `checklist.md` follows a fixed form: tasks start under `## In Progress`, move to
|
|
78
79
|
`## Completed` as they land, and a `## Session Log` table records what happened each session.
|
|
79
80
|
A task that depends on an owner decision landing first (a destructive step, a structural
|
|
80
81
|
ruling) gets an `[ORCHESTRATOR]` prefix and blocks every other item until that decision is
|