@zalom/plastic 1.12.0 → 1.14.0
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/hooks/statusline +300 -155
- package/package.json +1 -1
- package/scripts/doctor.rb +131 -9
- package/scripts/lib/bridge.rb +4 -3
- package/scripts/lib/doctor_core.rb +289 -35
- package/scripts/lib/doctor_exclusions.rb +54 -0
- package/scripts/lib/hook_registry.rb +20 -0
- package/scripts/lib/installer_core.rb +40 -7
- package/scripts/maintenance-run +124 -14
- package/scripts/scaffold-intent +2 -2
- package/skills/conventions/SKILL.md +1 -1
- package/skills/conventions/references/gates-and-enforcement.md +13 -8
- package/skills/conventions/references/maintenance-and-revisions.md +14 -0
- package/skills/doctor/SKILL.md +14 -0
- package/skills/skill-creating/SKILL.md +3 -0
- package/skills/skill-creating/references/skills.md +3 -0
package/scripts/maintenance-run
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# maintenance-run --tool rebuild-graph [--plastic-home PATH] [--apply]
|
|
16
16
|
# maintenance-run --tool restore-intent-v1 <id> --at <ref> [--plastic-home PATH] [--apply] [--skip-links]
|
|
17
17
|
# maintenance-run --tool rebuild-savepoint --intent <id> [--store <key>] [--plastic-home PATH] [--apply]
|
|
18
|
-
# maintenance-run --tool register-exclusions [--rule <name>] [--store <key>] [--plastic-home PATH] [--apply]
|
|
18
|
+
# maintenance-run --tool register-exclusions [--prune] [--rule <name>] [--store <key>] [--plastic-home PATH] [--apply]
|
|
19
19
|
#
|
|
20
20
|
# project-links here is ALWAYS single-intent: --intent is required. A store-wide
|
|
21
21
|
# project-links sweep is the rare, owner-approved batch exception (D2) and is run directly
|
|
@@ -77,7 +77,7 @@ end
|
|
|
77
77
|
|
|
78
78
|
def parse_argv(argv)
|
|
79
79
|
opts = { tool: nil, intent: nil, store: nil, plastic_home: DEFAULT_HOME, apply: false,
|
|
80
|
-
at: nil, skip_links: false, id: nil, rule: nil }
|
|
80
|
+
at: nil, skip_links: false, id: nil, rule: nil, prune: false }
|
|
81
81
|
i = 0
|
|
82
82
|
while i < argv.length
|
|
83
83
|
case argv[i]
|
|
@@ -89,6 +89,7 @@ def parse_argv(argv)
|
|
|
89
89
|
when "--at" then opts[:at] = argv[i += 1]
|
|
90
90
|
when "--skip-links" then opts[:skip_links] = true
|
|
91
91
|
when "--rule" then opts[:rule] = argv[i += 1]
|
|
92
|
+
when "--prune" then opts[:prune] = true
|
|
92
93
|
else
|
|
93
94
|
opts[:id] ||= argv[i] # positional id, restore-intent-v1 only
|
|
94
95
|
end
|
|
@@ -311,12 +312,53 @@ end
|
|
|
311
312
|
# structurally edit an intent's OWN files) does not apply, and writing one would mean editing
|
|
312
313
|
# every touched Completed intent directory - forbidden, completed intents are immutable. The
|
|
313
314
|
# scoped commit plus the diffable exclusion file itself are the receipt.
|
|
314
|
-
|
|
315
|
+
#
|
|
316
|
+
# `prune:` (intent 280) reverses the direction: instead of adding newly-violating ids, it
|
|
317
|
+
# removes rows that `DoctorExclusions.dead_rows` reports as suppressing nothing, through the
|
|
318
|
+
# SAME walk, the SAME comment-preserving writer, and the SAME dry-run/--apply gate. `known_ids`
|
|
319
|
+
# (post-review fix) is resolved against a direct scan of the store's own directory listing, never
|
|
320
|
+
# against which ids the INDEX walk happened to visit - an id can have a real directory without
|
|
321
|
+
# being listed in INDEX at all (a de-indexed "ghost"), and walk membership alone misclassified
|
|
322
|
+
# that as :no_intent (deleted) even though the directory plainly still exists. `evaluated_ids` is
|
|
323
|
+
# the narrower set the walk DOES visit; an id with a real directory the walk never evaluated
|
|
324
|
+
# carries no evidence either way, so `dead_rows` leaves it out of its result entirely - never
|
|
325
|
+
# called dead by the reporter, so `prune` never sees it as a pruning candidate in the first
|
|
326
|
+
# place.
|
|
327
|
+
#
|
|
328
|
+
# The walk here only ever evaluates `savepoint_operational`'s own finding bucket
|
|
329
|
+
# (`findings[:operational_gap]`), regardless of `--rule`: `run_register_exclusions` refuses
|
|
330
|
+
# `--prune --rule <other>` outright (a second review fix) rather than silently computing
|
|
331
|
+
# `found_ids` from an unrelated check and misreporting every row under `<other>` dead.
|
|
332
|
+
#
|
|
333
|
+
# Three classes of row are additionally held harmless before anything is written
|
|
334
|
+
# (`protected_ids`), belt-and-suspenders on top of the `evaluated_ids` gate: an id whose dir was
|
|
335
|
+
# skipped for a fresh delivery lock (D6 - the lock skip runs AFTER the id already lands in
|
|
336
|
+
# `evaluated_ids`, but BEFORE it can ever reach `found_ids`, since findings are never computed
|
|
337
|
+
# for it), an id whose intent has not reached a terminal state yet (D6a - savepoint_operational
|
|
338
|
+
# only fires on a terminal intent, so the row has nothing to suppress YET), and an id with a real
|
|
339
|
+
# directory that is not in `evaluated_ids` at all (on disk, unindexed - this last class never
|
|
340
|
+
# actually reaches `held` in practice, since `dead_rows`'s own gate already excludes it; the
|
|
341
|
+
# protection stays explicit anyway rather than relying solely on that gate). A rule left with
|
|
342
|
+
# zero ids after pruning is dropped from the hash entirely (D7): `render_exclusions_file` would
|
|
343
|
+
# otherwise write a bare `rule_name` line that `DoctorExclusions.parse` rejects as "lists no
|
|
344
|
+
# intent ids".
|
|
345
|
+
def run_register_exclusions(home, rule, store, apply, prune = false)
|
|
315
346
|
rule ||= "savepoint_operational"
|
|
316
347
|
unless RuleCatalog.excludable_check?(rule)
|
|
317
348
|
abort_loud("--rule #{rule.inspect} is not excludable (expected one of: " \
|
|
318
349
|
"#{RuleCatalog::EXCLUDABLE_CHECKS.keys.join(", ")})")
|
|
319
350
|
end
|
|
351
|
+
# Review fix: the walk below only ever evaluates savepoint_operational's own finding bucket
|
|
352
|
+
# (`findings[:operational_gap]`), regardless of --rule. Passing --prune --rule <other> would
|
|
353
|
+
# compute `found_ids` from an entirely unrelated check and subtract it against <other>'s
|
|
354
|
+
# registered ids, misreporting every one of them dead. Unreachable in v1 (EXCLUDABLE_CHECKS
|
|
355
|
+
# carries exactly one key) but one catalog entry away, so refuse it explicitly rather than
|
|
356
|
+
# silently mis-pruning the day a second excludable check exists.
|
|
357
|
+
if prune && rule != "savepoint_operational"
|
|
358
|
+
abort_loud("--prune evaluates only savepoint_operational; #{rule.inspect} rows are left " \
|
|
359
|
+
"untouched (the walk this tool runs only ever checks that one rule's finding " \
|
|
360
|
+
"bucket, regardless of --rule)")
|
|
361
|
+
end
|
|
320
362
|
|
|
321
363
|
doctor = Doctor.new(plastic_home: home)
|
|
322
364
|
stores = doctor.done_signal_stores(store ? [store] : nil)
|
|
@@ -340,15 +382,45 @@ def run_register_exclusions(home, rule, store, apply)
|
|
|
340
382
|
existing_text = File.exist?(existing[:path]) ? File.read(existing[:path]).scrub : nil
|
|
341
383
|
|
|
342
384
|
found_ids = []
|
|
385
|
+
protected_ids = []
|
|
386
|
+
|
|
387
|
+
# `known_ids` (post-review fix): every id with a REAL DIRECTORY in this store, scanned
|
|
388
|
+
# directly from disk - not from the INDEX walk below. Mirrors doctor.rb's own fix: an id can
|
|
389
|
+
# have a directory on disk without being listed in INDEX (a de-indexed "ghost"), and the walk
|
|
390
|
+
# alone would never visit it, misclassifying that ghost as :no_intent (deleted) even though
|
|
391
|
+
# it plainly still exists.
|
|
392
|
+
# Shares doctor.store_intent_dirs (doctor.rb:159, intent 189's store-discovery helper)
|
|
393
|
+
# rather than reimplementing the same directory scan (review fix): one predicate for "what
|
|
394
|
+
# is an intent directory in this store", never two that could drift apart.
|
|
395
|
+
known_ids = if File.directory?(s[:store_dir])
|
|
396
|
+
doctor.store_intent_dirs(s[:store_dir]).map { |e| e.split("--", 2).first }
|
|
397
|
+
else
|
|
398
|
+
[]
|
|
399
|
+
end
|
|
400
|
+
# `evaluated_ids`: the narrower set the walk below actually judges (INDEX-listed and on disk).
|
|
401
|
+
evaluated_ids = []
|
|
402
|
+
|
|
343
403
|
doctor.index_sections_by_dir(s[:index]).each do |dirname, in_sections|
|
|
344
404
|
dir = File.join(s[:store_dir], dirname)
|
|
345
405
|
next unless File.directory?(dir)
|
|
346
406
|
|
|
407
|
+
walked_id = dirname.split("--", 2).first
|
|
408
|
+
evaluated_ids << walked_id
|
|
409
|
+
# D6a: savepoint_operational only fires on a terminal intent, so a row naming a live
|
|
410
|
+
# non-terminal intent has nothing to suppress YET. Doctor reports it; prune leaves it.
|
|
411
|
+
protected_ids << walked_id unless (in_sections & ["Completed", "Abandoned"]).any?
|
|
412
|
+
|
|
347
413
|
terminal = (in_sections & ["Completed", "Abandoned"]).any?
|
|
348
414
|
next unless terminal
|
|
349
415
|
|
|
350
416
|
if Lock.fresh?(dir)
|
|
351
417
|
skip_lines << "#{s[:scope]}: #{dirname} skipped (fresh delivery lock)"
|
|
418
|
+
# Prune direction (intent 280 D6, post-review fix): the lock skip runs AFTER
|
|
419
|
+
# evaluated_ids already recorded this id (just above) but BEFORE findings are computed,
|
|
420
|
+
# so it never reaches `consumed` - dead_rows would read it as :no_finding (evaluated,
|
|
421
|
+
# nothing consumed) purely because we never checked. Hold it harmless via protected_ids
|
|
422
|
+
# regardless of what dead_rows would report.
|
|
423
|
+
protected_ids << walked_id
|
|
352
424
|
next
|
|
353
425
|
end
|
|
354
426
|
|
|
@@ -357,36 +429,74 @@ def run_register_exclusions(home, rule, store, apply)
|
|
|
357
429
|
dir, label: "#{s[:scope]} store/#{dirname}", scope: s[:scope], dirname: dirname,
|
|
358
430
|
terminal: terminal, active: active, excluded_rules: []
|
|
359
431
|
)
|
|
360
|
-
found_ids <<
|
|
432
|
+
found_ids << walked_id if findings[:operational_gap].any?
|
|
361
433
|
end
|
|
362
434
|
|
|
435
|
+
# Post-review fix: an id with a real directory that INDEX never lists at all is walked by
|
|
436
|
+
# neither branch above, so its terminal state is unknowable here. PURE DEFENSE-IN-DEPTH:
|
|
437
|
+
# `dead_rows`'s own `evaluated_ids` gate already excludes such an id from `dead` entirely (it
|
|
438
|
+
# is neither :no_finding nor :no_intent - no evidence either way), so `held` below never
|
|
439
|
+
# actually contains one and this concat's protection never needs to fire. Unlike D6 (fresh
|
|
440
|
+
# lock) and D6a (not yet terminal), which DO reach `held` and print "kept (protected, still
|
|
441
|
+
# live)", an unindexed id is never named in that skip output - it was never a live pruning
|
|
442
|
+
# candidate to begin with.
|
|
443
|
+
protected_ids.concat(known_ids - evaluated_ids)
|
|
444
|
+
|
|
363
445
|
already = existing[:rules][rule] || []
|
|
364
|
-
added = found_ids - already
|
|
365
|
-
next if added.empty?
|
|
366
446
|
|
|
367
|
-
|
|
368
|
-
|
|
447
|
+
if prune
|
|
448
|
+
# This tool calls done_signal_findings_for_dir with excluded_rules: [], so a registered id
|
|
449
|
+
# that still has a gap shows up in found_ids rather than in the :excluded bucket. found_ids
|
|
450
|
+
# IS the consumed set for this rule.
|
|
451
|
+
dead = DoctorExclusions.dead_rows(existing, consumed: { rule => found_ids }, known_ids: known_ids,
|
|
452
|
+
evaluated_ids: evaluated_ids)
|
|
453
|
+
.select { |row| row[:rule] == rule }
|
|
454
|
+
.map { |row| row[:id] }
|
|
455
|
+
held = dead & protected_ids # D6 (fresh lock) + D6a (not terminal) + on-disk-but-unindexed
|
|
456
|
+
held.each { |id| skip_lines << "#{s[:scope]}: #{id} kept (protected, still live)" }
|
|
457
|
+
dead -= held
|
|
458
|
+
next if dead.empty?
|
|
459
|
+
|
|
460
|
+
remaining = already - dead
|
|
461
|
+
merged_rules = existing[:rules].merge(rule => remaining.sort)
|
|
462
|
+
merged_rules.delete(rule) if remaining.empty? # D7: a bare rule line fails to reload
|
|
463
|
+
plan[s] = { content: render_exclusions_file(merged_rules, existing_text: existing_text),
|
|
464
|
+
removed: dead.sort }
|
|
465
|
+
else
|
|
466
|
+
added = found_ids - already
|
|
467
|
+
next if added.empty?
|
|
468
|
+
|
|
469
|
+
merged_rules = existing[:rules].merge(rule => (already | found_ids).sort)
|
|
470
|
+
plan[s] = { content: render_exclusions_file(merged_rules, existing_text: existing_text),
|
|
471
|
+
added: added.sort }
|
|
472
|
+
end
|
|
369
473
|
end
|
|
370
474
|
|
|
371
475
|
puts skip_lines.join("\n") unless skip_lines.empty?
|
|
372
476
|
|
|
373
477
|
if plan.empty?
|
|
374
|
-
puts "maintenance-run: no
|
|
478
|
+
puts(prune ? "maintenance-run: no dead #{rule} exclusion rows to prune."
|
|
479
|
+
: "maintenance-run: no new #{rule} violations to register.")
|
|
375
480
|
exit 0
|
|
376
481
|
end
|
|
377
482
|
|
|
378
483
|
unless apply
|
|
379
484
|
plan.each do |s, info|
|
|
380
|
-
puts
|
|
381
|
-
|
|
485
|
+
puts(if prune
|
|
486
|
+
"maintenance-run: DRY RUN, #{s[:scope]} would prune #{info[:removed].size} " \
|
|
487
|
+
"dead row(s) under #{rule}: #{info[:removed].join(", ")}"
|
|
488
|
+
else
|
|
489
|
+
"maintenance-run: DRY RUN, #{s[:scope]} would register #{info[:added].size} " \
|
|
490
|
+
"id(s) under #{rule}: #{info[:added].join(", ")}"
|
|
491
|
+
end)
|
|
382
492
|
end
|
|
383
493
|
exit 0
|
|
384
494
|
end
|
|
385
495
|
|
|
386
496
|
begin
|
|
387
497
|
result = MaintenanceGit.run_scoped(
|
|
388
|
-
repo_dir: home, branch_name: "maintenance
|
|
389
|
-
commit_message: "chore: maintenance - register doctor exclusions (#{rule})"
|
|
498
|
+
repo_dir: home, branch_name: "maintenance/#{prune ? "prune" : "register"}-exclusions-#{stamp}",
|
|
499
|
+
commit_message: "chore: maintenance - #{prune ? "prune dead" : "register"} doctor exclusions (#{rule})"
|
|
390
500
|
) do
|
|
391
501
|
plan.each { |s, info| File.write(DoctorExclusions.path_for(s[:index]), info[:content]) }
|
|
392
502
|
end
|
|
@@ -414,7 +524,7 @@ def main(argv)
|
|
|
414
524
|
when "rebuild-savepoint"
|
|
415
525
|
run_rebuild_savepoint(opts[:plastic_home], opts[:intent], opts[:store], opts[:apply])
|
|
416
526
|
when "register-exclusions"
|
|
417
|
-
run_register_exclusions(opts[:plastic_home], opts[:rule], opts[:store], opts[:apply])
|
|
527
|
+
run_register_exclusions(opts[:plastic_home], opts[:rule], opts[:store], opts[:apply], opts[:prune])
|
|
418
528
|
else
|
|
419
529
|
abort_loud("unknown --tool #{opts[:tool].inspect} (expected project-links|rebuild-graph|" \
|
|
420
530
|
"restore-intent-v1|rebuild-savepoint|register-exclusions)")
|
package/scripts/scaffold-intent
CHANGED
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
#
|
|
32
32
|
# outcome is an end-of-Exec step. Run it once the diff and the test summary exist, right
|
|
33
33
|
# before plastic-intent-ending writes the narrative. Writing outcome.md makes the intent
|
|
34
|
-
# read as Done to
|
|
35
|
-
#
|
|
34
|
+
# read as Done to any stage-derived display (Bridge.derive_stage keys on outcome.md's
|
|
35
|
+
# presence). This does NOT purge a bridge:
|
|
36
36
|
# Bridge.purge_done_bridges keys on INDEX Active status plus lock presence, never on the
|
|
37
37
|
# derived stage.
|
|
38
38
|
|
|
@@ -19,7 +19,7 @@ when the trigger in the second column applies to the work in front of you.
|
|
|
19
19
|
| `references/knowledge-graph.md` | when creating, linking, curating, or indexing intents and you need the sources-vs-chain doctrine, the tiers of influence, the `## Links` projection, or branch-vs-root directory semantics |
|
|
20
20
|
| `references/lifecycle-and-savepoints.md` | when running a lifecycle stage or a savepoint and you need the subagent report-home contract for how an insight reaches the intent |
|
|
21
21
|
| `references/tiers-and-dispatch.md` | when sizing an intent, choosing agent models, routing to the advisor, or writing an auto-mode human report |
|
|
22
|
-
| `references/gates-and-enforcement.md` | when a transition gate blocks you, or before using an audited escape, for the gate mechanics and the logging contract, or when naming, registering, or retiring a hook |
|
|
22
|
+
| `references/gates-and-enforcement.md` | when a transition gate blocks you, or before using an audited escape, for the gate mechanics and the logging contract, or when naming, registering, or retiring a hook or skill |
|
|
23
23
|
| `references/locks-and-worktrees.md` | before taking or releasing a delivery lock, and when working with claims, worktrees, solo mode, or the station ledger |
|
|
24
24
|
| `references/completion-and-done.md` | when ending an intent, for what "intent done" means and the End-stage tail |
|
|
25
25
|
| `references/maintenance-and-revisions.md` | before any structural maintenance edit, for WORK vs MAINTENANCE, the `revisions.md` move-and-record contract, the violation-tag catalog, and the context-economy measurement buckets |
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
This chapter holds the escape-and-logging depth for each transition gate.
|
|
4
4
|
|
|
5
|
-
#### Hook naming and ownership
|
|
6
|
-
|
|
7
|
-
The `plastic-` prefix
|
|
8
|
-
|
|
9
|
-
from the agent's hook config on every update, matching by registry launcher name
|
|
10
|
-
`RETIRED_HOOK_NAMES`), and doctor
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
#### Hook and skill naming and ownership
|
|
6
|
+
|
|
7
|
+
The `plastic-` prefix is reserved for both surfaces: hooks `HookRegistry` registers and skills
|
|
8
|
+
Plastic ships. A user-owned hook or skill must never take it: the installer purges Plastic's
|
|
9
|
+
registrations from the agent's hook config on every update, matching by registry launcher name
|
|
10
|
+
(current plus `RETIRED_HOOK_NAMES`), and doctor reports every violation it can find. On disk,
|
|
11
|
+
`hooks_no_orphans` reports an unregistered `plastic-*` launcher file the registry does not know.
|
|
12
|
+
In the live config, `hooks_entries_owned` (Claude) and `codex_hooks_entries_owned` (Codex)
|
|
13
|
+
report a config entry that is neither a current registration nor recognizably ours, and,
|
|
14
|
+
separately, a current registration whose launcher file is missing from disk. For skills,
|
|
15
|
+
`stray_skills` reports a `plastic-*` skill directory the manifest does not track. Renaming or
|
|
16
|
+
removing a hook from `events` means adding its old name to `RETIRED_HOOK_NAMES` in the same
|
|
17
|
+
change, or every existing install keeps a dead registration.
|
|
13
18
|
|
|
14
19
|
#### The gates by name
|
|
15
20
|
|
|
@@ -190,6 +190,20 @@ immutable forbids outright. The receipt is instead the scoped git commit
|
|
|
190
190
|
durable, diffable record - not a missing safeguard, a deliberate substitution for a receipt
|
|
191
191
|
shape that would otherwise require an illegal write.
|
|
192
192
|
|
|
193
|
+
`--prune` (intent 280) reverses the same tool's direction under the identical carve-out: instead
|
|
194
|
+
of adding newly-violating ids, it removes rows that suppress nothing this run (the intent's gap
|
|
195
|
+
got repaired, the id was mistyped, or the intent directory is gone), computed via the same
|
|
196
|
+
`DoctorExclusions.dead_rows` predicate doctor itself reports from, so the reporter and the
|
|
197
|
+
remover can never disagree about what a dead row is. Same dry-run-by-default, same `--apply`
|
|
198
|
+
gate, same comment-preserving writer, same one scoped commit, same no-`revisions.md`-entry rule -
|
|
199
|
+
this direction still modifies no intent directory, only the store-level table. It holds back two
|
|
200
|
+
kinds of row before writing even when they read as dead: an id whose intent dir carries a fresh
|
|
201
|
+
delivery lock (the lock skip would otherwise leave it out of the walk entirely and misclassify
|
|
202
|
+
it), and an id whose intent has not reached a terminal state yet (`savepoint_operational` only
|
|
203
|
+
fires on a terminal intent, so the row has nothing to suppress *yet*). Both are named in the
|
|
204
|
+
output as kept, never silently dropped, and a rule left with zero ids after pruning is removed
|
|
205
|
+
from the file rather than written as a bare `rule_name` line the loader would reject.
|
|
206
|
+
|
|
193
207
|
Like every other tool behind `maintenance-run`, it dry-runs by default (the owner-approval
|
|
194
208
|
gate), unions with any existing hand-edited file content so a manually added id is never
|
|
195
209
|
dropped, and skips (never aborts on) any intent dir holding a fresh delivery lock.
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -218,6 +218,12 @@ folds in the count and the file's path, e.g. `"... (3 excluded via ~/.plastic/do
|
|
|
218
218
|
A malformed line in the file forces the check to `warn` with the parse error in `details`, even
|
|
219
219
|
when zero real gaps remain, so a broken file is never silently permissive.
|
|
220
220
|
|
|
221
|
+
**Both surfaces, one line.** A registration is honored by the store-wide `savepoint_operational`
|
|
222
|
+
check and by the per-intent `doctor.rb --intent <id>` run, which reports the same missing
|
|
223
|
+
`savepoint.md` under the check name `intent_savepoint_truthful`. Register the id once. The
|
|
224
|
+
per-intent run honors it only for an intent that is terminal in `INDEX.md`, and never suppresses
|
|
225
|
+
a phantom-savepoint-line finding.
|
|
226
|
+
|
|
221
227
|
**Hand-editing.** The file is plain text; add a line (or append ids to an existing rule line) and
|
|
222
228
|
save. No installer step, no reindex, and no `revisions.md` entry is required or written.
|
|
223
229
|
|
|
@@ -233,6 +239,14 @@ without writing anything. Review the output, then re-run with `--apply` to write
|
|
|
233
239
|
land one scoped git commit. It unions with any existing hand-added ids (never drops one) and
|
|
234
240
|
skips, rather than aborts on, any intent dir holding a fresh delivery lock.
|
|
235
241
|
|
|
242
|
+
**Dead-row notice.** A registered row can go dead (gap repaired, id mistyped, or the intent
|
|
243
|
+
directory gone). When any row is dead, the message adds a second suffix next to the exclusion
|
|
244
|
+
count naming the count, the file, and the prune command - purely informational, status and exit
|
|
245
|
+
code unchanged. Prune it the same way, dry-run first: register-exclusions --prune [--apply]. It
|
|
246
|
+
removes exactly the dead rows through the same writer and commit, but holds back an id whose
|
|
247
|
+
intent dir carries a fresh lock or has not gone terminal yet (nothing to suppress there yet),
|
|
248
|
+
naming both as kept.
|
|
249
|
+
|
|
236
250
|
## References
|
|
237
251
|
|
|
238
252
|
- Read `references/gates-stuck-detection.md` for the full gate enforcement table, bridge file pattern, and the recorded stuck-detection signals when diagnosing gate failures or stuck agents
|
|
@@ -40,6 +40,9 @@ routes each authoring task to the reference that holds the depth.
|
|
|
40
40
|
them (use commas, periods, parentheses, colons). Existing internal files and the
|
|
41
41
|
sanctioned template emissions (templates/index.md's INDEX line shape) are not
|
|
42
42
|
violations.
|
|
43
|
+
- The `plastic-` prefix is reserved for skills and hooks Plastic itself ships. A skill
|
|
44
|
+
authored outside Plastic's own tree takes a different name; doctor's ownership checks
|
|
45
|
+
and the installer's purge both key off the prefix.
|
|
43
46
|
|
|
44
47
|
## Route the authoring task to its reference
|
|
45
48
|
|
|
@@ -58,6 +58,9 @@ Rules [A5]:
|
|
|
58
58
|
5. Must not contain `anthropic` or `claude`.
|
|
59
59
|
6. Prefer the gerund form, which reads as a capability (`processing-pdfs`, `creating-skills`,
|
|
60
60
|
not `pdf-tool`).
|
|
61
|
+
7. Never start with `plastic-`. That prefix is reserved for skills and hooks Plastic itself
|
|
62
|
+
ships; doctor's ownership checks (`stray_skills`) and the installer's purge both key off it,
|
|
63
|
+
so a user-authored skill carrying it reads as squatting on Plastic's own namespace.
|
|
61
64
|
|
|
62
65
|
## The `description` field (triggering)
|
|
63
66
|
|