@zalom/plastic 2.0.0-alpha.21 → 2.0.0-alpha.23
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 +20 -18
- package/agents/plastic-enforcer.md +6 -3
- package/agents/plastic-executor.md +4 -0
- package/agents/plastic-node-research.md +28 -0
- package/agents/plastic-node-verify.md +27 -0
- package/agents/plastic-node-work.md +32 -0
- package/bin/lib/context_budget.rb +1 -1
- package/hooks/hooks.json +12 -0
- package/hooks/statusline +28 -0
- package/hooks/stop +5 -0
- package/package.json +1 -1
- package/scripts/doctor.rb +159 -10
- package/scripts/end-intent +3 -3
- package/scripts/graph-measure +249 -0
- package/scripts/hook-capture +1 -0
- package/scripts/hook-savepoint +24 -2
- package/scripts/hook-session-start +333 -321
- package/scripts/hook-stop +57 -0
- package/scripts/insight-append +18 -4
- package/scripts/lib/active_delivery.rb +61 -0
- package/scripts/lib/agent_models.rb +10 -1
- package/scripts/lib/codex_adapter.rb +197 -0
- package/scripts/lib/doctor_core.rb +8 -3
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/graph_edges.rb +4 -4
- package/scripts/lib/graph_file.rb +4 -4
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +408 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +411 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +2 -2
- package/scripts/lib/handoff.rb +36 -5
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +13 -1
- package/scripts/lib/hook_replay.rb +23 -5
- package/scripts/lib/index_projection.rb +1 -1
- package/scripts/lib/installer_core.rb +112 -6
- package/scripts/lib/intent_screen.rb +1 -1
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/meter_watch.rb +15 -9
- package/scripts/lib/node_file.rb +3 -3
- package/scripts/lib/node_ledger.rb +8 -1
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/outcome_report.rb +1 -1
- package/scripts/lib/report_screen.rb +10 -6
- package/scripts/lib/roadmap_graph.rb +1 -1
- package/scripts/lib/roadmap_queue.rb +1 -1
- package/scripts/lib/roadmap_render.rb +1 -1
- package/scripts/lib/runner_absorb.rb +31 -5
- package/scripts/lib/runner_dispatch.rb +26 -11
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +3 -3
- package/scripts/lib/session_git.rb +2 -2
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/verify_intent.rb +2 -2
- package/scripts/lib/work_graph_validator.rb +6 -6
- package/scripts/new-intent +1 -1
- package/scripts/node-run +224 -0
- package/scripts/read-config +6 -0
- package/scripts/runner +203 -19
- package/scripts/skill-lint +115 -6
- package/scripts/verify-intent +1 -1
- package/skills/auto/SKILL.md +54 -56
- package/skills/auto/references/agent-architecture.md +10 -8
- package/skills/auto/references/human-report-contract.md +1 -1
- package/skills/conventions/references/completion-and-done.md +7 -7
- package/skills/conventions/references/knowledge-graph.md +9 -0
- package/skills/conventions/references/locks-and-worktrees.md +3 -3
- package/skills/conventions/references/maintenance-and-revisions.md +1 -1
- package/skills/doctor/SKILL.md +3 -3
- package/skills/doctor/report.md +1 -1
- package/skills/intent-continuing/references/boarding-matrix.md +2 -2
- package/skills/intent-creating/SKILL.md +58 -133
- package/skills/intent-creating/evals/evals.json +1 -1
- package/skills/intent-ending/SKILL.md +48 -56
- package/skills/intent-ending/evals/evals.json +1 -1
- package/skills/intent-executing/SKILL.md +43 -136
- package/skills/intent-speccing/SKILL.md +3 -0
- package/skills/releasing/SKILL.md +1 -1
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/tutorial/SKILL.md +2 -1
- package/skills/tutorial/references/track-1-guided.md +21 -40
- package/skills/tutorial/references/track-2-auto.md +3 -3
- package/templates/agents.md +2 -2
package/scripts/runner
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "stringio"
|
|
5
7
|
require_relative "lib/savepoint"
|
|
6
8
|
require_relative "lib/ready_set"
|
|
7
9
|
require_relative "lib/runner_core"
|
|
10
|
+
require_relative "lib/harness_adapter"
|
|
8
11
|
require_relative "lib/runner_proposals"
|
|
9
12
|
|
|
10
13
|
# runner - the one executable over the graph-ready loop's declared node graph
|
|
11
14
|
# (intent 340, G7, n1). A subcommand table: the public verbs (step, status,
|
|
12
15
|
# answer) are the only ones the skill body ever names; the internal verbs
|
|
13
|
-
# (ready, sweep, rewind) route and work so `step` can compose
|
|
14
|
-
# each can be tested alone, but stay out of the usage text (327
|
|
16
|
+
# (ready, sweep, rewind, until-empty) route and work so `step` can compose
|
|
17
|
+
# them and so each can be tested alone, but stay out of the usage text (327
|
|
18
|
+
# spec, 340b n7).
|
|
15
19
|
#
|
|
16
|
-
# `step`, `sweep`, `answer` and `
|
|
17
|
-
# deliver (RunnerDispatch and RunnerAbsorb for `step`;
|
|
18
|
-
# RunnerAnswer, RunnerRewind for the rest).
|
|
19
|
-
# its own verb branch: a verb whose module
|
|
20
|
-
# plainly and exits nonzero, and every other verb
|
|
21
|
-
# `status`, which an operator polls constantly across all
|
|
22
|
-
# intent's dispatches - keeps working.
|
|
20
|
+
# `step`, `sweep`, `answer`, `rewind` and `until-empty` route to modules
|
|
21
|
+
# later nodes deliver (RunnerDispatch and RunnerAbsorb for `step`;
|
|
22
|
+
# RunnerSweep, RunnerAnswer, RunnerRewind, RunnerUntilEmpty for the rest).
|
|
23
|
+
# Each is required LAZILY, inside its own verb branch: a verb whose module
|
|
24
|
+
# has not landed yet reports plainly and exits nonzero, and every other verb
|
|
25
|
+
# - most importantly `status`, which an operator polls constantly across all
|
|
26
|
+
# five of this intent's dispatches - keeps working.
|
|
23
27
|
#
|
|
24
28
|
# Usage:
|
|
25
29
|
# runner <step|status|answer> <intent_dir> [--node ID] [--answer TEXT]
|
|
@@ -34,7 +38,7 @@ module Runner
|
|
|
34
38
|
module_function
|
|
35
39
|
|
|
36
40
|
PUBLIC_VERBS = %w[step status answer].freeze
|
|
37
|
-
INTERNAL_VERBS = %w[ready sweep rewind].freeze
|
|
41
|
+
INTERNAL_VERBS = %w[ready sweep rewind until-empty watch].freeze
|
|
38
42
|
VERBS = (PUBLIC_VERBS + INTERNAL_VERBS).freeze
|
|
39
43
|
|
|
40
44
|
# verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
|
|
@@ -47,6 +51,8 @@ module Runner
|
|
|
47
51
|
"sweep" => [["RunnerSweep", "runner_sweep"]],
|
|
48
52
|
"answer" => [["RunnerAnswer", "runner_answer"]],
|
|
49
53
|
"rewind" => [["RunnerRewind", "runner_rewind"]],
|
|
54
|
+
"until-empty" => [["RunnerUntilEmpty", "runner_until_empty"]],
|
|
55
|
+
"watch" => [["RunnerWatch", "runner_watch"]],
|
|
50
56
|
}.freeze
|
|
51
57
|
|
|
52
58
|
def usage
|
|
@@ -77,11 +83,21 @@ module Runner
|
|
|
77
83
|
# a mistyped `--session` used to vanish with no effect and the runner
|
|
78
84
|
# simply acted as whichever session `CLAUDE_CODE_SESSION_ID` names.
|
|
79
85
|
KNOWN_FLAGS = {
|
|
80
|
-
"step" => %w[--return --allow-core-drift],
|
|
86
|
+
"step" => %w[--return --allow-core-drift --harness],
|
|
81
87
|
"answer" => %w[--node --answer],
|
|
82
88
|
"rewind" => %w[--node --confirm],
|
|
89
|
+
"until-empty" => %w[--harness],
|
|
90
|
+
"watch" => %w[--dispatch --harness --install-timer --home],
|
|
83
91
|
}.freeze
|
|
84
92
|
|
|
93
|
+
# The explicit YAML document-end marker (intent 340b, G7c, n1, row 1.28)
|
|
94
|
+
# separating the dispatch plan `step` has always printed from the
|
|
95
|
+
# harness-rendered block underneath it: stdout has never been one YAML
|
|
96
|
+
# document (absorb/reclaim/extend/reap lines print before the plan,
|
|
97
|
+
# decision/park lines after it), so a reader splits on this marker rather
|
|
98
|
+
# than guessing where the plan ends.
|
|
99
|
+
DOCUMENT_BOUNDARY = "...".freeze
|
|
100
|
+
|
|
85
101
|
# unrecognized_flag(verb, args) -> the first token in `args` that looks
|
|
86
102
|
# like a flag (`--...`) and is not in `verb`'s own KNOWN_FLAGS, or nil. A
|
|
87
103
|
# verb absent from KNOWN_FLAGS (status, ready, sweep) accepts none.
|
|
@@ -203,6 +219,10 @@ module Runner
|
|
|
203
219
|
run_answer(intent_dir, args)
|
|
204
220
|
when "rewind"
|
|
205
221
|
run_rewind(intent_dir, args)
|
|
222
|
+
when "until-empty"
|
|
223
|
+
run_until_empty(intent_dir, args)
|
|
224
|
+
when "watch"
|
|
225
|
+
run_watch(intent_dir, args)
|
|
206
226
|
else
|
|
207
227
|
warn "runner: #{verb}'s module loaded but no dispatcher is wired up yet"
|
|
208
228
|
exit 3
|
|
@@ -284,14 +304,156 @@ module Runner
|
|
|
284
304
|
exit 0
|
|
285
305
|
end
|
|
286
306
|
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
307
|
+
# run_until_empty (intent 340b, G7c, n7): the Codex loop. Internal, like
|
|
308
|
+
# `rewind` (row 7.13) - `RunnerUntilEmpty.run` composes `step` and
|
|
309
|
+
# `node-run` itself, since Codex has no session on the other end to make
|
|
310
|
+
# the subagent calls `step` only ever prints a plan for. `--harness`
|
|
311
|
+
# threads into every turn the loop runs (row 7.5), never re-resolved per
|
|
312
|
+
# iteration from a literal. The lock check here is the same defence in
|
|
313
|
+
# depth `run_step_body` carries (B4): `RunnerUntilEmpty.step_once` refuses
|
|
314
|
+
# `lock_not_held` on its own first call regardless, so this exits before
|
|
315
|
+
# even constructing the loop's own state.
|
|
316
|
+
def run_until_empty(intent_dir, args)
|
|
317
|
+
harness_override = opt(args, "--harness")
|
|
318
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
319
|
+
|
|
320
|
+
unless context.session
|
|
321
|
+
warn "runner: until-empty refused (lock_not_held)"
|
|
322
|
+
exit 1
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
result = RunnerUntilEmpty.run(context, harness: harness_override)
|
|
326
|
+
|
|
327
|
+
case result[:status]
|
|
328
|
+
when "complete", "stalled", "needs_decision", "iteration_cap"
|
|
329
|
+
exit 0
|
|
330
|
+
else
|
|
331
|
+
exit 1
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# run_watch (intent 340a, G7b, n2): one tick over disk truth. Internal,
|
|
336
|
+
# like until-empty and rewind - the timer that carries it lives outside
|
|
337
|
+
# this process (a local `/loop` on Claude Code, a launchd job on Codex,
|
|
338
|
+
# n3/n4). `--dispatch` is unattended start (graph.md D7): refused here,
|
|
339
|
+
# before RunnerWatch.tick ever takes the tick lock or writes anything,
|
|
340
|
+
# when the resolved harness's `HarnessAdapter.unattended_start?` is
|
|
341
|
+
# false, so a refusal never leaves a `watch.record` line behind that
|
|
342
|
+
# looks like a completed tick.
|
|
343
|
+
# --install-timer (intent 340a, G7b, n3, graph.md D9): the Codex carrier.
|
|
344
|
+
# Writes the plist and prints the path plus the `launchctl load` command;
|
|
345
|
+
# never runs `launchctl` itself, so the owner stays in control of when the
|
|
346
|
+
# job actually loads. `--home` defaults to the OS home, as
|
|
347
|
+
# `scripts/meter-watch --install-timer` already does.
|
|
348
|
+
def run_watch(intent_dir, args)
|
|
349
|
+
dispatch = args.include?("--dispatch")
|
|
350
|
+
harness_override = opt(args, "--harness")
|
|
351
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
352
|
+
agent_config = load_agent_config(context.plastic_home)
|
|
353
|
+
harness = HarnessAdapter.resolve_key(config: agent_config, override: harness_override)
|
|
354
|
+
|
|
355
|
+
if args.include?("--install-timer")
|
|
356
|
+
home = opt(args, "--home") || Dir.home
|
|
357
|
+
plist_path = RunnerWatch.install_timer(context, home: home, harness_key: harness)
|
|
358
|
+
puts "plist: #{plist_path}"
|
|
359
|
+
puts "launchctl load #{plist_path}"
|
|
360
|
+
exit 0
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
if dispatch && !HarnessAdapter.unattended_start?(harness)
|
|
364
|
+
warn HarnessAdapter::UNATTENDED_START_SENTENCE
|
|
365
|
+
exit 1
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
result = RunnerWatch.tick(context, dispatch: dispatch, harness: harness)
|
|
369
|
+
|
|
370
|
+
puts "class: #{result[:class]}"
|
|
371
|
+
result[:blockers].each { |b| puts "blocked: #{b}" }
|
|
372
|
+
puts "ready: #{result[:ready].empty? ? '(none)' : result[:ready].join(', ')}"
|
|
373
|
+
puts "dispatched: #{result[:dispatched].empty? ? '(none)' : result[:dispatched].join(', ')}"
|
|
374
|
+
exit 0
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# run_step (intent 340, G7, n5): one turn of the loop. The real work lives
|
|
378
|
+
# in #run_step_body below; this wrapper (intent 340b, G7c, n1, rows
|
|
379
|
+
# 1.21-1.25 and 1.29-1.31) exists ONLY to capture every byte `step` prints
|
|
380
|
+
# - stdout and stderr both, refusals included - and persist it to
|
|
381
|
+
# `<intent_dir>/runner-step.last` before the process actually exits, so a
|
|
382
|
+
# PreCompact hand-off (n3) always has something to render, even after one
|
|
383
|
+
# of the three pre-plan refusals that used to warn to stderr alone.
|
|
384
|
+
#
|
|
385
|
+
# `run_step_body` still calls Kernel#exit(N) at every one of its own exit
|
|
386
|
+
# points, exactly as before this node; wrapping it in a rescue for
|
|
387
|
+
# SystemExit lets this method capture the intended status, persist the
|
|
388
|
+
# file, replay the captured output onto the REAL stdout/stderr (so a
|
|
389
|
+
# subprocess caller like Open3.capture3 sees byte-identical output to
|
|
390
|
+
# before this node), and only then perform the one real process exit.
|
|
294
391
|
def run_step(intent_dir, args)
|
|
392
|
+
out_buf = StringIO.new
|
|
393
|
+
err_buf = StringIO.new
|
|
394
|
+
real_stdout = $stdout
|
|
395
|
+
real_stderr = $stderr
|
|
396
|
+
exit_code = 0
|
|
397
|
+
begin
|
|
398
|
+
$stdout = out_buf
|
|
399
|
+
$stderr = err_buf
|
|
400
|
+
run_step_body(intent_dir, args)
|
|
401
|
+
rescue SystemExit => e
|
|
402
|
+
exit_code = e.status || 1
|
|
403
|
+
ensure
|
|
404
|
+
$stdout = real_stdout
|
|
405
|
+
$stderr = real_stderr
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
real_stdout.print(out_buf.string)
|
|
409
|
+
real_stderr.print(err_buf.string)
|
|
410
|
+
persist_runner_step_last(intent_dir, out_buf.string, err_buf.string)
|
|
411
|
+
|
|
412
|
+
exit(exit_code)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Row 1.24: lives beside savepoint.md, never inside packets/ (which the
|
|
416
|
+
# reaper treats as attempt evidence). Row 1.32: registers itself in the
|
|
417
|
+
# store's own top-level `.gitignore` (Worktree.ensure_gitignored's own
|
|
418
|
+
# idempotent, non-raising append) the same way `*.lock` already is, so a
|
|
419
|
+
# file rewritten on every step never dirties the store tree. Row 1.25:
|
|
420
|
+
# wrapped in its own rescue - an unwritable intent directory must never
|
|
421
|
+
# crash a step that has already written its transitions.
|
|
422
|
+
def persist_runner_step_last(intent_dir, stdout_text, stderr_text)
|
|
423
|
+
home_root = Arm.home_for(intent_dir)
|
|
424
|
+
Worktree.ensure_gitignored(File.join(home_root, ".plastic"), "runner-step.last")
|
|
425
|
+
|
|
426
|
+
path = File.join(intent_dir, "runner-step.last")
|
|
427
|
+
File.write(path, "#{stdout_text}#{stderr_text}")
|
|
428
|
+
rescue StandardError => e
|
|
429
|
+
warn "runner: could not persist runner-step.last: #{e.message}"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Intent 340b, G7c, n1, row 1.1: the config `HarnessAdapter.resolve_key`
|
|
433
|
+
# reads `agent.type` from. `context.plastic_home` is derived from the
|
|
434
|
+
# intent_dir's OWN ancestry (Arm.home_for -> Worktree.home_from_store), so
|
|
435
|
+
# a sandboxed test fixture never reaches the real ~/.plastic/config.yml
|
|
436
|
+
# (Arm.home_for's own docstring: "a sandboxed store never resolves to the
|
|
437
|
+
# real Dir.home") - this stays hermetic for free, the same way every other
|
|
438
|
+
# `context.plastic_home` read in this file already does. A missing or
|
|
439
|
+
# unparseable config.yml reads as {}, never raises.
|
|
440
|
+
def load_agent_config(plastic_home)
|
|
441
|
+
path = File.join(plastic_home.to_s, "config.yml")
|
|
442
|
+
return {} unless File.exist?(path)
|
|
443
|
+
|
|
444
|
+
YAML.safe_load(File.read(path)) || {}
|
|
445
|
+
rescue StandardError
|
|
446
|
+
{}
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# run_step_body (intent 340, G7, n5): one turn of the loop, in the fixed
|
|
450
|
+
# order 327 D14 and RunnerSweep's own docstring name - abort on a stuck
|
|
451
|
+
# merge, the delivery-lease heartbeat, absorb every `--return NODE=PATH`
|
|
452
|
+
# this call carries, THEN reclaim (so a node this very call just absorbed
|
|
453
|
+
# is never reclaimed out from under its own return, matrix row 2.19/2.20) -
|
|
454
|
+
# and only then dispatch whatever is left ready. Never spawns an agent (327
|
|
455
|
+
# D42): the session makes every subagent call from the plan this prints.
|
|
456
|
+
def run_step_body(intent_dir, args)
|
|
295
457
|
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
296
458
|
|
|
297
459
|
# v1 minor 3/row 11.18: RunnerSweep.abort_if_merging already prints the
|
|
@@ -322,6 +484,12 @@ module Runner
|
|
|
322
484
|
# never defaulted true.
|
|
323
485
|
allow_core_drift = args.include?("--allow-core-drift")
|
|
324
486
|
|
|
487
|
+
# Intent 340b, G7c, n1, row 1.2/1.4: `--harness KEY` overrides config
|
|
488
|
+
# `agent.type` for this one call - how one machine drives both adapters
|
|
489
|
+
# without editing config. `nil` when absent, so HarnessAdapter falls
|
|
490
|
+
# through to config's own value untouched.
|
|
491
|
+
harness_override = opt(args, "--harness")
|
|
492
|
+
|
|
325
493
|
returns = {}
|
|
326
494
|
opt_all(args, "--return").each do |pair|
|
|
327
495
|
node, path = pair.to_s.split("=", 2)
|
|
@@ -351,7 +519,8 @@ module Runner
|
|
|
351
519
|
reaped = NodeWorktree.reap(context)
|
|
352
520
|
reaped[:removed].each { |r| puts "reaped #{r[:node]} worktree" }
|
|
353
521
|
|
|
354
|
-
|
|
522
|
+
agent_config = load_agent_config(context.plastic_home)
|
|
523
|
+
result = RunnerDispatch.dispatch(context, config: agent_config, harness: harness_override)
|
|
355
524
|
unless result[:ok]
|
|
356
525
|
warn "runner: step refused (#{result[:reason]}): #{Array(result[:errors]).join('; ')}"
|
|
357
526
|
warn result[:rearm_command] if result[:rearm_command]
|
|
@@ -374,6 +543,21 @@ module Runner
|
|
|
374
543
|
result[:blockers].each { |b| puts "blocked: #{b}" }
|
|
375
544
|
end
|
|
376
545
|
|
|
546
|
+
# Intent 340b, G7c, n1, rows 1.6/1.13/1.14/1.27/1.28: the YAML plan keeps
|
|
547
|
+
# printing exactly as it did before this node (327's contract) - this is
|
|
548
|
+
# the rendered block UNDERNEATH it, for the harness `result[:harness]`
|
|
549
|
+
# resolved (config's `agent.type`, or this call's own `--harness`
|
|
550
|
+
# override), separated by an explicit YAML document-end marker so a
|
|
551
|
+
# reader splits on that marker rather than a heuristic. `render` returns
|
|
552
|
+
# nil for an empty dispatch list (row 1.14), so nothing extra ever
|
|
553
|
+
# prints when this step dispatched nothing.
|
|
554
|
+
block = HarnessAdapter.render(result[:dispatched], harness: result[:harness],
|
|
555
|
+
return_contract: RunnerDispatch::RETURN_CONTRACT)
|
|
556
|
+
if block
|
|
557
|
+
puts DOCUMENT_BOUNDARY
|
|
558
|
+
puts block
|
|
559
|
+
end
|
|
560
|
+
|
|
377
561
|
# M11/row 10.14: a `needs_decision` stop must print even when THIS SAME
|
|
378
562
|
# step also dispatched other nodes ahead of the decision node in ranked
|
|
379
563
|
# order - `result[:status]` reads "dispatched" in that case (dispatched
|
package/scripts/skill-lint
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
# skill-lint: deterministic CLI over SkillLint (intent 85b)
|
|
5
|
+
# skill-lint: deterministic CLI over SkillLint (intent 85b), plus one CLI-local
|
|
6
|
+
# check (intent 341, G8, n1, C35).
|
|
6
7
|
#
|
|
7
|
-
# Runs the five structural
|
|
8
|
-
# bare-pointer, orphan-files, references-depth)
|
|
9
|
-
#
|
|
8
|
+
# Runs the five structural checks (body-budget, frontmatter-validity,
|
|
9
|
+
# bare-pointer, orphan-files, references-depth) via SkillLint, then this
|
|
10
|
+
# script's own refusal-restatement check, over a directory of Agent Skills,
|
|
11
|
+
# and reports every violation. Mirrors `scripts/validate-intent`'s
|
|
10
12
|
# CLI-over-lib shape and exit-code contract.
|
|
11
13
|
#
|
|
12
14
|
# Usage:
|
|
@@ -17,6 +19,112 @@
|
|
|
17
19
|
|
|
18
20
|
require_relative "lib/skill_lint"
|
|
19
21
|
|
|
22
|
+
# RefusalRestatementCheck (C35): a skill body must not restate, past a short
|
|
23
|
+
# run of words, a refusal rule the conventions chapter already carries; it
|
|
24
|
+
# must link that chapter instead. No-op when <skills_dir>/conventions/references
|
|
25
|
+
# does not exist, so an older tree or a fixture dir with no conventions chapter
|
|
26
|
+
# is unaffected.
|
|
27
|
+
#
|
|
28
|
+
# Deliberately lives in this CLI, not in scripts/lib/skill_lint.rb: C35 is a
|
|
29
|
+
# doctrine-duplication check, one level removed from SkillLint's five
|
|
30
|
+
# structural checks, and keeping it here means adding it touches only the
|
|
31
|
+
# files this change is scoped to.
|
|
32
|
+
class RefusalRestatementCheck
|
|
33
|
+
REFUSAL_KEYWORD_RE = /\brefus(e|es|ed|ing|al)\b/i
|
|
34
|
+
CONVENTIONS_LINK_RE = %r{conventions/references/|plastic-conventions}i
|
|
35
|
+
|
|
36
|
+
# How many consecutive normalized words must match, verbatim, between a
|
|
37
|
+
# skill's refusal paragraph and the doctrine text before it counts as a
|
|
38
|
+
# restatement rather than a coincidental shared phrase.
|
|
39
|
+
NGRAM = 6
|
|
40
|
+
|
|
41
|
+
def initialize(skills_dir)
|
|
42
|
+
@skills_dir = skills_dir
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run
|
|
46
|
+
doctrine = doctrine_blob
|
|
47
|
+
return [] unless doctrine
|
|
48
|
+
|
|
49
|
+
skill_md_paths.flat_map do |skill_md|
|
|
50
|
+
skill_dir = File.dirname(skill_md)
|
|
51
|
+
next [] if File.basename(skill_dir) == "conventions" # the source never restates itself
|
|
52
|
+
|
|
53
|
+
check_skill(skill_dir, skill_md, File.read(skill_md), doctrine)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def skill_md_paths
|
|
60
|
+
Dir.glob(File.join(@skills_dir, "*", "SKILL.md")).sort
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def doctrine_blob
|
|
64
|
+
chapters = Dir.glob(File.join(@skills_dir, "conventions", "references", "*.md")).sort
|
|
65
|
+
return nil if chapters.empty?
|
|
66
|
+
|
|
67
|
+
text = chapters.map { |f| File.read(f) }.join(" ")
|
|
68
|
+
" #{normalize_words(text).join(" ")} "
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_words(text)
|
|
72
|
+
text.downcase.gsub("`", "").gsub(/[^a-z0-9\s-]/, " ").split(/\s+/).reject(&:empty?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Same split as SkillLint's own frontmatter/body divide: `content.split("---", 3)`.
|
|
76
|
+
def body_and_offset(content)
|
|
77
|
+
parts = content.split("---", 3)
|
|
78
|
+
return [content, 0] if parts.length < 3
|
|
79
|
+
|
|
80
|
+
prefix_len = parts[0].length + 3 + parts[1].length + 3
|
|
81
|
+
[parts[2], content[0...prefix_len].count("\n")]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Blank-line-delimited paragraph blocks, so a refusal keyword and its
|
|
85
|
+
# doctrine echo are compared across the whole paragraph, not one line.
|
|
86
|
+
def paragraph_blocks(lines)
|
|
87
|
+
blocks = []
|
|
88
|
+
start = nil
|
|
89
|
+
lines.each_with_index do |line, i|
|
|
90
|
+
if line.strip.empty?
|
|
91
|
+
blocks << { start: start, text: lines[start..(i - 1)].join } if start
|
|
92
|
+
start = nil
|
|
93
|
+
else
|
|
94
|
+
start ||= i
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
blocks << { start: start, text: lines[start..].join } if start
|
|
98
|
+
blocks
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def restated?(block_text, doctrine)
|
|
102
|
+
words = normalize_words(block_text)
|
|
103
|
+
return false if words.length < NGRAM
|
|
104
|
+
|
|
105
|
+
(0..(words.length - NGRAM)).any? do |i|
|
|
106
|
+
doctrine.include?(" #{words[i, NGRAM].join(" ")} ")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def check_skill(skill_dir, skill_md, content, doctrine)
|
|
111
|
+
name = File.basename(skill_dir)
|
|
112
|
+
body, offset = body_and_offset(content)
|
|
113
|
+
|
|
114
|
+
paragraph_blocks(body.lines).filter_map do |block|
|
|
115
|
+
next unless block[:text].match?(REFUSAL_KEYWORD_RE)
|
|
116
|
+
next if block[:text].match?(CONVENTIONS_LINK_RE) # links the chapter instead of restating
|
|
117
|
+
next unless restated?(block[:text], doctrine)
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
check: "refusal-restatement", skill: name, file: skill_md, line: offset + block[:start] + 1,
|
|
121
|
+
message: "this paragraph restates a refusal rule word-for-word from " \
|
|
122
|
+
"skills/conventions/references/; link the chapter instead of repeating its text",
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
20
128
|
def resolve_skills_dir(args)
|
|
21
129
|
if (i = args.index("--skills-dir"))
|
|
22
130
|
args[i + 1]
|
|
@@ -37,14 +145,15 @@ end
|
|
|
37
145
|
|
|
38
146
|
dir = File.expand_path(resolve_skills_dir(ARGV))
|
|
39
147
|
result = SkillLint.new(skills_dir: dir).run
|
|
148
|
+
violations = result.violations + RefusalRestatementCheck.new(dir).run
|
|
40
149
|
|
|
41
|
-
if
|
|
150
|
+
if violations.empty?
|
|
42
151
|
puts "OK: #{dir}"
|
|
43
152
|
exit 0
|
|
44
153
|
end
|
|
45
154
|
|
|
46
155
|
warn "VIOLATIONS: #{dir}"
|
|
47
|
-
|
|
156
|
+
violations.each do |v|
|
|
48
157
|
warn "#{v[:check]} #{v[:skill]} #{v[:file]}:#{v[:line].nil? ? "-" : v[:line]} #{v[:message]}"
|
|
49
158
|
end
|
|
50
159
|
exit 1
|
package/scripts/verify-intent
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# --base overrides the auto-detected diff base (default: the merge base with the repo's
|
|
13
13
|
# detected default branch) for repos where detection is ambiguous. --suite is an optional
|
|
14
|
-
# caller-supplied command to run (in the repo directory) and
|
|
14
|
+
# caller-supplied command to run (in the repo directory) and merge into the verdict. No
|
|
15
15
|
# other flags exist.
|
|
16
16
|
#
|
|
17
17
|
# Checks, all run every invocation (the fourth only when --suite is given):
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plastic-auto
|
|
3
3
|
description: >-
|
|
4
|
-
Autonomous intent delivery - a background team takes a registered intent from How to
|
|
4
|
+
Autonomous intent delivery - a background team takes a registered intent from How to End.
|
|
5
5
|
Use when user says "auto", "take it from here", "deliver this", or when a thinking
|
|
6
6
|
conversation concludes and the user confirms autonomous execution. Requires an active intent
|
|
7
7
|
in INDEX.md.
|
|
@@ -71,35 +71,29 @@ owner override. Do not proceed as the owner after an exit 1.
|
|
|
71
71
|
Read `../plastic-conventions/references/locks-and-worktrees.md` for what the lock and the
|
|
72
72
|
worktree mean and the station table behind them. Code edits happen only inside the worktree.
|
|
73
73
|
|
|
74
|
-
## The shape
|
|
74
|
+
## The shape
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
Work is a graph (`graph.md`: Goal, Decisions, Graph, Status; one `nodes/*.md` per node) or,
|
|
77
|
+
for work small enough to skip speccing, delivered inline with no separate plan review. There
|
|
78
|
+
is no intent tier and no stage agent; depth follows the work.
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
| 3. Execute, tests first | boot 2, `plastic-executor` | the red commit (the matrix's tests, failing), then the code, then a green suite |
|
|
84
|
-
| 4. Review by risk | boot 3 only when risk calls for it (below) | a pass or a list of fixes the executor applies |
|
|
85
|
-
| 5. One suite run, then close | this session | `outcome.md`, `end-intent`, the roadmap ledger |
|
|
80
|
+
`runner step` computes readiness and prints a spawn block per dispatched node - agent, model,
|
|
81
|
+
packet path, the test command, the call cap - fenced for a session to paste into the Agent
|
|
82
|
+
tool; the runner never spawns (327 D42). `runner status` renders the ledger; `runner answer`
|
|
83
|
+
closes a `needs_decision` node.
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
spawn block per node - agent, model, packet path, the test command, the call cap - fenced for a
|
|
92
|
-
session to paste into the Agent tool; the runner never spawns (327 D42). A lead earns its keep
|
|
93
|
-
on a graph carrying a decision node, weighing its `needs_decision` stop; a graph with none runs
|
|
94
|
-
end to end from `runner step` alone.
|
|
85
|
+
A lead is a choice, not a requirement (D8, 355). A lead earns its keep on a graph carrying a
|
|
86
|
+
decision node, weighing its `needs_decision` stop; a graph with none runs end to end from
|
|
87
|
+
`runner step` alone. When this session leads, it is the `plastic-enforcer` role, never a
|
|
88
|
+
dispatched agent.
|
|
95
89
|
|
|
96
90
|
## Team
|
|
97
91
|
|
|
98
|
-
- **plastic-enforcer**: this session. Writes the Why and How record, dispatches,
|
|
99
|
-
verifies, closes.
|
|
92
|
+
- **plastic-enforcer**: this session. Writes the Why and How record, dispatches, applies
|
|
93
|
+
review findings, verifies, closes.
|
|
100
94
|
- **plastic-executor**: one dispatch per intent, implements the consolidated action tests first,
|
|
101
95
|
ticks the checklist, appends `## Insights`, drives the suite green.
|
|
102
|
-
- **the plan reviewer**:
|
|
96
|
+
- **the plan reviewer**: an optional dispatch before code, from `plastic-intent-executing`'s
|
|
103
97
|
`plan-reviewer-prompt.md`; a fresh agent, never the lead.
|
|
104
98
|
- **the post-execution reviewer**: dispatched only by the risk rule, from
|
|
105
99
|
`code-quality-reviewer-prompt.md`; a fresh agent, never the maker.
|
|
@@ -154,7 +148,9 @@ Only the owner can delegate. Delegates cannot re-delegate or release.
|
|
|
154
148
|
Headless note: in a headless or background run the session id may be unset; the arm verb then keys the lock by a derived key and the record hook still writes the ledger.
|
|
155
149
|
Verify with `plastic-lock status` rather than assuming.
|
|
156
150
|
|
|
157
|
-
Solo fallback: on a harness with no agent dispatch (Codex CLI today), this session walks the
|
|
151
|
+
Solo fallback: on a harness with no agent dispatch (Codex CLI today), this session walks the
|
|
152
|
+
graph (or the plan) itself, still writing the matrix and the tests first and reviewing its own
|
|
153
|
+
plan against the matrix before code, saying so in `## Insights`.
|
|
158
154
|
|
|
159
155
|
## Stage-Aware Entry
|
|
160
156
|
|
|
@@ -168,8 +164,8 @@ ledger is missing (then rebuild it with `Savepoint.rebuild_savepoint`).
|
|
|
168
164
|
| `Why spec.md created` | How |
|
|
169
165
|
| `How plan.md created` / `How checklist.md created` / `Exec started` | Exec (verify plan, matrix, checklist) |
|
|
170
166
|
| `Exec outcome.md created` | Exec done; complete the intent |
|
|
171
|
-
| `
|
|
172
|
-
| A node or `Intent` transition line (`n1 running ...`, `Intent needs_decision ...`) | Exec; a graph delivery is in progress - drive it through `scripts/runner`'s three public verbs, `step` (one turn of the dispatch loop), `status` (renders ledger state, safe to poll constantly), and `answer` (closes a `needs_decision` node) - read node status through `NodeLedger.status` before dispatching anything, never re-derive it by eye |
|
|
167
|
+
| A terminal savepoint line (`delivered` or `abandoned`) | Terminal; do not resume |
|
|
168
|
+
| A node or `Intent` transition line (`n1 running ...`, `Intent needs_decision ...`) | Exec; a graph delivery is in progress - drive it through `scripts/runner`'s three public verbs, `step` (one turn of the dispatch loop), `status` (renders ledger state, safe to poll constantly), and `answer` (closes a `needs_decision` node) - read node status through `NodeLedger.status` before dispatching anything, never re-derive it by eye. `scripts/graph-measure` is the read-only sibling over that same ledger, with its own three public verbs `intent`, `budget`, and `cohorts` - it never dispatches and never writes |
|
|
173
169
|
|
|
174
170
|
Filesystem fallback, in order: `checklist.md` with items checked means resume Exec from the
|
|
175
171
|
first unchecked item; `plan.md` plus `checklist.md` means enter Exec; `spec.md` alone means
|
|
@@ -179,42 +175,43 @@ Announce which stage you are entering and why.
|
|
|
179
175
|
|
|
180
176
|
## Why (the lead)
|
|
181
177
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
For a graph delivery, Why is already written into `graph.md`'s Goal and Decisions; nothing
|
|
179
|
+
else to do here. For work with no graph: read `## Context` and `### Decisions`, assess the
|
|
180
|
+
gaps, research them yourself (code, docs, related intents through `## Links`, the web if
|
|
181
|
+
needed; no questions to the human), record each decision in `## Context > ### Decisions` with
|
|
182
|
+
its rationale, log it in `## Insights` with the `(autonomous)` marker through
|
|
183
|
+
`scripts/insight-append`, and write `spec.md` only when the intent needs one (speccing is
|
|
184
|
+
optional). Then How.
|
|
189
185
|
|
|
190
186
|
## How (the lead), then the plan review
|
|
191
187
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
188
|
+
For a graph delivery, How is `graph.md` itself: no `plan.md`, no separate plan review (D1,
|
|
189
|
+
341). For work with no graph: write `plan.md` (numbered steps) and at least one real
|
|
190
|
+
`actions/ACTION_N.md` carrying the failure-mode matrix (one row per operation, the failure
|
|
191
|
+
mode, the test that catches it; a `.gitkeep`-only `actions/` is not a finished How), then
|
|
192
|
+
`checklist.md` covering every action. The plan reviewer is optional, not a required step:
|
|
193
|
+
when the delivery warrants review before code, dispatch it (boot 1) with
|
|
194
|
+
`plastic-intent-executing`'s `plan-reviewer-prompt.md`, the spawn preamble, and the intent
|
|
195
|
+
directory, apply every finding to the spec, the matrix, and the tests, and record what was
|
|
196
|
+
dropped and why in the action file's review notes. A REVISE verdict is applied and not
|
|
197
|
+
re-reviewed unless a finding changes a decision.
|
|
198
|
+
|
|
199
|
+
Print `ruby ~/.plastic/scripts/report-screen plan <intent_dir>` as the first characters of
|
|
200
|
+
the reply, nothing before it, no fence, before dispatching the executor (see
|
|
201
|
+
`references/human-report-contract.md` for the full binding table). It informs; it does not wait.
|
|
206
202
|
|
|
207
203
|
Then Exec.
|
|
208
204
|
|
|
209
205
|
## Exec (the executor)
|
|
210
206
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
with the missing context; BLOCKED stops under the error procedure.
|
|
217
|
-
|
|
207
|
+
For a graph delivery, `runner step` prints the spawn block for the next ready node; paste it
|
|
208
|
+
into the Agent tool, verbatim. For work with no graph, dispatch `plastic-executor` (boot 2)
|
|
209
|
+
through `plastic-intent-executing` with the whole consolidated action pasted in.
|
|
210
|
+
|
|
211
|
+
1. Read the executor's return by code: DONE or DONE_WITH_CONCERNS proceeds; NEEDS_CONTEXT
|
|
212
|
+
re-dispatches with the missing context; BLOCKED stops under the error procedure.
|
|
213
|
+
2. Tick the checklist as items land (the executor does this); verify tick-versus-diff against
|
|
214
|
+
the diff. A mismatch is a review finding, not a lead cleanup.
|
|
218
215
|
|
|
219
216
|
## Review by risk (boot 3, only when a rule fires)
|
|
220
217
|
|
|
@@ -224,13 +221,14 @@ holds, each checkable from disk with no judgment; otherwise the green suite is t
|
|
|
224
221
|
1. `git diff --name-only <red-commit>..HEAD` touches a path on the risk list in
|
|
225
222
|
`references/agent-architecture.md` (hooks, the lock, the arming module, the installer, a
|
|
226
223
|
release file).
|
|
227
|
-
2. A row of any `actions/ACTION_N.md`
|
|
228
|
-
diff, or a test the green run did not execute.
|
|
224
|
+
2. A row of any failure-mode matrix (an `actions/ACTION_N.md` or a `nodes/*.md` file) names a
|
|
225
|
+
test file that is not in that diff, or a test the green run did not execute.
|
|
229
226
|
3. The executor's completion report carries a status other than `delivered`, or a non-empty
|
|
230
227
|
`deviations` or `blockers` field.
|
|
231
228
|
|
|
232
229
|
The reviewer returns a pass or a list of fixes; the executor (re-dispatched) applies them, then
|
|
233
|
-
the suite runs once more.
|
|
230
|
+
the suite runs once more. On a graph, the risk rule maps onto the verify nodes named in
|
|
231
|
+
`graph.md`'s decisions; at most one review-fix round, never more.
|
|
234
232
|
## Project Creation
|
|
235
233
|
|
|
236
234
|
If the plan calls for creating a new project, determine the path from `~/.plastic/config.yml`
|