@zalom/plastic 1.0.0-beta.21 → 1.0.0-beta.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 CHANGED
@@ -20,6 +20,7 @@ store/
20
20
  actions/ # optional — individual work items
21
21
  resources/ # optional — research, references, screenshots, diagrams
22
22
  savepoint.md # optional — deterministic cycle-step ledger (auto-written)
23
+ revisions.md # optional - append-only structural-maintenance audit trail
23
24
  ```
24
25
 
25
26
  Lifecycle files (`spec.md`, `plan.md`, `checklist.md`, `outcome.md`) have defined
@@ -27,6 +28,33 @@ roles. Supporting artifacts that aren't lifecycle deliverables — research repo
27
28
  reference docs, external API snapshots, screenshots, diagrams — go in `resources/`.
28
29
  Name files inside as `{type}--{description}.md` (e.g., `deep-research--gsd-core.md`).
29
30
 
31
+ `revisions.md` is an optional, append-only structural-maintenance audit trail. It is not a
32
+ lifecycle deliverable and is never scaffolded at intent birth. Its mere existence signals that
33
+ the intent underwent structural (not conceptual) change. Structural maintenance is move-and-record:
34
+ it removes a misplaced section, file, or ref from its artifact and preserves that content in full
35
+ inside `revisions.md` (newest entry at the bottom, one entry per relocated item), so no record is
36
+ lost and the delivered meaning is never altered. Changing what an intent delivered is a new intent,
37
+ not a revision.
38
+
39
+ ### Structural maintenance and revisions.md
40
+
41
+ When a delivered intent accumulates structural junk (an unsanctioned section, a stray file, a
42
+ frontmatter edge to an intent that no longer exists), the intent-curator relocates it into
43
+ `revisions.md` instead of reopening the work. Each entry is a versioned, dated header
44
+ (`## Revision vN - YYYY-MM-DD-HH:MM`) plus `Why` (one sentence naming the broken rule, ending
45
+ with `[rule: <tag>]`), `Prior location`, and either `Content held` (the verbatim removed
46
+ content) or, for a frontmatter edit, a one-line `Change` (before and after). A stray file has
47
+ its full content embedded and the original is deleted.
48
+
49
+ Violation tags (starter set, free-text tags allowed):
50
+ - `unsanctioned-section`: a top-level section the sanctioned-section rule now rejects
51
+ - `phantom-section`: a section referenced but not present or not sanctioned
52
+ - `stray-file`: a file that does not belong in the intent directory
53
+ - `dangling-ref`: a link or reference to something that no longer exists
54
+ - `broken-chain`: a chain frontmatter edge to an intent that no longer exists
55
+ - `broken-source`: a sources frontmatter edge to an intent that no longer exists
56
+ - `misplaced-content`: content that belongs in a different artifact or section
57
+
30
58
  ## Frontmatter
31
59
 
32
60
  Identity and knowledge graph only. Nothing operational.
@@ -23,6 +23,7 @@ You are the Plastic Intent Curator. Your role is to maintain the health and navi
23
23
  3. **Link discovery** — suggest connections between intents that share topics but aren't linked
24
24
  4. **Cluster management** — create new clusters when 3+ unlinked intents share tags, merge or rename clusters as topics evolve
25
25
  5. **Orphan detection** — flag intents with no links and no cluster membership
26
+ 6. **Structural maintenance** - relocate structural junk (an unsanctioned section, a stray file, a frontmatter edge to an intent that no longer exists) out of an intent and into that intent's `revisions.md`, without altering what the intent delivered
26
27
 
27
28
  ## How You Work
28
29
 
@@ -32,11 +33,13 @@ You are the Plastic Intent Curator. Your role is to maintain the health and navi
32
33
  3. Compare: are there intents not in any cluster? Missing from Active/Completed/Abandoned? Status mismatches?
33
34
  4. Make targeted edits to INDEX.md and intent frontmatter/links
34
35
  5. On a terminal-state transition: whenever you move an intent to Completed OR Abandoned, refresh the QMD index for that store so the new outcome (or abandonment rationale) is searchable. This is mandatory on any terminal-state move and a no-op when QMD is absent, and it runs in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`
35
- 6. Report what you changed
36
+ 6. Structural maintenance is move-and-record: remove the misplaced section, file, or ref from its artifact, then create or append `revisions.md` in that intent directory (copy the FORM from `~/.plastic/templates/revisions.md`). One entry per relocated item, newest at the bottom: a `## Revision vN - YYYY-MM-DD-HH:MM` header, a one-sentence `Why` ending with `[rule: <tag>]`, `Prior location`, and either `Content held` (verbatim) or a one-line `Change` for a frontmatter edit. For a stray file, embed its full content and delete the original. The violation-tag catalog is canonical in PLASTIC.md.
37
+ 7. Report what you changed
36
38
 
37
39
  ## Constraints
38
40
 
39
41
  - You only edit `~/.plastic/INDEX.md` (or project INDEX.md) and `~/.plastic/store/*/ID--slug.md` (or project store) files
40
42
  - You never create new intents — that's the creating-intent skill's job
41
- - You never modify `## Insights`, `## Context`, or `## Outcome` content sections — those belong to the worker
43
+ - You never modify `## Insights`, `## Context`, or `## Outcome` content sections — those belong to the worker. Relocating a whole misplaced block out of an intent and into `revisions.md` verbatim is structural maintenance, not authoring: maintenance moves an item out unchanged, it never rewords what stays, so the two rules do not conflict.
44
+ - For structural maintenance you may edit any Plastic artifact in an intent directory (intent file, `spec.md`, `plan.md`, `checklist.md`, `outcome.md`, frontmatter, or a stray file) and may create or append `revisions.md`. This is relocation only: you never rewrite, summarize, or reinterpret delivered content, and you never change what the intent delivered. A change to delivered meaning is a new intent, not a revision.
42
45
  - For discovery, put QMD first when available (`qmd-sync search`), then fall back to Read and grep/find; use Edit for targeted changes
package/hooks/hooks.json CHANGED
@@ -37,6 +37,11 @@
37
37
  "type": "command",
38
38
  "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" code-gate",
39
39
  "statusMessage": "Checking lifecycle gate..."
40
+ },
41
+ {
42
+ "type": "command",
43
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" lock-gate",
44
+ "statusMessage": "Checking lock gate..."
40
45
  }
41
46
  ]
42
47
  },
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ INPUT=$(cat)
3
+ FILE_PATH=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("tool_params", "file_path") || data.dig("tool_input", "file_path") || ""' 2>/dev/null)
4
+
5
+ if [ -z "$FILE_PATH" ]; then
6
+ exit 0
7
+ fi
8
+
9
+ SESSION_ID=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("session_id") || ""' 2>/dev/null)
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ ruby "$SCRIPT_DIR/../scripts/hook-lock-gate" "$FILE_PATH" "$SESSION_ID"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.23",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+ #
5
+ # Usage: hook-lock-gate <file_path> [session_id]
6
+ # Fail-CLOSED PreToolUse gate (intent 96). Blocks a mutating write to an active
7
+ # intent's lifecycle dir when THIS session holds no live lock. Project code is NOT
8
+ # gated here (D2). Emits the PreToolUse JSON deny contract at exit 0; reserves
9
+ # non-zero exit for hook-internal errors only. Matcher Write|Edit|NotebookEdit, so
10
+ # reads never reach here.
11
+ require "json"
12
+ require_relative "lib/bridge"
13
+
14
+ begin
15
+ file_path = ARGV[0]
16
+ exit 0 unless file_path && !file_path.empty?
17
+ session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]
18
+
19
+ bridge_data = Bridge.discover_bridge(session: session, cwd: Dir.pwd) # nil = no lock held
20
+ reason = Bridge.lock_gate_decision(bridge_data, file_path)
21
+ exit 0 unless reason
22
+
23
+ print JSON.generate(
24
+ "hookSpecificOutput" => {
25
+ "hookEventName" => "PreToolUse",
26
+ "permissionDecision" => "deny",
27
+ "permissionDecisionReason" => reason # "run /plastic-intent-starting to lock and begin"
28
+ }
29
+ )
30
+ exit 0
31
+ rescue => e
32
+ # Hook-internal failure only: do NOT impersonate a user-deny. Log and fail open
33
+ # here so a gate bug never bricks editing; the fail-CLOSED intent is about MISSING
34
+ # locks, not crashes.
35
+ $stderr.puts "plastic lock-gate error: #{e.message}"
36
+ exit 0
37
+ end
@@ -585,15 +585,19 @@ module Bridge
585
585
 
586
586
  # --- Auto mode (intent 27) ---
587
587
 
588
- # Arm auto mode for a session+intent. Works even when no bridge exists yet
589
- # (mid-session intent creation). Re-derives intent state, then sets build.auto.
590
- def self.arm_auto(session, intent_id:, intent_dir:, store:, name:)
588
+ # Shared arming spine (intent 96): resolve the session key, derive intent state,
589
+ # set the caller-controlled auto flag, acquire the delivery lock, provision the
590
+ # per-intent worktrees, persist, and purge terminal bridges. arm_auto (auto: true)
591
+ # and arm_guided (auto: false) are thin delegators so the lock-stamp + provision
592
+ # behaviour stays identical across both modes. Works even when no bridge exists
593
+ # yet (mid-session intent creation).
594
+ def self.arm(session, intent_id:, intent_dir:, store:, name:, auto:)
591
595
  key = resolve_session(session, intent_id: intent_id, store: store)
592
596
  if blank?(session) && blank?(ENV["CLAUDE_CODE_SESSION_ID"])
593
- $stderr.puts "plastic: no session id available; arming auto with derived bridge key #{key}"
597
+ $stderr.puts "plastic: no session id available; arming with derived bridge key #{key}"
594
598
  end
595
599
  data = derive(key, intent_id: intent_id, intent_dir: intent_dir, store: store, name: name)
596
- data["build"]["auto"] = true
600
+ data["build"]["auto"] = auto
597
601
 
598
602
  # Acquire the delivery lock: this armed bridge is now the single owner of the
599
603
  # intent's delivery (intent 73c). Stamp owner + pid liveness fields.
@@ -617,6 +621,20 @@ module Bridge
617
621
  purge_done_bridges(session: key)
618
622
  data
619
623
  end
624
+ private_class_method :arm
625
+
626
+ # Arm auto mode for a session+intent. Works even when no bridge exists yet
627
+ # (mid-session intent creation). Re-derives intent state, then sets build.auto.
628
+ def self.arm_auto(session, intent_id:, intent_dir:, store:, name:)
629
+ arm(session, intent_id: intent_id, intent_dir: intent_dir, store: store, name: name, auto: true)
630
+ end
631
+
632
+ # Acquire the delivery lock WITHOUT auto mode (intent 96 / Start guided branch).
633
+ # Mirrors arm_auto's lock-stamp + worktree provision but leaves build.auto = false.
634
+ # Same signature as arm_auto; disarm_auto (mode-agnostic) releases a guided lock.
635
+ def self.arm_guided(session, intent_id:, intent_dir:, store:, name:)
636
+ arm(session, intent_id: intent_id, intent_dir: intent_dir, store: store, name: name, auto: false)
637
+ end
620
638
 
621
639
  # Disarm auto mode. No-op if no bridge exists for the session.
622
640
  def self.disarm_auto(session)
@@ -673,6 +691,68 @@ module Bridge
673
691
  "(blocked edit: #{file_abs})"
674
692
  end
675
693
 
694
+ # --- Fail-closed lock gate (intent 96) -------------------------------------
695
+
696
+ # Returns a reason String to BLOCK, or nil to ALLOW. bridge_data is whatever
697
+ # discover_bridge(session:, cwd:) resolved for THIS session (own-session strict,
698
+ # or the lone headless/derived-key bridge), or nil = no lock held.
699
+ #
700
+ # BLOCK iff the target is an ACTIVE intent's lifecycle dir AND this session does
701
+ # NOT hold a LIVE lock FOR THAT SAME INTENT. ALLOW otherwise: a lock held for the
702
+ # target intent; a not-yet-active intent (creation, What); project code; anything
703
+ # that is not an active intent dir. The lock must match the TARGET intent: holding
704
+ # a live lock on intent A never green-lights a mutating write into intent B's dir.
705
+ # Project code is NOT gated here (D2) - it is isolated per-intent by worktree +
706
+ # branch, not by a write-time lock.
707
+ def self.lock_gate_decision(bridge_data, file_path)
708
+ return nil if blank?(file_path)
709
+
710
+ # Resolve the TARGET intent first so allow-by-lock is scoped to that intent.
711
+ target_dir = intent_dir_for(file_path)
712
+ return nil unless target_dir # not an intent dir (project code, scratch) -> ALLOW
713
+ id = intent_id_from_dir(target_dir) # "<id>" from "<id>--<slug>"
714
+ store = File.dirname(target_dir) # the store/ dir holding the intent
715
+ return nil unless id && intent_active?(id, store: store) # not-yet-active (creation/What) -> ALLOW
716
+
717
+ # ALLOW only when this session holds a live lock for THIS SAME intent. A live
718
+ # lock on a different intent does not authorize writing into this intent's dir.
719
+ return nil if holds_live_lock?(bridge_data) &&
720
+ bridge_data.is_a?(Hash) &&
721
+ bridge_data.dig("intent", "id").to_s == id.to_s
722
+
723
+ "run /plastic-intent-starting to lock and begin"
724
+ end
725
+
726
+ # A discovered bridge counts as a held lock only if its lock is stamped AND the
727
+ # owner process is live on THIS host. Staleness/identity lives HERE (gate read
728
+ # path), never in arm_auto's write path. On host-mismatch we cannot probe the
729
+ # pid, so fall back to "held" (a remote live owner should still block).
730
+ def self.holds_live_lock?(bridge_data)
731
+ return false unless bridge_data.is_a?(Hash)
732
+ lock = bridge_data["lock"] || {}
733
+ return false if blank?(lock["owner_session"])
734
+ pid = lock["pid"]
735
+ host = lock["host"]
736
+ this_host = (Socket.gethostname rescue nil)
737
+ if host && this_host && host == this_host && pid.is_a?(Integer)
738
+ begin
739
+ Process.kill(0, pid) # raises Errno::ESRCH if dead
740
+ return true
741
+ rescue Errno::ESRCH
742
+ return false # dead owner -> not a held lock -> gate blocks (re-lock)
743
+ rescue Errno::EPERM
744
+ return true # exists, not ours to signal -> live
745
+ end
746
+ end
747
+ true # different/unknown host: treat the stamped lock as held
748
+ end
749
+
750
+ # "<id>" from a ".../store/<id>--<slug>" dir, else nil.
751
+ def self.intent_id_from_dir(dir)
752
+ base = File.basename(dir.to_s)
753
+ base.include?("--") ? base.split("--", 2).first : nil
754
+ end
755
+
676
756
  # --- Worktree isolation gate (intent 73c2) ---
677
757
 
678
758
  # Returns a reason String to BLOCK, or nil to ALLOW. Two independent rules,
@@ -208,6 +208,7 @@ class InstallerCore
208
208
  "scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
209
209
  "scripts/lib/power_tools.rb" => "scripts/lib/power_tools.rb",
210
210
  "scripts/hook-code-gate" => "scripts/hook-code-gate",
211
+ "scripts/hook-lock-gate" => "scripts/hook-lock-gate",
211
212
  "scripts/hook-bash-gate" => "scripts/hook-bash-gate",
212
213
  "scripts/hook-retrieval-gate" => "scripts/hook-retrieval-gate",
213
214
  "scripts/lib/retrieval_gate.rb" => "scripts/lib/retrieval_gate.rb",
@@ -235,6 +236,7 @@ class InstallerCore
235
236
  "templates/plan.md" => "templates/plan.md",
236
237
  "templates/checklist.md" => "templates/checklist.md",
237
238
  "templates/outcome.md" => "templates/outcome.md",
239
+ "templates/revisions.md" => "templates/revisions.md",
238
240
  "scripts/spawn-preamble" => "scripts/spawn-preamble",
239
241
  "scripts/lib/store_provisioning.rb" => "scripts/lib/store_provisioning.rb",
240
242
  "scripts/provision-project-store" => "scripts/provision-project-store",
@@ -562,6 +564,10 @@ class InstallerCore
562
564
  "matcher" => "Write|Edit|NotebookEdit",
563
565
  "hooks" => [
564
566
  { "type" => "command", "command" => "#{hook_dir}/plastic-code-gate", "statusMessage" => "Checking lifecycle gate..." },
567
+ # Fail-closed lock gate (intent 96): a 2nd ordered command INSIDE the
568
+ # code-gate group (NOT a new same-matcher group, which the merge loop
569
+ # below would collapse). Blocks no-lock writes to an active intent's dir.
570
+ { "type" => "command", "command" => "#{hook_dir}/plastic-lock-gate", "statusMessage" => "Checking lock gate..." },
565
571
  ],
566
572
  },
567
573
  {
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: plastic-intent-starting
3
+ description: >-
4
+ Board a session onto an intent: take the lock FIRST, confirm savepoint state, ask auto or
5
+ guided ONCE, then resume at the latest delivered station and run the cycle to Done. Use on
6
+ `continuing --intent {id}`, when a new intent is registered and the user asks to work it,
7
+ or when the user picks an intent to work. Requires the intent in INDEX `## Active`.
8
+ ---
9
+
10
+ # Intent Starting — board a session onto an intent
11
+
12
+ Announce: "Boarding intent [ID] — [name]."
13
+
14
+ `plastic-intent-starting` is the Start procedure. It boards a session onto one intent: take
15
+ the lock FIRST, confirm the delivery state, ask **auto or guided ONCE**, board at the latest
16
+ delivered station, then run the cycle to Done. The What → Why → How → Exec stations are the
17
+ train track; Start boards the train, the ending procedure (~93) exits it.
18
+
19
+ ## Precondition + trigger
20
+
21
+ Fires when the user picks an intent to work, when an agent is told to continue a SPECIFIC
22
+ intent, or on `continuing --intent {id}` (the `continuing` → `starting` router is 106's job;
23
+ this skill is invokable standalone now).
24
+
25
+ If the intent is **terminal** (Done / Abandoned in INDEX): report only. Take NO lock, run NO
26
+ resume, do NOT reopen it. Summarize the outcome and ask what is next. Stop here.
27
+
28
+ ## Lock FIRST (the spine)
29
+
30
+ The lock is non-negotiable and comes before any mutating work. The ACTION-3 lock-gate
31
+ enforces it: without a held lock, mutating writes to this active intent's dir are denied with
32
+ "run /plastic-intent-starting to lock and begin".
33
+
34
+ 1. **Ensure the intent is in INDEX `## Active`.** If it sits in `## Future`, activate it
35
+ (move it to `## Active`, auto-commit) before arming. Creation precedes activation, so a
36
+ brand-new What intent is activated here, then locked.
37
+ 2. **Arm the bridge.** Which arm is chosen by the mode answer (below), but the lock itself is
38
+ taken first. Reuse the arm one-liner shape from `plastic-auto`:
39
+ ```bash
40
+ # guided (lock only):
41
+ ruby -r ~/.plastic/scripts/lib/bridge -e \
42
+ 'Bridge.arm_guided(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
43
+ # auto (lock + auto), then hand to plastic-auto:
44
+ ruby -r ~/.plastic/scripts/lib/bridge -e \
45
+ 'Bridge.arm_auto(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
46
+ ```
47
+ Replace `<ID>`, `<STORE>` (`~/.plastic/projects/<slug>/store` or `~/.plastic/store`),
48
+ `<dir>` (the `ID--slug` directory), and `<name>`.
49
+
50
+ **Session id resolution (verbatim from `plastic-auto`).** The first argument is the session
51
+ id the bridge is keyed by: pass the hook stdin `session_id` when you have it, otherwise
52
+ `ENV["CLAUDE_CODE_SESSION_ID"]`, otherwise `nil`. Both arms call `resolve_session`, which
53
+ picks the first non-empty of: the explicit id you pass → `CLAUDE_CODE_SESSION_ID` → a
54
+ deterministic derived key (a hash of the store and intent id). It never returns nil, so the
55
+ lock is taken even when every session env var is empty; arming prints a one-line stderr
56
+ notice when it falls through to the derived key.
57
+
58
+ Idempotent re-arm: arming again with the same owner just refreshes the lock (re-derives and
59
+ rewrites the bridge); it is not an error to re-board an intent this session already owns.
60
+
61
+ ## Confirm delivery state
62
+
63
+ Read `savepoint.md` and classify from the **last line** alone, then verify ONLY that line's
64
+ artifact is real (sentinel-aware via `Bridge.stage_file_present?`). On drift (the last line
65
+ disagrees with files on disk), rebuild the ledger from disk and note the correction. Do not
66
+ inline the rebuild; the `plastic-savepoint` skill owns it:
67
+ ```bash
68
+ ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.rebuild_savepoint("<intent_dir>")'
69
+ ```
70
+
71
+ ## Report + ask "auto or guided?" ONCE
72
+
73
+ Report: the intent, the station it lands at (the matrix below), what is delivered, the next
74
+ step. Then ask the user **"auto or guided?"** — exactly ONCE, whatever station it lands at.
75
+ Never re-ask at a later station.
76
+
77
+ - **guided** → `arm_guided` (lock only); continue step by step with the user through the
78
+ station's work below.
79
+ - **auto** → `arm_auto` (lock + auto), then hand off to `plastic-auto`. The auto branch's
80
+ only remaining job is the handoff; `plastic-auto` runs the cycle from here.
81
+
82
+ ## Board at the latest delivered station
83
+
84
+ The station is derived from `savepoint.md` last line + real artifacts on disk. See
85
+ `references/boarding-matrix.md` for the full table (last line → latest delivered → boards at →
86
+ continue with) and the per-station notes. Summary of what "continue" means per station:
87
+
88
+ - **What** → do what What requires (106-expanded), then brainstorm → `spec.md`.
89
+ - **Why** → continue brainstorming → `spec.md`.
90
+ - **How** → continue `plan.md` + `actions/` + `checklist.md`.
91
+ - **Exec** → verify what has been delivered, then continue (or restart) the delivery /
92
+ research; tick the checklist.
93
+ - **ready to complete** (`Exec outcome.md created`) → exit at Done.
94
+ - **Done** → report only, ask what is next, never reopen.
95
+
96
+ ## Disarm / release on done
97
+
98
+ When delivery finishes, disarm and release per the `plastic-auto` disarm/release prose (do
99
+ not duplicate it here). The guided branch releases the lock via `disarm_auto`, which is
100
+ mode-agnostic (it sets `auto = false` and calls `Worktree.release`), so it releases a guided
101
+ lock too. When the work ships through a release, the release path merges the branch before the
102
+ worktree is removed; the plain disarm remove is only for the no-release case.
103
+
104
+ ## References
105
+
106
+ - `references/boarding-matrix.md` — the full boarding table and per-station behaviour.
@@ -0,0 +1,117 @@
1
+ {
2
+ "skill_name": "plastic-intent-starting",
3
+ "notes": "Intent 96. Scopes: description triggering (1-5) and behavior (6-10: lock-first spine, ask-mode-once, terminal report-only, mode->arm mapping, boarding matrix). The lock-first and gate behaviors are also proven by Ruby tests (test/bridge_guided_test.rb, test/lock_gate_test.rb, test/lock_gate_hook_test.rb).",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "triggering",
8
+ "set": "train",
9
+ "prompt": "start work on intent 96",
10
+ "expected_output": "Activates plastic-intent-starting (board a session onto the named intent).",
11
+ "files": [],
12
+ "assertions": [
13
+ { "type": "code", "check": "router CHOICE == plastic-intent-starting", "result": "expect-pass" }
14
+ ]
15
+ },
16
+ {
17
+ "id": 2,
18
+ "scope": "triggering",
19
+ "set": "train",
20
+ "prompt": "continuing --intent 96",
21
+ "expected_output": "Activates plastic-intent-starting (the --intent form boards a specific intent).",
22
+ "files": [],
23
+ "assertions": [
24
+ { "type": "code", "check": "router CHOICE == plastic-intent-starting", "result": "expect-pass" }
25
+ ]
26
+ },
27
+ {
28
+ "id": 3,
29
+ "scope": "triggering",
30
+ "set": "validation",
31
+ "prompt": "work this intent",
32
+ "expected_output": "Activates plastic-intent-starting when an intent is in context to be worked.",
33
+ "files": [],
34
+ "assertions": [
35
+ { "type": "code", "check": "router CHOICE == plastic-intent-starting", "result": "expect-pass" }
36
+ ]
37
+ },
38
+ {
39
+ "id": 4,
40
+ "scope": "triggering",
41
+ "set": "validation",
42
+ "prompt": "continue",
43
+ "expected_output": "Does NOT activate plastic-intent-starting; bare 'continue' is plastic-continuing (no specific intent named).",
44
+ "files": [],
45
+ "assertions": [
46
+ { "type": "code", "check": "router CHOICE != plastic-intent-starting", "result": "expect-pass" }
47
+ ]
48
+ },
49
+ {
50
+ "id": 5,
51
+ "scope": "triggering",
52
+ "set": "validation",
53
+ "prompt": "create a new intent for the uploader retry policy",
54
+ "expected_output": "Does NOT activate plastic-intent-starting; activates plastic-creating-intent.",
55
+ "files": [],
56
+ "assertions": [
57
+ { "type": "code", "check": "router CHOICE != plastic-intent-starting", "result": "expect-pass" }
58
+ ]
59
+ },
60
+ {
61
+ "id": 6,
62
+ "scope": "behavior",
63
+ "set": "train",
64
+ "prompt": "Active intent X exists at the Why station. Start work on it (guided).",
65
+ "expected_output": "Takes the lock FIRST via Bridge.arm_guided (auto stays false) before any mutating work, confirms savepoint, then boards at Why and continues brainstorming toward spec.md.",
66
+ "files": [],
67
+ "assertions": [
68
+ { "type": "human", "check": "lock acquired before any mutating write; SKILL.md 'Lock FIRST' section precedes station work", "result": "expect-pass" },
69
+ { "type": "code", "check": "Bridge.arm_guided stamps the lock with auto=false (test/bridge_guided_test.rb green); lock-gate denies a no-lock write to the active intent dir (test/lock_gate_hook_test.rb green)", "result": "pass" }
70
+ ]
71
+ },
72
+ {
73
+ "id": 7,
74
+ "scope": "behavior",
75
+ "set": "train",
76
+ "prompt": "Start work on an active intent without saying auto or guided.",
77
+ "expected_output": "Asks 'auto or guided?' exactly once after reporting the station; never re-asks at later stations.",
78
+ "files": [],
79
+ "assertions": [
80
+ { "type": "human", "check": "SKILL.md asks the mode exactly once and states 'never re-asked at a later station'", "result": "expect-pass" }
81
+ ]
82
+ },
83
+ {
84
+ "id": 8,
85
+ "scope": "behavior",
86
+ "set": "train",
87
+ "prompt": "Start work on an intent that is Done in INDEX.",
88
+ "expected_output": "Reports only: no lock, no resume, no reopen. Summarizes the outcome and asks what is next.",
89
+ "files": [],
90
+ "assertions": [
91
+ { "type": "human", "check": "terminal intent -> report only, no arm call", "result": "expect-pass" }
92
+ ]
93
+ },
94
+ {
95
+ "id": 9,
96
+ "scope": "behavior",
97
+ "set": "validation",
98
+ "prompt": "Start work and choose auto.",
99
+ "expected_output": "guided -> Bridge.arm_guided; auto -> Bridge.arm_auto then hand off to plastic-auto (auto branch's only remaining job is the handoff).",
100
+ "files": [],
101
+ "assertions": [
102
+ { "type": "human", "check": "SKILL.md maps guided->arm_guided and auto->arm_auto+handoff to plastic-auto", "result": "expect-pass" }
103
+ ]
104
+ },
105
+ {
106
+ "id": 10,
107
+ "scope": "behavior",
108
+ "set": "validation",
109
+ "prompt": "Start an intent whose savepoint last line is 'How checklist.md created'.",
110
+ "expected_output": "Boards at Exec (verify plan + checklist real), per references/boarding-matrix.md, and continues delivery/ticks the checklist.",
111
+ "files": [],
112
+ "assertions": [
113
+ { "type": "human", "check": "boarding matrix lands 'How checklist.md created' / 'Exec started' at Exec", "result": "expect-pass" }
114
+ ]
115
+ }
116
+ ]
117
+ }
@@ -0,0 +1,35 @@
1
+ # Boarding matrix — which station Start drops you at
2
+
3
+ The station is derived from `savepoint.md`'s last line plus the real artifacts on disk.
4
+ Classify from the last line alone, then verify ONLY that line's artifact is real
5
+ (sentinel-aware). On drift, rebuild the ledger from disk and note it.
6
+
7
+ | savepoint last line | latest delivered | boards at | continue with |
8
+ |---|---|---|---|
9
+ | `What {id}--{slug}.md` (born) | What | **What / Why** | What work (106-expanded), then brainstorm → `spec.md` |
10
+ | `Why started` (spec still sentinel) | What | **Why** | continue brainstorming → `spec.md` |
11
+ | `Why spec.md created` | Why | **How** | `plan.md` + `actions/` + `checklist.md` |
12
+ | `How started` / `How plan.md created` | (How in progress) | **How** | finish `plan.md` → `checklist.md` |
13
+ | `How checklist.md created` / `Exec started` | How | **Exec** | implement, tick the checklist |
14
+ | `Exec outcome.md created` | Exec | **ready to complete** | exit at Done |
15
+ | `Done delivered` / `Done abandoned` | terminal | **report only** | immutable; ask what is next |
16
+
17
+ ## Per-station behaviour (what "continue" means)
18
+
19
+ - **What** → do what What requires (to be expanded in 106), then brainstorm → `spec.md`.
20
+ - **Why** → continue brainstorming; deliver `spec.md`.
21
+ - **How** → continue `plan.md` + `actions/` + `checklist.md`.
22
+ - **Exec** → verify what has been delivered, then continue (or restart) the delivery /
23
+ research. The first unchecked `checklist.md` item is the next step; the newest `## Insights`
24
+ entry supplies human-readable context.
25
+ - **ready to complete** → `outcome.md` is real; run the ending procedure (~93).
26
+ - **Done** → terminal. Report the outcome, ask what is next. Never reopen; INDEX is
27
+ authoritative.
28
+
29
+ ## Notes
30
+
31
+ - The mode (auto / guided) is asked exactly ONCE, whatever station Start lands at. It is never
32
+ re-asked at a later station. The lock is taken FIRST regardless of station (terminal intents
33
+ excepted: they get no lock and no resume).
34
+ - An `## Insights` entry marked `(autonomous)` means the intent was being delivered
35
+ autonomously; in guided mode, surface that and offer to hand back to `plastic-auto`.
@@ -0,0 +1,58 @@
1
+ # revisions.md
2
+
3
+ <!--
4
+ FORM reference. A live revisions.md exists in an intent directory ONLY when structural
5
+ maintenance actually happened. It is never scaffolded at intent birth, it carries no
6
+ placeholder sentinel, and its mere presence signals that this intent underwent structural
7
+ (not conceptual) change.
8
+
9
+ Rules:
10
+ - Append-only. Newest entry at the BOTTOM. Never edit or reorder past entries.
11
+ - One entry per relocated item (one section, one file, or one ref per entry).
12
+ - Move-and-record: remove the misplaced thing from its artifact and preserve it IN FULL
13
+ here. Never rewrite, summarize, or reinterpret delivered meaning. A change to delivered
14
+ meaning is a new intent, not a revision.
15
+ - Header form: "## Revision vN - YYYY-MM-DD-HH:MM" (N increments by one per entry).
16
+
17
+ Fields:
18
+ - Why: one sentence naming the broken rule, ending with "[rule: <tag>]".
19
+ - Prior location: the artifact plus the section or path the content came from.
20
+ - Content held: the verbatim removed content, as an indented block (block form), OR
21
+ - Change: a one-line "removed X (before: ... -> after: ...)" for a frontmatter
22
+ edit, used instead of a Content held block.
23
+
24
+ Stray file: embed the file's full content as the Content held block and delete the
25
+ original; Prior location names the filename. revisions.md is the single container.
26
+
27
+ Violation-tag catalog (starter set; free-text tags are allowed):
28
+ - unsanctioned-section : a top-level section the sanctioned-section rule now rejects
29
+ - phantom-section : a section referenced but not present or not sanctioned
30
+ - stray-file : a file that does not belong in the intent directory
31
+ - dangling-ref : a link or reference to something that no longer exists
32
+ - broken-chain : a chain frontmatter edge to an intent that no longer exists
33
+ - broken-source : a sources frontmatter edge to an intent that no longer exists
34
+ - misplaced-content : content that belongs in a different artifact or section
35
+
36
+ The examples below show the three variants. Delete them when you write the first real
37
+ entry; keep the "# revisions.md" title line above.
38
+ -->
39
+
40
+ ## Revision v1 - 2026-06-30-14:35
41
+ - Why: unsanctioned top-level section left over from an early draft [rule: unsanctioned-section]
42
+ - Prior location: intent.md - ## Scope
43
+ - Content held:
44
+
45
+ ## Scope
46
+ <the full content that was removed, verbatim>
47
+
48
+ ## Revision v2 - 2026-06-30-15:02
49
+ - Why: chain edge to an intent that no longer exists [rule: broken-chain]
50
+ - Prior location: intent.md frontmatter - chain
51
+ - Change: removed "99x" (before: ["4a1a", "99x"] -> after: ["4a1a"])
52
+
53
+ ## Revision v3 - 2026-06-30-15:20
54
+ - Why: a stray notes file that does not belong in the intent directory [rule: stray-file]
55
+ - Prior location: notes-old.md
56
+ - Content held:
57
+
58
+ <the full content of notes-old.md, verbatim; the original file is then deleted>