@zalom/plastic 1.1.0 → 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 +12 -9
- package/README.md +3 -3
- package/agents/plastic-enforcer.md +8 -4
- package/agents/plastic-executor.md +5 -5
- package/agents/plastic-future-intent-researcher.md +1 -1
- package/agents/plastic-planner.md +15 -11
- package/hooks/check-update +1 -1
- package/hooks/continue +1 -1
- package/package.json +1 -1
- package/scripts/dashboard.rb +29 -24
- package/scripts/doctor.rb +180 -5
- package/scripts/hook-continue +3 -3
- package/scripts/install.rb +2 -1
- package/scripts/lib/bridge.rb +114 -11
- package/scripts/lib/dashboard_banner.rb +8 -9
- package/scripts/lib/installer_core.rb +12 -3
- package/scripts/lib/legacy_bookend_amnesty.rb +35 -0
- package/scripts/lib/release_guard.rb +62 -0
- package/scripts/lib/roadmap_queue.rb +285 -0
- package/scripts/lib/roadmap_savepoint.rb +213 -0
- package/scripts/lib/skill_lint.rb +304 -0
- package/scripts/lib/worktree.rb +21 -0
- package/scripts/new-intent +1 -0
- package/scripts/read-config +3 -3
- package/scripts/roadmap-next +44 -0
- package/scripts/roadmap-savepoint +64 -0
- package/scripts/skill-lint +50 -0
- package/skills/auto/SKILL.md +32 -11
- package/skills/auto/references/tiers.md +4 -3
- package/skills/continuing/SKILL.md +34 -0
- package/skills/continuing/evals/evals.json +91 -0
- package/skills/dashboard/SKILL.md +17 -14
- package/skills/dashboard/references/classification.md +3 -3
- package/skills/dashboard/templates/dashboard-global.md +8 -23
- package/skills/dashboard/templates/dashboard-project.md +7 -26
- package/skills/doctor/SKILL.md +1 -1
- package/skills/install/SKILL.md +10 -10
- package/skills/intent-continuing/SKILL.md +26 -68
- package/skills/intent-continuing/evals/evals.json +26 -26
- package/skills/intent-continuing/references/context-management.md +15 -19
- 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/intent-savepoint/SKILL.md +12 -0
- package/skills/intent-starting/evals/evals.json +1 -1
- package/skills/project-continuing/SKILL.md +104 -0
- package/skills/project-continuing/evals/evals.json +100 -0
- package/skills/project-continuing/references/board-fill.md +33 -0
- package/skills/releasing/SKILL.md +48 -0
- package/skills/releasing/references/release-lines.md +105 -0
- package/skills/roadmap/SKILL.md +7 -1
- package/skills/roadmap/references/file-format.md +30 -1
- package/skills/roadmap/references/operations.md +26 -6
- package/skills/roadmap-continuing/SKILL.md +85 -0
- package/skills/roadmap-continuing/evals/evals.json +82 -0
- package/skills/roadmap-continuing/references/liveness-ranking.md +56 -0
- package/skills/skill-evaluating/evals/evals.json +1 -1
- package/skills/tutorial/references/track-1-guided.md +5 -4
- package/skills/tutorial/references/track-2-auto.md +1 -1
- package/skills/uninstall/SKILL.md +2 -2
- package/skills/update/SKILL.md +2 -2
- package/templates/config.yml +2 -1
- package/templates/index.md +4 -1
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
|
|
@@ -167,8 +168,10 @@ and Exec, spins up the team above, and works the dashboard's dispatchable queue.
|
|
|
167
168
|
`human_only` (intents that need a person); auto mode consumes the former.
|
|
168
169
|
|
|
169
170
|
**Model contract.** Every agent in `agents/*.md` pins an explicit Claude Code model alias in
|
|
170
|
-
its own frontmatter: `opus`, `sonnet`, or `haiku`. Never `inherit`, never Fable
|
|
171
|
-
|
|
171
|
+
its own frontmatter: `opus`, `sonnet`, or `haiku`. Never `inherit`, never Fable by default,
|
|
172
|
+
unless an explicit `agents.models.<name>` config override names Fable for that role, in which
|
|
173
|
+
case the override is honored as written. Aliases track "latest per tier" so no Plastic release
|
|
174
|
+
is required to advance a tier. The tier by role:
|
|
172
175
|
`plastic-enforcer`, `plastic-brainstorming`, `plastic-planner` are `opus`;
|
|
173
176
|
`plastic-spec-specialist`, `plastic-executor`, `plastic-intent-curator`,
|
|
174
177
|
`plastic-future-intent-researcher`, `plastic-intent-discovery` are `sonnet`.
|
|
@@ -423,8 +426,8 @@ purge. See `docs/internals.md` for depth.
|
|
|
423
426
|
| Transition | Trigger | Gate |
|
|
424
427
|
|---|---|---|
|
|
425
428
|
| What → Why | `spec.md` written | (none) |
|
|
426
|
-
| Why → How | `plan.md` + `actions
|
|
427
|
-
| 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 |
|
|
428
431
|
| Exec → Done | `outcome.md` written | All checklist items checked |
|
|
429
432
|
|
|
430
433
|
Hard blocking: hooks exit code 2 on gate failure.
|
|
@@ -528,7 +531,7 @@ each station.
|
|
|
528
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 |
|
|
529
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` |
|
|
530
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` |
|
|
531
|
-
| 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` |
|
|
532
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 |
|
|
533
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 |
|
|
534
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 |
|
package/README.md
CHANGED
|
@@ -136,8 +136,8 @@ Walk this once in
|
|
|
136
136
|
| Family | Commands |
|
|
137
137
|
| ------ | -------- |
|
|
138
138
|
| Mode | `plastic-tutorial`, `plastic-auto` |
|
|
139
|
-
| Intent | `plastic-intent-creating`, `plastic-intent-starting`, `plastic-intent-brainstorming`, `plastic-intent-grilling`, `plastic-intent-speccing`, `plastic-intent-planning`, `plastic-intent-executing`, `plastic-intent-ending`, `plastic-intent-continuing`, `plastic-intent-researching` |
|
|
140
|
-
| Project and delivery | `plastic-project-creating`, `plastic-roadmap`, `plastic-releasing` |
|
|
139
|
+
| Intent | `plastic-continuing`, `plastic-intent-creating`, `plastic-intent-starting`, `plastic-intent-brainstorming`, `plastic-intent-grilling`, `plastic-intent-speccing`, `plastic-intent-planning`, `plastic-intent-executing`, `plastic-intent-ending`, `plastic-intent-continuing`, `plastic-intent-researching` |
|
|
140
|
+
| Project and delivery | `plastic-project-creating`, `plastic-project-continuing`, `plastic-roadmap`, `plastic-roadmap-continuing`, `plastic-releasing` |
|
|
141
141
|
| Skill | `plastic-skill-creating`, `plastic-skill-evaluating` |
|
|
142
142
|
| Product | `plastic-install`, `plastic-update`, `plastic-uninstall`, `plastic-rollback`, `plastic-doctor`, `plastic-humanizer` |
|
|
143
143
|
|
|
@@ -165,7 +165,7 @@ Plastic is developed through Plastic.
|
|
|
165
165
|
The roadmap, intent history, plans, decisions, and outcomes
|
|
166
166
|
behind releases are part of the repository, not a hidden process.
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
[changelog](CHANGELOG.md).
|
|
169
169
|
|
|
170
170
|
## Documentation
|
|
171
171
|
|
|
@@ -12,7 +12,9 @@ You are the Plastic Enforcer. You ARE the auto orchestrator, spanning the whole
|
|
|
12
12
|
orchestrating main session on the best available thinking model (Fable, Opus, or whatever
|
|
13
13
|
supersedes them) for the sharpest gating and synthesis. This is advice only: it changes no
|
|
14
14
|
behavior and blocks nothing if ignored. It concerns the human's MAIN session; dispatched
|
|
15
|
-
subagents keep their pinned tier and never resolve to Fable
|
|
15
|
+
subagents keep their pinned tier and never resolve to Fable, unless an explicit
|
|
16
|
+
`agents.models.<name>` config override names Fable for that role, in which case the override
|
|
17
|
+
is honored as written.
|
|
16
18
|
|
|
17
19
|
## Your Responsibilities
|
|
18
20
|
|
|
@@ -29,8 +31,9 @@ subagents keep their pinned tier and never resolve to Fable.
|
|
|
29
31
|
4. **Sequence the team** — dispatch specialists per the chosen topology with a constructed
|
|
30
32
|
context bundle:
|
|
31
33
|
- S/M: ONE thinker agent, one boot, two stations — it writes spec.md, then plan.md +
|
|
32
|
-
checklist.md, in a single context. Sections may be one
|
|
33
|
-
|
|
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
|
|
34
37
|
discovery deposit when chain and sources are both empty. A sonnet executor implements.
|
|
35
38
|
- L: today's full team, one specialist per stage (brainstorming, spec-specialist, planner,
|
|
36
39
|
executor), each in a fresh context.
|
|
@@ -42,7 +45,8 @@ dispatch also resolve the target agent's model through the config chain (`read-c
|
|
|
42
45
|
agents.models.<basename> --project <repo>`: project override, then global, then the shipped
|
|
43
46
|
tier default) and pass it explicitly as the dispatch call's model parameter, alongside the
|
|
44
47
|
spawn-preamble live-state injection. Never rely on the dispatched role's frontmatter alone. A
|
|
45
|
-
resolved subagent model is never Fable.
|
|
48
|
+
resolved subagent model is never Fable, unless an explicit `agents.models.<name>` config
|
|
49
|
+
override names Fable for that role, in which case the override is honored as written.
|
|
46
50
|
5. **Gate each handoff** — check each stage deliverable against its exit criteria before handing to the next stage
|
|
47
51
|
6. **Run the final review** — at the final gate, dispatch an INDEPENDENT reviewer subagent (not a sixth standing role)
|
|
48
52
|
|
|
@@ -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
|
|
@@ -31,4 +31,4 @@ You are the Plastic Future Intent Researcher. Your role is to pick up parked fut
|
|
|
31
31
|
- You never modify `## Insights` or `## Outcome` sections — those belong to the worker
|
|
32
32
|
- You use Read, WebSearch, WebFetch, and Bash (read-only grep/find) for research
|
|
33
33
|
- You never change status fields — status is convention-derived from INDEX.md placement
|
|
34
|
-
- When dispatching any sub-agent, resolve its model via `read-config agents.models.<basename> --project <repo>` and pass it explicitly at dispatch, never relying on inherited frontmatter; a resolved subagent model is never Fable
|
|
34
|
+
- When dispatching any sub-agent, resolve its model via `read-config agents.models.<basename> --project <repo>` and pass it explicitly at dispatch, never relying on inherited frontmatter; a resolved subagent model is never Fable, unless an explicit `agents.models.<name>` config override names Fable for that role, in which case the override is honored as written
|
|
@@ -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/hooks/check-update
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# upgrade target instead of blindly trusting the `latest` tag.
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
-
PLASTIC_HOME="$HOME/.plastic"
|
|
8
|
+
PLASTIC_HOME="${PLASTIC_HOME:-$HOME/.plastic}"
|
|
9
9
|
CACHE_DIR="$PLASTIC_HOME/.cache"
|
|
10
10
|
CACHE_FILE="$CACHE_DIR/update-check.json"
|
|
11
11
|
VERSION_FILE="$PLASTIC_HOME/VERSION"
|
package/hooks/continue
CHANGED
|
@@ -23,7 +23,7 @@ if echo "$MESSAGE" | grep -iq '\bcontinue\b'; then
|
|
|
23
23
|
{
|
|
24
24
|
"hookSpecificOutput": {
|
|
25
25
|
"hookEventName": "UserPromptSubmit",
|
|
26
|
-
"additionalContext": "PLASTIC CONTINUE
|
|
26
|
+
"additionalContext": "PLASTIC CONTINUE: The user wants to resume previous work.\n\nRead ~/.plastic/INDEX.md and follow the plastic-continuing skill workflow."
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
HOOKJSON
|
package/package.json
CHANGED
package/scripts/dashboard.rb
CHANGED
|
@@ -33,7 +33,7 @@ def today
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# A generic "about a month" threshold, not tuned to any one store's item count.
|
|
36
|
-
# Deliberately independent from plastic-
|
|
36
|
+
# Deliberately independent from plastic-project-continuing's separate stale_threshold_days
|
|
37
37
|
# config: that one is a proactive boot-time triage nudge, this is a board annotation.
|
|
38
38
|
# Unifying the two is a follow-up, not this intent.
|
|
39
39
|
STALE_DAYS = 30
|
|
@@ -355,10 +355,7 @@ LIFECYCLE_GLYPH = { "what" => "○", "why" => "◔", "how" => "◑", "exec" => "
|
|
|
355
355
|
DISPOSITION_GLYPH = { "drive" => "▸", "defer" => "⇢", "research" => "⊙", "triage" => "⚑" }.freeze
|
|
356
356
|
LEGEND = "legend ○ What ◔ Why ◑ How ◕ Exec ● Done │ ▸drive ⇢defer ⊙research ⚑triage │ ⇡unblocked"
|
|
357
357
|
|
|
358
|
-
# Markdown board glyphs (intent 37).
|
|
359
|
-
QUADRANT_BULLET = {
|
|
360
|
-
"quick_win" => "⚡", "next_big" => "★", "defer" => "→", "triage" => "⚑",
|
|
361
|
-
}.freeze
|
|
358
|
+
# Markdown board glyphs (intent 37). Status signature for the project active/future lists.
|
|
362
359
|
STATUS_GLYPH = { "active" => "◑", "completed" => "●", "future" => "○" }.freeze
|
|
363
360
|
|
|
364
361
|
# ---------------------------------------------------------------------------
|
|
@@ -375,10 +372,10 @@ end
|
|
|
375
372
|
CELL_CAP = 6
|
|
376
373
|
|
|
377
374
|
# Markdown-board caps (Task 5, D6/D7): the ASCII renderer already caps via CELL_CAP/
|
|
378
|
-
# cap_cell, but the Markdown board's
|
|
375
|
+
# cap_cell, but the Markdown board's next_work list and the project board's
|
|
379
376
|
# active/future lists had no cap and no per-line truncation, so a large store printed
|
|
380
377
|
# hundreds of full-length lines. These two constants fix that on the Markdown side only.
|
|
381
|
-
|
|
378
|
+
NEXT_WORK_CAP = 8
|
|
382
379
|
INTENT_LINE_MAX_CHARS = 120
|
|
383
380
|
|
|
384
381
|
def matrix(records, scope_tag: false)
|
|
@@ -584,31 +581,39 @@ def intent_line(rec, bullet)
|
|
|
584
581
|
scope: rec[:scope], line: "#{bullet} #{rec[:id]} #{text}#{note}".rstrip }
|
|
585
582
|
end
|
|
586
583
|
|
|
587
|
-
# Cap a raw record list to
|
|
584
|
+
# Cap a raw record list to NEXT_WORK_CAP entries, then map to intent_line-shaped
|
|
588
585
|
# hashes, appending a plain "+N more" line (no id, not a real record) when truncated.
|
|
589
586
|
# Caps the record list first so the "+N more" entry never goes through intent_line.
|
|
590
587
|
def cap_lines(list, bullet)
|
|
591
|
-
capped = list.first(
|
|
588
|
+
capped = list.first(NEXT_WORK_CAP)
|
|
592
589
|
lines = capped.map { |r| intent_line(r, bullet) }
|
|
593
|
-
if list.size >
|
|
590
|
+
if list.size > NEXT_WORK_CAP
|
|
594
591
|
lines << { id: "", intent: "", created: "", bullet: bullet, scope: "",
|
|
595
|
-
line: "#{bullet} +#{list.size -
|
|
592
|
+
line: "#{bullet} +#{list.size - NEXT_WORK_CAP} more" }
|
|
596
593
|
end
|
|
597
594
|
lines
|
|
598
595
|
end
|
|
599
596
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
597
|
+
# Flat, rank-ordered "most-valuable next work" list (intent 149). Replaces the
|
|
598
|
+
# quadrant-keyed matrix_data with a single list ranked by the same rank_key every
|
|
599
|
+
# other ordering uses, capped at NEXT_WORK_CAP with a trailing "+N more" marker
|
|
600
|
+
# when the pool overflows. No grid glyph on the line; the prose surface supplies
|
|
601
|
+
# its own bullet.
|
|
602
|
+
def next_work(records)
|
|
603
|
+
ranked = records.sort_by { |r| rank_key(r) }
|
|
604
|
+
capped = ranked.first(NEXT_WORK_CAP)
|
|
605
|
+
lines = capped.map do |r|
|
|
606
|
+
text = r[:intent].to_s
|
|
607
|
+
text = "#{text[0, INTENT_LINE_MAX_CHARS]}…" if text.length > INTENT_LINE_MAX_CHARS
|
|
608
|
+
{ id: r[:id], intent: r[:intent], scope: r[:scope], lifecycle: r[:lifecycle],
|
|
609
|
+
value: r[:value].to_s, disposition: r[:disposition], flags: r[:flags],
|
|
610
|
+
line: "#{r[:id]} #{text}" }
|
|
606
611
|
end
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
+
if ranked.size > NEXT_WORK_CAP
|
|
613
|
+
lines << { id: "", intent: "", scope: "", lifecycle: "", value: "", disposition: "",
|
|
614
|
+
flags: [], line: "+#{ranked.size - NEXT_WORK_CAP} more" }
|
|
615
|
+
end
|
|
616
|
+
lines
|
|
612
617
|
end
|
|
613
618
|
|
|
614
619
|
def short_description(scope)
|
|
@@ -654,7 +659,7 @@ def render_data_global(records)
|
|
|
654
659
|
{ mode: "global", date: today.to_s,
|
|
655
660
|
store_health: store_health(:global),
|
|
656
661
|
recently_worked: recently_worked(records),
|
|
657
|
-
|
|
662
|
+
next_work: next_work(matrix_pool),
|
|
658
663
|
counts: counts_of(global),
|
|
659
664
|
projects: projs,
|
|
660
665
|
project_totals: {
|
|
@@ -671,7 +676,7 @@ def render_data_project(records, slug)
|
|
|
671
676
|
store_health: store_health(slug),
|
|
672
677
|
description: short_description(scope),
|
|
673
678
|
recently_worked: recently_worked(records, project_scope: scope),
|
|
674
|
-
|
|
679
|
+
next_work: next_work(matrix_pool),
|
|
675
680
|
counts: counts_of(scoped),
|
|
676
681
|
active: cap_lines(scoped.select { |r| r[:status] == "active" }
|
|
677
682
|
.sort_by { |r| invert_ts(r[:last_accessed_at]) }, STATUS_GLYPH["active"]),
|
package/scripts/doctor.rb
CHANGED
|
@@ -23,6 +23,9 @@ require_relative "lib/links_section"
|
|
|
23
23
|
require_relative "lib/hook_registry"
|
|
24
24
|
require_relative "lib/lock"
|
|
25
25
|
require_relative "lib/bridge"
|
|
26
|
+
require_relative "lib/agent_models"
|
|
27
|
+
require_relative "lib/legacy_bookend_amnesty"
|
|
28
|
+
require_relative "lib/skill_lint"
|
|
26
29
|
|
|
27
30
|
# Diagnostic engine, instantiable with an injected store/agent map so tests can
|
|
28
31
|
# run it hermetically (no eval, no global-constant rewriting).
|
|
@@ -41,6 +44,11 @@ class Doctor
|
|
|
41
44
|
# (intent 60). Alias it here so the two can never drift.
|
|
42
45
|
REQUIRED_FRONTMATTER_FIELDS = IntentValidator::REQUIRED_FIELDS
|
|
43
46
|
|
|
47
|
+
# Single source of truth for the pre-161 legacy Done-bookend amnesty list
|
|
48
|
+
# lives in LegacyBookendAmnesty (intent 170a). Alias it here so the two can
|
|
49
|
+
# never drift.
|
|
50
|
+
LEGACY_BOOKEND_AMNESTY = LegacyBookendAmnesty::LIST
|
|
51
|
+
|
|
44
52
|
CLAUDE_HOOK_SCRIPTS = %w[
|
|
45
53
|
plastic-session-start
|
|
46
54
|
plastic-check-update
|
|
@@ -66,9 +74,11 @@ class Doctor
|
|
|
66
74
|
|
|
67
75
|
attr_reader :plastic_home, :agents
|
|
68
76
|
|
|
69
|
-
def initialize(plastic_home: DEFAULT_PLASTIC_HOME, agents: DEFAULT_AGENTS
|
|
77
|
+
def initialize(plastic_home: DEFAULT_PLASTIC_HOME, agents: DEFAULT_AGENTS,
|
|
78
|
+
bookend_amnesty: LEGACY_BOOKEND_AMNESTY)
|
|
70
79
|
@plastic_home = plastic_home
|
|
71
80
|
@agents = agents
|
|
81
|
+
@bookend_amnesty = bookend_amnesty
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
# --- Flag parsing ---
|
|
@@ -577,6 +587,7 @@ class Doctor
|
|
|
577
587
|
conflicts = [] # canonical INDEX-wins disagreement (fail)
|
|
578
588
|
gaps = [] # terminal completeness gaps on immutable/legacy history (warn)
|
|
579
589
|
stalled = [] # terminal but End tail unfinished (warn)
|
|
590
|
+
phantoms = [] # savepoint.md line(s) disk evidence contradicts (warn, never fail; intent 134)
|
|
580
591
|
|
|
581
592
|
done_signal_stores(scopes).each do |store|
|
|
582
593
|
index_sections_by_dir(store[:index]).each do |dirname, in_sections|
|
|
@@ -596,6 +607,24 @@ class Doctor
|
|
|
596
607
|
"(INDEX is canonical — move it to its terminal section or revert outcome.md)"
|
|
597
608
|
end
|
|
598
609
|
|
|
610
|
+
# Savepoint truthfulness (advisory, never fail; intent 134): a line the disk contradicts
|
|
611
|
+
# (a phantom milestone, a duplicate pair, or a state line with an absent prerequisite).
|
|
612
|
+
# Live intents auto-rebuild via plastic-intent-savepoint; terminal intents are immutable,
|
|
613
|
+
# so a phantom there is report-only. Composition with intent 170a: a pre-161 legacy intent
|
|
614
|
+
# on the frozen bookend amnesty is grandfathered here too. Its immutable history carries
|
|
615
|
+
# known pre-convention drift (the same class the audit-echo gap forgives), so it must not
|
|
616
|
+
# resurface through this advisory. The suppression is scope-qualified exactly like the
|
|
617
|
+
# audit-echo amnesty; every live intent and every non-amnestied terminal still fires.
|
|
618
|
+
phantom_lines = Bridge.savepoint_phantom_lines(dir)
|
|
619
|
+
phantom_id = dirname.split("--", 2).first
|
|
620
|
+
phantom_amnestied = terminal && @bookend_amnesty.fetch(store[:scope], []).include?(phantom_id)
|
|
621
|
+
if phantom_lines.any? && !phantom_amnestied
|
|
622
|
+
detail = phantom_lines.map { |line, reason| "#{line} (#{reason})" }.join("; ")
|
|
623
|
+
scope_note = terminal ? "terminal in INDEX, report-only (immutable history)" : "live intent, auto-rebuildable"
|
|
624
|
+
phantoms << "#{label}: #{phantom_lines.size} phantom savepoint line(s) contradicted by " \
|
|
625
|
+
"disk, #{scope_note}: #{detail}"
|
|
626
|
+
end
|
|
627
|
+
|
|
599
628
|
# Completeness gap (warn, not fail): a terminal intent whose outcome.md is
|
|
600
629
|
# missing/placeholder. Legacy terminal intents predate this convention and
|
|
601
630
|
# are immutable, so this is advisory, never breakage.
|
|
@@ -608,11 +637,18 @@ class Doctor
|
|
|
608
637
|
next unless terminal
|
|
609
638
|
|
|
610
639
|
# Audit echo (weakest signal, D1): the savepoint should carry a
|
|
611
|
-
# `Done delivered|abandoned` line. Missing on legacy history -> advisory
|
|
640
|
+
# `Done delivered|abandoned` line. Missing on legacy history -> advisory
|
|
641
|
+
# gap, unless the (scope, id) is on the frozen pre-161 amnesty list
|
|
642
|
+
# (intent 170a). The amnesty is scope-qualified: an id on the list for
|
|
643
|
+
# one scope does not grandfather the same id in another scope.
|
|
612
644
|
savepoint = File.join(dir, "savepoint.md")
|
|
613
645
|
if File.exist?(savepoint) && File.read(savepoint) !~ /\bDone\b.*\b(delivered|abandoned)\b/
|
|
614
|
-
|
|
615
|
-
|
|
646
|
+
id = dirname.split("--", 2).first
|
|
647
|
+
amnestied = @bookend_amnesty.fetch(store[:scope], []).include?(id)
|
|
648
|
+
unless amnestied
|
|
649
|
+
gaps << "#{label}: terminal in INDEX but savepoint.md has no " \
|
|
650
|
+
"`Done delivered|abandoned` line (audit echo missing)"
|
|
651
|
+
end
|
|
616
652
|
end
|
|
617
653
|
|
|
618
654
|
# Stalled completion: the End tail never released the delivery lock, so the
|
|
@@ -682,6 +718,26 @@ class Doctor
|
|
|
682
718
|
)
|
|
683
719
|
end
|
|
684
720
|
|
|
721
|
+
# savepoint_truthful: advisory only, never fails the run (intent 134). A phantom line on a
|
|
722
|
+
# live intent auto-rebuilds; on terminal (immutable) history it stays report-only.
|
|
723
|
+
if phantoms.empty?
|
|
724
|
+
checks << check(
|
|
725
|
+
category: "done_signals", name: "savepoint_truthful", status: "pass",
|
|
726
|
+
message: "No savepoint.md lines are contradicted by disk (phantom-line check clean)"
|
|
727
|
+
)
|
|
728
|
+
else
|
|
729
|
+
checks << check(
|
|
730
|
+
category: "done_signals", name: "savepoint_truthful", status: "warn",
|
|
731
|
+
message: "#{phantoms.size} intent#{phantoms.size == 1 ? "" : "s"} carry savepoint.md " \
|
|
732
|
+
"line(s) contradicted by disk (advisory; terminal history stays report-only)",
|
|
733
|
+
details: phantoms, fixable: true,
|
|
734
|
+
fix_hint: "For a live (Active) intent, run plastic-intent-savepoint to rebuild via " \
|
|
735
|
+
"Bridge.rebuild_savepoint. Terminal (Completed/Abandoned) intents are immutable: " \
|
|
736
|
+
"a phantom there stays advisory unless an explicit human grant authorizes the " \
|
|
737
|
+
"124a manual Done-bookend repair."
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
|
|
685
741
|
checks
|
|
686
742
|
end
|
|
687
743
|
|
|
@@ -1271,9 +1327,85 @@ class Doctor
|
|
|
1271
1327
|
end
|
|
1272
1328
|
end
|
|
1273
1329
|
|
|
1330
|
+
checks += check_agent_model_drift(agent_key)
|
|
1331
|
+
|
|
1274
1332
|
checks
|
|
1275
1333
|
end
|
|
1276
1334
|
|
|
1335
|
+
# agent_model_drift - advisory (never fail) config-vs-frontmatter comparison
|
|
1336
|
+
# for every installed <agent_dir>/agents/plastic-*.md file (intent 170, D1).
|
|
1337
|
+
# Resolution mirrors read-config's own precedence (project override -> global
|
|
1338
|
+
# override -> shipped AgentModels::TIER_DEFAULTS), reusing AgentModels'
|
|
1339
|
+
# `override_map` directly so this stays hermetically DI-testable (no
|
|
1340
|
+
# shell-out to `read-config`, no ENV/global seam). There is no live project
|
|
1341
|
+
# scope at doctor-run time for a globally-installed agent file, so the
|
|
1342
|
+
# project layer is empty here; the resolver still supports one, matching the
|
|
1343
|
+
# same `override_map(project_config:, global_config:)` shape the installer
|
|
1344
|
+
# already calls.
|
|
1345
|
+
#
|
|
1346
|
+
# Classification per installed agent basename:
|
|
1347
|
+
# - no `agents.models.<basename>` override AND frontmatter == resolved
|
|
1348
|
+
# default -> pass (clean)
|
|
1349
|
+
# - no override AND frontmatter != resolved default -> warn (real drift)
|
|
1350
|
+
# - an override IS configured -> pass/informational, LISTED as a
|
|
1351
|
+
# sanctioned override, regardless of whether frontmatter matches (that
|
|
1352
|
+
# mismatch is the override working, e.g. `plastic-brainstorming: fable`)
|
|
1353
|
+
# - zero installed agent files -> pass (nothing to check)
|
|
1354
|
+
# This check never returns "fail".
|
|
1355
|
+
def check_agent_model_drift(agent_key)
|
|
1356
|
+
agent_config = agents[agent_key]
|
|
1357
|
+
return [] unless agent_config
|
|
1358
|
+
|
|
1359
|
+
agents_dir = File.join(agent_config[:dir], "agents")
|
|
1360
|
+
installed = Dir.glob(File.join(agents_dir, "plastic-*.md")).sort
|
|
1361
|
+
|
|
1362
|
+
if installed.empty?
|
|
1363
|
+
return [check(
|
|
1364
|
+
category: "core_files", name: "agent_model_drift", status: "pass",
|
|
1365
|
+
message: "No installed plastic-* agent files to check for model drift"
|
|
1366
|
+
)]
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
global_config = load_yaml_safe(File.join(plastic_home, "config.yml")) || {}
|
|
1370
|
+
overrides = AgentModels.override_map(project_config: {}, global_config: global_config)
|
|
1371
|
+
|
|
1372
|
+
drifted = []
|
|
1373
|
+
sanctioned = []
|
|
1374
|
+
|
|
1375
|
+
installed.each do |path|
|
|
1376
|
+
basename = File.basename(path, ".md")
|
|
1377
|
+
installed_model = (parse_frontmatter(path) || {})["model"]
|
|
1378
|
+
resolved_default = AgentModels::TIER_DEFAULTS[basename]
|
|
1379
|
+
override = overrides[basename]
|
|
1380
|
+
|
|
1381
|
+
if override
|
|
1382
|
+
sanctioned << "#{basename}: frontmatter=#{installed_model.inspect}, sanctioned override=#{override.inspect}"
|
|
1383
|
+
elsif installed_model != resolved_default
|
|
1384
|
+
drifted << "#{basename}: frontmatter=#{installed_model.inspect}, resolved default=#{resolved_default.inspect}"
|
|
1385
|
+
end
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
if drifted.empty?
|
|
1389
|
+
message = if sanctioned.empty?
|
|
1390
|
+
"No agent-model drift (#{installed.size} installed agent(s) match the resolved default)"
|
|
1391
|
+
else
|
|
1392
|
+
"No unsanctioned agent-model drift; #{sanctioned.size} sanctioned override(s) in effect"
|
|
1393
|
+
end
|
|
1394
|
+
[check(
|
|
1395
|
+
category: "core_files", name: "agent_model_drift", status: "pass",
|
|
1396
|
+
message: message,
|
|
1397
|
+
details: sanctioned
|
|
1398
|
+
)]
|
|
1399
|
+
else
|
|
1400
|
+
[check(
|
|
1401
|
+
category: "core_files", name: "agent_model_drift", status: "warn",
|
|
1402
|
+
message: "#{drifted.size} installed agent(s) have unsanctioned model drift vs the config-resolved default",
|
|
1403
|
+
details: drifted + sanctioned,
|
|
1404
|
+
fixable: false
|
|
1405
|
+
)]
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1277
1409
|
# --- Check category: manifest sync (binary core integrity) ---
|
|
1278
1410
|
|
|
1279
1411
|
# Verify, for BOTH the global manifest and the agent-side manifest, that every
|
|
@@ -1674,6 +1806,48 @@ class Doctor
|
|
|
1674
1806
|
)
|
|
1675
1807
|
end
|
|
1676
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
|
+
|
|
1677
1851
|
# --- Run all checks ---
|
|
1678
1852
|
|
|
1679
1853
|
def run_checks(agent_key)
|
|
@@ -1686,6 +1860,7 @@ class Doctor
|
|
|
1686
1860
|
all_checks += check_deprecations
|
|
1687
1861
|
all_checks += check_qmd
|
|
1688
1862
|
all_checks += check_done_signals
|
|
1863
|
+
all_checks += check_skill_lint
|
|
1689
1864
|
|
|
1690
1865
|
summarize(all_checks, agent_key)
|
|
1691
1866
|
end
|
|
@@ -1801,4 +1976,4 @@ class Doctor
|
|
|
1801
1976
|
|
|
1802
1977
|
end
|
|
1803
1978
|
|
|
1804
|
-
Doctor.new.cli(ARGV) if $PROGRAM_NAME == __FILE__
|
|
1979
|
+
Doctor.new(plastic_home: ENV.fetch("PLASTIC_HOME") { Doctor::DEFAULT_PLASTIC_HOME }).cli(ARGV) if $PROGRAM_NAME == __FILE__
|
package/scripts/hook-continue
CHANGED
|
@@ -19,9 +19,9 @@ cockpit, _err, status = Open3.capture3("ruby", dashboard, "continue")
|
|
|
19
19
|
exit 0 unless status.success? && !cockpit.strip.empty?
|
|
20
20
|
|
|
21
21
|
context = cockpit.rstrip +
|
|
22
|
-
"\n\nFollow the plastic-
|
|
23
|
-
"if active intents exist, read their state and resume; otherwise
|
|
24
|
-
"
|
|
22
|
+
"\n\nFollow the plastic-continuing skill workflow to resume: " \
|
|
23
|
+
"if active intents exist, read their state and resume; otherwise surface the " \
|
|
24
|
+
"most-valuable next work and any stale or triage intents from the board above."
|
|
25
25
|
|
|
26
26
|
payload = {
|
|
27
27
|
"hookSpecificOutput" => {
|
package/scripts/install.rb
CHANGED
|
@@ -168,4 +168,5 @@ class Install < InstallerCore
|
|
|
168
168
|
end
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
-
exit(Install.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__)
|
|
171
|
+
exit(Install.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__),
|
|
172
|
+
plastic_home: ENV.fetch("PLASTIC_HOME") { InstallerCore::DEFAULT_PLASTIC_HOME }).cli(ARGV)) if $PROGRAM_NAME == __FILE__
|