packet-tracer-skill 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,122 @@ All notable changes to this project should be recorded in this file.
4
4
 
5
5
  The format is intentionally simple and release-oriented.
6
6
 
7
+ ## [0.3.2] - 2026-09-12
8
+
9
+ ### Picking up a half-finished edit session
10
+
11
+ Almost nothing here needs remembering between turns: `--doctor`,
12
+ `--explain-plan` and `--parity-report` all recompute from the install, the
13
+ donor registry and the bridge, so an agent that has lost its earlier context
14
+ re-runs one and is exactly as certain as before. One question had no such
15
+ source -- part-way through `--explain-plan` -> `--edit` -> `--parity-report`,
16
+ *which lab am I working on and what comes next*.
17
+
18
+ ### Added
19
+
20
+ - `output/session-log.jsonl`, one line per run, and two flags that read it
21
+ back: `--session-state` lists the labs in flight, `--resume <lab.pkt>` says
22
+ where one was left.
23
+ - `--resume` re-hashes the lab and compares it with what the last step
24
+ recorded, and claims a position **only when the two agree**. When they do
25
+ not it says the lab has changed and to re-derive instead -- the same
26
+ refusal-first stance the rest of the skill takes. Verified first that a
27
+ digest can carry that weight: decoding a lab and re-encoding it reproduces
28
+ the file byte for byte.
29
+ - Three constraints, each with a test. No secrets: facts are allow-listed
30
+ rather than filtered, because an edit prompt holds a passphrase in a field
31
+ called `passphrase` and a deny-list would leak whatever secret field is added
32
+ next; the prompt survives only as a non-reversible shape fingerprint. No
33
+ weight: a test regenerates a lab with and without the log and compares the
34
+ decoded content. Ships nowhere: `output/` is gitignored and in no
35
+ `package.json` file list, checked rather than assumed.
36
+
37
+ ### Fixed
38
+
39
+ - `PKT_USAGE_LEDGER=on`, the obvious way to switch learning on, made the path
40
+ resolver treat `"on"` as a filename and write the ledger to a file called
41
+ `on` in the working directory. One variable carrying two meanings with only
42
+ one of its two readers knowing the switch words. Found by doing it.
43
+ - `--session-state` and `--resume` were recorded as steps by the same wrapper
44
+ as the work they report on, so each recovery pushed itself in front of the
45
+ real chain. In a bounded log a reader that writes eventually evicts the steps
46
+ it exists to report.
47
+
48
+ ### Changed
49
+
50
+ - `main()` is split into `_build_parser`, `_dispatch` and a wrapper that
51
+ records once around the whole dispatch. Fourteen branches with twelve return
52
+ points is not somewhere to repeat a hook.
53
+
54
+ 847 passed, 1 skipped. Corpus: 32 of 33 generated, 31 opened, 0 unexpected.
55
+
56
+ ## [0.3.1] - 2026-09-06
57
+
58
+ ### Wireless labs that actually carry traffic
59
+
60
+ `0.3.0` shipped wireless topologies that opened and passed every static check
61
+ while no client could reach anything. Measured on a generated lab after this
62
+ release: a laptop takes its lease from the home router and pings the gateway
63
+ and the other laptop 4/4, over Wi-Fi, on an open network and on WPA2.
64
+
65
+ Six facts about one wireless lab were each decided in two places with nothing
66
+ comparing them, and every one of them had to be fixed before a packet moved.
67
+
68
+ ### Fixed
69
+
70
+ - **The access point's key was written where Packet Tracer does not read it.**
71
+ A working WPA2 home router keeps `WIRELESS_COMMON/WEP_PROCESS/KEY` with
72
+ `WEP_PROCESS/ENCRYPTION`, and carries no `WPA_PASSPHRASE` at all -- the field
73
+ names are legacy and WPA2 uses them. Choosing the field by authentication
74
+ type left the access point running WPA2 with no key while its clients had
75
+ one. Both sides looked right in every field anyone was reading.
76
+ - **A home router's DHCP lease records were deleted when its LAN moved.** That
77
+ is how a client gets its address back when the file opens; they are
78
+ renumbered into the new pool now, and a lease naming a client the prune
79
+ removed is dropped.
80
+ - **The client's security, network name and addressing mode never followed the
81
+ access point.** `set_wireless_ssid` wrote the access point;
82
+ `associate_wireless_client` wrote only the SSID. A repair pass now carries
83
+ authentication, encryption, key and network type across, and only into the
84
+ live profile -- every working client leaves its saved `PROFILES` list as the
85
+ donor's boilerplate.
86
+ - **A home router served whatever network its donor had.** The laptops were
87
+ addressed on the planned network and the router on the donor's; the router
88
+ now moves onto the one its own clients point at, and its pool with it.
89
+ - **The layout placed wireless clients out of radio range.** Hosts were laid
90
+ out in rows under the switch they hang off, and a wireless client hangs off
91
+ nothing -- one landed 420 units from a router whose radio reaches 250.
92
+ - **`port_exists` accepted interface names a home router does not have.** It
93
+ checked only the port index, so `FastEthernet0/1` passed on a device whose
94
+ sockets are `Ethernet 1` .. `4`, and with no interfaces in its configuration
95
+ `Ethernet 99` passed too. Both models were read off the live devices; the
96
+ repair pass reads the same list, so a wrong name is renamed rather than the
97
+ cable dropped.
98
+ - **The coherence checker could not see a home router's LAN address.** It walks
99
+ running configs, and that address is a setting, so it reported
100
+ `gateway_answers_for_nobody` against a router holding exactly that address.
101
+
102
+ ### Changed
103
+
104
+ - The committed sample catalogue is limited to the labs it may publish.
105
+ Rebuilding it on a machine with saved labs had staged 350 entries naming
106
+ their owner, with absolute paths under their home directory, into a file
107
+ bound for a public repository. Local labs go to a git-ignored file beside it
108
+ and donor ranking reads both, so nothing about donor choice changed.
109
+ - Comments, docs and fixtures no longer identify their evidence by the filename
110
+ of someone's saved lab.
111
+
112
+ ### Notes on measurement
113
+
114
+ `pt_inspect_ports` immediately after a lab opens shows a wireless client as
115
+ `up`, `linked`, `ip 0.0.0.0`. That is a first reading and means nothing -- a lab
116
+ Packet Tracer saved itself, seconds after it pinged 4/4, reads the same way on
117
+ reopen. Several hypotheses were rejected against that control rather than
118
+ argued: radio bandwidth matching, channel matching, `NETWORK_TYPE`, and the
119
+ belief that only a runtime nudge could make association work.
120
+
121
+ 833 passed, 1 skipped. Corpus: 32 of 33 generated, 31 opened, 0 unexpected.
122
+
7
123
  ## [0.3.0] - 2026-08-06
8
124
 
9
125
  ### First verified generation
package/README.md CHANGED
@@ -10,7 +10,7 @@ This repository is built for one job: take a natural-language network request, b
10
10
 
11
11
  It is intended for networking labs where correctness matters more than producing a pretty but unverifiable diagram. The skill can plan, inspect, edit, compare, and explain Packet Tracer scenarios, but it deliberately separates "recognized by the parser", "visible in inventory", "edit-proven", "donor-backed ready", and "generate-ready" support.
12
12
 
13
- ## What `0.3.0` does
13
+ ## What `0.3.2` does
14
14
 
15
15
  In this release a prompt produces a lab Packet Tracer opens.
16
16
 
@@ -24,11 +24,12 @@ inspecting the file.
24
24
 
25
25
  | measurement | result |
26
26
  | --- | --- |
27
- | corpus scenarios generated | 32 of 33 |
28
- | of those, opened by Packet Tracer | 32 of 32 |
29
- | tests | 657 passed, 1 skipped |
27
+ | corpus scenarios generated | 32 of 33 (the 33rd asks for no devices and is refused) |
28
+ | of those, opened by Packet Tracer | 31 of 32 |
29
+ | tests | 833 passed, 1 skipped |
30
30
  | generated DHCP lab | four PCs took leases from the router pool and pinged their gateway and each other 4/4 |
31
31
  | generated leased line | traffic crossed `Serial0/1/1 <-> Serial0/1/0` 4/4 |
32
+ | generated home-router lab | both hosts pinged the gateway and each other 4/4, cabled and over Wi-Fi, open and WPA2 |
32
33
 
33
34
  Three defects had made every generated WAN lab unopenable, each hiding the next.
34
35
  A donor the selector had *rejected* still rewrote the request, so a planned
@@ -43,6 +44,14 @@ donor-prune generation: a real lab is pruned and rewired to match the request.
43
44
  The atlas `generate_ready` count further down is a stricter per-feature
44
45
  acceptance gate, still `0` by design.
45
46
 
47
+ **One thing to know when you check a Wi-Fi lab.** Read a wireless client once,
48
+ straight after the file opens, and it will look broken: port `up` and `linked`,
49
+ `ip 0.0.0.0`, the radio at its un-negotiated rate. That is a first reading and
50
+ means nothing -- the client has not finished taking its lease. Read it again and
51
+ it holds its address and pings. A lab Packet Tracer saved itself, seconds after
52
+ it pinged 4/4, reads the same way on reopen, which is how we know the reading
53
+ rather than the lab is at fault.
54
+
46
55
  The previous line, the `0.2.3` capability release, was focused on:
47
56
 
48
57
  - donor-backed and scenario-aware public messaging
@@ -220,6 +229,34 @@ Launch references:
220
229
  - [docs/security-edge-deepening-proof.md](docs/security-edge-deepening-proof.md)
221
230
  - [docs/packet-tracer-feature-gap-atlas.md](docs/packet-tracer-feature-gap-atlas.md)
222
231
 
232
+ ## Picking Up A Half-Finished Edit Session
233
+
234
+ Almost nothing in this skill needs remembering between turns. `--doctor`,
235
+ `--explain-plan` and `--parity-report` recompute from the install, the donor
236
+ registry and the bridge, so an agent that has lost its earlier context re-runs
237
+ one and is exactly as certain as before.
238
+
239
+ One question has no such source: part-way through
240
+ `--explain-plan` -> `--edit` -> `--parity-report`, *which lab am I working on
241
+ and what comes next*. Each run now appends a line to
242
+ `output/session-log.jsonl`, and two flags read it back:
243
+
244
+ ```powershell
245
+ python scripts\generate_pkt.py --session-state
246
+ python scripts\generate_pkt.py --resume output\lab.pkt
247
+ ```
248
+
249
+ `--resume` re-hashes the lab and compares it with what the last step recorded.
250
+ A position is reported **only when the two agree**; otherwise it says the lab
251
+ has changed and to re-derive instead. That refusal is the useful answer, and it
252
+ is the same stance the rest of the skill takes when it cannot prove something.
253
+
254
+ The log carries no secrets (facts are allow-listed, and the prompt survives
255
+ only as a non-reversible shape fingerprint), carries no weight (a test
256
+ regenerates a lab with and without it and compares the decoded content), and
257
+ ships nowhere (`output/` is gitignored and in no `package.json` file list).
258
+ `PKT_SESSION_LOG=off` disables it.
259
+
223
260
  ## Runtime Doctor Contract
224
261
 
225
262
  `--doctor` is a product surface, not a debug afterthought. It reports:
@@ -589,7 +626,7 @@ See also:
589
626
 
590
627
  ## Release and Launch State
591
628
 
592
- The current line is `packet-tracer-skill@0.3.0`, and it is the first release
629
+ The current line is `packet-tracer-skill@0.3.2`, and it is the first release
593
630
  where generation is the headline rather than a deferred promise. `0.2.3`, the
594
631
  previous published line, was a capability proof and readiness release that
595
632
  deliberately refused broad generation.
@@ -643,21 +680,30 @@ qurur, mövcud faylı redaktə edir və hər iddiasını ölçü ilə əsasland
643
680
  fayl Packet Tracer-də açılır və cihazları bir-birini ping edir; alınmayanda
644
681
  səbəbini açıq deyir.
645
682
 
646
- ### `0.3.0` nə dəyişdi
683
+ ### `0.3.2` nə dəyişdi
647
684
 
648
685
  Bu, promptun Packet Tracer-in açdığı fayla çevrildiyi ilk buraxılışdır.
649
686
 
650
687
  | Ölçü | Nəticə |
651
688
  | --- | --- |
652
- | korpusda qurulan ssenari | 33-dən 32 |
653
- | onlardan Packet Tracer-in açdığı | 32-dən 32 |
654
- | testlər | 657 keçdi, 1 ötürüldü |
689
+ | korpusda qurulan ssenari | 33-dən 32 (33-cü heç bir cihaz istəmir, rədd edilir) |
690
+ | onlardan Packet Tracer-in açdığı | 32-dən 31 |
691
+ | testlər | 833 keçdi, 1 ötürüldü |
655
692
  | DHCP laboratoriyası | 4 kompüter routerin hovuzundan ünvan aldı, şlüzə və bir-birinə 4/4 ping |
656
693
  | icarə xətti (leased line) | trafik `Serial0/1/1 <-> Serial0/1/0` üzərindən 4/4 keçdi |
694
+ | ev routeri laboratoriyası | hər iki host şlüzə və bir-birinə 4/4 ping etdi -- kabel və Wi-Fi, açıq və WPA2 |
657
695
 
658
696
  Ping rəqəmləri cihazların özündə `ping` işlədilməklə alınıb. Bu layihədə bütün
659
697
  statik yoxlamaları keçən, amma heç nəyin ping etmədiyi laboratoriyalar olub.
660
698
 
699
+ **Wi-Fi laboratoriyasını yoxlayarkən bilməli olduğunuz bir şey.** Fayl açılan
700
+ kimi simsiz klienti bir dəfə oxusanız, xarab görünəcək: port `up` və `linked`,
701
+ `ip 0.0.0.0`, radio danışılmamış sürətdə. Bu **ilk oxunuşdur** və heç nə demir --
702
+ klient hələ icarəsini almayıb. İkinci dəfə oxuyun: ünvanı olur və ping edir.
703
+ Packet Tracer-in özünün yaddaşa verdiyi, saniyələr əvvəl 4/4 ping edən fayl da
704
+ yenidən açılanda eyni cür oxunur -- səhv olanın lab yox, oxunuş olduğunu bundan
705
+ bilirik.
706
+
661
707
  ### Nə düzəldildi
662
708
 
663
709
  Generasiya edilən hər WAN laboratoriyası açılmırdı və bunun arxasında bir-birini
package/SKILL.md CHANGED
@@ -37,6 +37,210 @@ Useful ideas from `MCP-Packet-Tracer` were adopted only at the architecture
37
37
  level. PTBuilder live deploy and external donor usage were intentionally not
38
38
  adopted.
39
39
 
40
+ ## Using This Skill Correctly
41
+
42
+ Read this before generating anything. Every rule below was learned by measuring
43
+ a lab that looked finished and was not.
44
+
45
+ ### A lab that opens is not a lab that works
46
+
47
+ This is the single most expensive mistake available here. Generated labs have
48
+ passed every structural check, opened cleanly in Packet Tracer, read correctly
49
+ line by line, and been unable to pass a single packet. "It opened" tells you
50
+ the container is valid. It tells you nothing about the network.
51
+
52
+ Two things separate a finished lab from a plausible one:
53
+
54
+ ```bash
55
+ python scripts/generate_pkt.py --coherence-report output/lab.pkt
56
+ ```
57
+
58
+ and a live ping. Do both. `--coherence-report` exits non-zero when the lab
59
+ contradicts itself, and generation prints the same summary as a `WARNING:` line
60
+ when it hands the file over — never ignore that line.
61
+
62
+ ### What the coherence report is telling you
63
+
64
+ Every defect this project has paid for has one shape: **a fact derived twice,
65
+ in two passes, with nothing comparing the derivations.** Each half reads
66
+ correctly on its own. The report is that comparison:
67
+
68
+ | Finding | What it means |
69
+ |---|---|
70
+ | `interface_declared_twice` | IOS keeps the last block; readers scan the first |
71
+ | `port_not_on_device` | Packet Tracer refuses to open the file |
72
+ | `port_double_booked` | two cables on one socket |
73
+ | `duplicate_address` | two interfaces claim one address |
74
+ | `real_address_is_also_virtual` | a router holds an address that is someone's HSRP virtual |
75
+ | `native_vlan_mismatch` | spanning tree blocks a cabled, configured port |
76
+ | `etherchannel_peer_does_not_bundle` | the switch behind it drops off the network |
77
+ | `gateway_answers_for_nobody` | a static host points at an address nothing holds |
78
+ | `pool_without_interface` | DHCP hands out addresses nothing can route |
79
+
80
+ The report never repairs. A checker that fixes what it finds stops being able
81
+ to tell you whether the thing it checks is working.
82
+
83
+ ### Measuring in a live Packet Tracer
84
+
85
+ Three steps, in order, every time:
86
+
87
+ 1. **Confirm which document is open.** With two windows open the bridge answers
88
+ for the other one. A pass rate measured against the wrong lab is worse than
89
+ no measurement, because it is believed. Check the device list first.
90
+ 2. **Throw the first reading away.** Spanning tree has not converged; the first
91
+ ping after opening a large lab reports 0/4 on a path that works. It is not a
92
+ measurement, it is the lab starting up.
93
+ 3. **Then measure.** A *failing* ping takes far longer than a passing one --
94
+ each packet waits out its own timeout, about 13 seconds for four -- so
95
+ budget 45-60s per call rather than reading a timeout as a failure.
96
+
97
+ Ping from a host. Routers and switches answer through a different bridge path
98
+ that is not reliable here.
99
+
100
+ ### When two repairs in a row do not change the measurement
101
+
102
+ Stop repairing. The symptom is not where the defect is. Find the pass that
103
+ wrote the state, and fix it there. The fastest way in: **diff the broken device
104
+ against a working sibling.** One switch unreachable while identical ones work
105
+ has one extra line, and reading configuration top to bottom will not find it as
106
+ quickly as `diff` will.
107
+
108
+ ### Facts about Packet Tracer that cost a lab each
109
+
110
+ - a copper cable in a fibre socket is **dropped in silence** -- the file opens,
111
+ the cable is simply not there
112
+ - a duplicate `interface` block is applied last-wins, while readers scan the first
113
+ - a subinterface `ip address` before its `encapsulation dot1Q` is refused
114
+ silently
115
+ - `PORT_DHCP_ENABLE=true` makes Packet Tracer ignore the static address in the
116
+ file; a host with both is a DHCP client, and judging it on its stale address
117
+ is a false reading
118
+ - DHCP snooping with no trusted uplink eats every offer the router sends
119
+ - port security on a trunk isolates the whole switch behind it
120
+ - a `channel-group` whose peer does not bundle takes that switch off the network
121
+ - an interface name the device does not own blocks the file from opening at all;
122
+ a double-booked port does not
123
+ - a `vlan N` line in a switch's configuration does **not** create the VLAN --
124
+ Packet Tracer keeps the database separately, and a port assigned to a VLAN
125
+ that is not in it forwards nothing
126
+ - a top-level command written straight after an indented sub-block line is
127
+ swallowed; the `!` between them is load-bearing
128
+ - a home router's sockets are named, not slotted, and the spelling is the
129
+ model's: `Ethernet 1` .. `4` on `WirelessRouter`, `GigabitEthernet 1` .. `4`
130
+ on `WirelessRouterNewGeneration`, and `Internet` for the uplink on both. It
131
+ writes no interfaces into its configuration, so nothing else can tell you
132
+ - a home router's LAN address is a **setting**, not a config line, and lives in
133
+ `ENGINE/LAN_IP_ADDRESS` with its pool under `ENGINE/DHCP_SERVER/POOLS/POOL`;
134
+ a reader that only walks running configs cannot see it
135
+ - a wireless client's addressing comes from its `WIRELESS_PROFILE`, not from
136
+ its port -- both record it, and Packet Tracer obeys the profile
137
+ - a radio link is made by **distance**. A client outside the access point's
138
+ `COVERAGERANGE` reports its port `up` and `linked` and passes nothing
139
+ - an access point's WPA key lives in `WIRELESS_COMMON/WEP_PROCESS/KEY` with
140
+ `WEP_PROCESS/ENCRYPTION`, never in `WPA_PASSPHRASE`. The names are legacy and
141
+ WPA2 uses them; the client side is the same shape. Put it in the wrong field
142
+ and the access point runs WPA2 with no key while its clients have one
143
+ - a wireless client reads `ip 0.0.0.0` at its un-negotiated rate for the first
144
+ moments after a lab opens, even in a lab Packet Tracer saved itself while it
145
+ was pinging 4/4. `pt_inspect_ports` right after opening is a **first reading**
146
+ and has to be discarded exactly like a first ping
147
+ - a home router's DHCP **lease records** are how a client gets its address back
148
+ when the file opens; move its LAN and they have to be renumbered, not dropped
149
+ - the builder MCP's device table is not authoritative about port names; it
150
+ gives the AC home router `Ethernet 1` .. `4`, and the device itself says
151
+ `GigabitEthernet 1` .. `4`. `pt_inspect_ports` answers at the IOS layer,
152
+ where those sockets are bridged into `Vlan1` and do not appear at all --
153
+ `pt_query_topology` is the one that lists them
154
+
155
+ ### Drawing the topology yourself
156
+
157
+ The generator packs switch blocks into a roughly square grid, which is fine for
158
+ a lab nobody planned. When you have already decided the shape -- which
159
+ department sits where, which switch faces which, where the core belongs -- say
160
+ so in the blueprint and set `PACKET_TRACER_LAYOUT=keep`. Every `x`/`y` in the
161
+ blueprint is then left exactly as given, and the frames are drawn around the
162
+ devices where they land.
163
+
164
+ Sketch first, build second, is the better order for anything a person will
165
+ look at: you can see the whole diagram before a single cable exists, and the
166
+ skill's job narrows to making the file match the drawing.
167
+
168
+ Without the knob the packing applies, and it is deterministic: the same
169
+ blueprint gives the same coordinates every time, so a regenerated lab does not
170
+ drift.
171
+
172
+ ### Scale
173
+
174
+ There is no artificial device limit. The only ceiling is physical: a switch has
175
+ the ports it has, and the generator says so plainly when it runs out. Ask for
176
+ more switches, not fewer hosts.
177
+
178
+ ### If you lost the thread part-way through an edit chain
179
+
180
+ Almost nothing here needs remembering. `--doctor`, `--explain-plan` and
181
+ `--parity-report` all recompute from the install, the donor registry and the
182
+ bridge, so re-running one costs time and nothing else. Losing the conversation
183
+ does not make the skill less certain about anything it reports.
184
+
185
+ One question has no such source. Part-way through
186
+ `--explain-plan` -> `--edit` -> `--parity-report`, *which lab am I working on
187
+ and what comes next* lived only in the conversation. So each run appends a line
188
+ to `output/session-log.jsonl`, and two flags read it back:
189
+
190
+ ```bash
191
+ python scripts/generate_pkt.py --session-state # which labs are in flight
192
+ python scripts/generate_pkt.py --resume output/lab.pkt # where this one was left
193
+ ```
194
+
195
+ `--resume` re-hashes the lab and compares it to what the last step recorded. It
196
+ reports a position **only when the two agree**; when they do not, it says the
197
+ lab has changed and tells you to re-derive instead. Treat "position not
198
+ claimed" as the correct answer, not a failure -- it is the same refusal-first
199
+ stance as everything else here.
200
+
201
+ Three things the log deliberately is not:
202
+
203
+ - **not a secret store.** Facts are allow-listed, never filtered. An edit
204
+ prompt carries a passphrase in an ordinary field, so nothing is written but
205
+ the named, known-safe keys, and the prompt survives only as the same
206
+ non-reversible shape fingerprint the usage ledger uses.
207
+ - **not load-bearing.** Delete it and every result is identical; there is a
208
+ test that regenerates a lab with and without it and compares the content.
209
+ - **not shipped.** It lives under `output/`, which is gitignored and in no
210
+ `package.json` file list.
211
+
212
+ `PKT_SESSION_LOG=off` turns it off; `PKT_SESSION_LOG=<path>` moves it.
213
+
214
+ ### A generated lab becomes the next build's donor
215
+
216
+ Donor selection can pick a lab this skill produced, so every repair pass
217
+ eventually runs over its own output. Write interface configuration with
218
+ `_set_config_block`, never by appending, and check a new pass by running it
219
+ three times and asserting the document stops changing after the first.
220
+
221
+ ### The palette is not the vocabulary
222
+
223
+ Packet Tracer's device palette lists more kinds than a saved lab distinguishes:
224
+ `SMARTPHONE-PT` saves as `Pda`, `Fiber Patch Panel` saves as `Patch Panel`.
225
+ Adding an askable kind with no donor behind it produces a request that can only
226
+ come back as an undelivered device. `tests/test_askable_kinds_have_donors.py`
227
+ holds the vocabulary to what real labs actually contain, in both directions.
228
+
229
+ ### When generation refuses
230
+
231
+ A refusal returns `blocking_gaps` together with a `blueprint_plan`. That is not
232
+ a failure to work around by loosening a policy -- it is the skill saying no
233
+ donor can serve the request. Read the gaps, adjust the request or supply a
234
+ donor, and try again.
235
+
236
+ ### What works without Packet Tracer installed
237
+
238
+ Decode, inventory, edit, generate and structural verification all work from the
239
+ donor cache with no install and no environment variables -- the Twofish engine
240
+ is vendored pure Python. Only `--validate-open` and live pings need the
241
+ application itself. Tests that build a lab are marked `requires_donors` and skip
242
+ where there is nothing to build from.
243
+
40
244
  ## How `.pkt` Files Work
41
245
 
42
246
  For the modern format targeted by this skill, the pipeline is:
@@ -226,8 +430,10 @@ If the user does not specify details:
226
430
 
227
431
  ## Constraints
228
432
 
229
- - The builder depends on a local Packet Tracer installation with the bundled
230
- sample saves present, on any of Windows, macOS or Linux
433
+ - The builder needs Packet Tracer's bundled sample saves as prototype sources,
434
+ from a local installation on Windows, macOS or Linux **or** from the donor
435
+ cache under `~/.pkt/saves`, which a single generate run on an installed
436
+ machine populates and which can then be copied anywhere
231
437
  - Donor devices the plan does not need are deleted. `PACKET_TRACER_SPARE_STRATEGY=park`
232
438
  restores the older behaviour of renaming them `UNUSED-*` / `*-SPARE-*` and
233
439
  moving them offscreen, if a donor turns out to depend on one staying present
@@ -252,6 +458,12 @@ Generate from a blueprint file:
252
458
  python scripts/generate_pkt.py --blueprint examples/blueprint_minimal.json --output output\minimal.pkt
253
459
  ```
254
460
 
461
+ Check what a finished lab contradicts about itself, and exit non-zero if it does:
462
+
463
+ ```powershell
464
+ python scripts/generate_pkt.py --coherence-report output\campus.pkt
465
+ ```
466
+
255
467
  Generate from a hybrid prompt:
256
468
 
257
469
  ```powershell
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "packet-tracer-skill",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "description": "Donor-backed Cisco Packet Tracer 9.x skill for natural-language planning, safe-open generation, editing, parity reporting, and runtime diagnostics across coding hosts.",
6
6
  "keywords": [
@@ -75,6 +75,7 @@
75
75
  "scripts/vendor/twofish_pure.py",
76
76
  "templates",
77
77
  "references/*.json",
78
+ "!references/local-donor-catalog.json",
78
79
  "references/*.md",
79
80
  "examples/README.md",
80
81
  "examples/gallery.md",
@@ -11,19 +11,61 @@ now calls it rather than keeping a second, weaker copy.
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ from pathlib import Path
15
+
14
16
  from packet_tracer_env import require_packet_tracer_saves_root
15
17
  from sample_catalog import _summarize_pkt, write_catalog_outputs
16
18
 
17
19
 
20
+ def _local_donor_items(catalogued: set[str]) -> list[dict]:
21
+ """Labs the user saved outside the Packet Tracer installation.
22
+
23
+ Donor selection ranks candidates from this catalogue, and the catalogue
24
+ only ever held the installed samples. So a lab saved specifically to serve
25
+ as a donor was discovered by `discover_local_donors`, reported as present,
26
+ and never ranked -- generation for a five-switch topology stayed blocked
27
+ with "no ranked donor candidate passed validation" while a donor that fit
28
+ sat on disk.
29
+
30
+ These entries carry an absolute `path` rather than one relative to the
31
+ saves root, which is why `_catalog_item_path` has to keep it.
32
+ """
33
+ try:
34
+ from local_donors import discover_local_donors
35
+ except ImportError: # pragma: no cover - discovery is optional
36
+ return []
37
+
38
+ items: list[dict] = []
39
+ for donor in discover_local_donors():
40
+ path = Path(donor.path)
41
+ if str(path) in catalogued or not path.is_file():
42
+ continue
43
+ try:
44
+ item = _summarize_pkt(path, path.name, "user-local", True)
45
+ except Exception as exc:
46
+ items.append(
47
+ {"relative_path": path.name, "source_path": str(path), "error": f"{type(exc).__name__}: {exc}"}
48
+ )
49
+ continue
50
+ # `source_path`, not `path`: the writer drops `path` so the committed
51
+ # catalogue does not depend on whose machine built it.
52
+ item["source_path"] = str(path)
53
+ items.append(item)
54
+ return items
55
+
56
+
18
57
  def build_catalog() -> list[dict]:
19
58
  saves_root = require_packet_tracer_saves_root()
20
59
  items: list[dict] = []
60
+ catalogued: set[str] = set()
21
61
  for path in sorted(saves_root.rglob("*.pkt")):
22
62
  relative_path = str(path.relative_to(saves_root))
63
+ catalogued.add(str(path))
23
64
  try:
24
65
  items.append(_summarize_pkt(path, relative_path, "cisco-local", True))
25
66
  except Exception as exc:
26
67
  items.append({"relative_path": relative_path, "error": f"{type(exc).__name__}: {exc}"})
68
+ items.extend(_local_donor_items(catalogued))
27
69
  return items
28
70
 
29
71