riskrails 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +25 -15
  5. package/agents/ai-reviewer.md.tmpl +14 -1
  6. package/ai/agents/implementer.md +6 -0
  7. package/ai/agents/reviewer.md +8 -1
  8. package/bin/ansi.js +12 -0
  9. package/bin/menu.js +71 -12
  10. package/bin/riskrails.js +240 -14
  11. package/bin/uninstall.js +115 -0
  12. package/bin/wizard.js +72 -0
  13. package/hooks/context-guard.py +11 -1
  14. package/hooks/dialog-gate.py +3 -2
  15. package/hooks/guard/decision.py +4 -3
  16. package/hooks/guard/dialog.py +12 -2
  17. package/hooks/guard/ere.py +29 -10
  18. package/hooks/guard/git.py +2 -1
  19. package/hooks/guard/paths_base.py +7 -0
  20. package/hooks/guard/paths_bash.py +3 -3
  21. package/hooks/runtime-gate.py +87 -0
  22. package/hooks/update_notice.py +23 -8
  23. package/package.json +1 -1
  24. package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
  25. package/skills/ai-task/rr_state/dialog.py +32 -7
  26. package/skills/ai-task/rr_state/dialog_cmd.py +38 -5
  27. package/skills/ai-task/rr_state/gate.py +10 -4
  28. package/skills/ai-task/rr_state/labels.py +33 -4
  29. package/skills/ai-task/rr_state/questions_cmd.py +5 -4
  30. package/skills/ai-task/rr_state/report.py +73 -10
  31. package/skills/ai-task/rr_state/report_cmd.py +2 -0
  32. package/skills/ai-task/rr_state/review.py +18 -14
  33. package/skills/ai-task/rr_state/start.py +12 -9
  34. package/skills/ai-task/rr_state/steps.py +16 -7
  35. package/skills/ai-task/rr_state/verify.py +17 -12
  36. package/skills/project-update/history/blobs/dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a +41 -0
  37. package/skills/project-update/history/index.json +6 -3
  38. package/skills/rr-build/SKILL.md +5 -2
  39. package/skills/rr-ship/SKILL.md +2 -2
  40. package/skills/rr-spec/SKILL.md +6 -0
  41. package/skills/rr-status/SKILL.md +2 -0
  42. package/skills/rr-status/status.py +5 -3
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
8
- "version": "4.2.0"
8
+ "version": "4.3.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -14,7 +14,7 @@
14
14
  "source": "url",
15
15
  "url": "https://github.com/vanssata/claude-agentic-sdlc-module.git"
16
16
  },
17
- "version": "4.2.0",
17
+ "version": "4.3.0",
18
18
  "strict": true
19
19
  }
20
20
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskrails",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
5
5
  "author": {
6
6
  "name": "Ivan Kakurov"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskrails",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
5
5
  "author": {
6
6
  "name": "Ivan Kakurov"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RiskRails
2
2
 
3
- [![Version](https://img.shields.io/badge/version-4.2.0-d2f878?labelColor=101210)](https://github.com/vanssata/claude-agentic-sdlc-module/releases/tag/v4.2.0)
3
+ [![Version](https://img.shields.io/badge/version-4.3.0-d2f878?labelColor=101210)](https://github.com/vanssata/claude-agentic-sdlc-module/releases/tag/v4.3.0)
4
4
  [![Runtimes](https://img.shields.io/badge/runtimes-Claude%20Code%20%7C%20Codex-d2f878?labelColor=101210)](docs/routing.md)
5
5
  [![Known risks](https://img.shields.io/badge/known%20risks-documented-d2f878?labelColor=101210)](docs/known-risks.md)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-d2f878?labelColor=101210)](LICENSE)
@@ -18,7 +18,7 @@ approval. Risky actions are stopped by a hook, not by a polite request in a
18
18
  prompt.
19
19
 
20
20
  ```bash
21
- npx riskrails@4.2.0 # detects Claude Code, Codex or both (Node 18+, Python 3.11+)
21
+ npx riskrails@4.3.0 # detects Claude Code, Codex or both (Node 18+, Python 3.11+)
22
22
  ```
23
23
 
24
24
  Then, in any project: `/rr-setup` once, and `/rr-build <what you want>` for
@@ -50,7 +50,7 @@ Tuned for **one developer who knows the codebase**, on a Claude Pro, Max or
50
50
  Team plan, or a ChatGPT Plus or Pro plan, following the
51
51
  [AI-native SDLC playbook](https://claude.com/blog/the-ai-native-sdlc-playbook).
52
52
 
53
- **Version 4.2.0.** The guards have limits: read [Known risks](docs/known-risks.md)
53
+ **Version 4.3.0.** The guards have limits: read [Known risks](docs/known-risks.md)
54
54
  before relying on them. What is decided but not built is in [ROADMAP.md](ROADMAP.md).
55
55
 
56
56
  ## Upgrade to 4.0
@@ -64,8 +64,10 @@ the plugin. Step by step, in this order:
64
64
  without the conversions it missed. So a project last updated by an older
65
65
  riskrails goes through the version that still converts it: install it
66
66
  (`npx riskrails@3.1.0`), run `/rr-setup --update` with it, then install 4.0.
67
- 2. **Commit every project's `.ai/`** first; `git restore` is the rollback.
68
- 3. **Update the home** on every machine, for every runtime: `npx riskrails@4.2.0`
67
+ 2. **Commit every project's `.ai/`** first; `git restore` is the rollback. From
68
+ 4.2 that commit holds the configuration: `.ai/memory/` and `.ai/reports/` are
69
+ git-ignored, so copy them aside if you want them back.
70
+ 3. **Update the home** on every machine, for every runtime: `npx riskrails@4.3.0`
69
71
  (the plugin route and the npm route are separate installs: reinstall both
70
72
  if you use both). Restart the runtime. In Codex, open `/hooks` and trust the
71
73
  changed entries.
@@ -84,13 +86,13 @@ are removed; use the `/rr-*` commands. Details are in
84
86
  ## Install
85
87
 
86
88
  ```bash
87
- npx riskrails@4.2.0 # detects Claude Code, Codex, or both; proposes the detected plan
88
- npx riskrails@4.2.0 --target codex # auto | claude | codex | both
89
- npx riskrails@4.2.0 --plan max --fable no # pro | team-pro | team-max | max | max20 | balanced-max
90
- npx riskrails@4.2.0 --codex-plan plus # plus | pro | balanced-max
91
- npx riskrails@4.2.0 --context economy # the plan's compaction window; the context guard also blocks
92
- npx riskrails@4.2.0 --statusline own # keep your own statusline command (from 3.2.1; default: the plugin's two-line statusline)
93
- npx riskrails@4.2.0 --dry-run # print what would be written, write nothing
89
+ npx riskrails@4.3.0 # detects Claude Code, Codex, or both; proposes the detected plan
90
+ npx riskrails@4.3.0 --target codex # auto | claude | codex | both
91
+ npx riskrails@4.3.0 --plan max --fable no # pro | team-pro | team-max | max | max20 | balanced-max
92
+ npx riskrails@4.3.0 --codex-plan plus # plus | pro | balanced-max
93
+ npx riskrails@4.3.0 --context economy # the plan's compaction window; the context guard also blocks
94
+ npx riskrails@4.3.0 --statusline own # keep your own statusline command (from 3.2.1; default: the plugin's two-line statusline)
95
+ npx riskrails@4.3.0 --dry-run # print what would be written, write nothing
94
96
  npx -y riskrails@latest --yes # update in place, keeping the recorded plan
95
97
  ```
96
98
 
@@ -102,6 +104,9 @@ stop with nothing written. Enter through every menu installs what detection
102
104
  proposes. A flag skips its menu; `--yes` or no terminal asks nothing, and
103
105
  `RISKRAILS_MENU=off` (or `TERM=dumb`) brings back the typed plan question.
104
106
 
107
+ <!-- wizard recording: placeholder until the human records docs/media/wizard.tape (`vhs docs/media/wizard.tape`) and puts the GIF here -->
108
+ *A recording of the installer wizard goes here.*
109
+
105
110
  Pin the version: the hooks it installs see every command an agent runs. It
106
111
  installs the managed block, the plan's settings and limits, the five agents
107
112
  with their tiers baked in, the hooks, the skills, the shared tree at
@@ -172,11 +177,14 @@ until it passes. After three failed review passes the next verdict is yours.
172
177
 
173
178
  ### The record
174
179
 
175
- A closed task's who, what, how and why travel as a compact record: `state.py
180
+ A closed task's who, what, how and why are kept as a compact record: `state.py
176
181
  memory enable` (yours, in a terminal, or as your pick in the `/rr-setup`
177
182
  dialog) switches a project to packages mode, in
178
183
  which `close` appends the task's entries to a per-developer batch under
179
- `.ai/memory/` and a pre-push hook validates them; `state.py recall --files …`
184
+ `.ai/memory/`. From 4.2 the record is local by default (`.ai/memory/` and
185
+ `.ai/reports/` are git-ignored); `state.py memory enable --share git`, in your
186
+ own terminal, shares it through git and installs a pre-push hook that validates
187
+ it, and `--share local` takes it back. `state.py recall --files …`
180
188
  gives an agent what the record holds about the files it is about to touch,
181
189
  within a token budget. Task ids are `T-YYYY-MM-DD-HHMM-<slug>`
182
190
  ([docs/local-memory.md](docs/local-memory.md)).
@@ -243,7 +251,7 @@ python3 -I -m unittest discover -s tests -p 'test_*.py' # CI's serial oracl
243
251
  AI_E2E=1 python3 -I -m unittest discover -s tests -p test_end_to_end.py
244
252
  ```
245
253
 
246
- Sixty suites, 1217 tests, about 75 seconds in parallel. The guards against JSON fixtures for both runtimes and a
254
+ Seventy-eight suites, 1569 tests, about 85 seconds in parallel. The guards against JSON fixtures for both runtimes and a
247
255
  golden of 1 341 recorded decisions, byte for byte, with timed cases that hold
248
256
  the hot path under its budget; the state machine, the weight, the batch loop,
249
257
  the dialog contract and the documents rule; the compact record and its
@@ -272,6 +280,8 @@ the developer's real `~/.claude` or `~/.codex`.
272
280
  | [docs/local-memory.md](docs/local-memory.md) | the compact record: task ids, packages, recall, the pre-push check, migration |
273
281
  | [docs/known-risks.md](docs/known-risks.md) | what the guards do not protect against, and what to do about it |
274
282
  | [docs/faq.md](docs/faq.md) | why something was blocked, and how to change it |
283
+ | [docs/glossary.md](docs/glossary.md) | the words the dialog, the statusline and `/rr-status` use, and what still needs a terminal |
284
+ | [docs/dialog-probe.md](docs/dialog-probe.md) | the checklist for Remote Control, the desktop app and the web — not yet filled |
275
285
  | [CHANGELOG.md](CHANGELOG.md) | what changed in each release, and the upgrade notes |
276
286
  | [docs/sdlc/README.md](docs/sdlc/README.md) | the spec → plan → build artefacts in a project |
277
287
  | [ROADMAP.md](ROADMAP.md) | what is decided but not built; the open rows |
@@ -76,7 +76,12 @@ previous version · state-machine transitions now reachable that should not be
76
76
  API and event contract changes · security problems · missing logging on a
77
77
  critical path · a rollback that would not actually work · scope creep and
78
78
  refactoring nobody asked for · a test weakened or skipped, or a static-analysis
79
- level lowered, to make CI pass.
79
+ level lowered, to make CI pass · a new dependency without its line in the plan,
80
+ or one the standard library or a short function would replace · padding, last
81
+ and on a first pass only, in the lines this change added: an abstraction with
82
+ one implementation, an option nobody sets, a block or a test that repeats
83
+ another, lines no requirement and no test asks for — MEDIUM at most, three
84
+ findings at most, each titled `padding:` and naming what to delete.
80
85
 
81
86
  ## Focus: security
82
87
 
@@ -117,6 +122,14 @@ verdict: pass | pass_with_findings | blockers_open
117
122
  scope_assessment: <did the change stay inside the approved plan>
118
123
  ```
119
124
 
125
+ In full mode only, after VERDICT:
126
+
127
+ ```
128
+ ## READ FIRST
129
+ - <file:line> — <why a human reads this before approving> # at most five
130
+ unverified: <what the review could not check, and what would check it> # or: none
131
+ ```
132
+
120
133
  Label each finding with its severity's letter and a number, counted per
121
134
  severity from 1; a re-review continues the numbers already in
122
135
  `.ai/reports/<task-id>/task.md` §Review, when it has a pass.
@@ -37,6 +37,12 @@ do not rename anything to make the change fit.
37
37
  `project/known-risks.md`.
38
38
  - Do not reformat lines you did not change — it hides the real diff from the
39
39
  reviewer.
40
+ - Write the simplest version that passes the step's tests. A branch, an option,
41
+ a parameter or a layer is added only when a requirement or a failing test
42
+ asks for it.
43
+ - A new dependency is not yours to add. Ask first whether the standard library
44
+ or one function of about fifty lines does the job; when it does not, return
45
+ `SCOPE_CHANGE_REQUIRED` naming the manifest and the reason.
40
46
 
41
47
  ## Output
42
48
 
@@ -20,7 +20,14 @@ usually a review that read the diff instead of the system.
20
20
  - missing logging and observability on a critical path;
21
21
  - a rollback that would not actually work;
22
22
  - scope creep and refactoring that nobody asked for;
23
- - a test weakened, skipped, or a static-analysis level lowered to make CI pass.
23
+ - a test weakened, skipped, or a static-analysis level lowered to make CI pass;
24
+ - a new dependency without its line in the plan, or one the standard library
25
+ or a short function would replace;
26
+ - padding, last and on a first pass only, in the lines this change added: an
27
+ abstraction with one implementation, an option nobody sets, a block or a test
28
+ that repeats another, lines no requirement and no test asks for — MEDIUM at
29
+ most, three findings at most, each titled `padding:` and naming what to
30
+ delete.
24
31
 
25
32
  ## Boundary
26
33
 
package/bin/ansi.js ADDED
@@ -0,0 +1,12 @@
1
+ // The installer's colour gate and the few styles the menu uses. Pure: the stream's isTTY and the
2
+ // environment come in as parameters. Colour goes to stderr only (spec ui-first-approvals R14).
3
+ 'use strict';
4
+
5
+ // Colour only on a terminal, with NO_COLOR empty or unset and TERM not dumb.
6
+ const useColor = (isTTY, env) => isTTY === true && !env.NO_COLOR && env.TERM !== 'dumb';
7
+
8
+ const CODES = { bold: '1', dim: '2', inverse: '7' };
9
+ // text in the style when on is true, else unchanged.
10
+ const paint = (on, style, text) => (on ? `\x1b[${CODES[style]}m${text}\x1b[0m` : text);
11
+
12
+ module.exports = { useColor, paint };
package/bin/menu.js CHANGED
@@ -11,10 +11,25 @@
11
11
  // Ctrl-C/Esc/q erases the menu, exit 130
12
12
  // no terminal exit 3, nothing drawn: the parent falls back to its typed prompt
13
13
  // Keys: ↑/k ↓/j (stop at the ends), Home/End, 1–9 jump to that row, Enter picks.
14
+ //
15
+ // A spec with any of the keys below is drawn as a wizard screen; without them it is the 4.2.0 menu.
16
+ // step {"n": 3, "names": ["runtimes", "plan", "context", …]}: the stepper line, step n of names
17
+ // help the help line, replacing the default
18
+ // back true: ←, h or Backspace exit 4, nothing drawn, nothing on stdout
19
+ // multi true: Space toggles a row, Enter confirms the set; stdout is the checked values in
20
+ // item order joined by "," ("" when none is checked)
21
+ // checked the values checked at the start (multi)
22
+ // color true: styles on stderr, when ansi.useColor allows it
23
+ // Items may carry "desc", shown under the list while the item is highlighted. → also confirms.
24
+ // Exit codes: 0 pick, 3 no terminal, 4 back, 130 abort.
14
25
  'use strict';
26
+ const { useColor, paint } = require('./ansi');
27
+
28
+ const WIZARD_KEYS = ['help', 'multi', 'checked', 'step', 'back', 'color'];
29
+ const isWizard = (spec) => WIZARD_KEYS.some((k) => spec[k] !== undefined);
15
30
 
16
31
  // The key, as readline's keypress event gives it, as one of the reducer's tokens (or null).
17
- function token(key) {
32
+ function token(key, wizard) {
18
33
  if (!key) return null;
19
34
  if (key.ctrl && key.name === 'c') return 'abort';
20
35
  if (key.ctrl || key.meta) return null;
@@ -24,11 +39,17 @@ function token(key) {
24
39
  if (name === 'home' || name === 'end') return name;
25
40
  if (name === 'return' || name === 'enter') return 'enter';
26
41
  if (name === 'escape' || name === 'q') return 'abort';
42
+ if (wizard) {
43
+ if (name === 'left' || name === 'h' || name === 'backspace') return 'back';
44
+ if (name === 'right') return 'enter';
45
+ if (name === 'space' || name === ' ') return 'space';
46
+ }
27
47
  if (/^[1-9]$/.test(String(name))) return String(name);
28
48
  return null;
29
49
  }
30
50
 
31
- // The pure step: {index, count, done} and a token → the next state. done is null, 'pick' or 'abort'.
51
+ // The pure step: {index, count, done, checked?} and a token → the next state. done is null, 'pick',
52
+ // 'back' or 'abort'. checked, an array of booleans, is there only for a multi-select.
32
53
  function reduce(state, tok) {
33
54
  const last = state.count - 1;
34
55
  switch (tok) {
@@ -38,6 +59,9 @@ function reduce(state, tok) {
38
59
  case 'end': return { ...state, index: last };
39
60
  case 'enter': return { ...state, done: 'pick' };
40
61
  case 'abort': return { ...state, done: 'abort' };
62
+ case 'back': return { ...state, done: 'back' };
63
+ case 'space': return state.checked
64
+ ? { ...state, checked: state.checked.map((c, i) => (i === state.index ? !c : c)) } : state;
41
65
  default: {
42
66
  const n = /^[1-9]$/.test(String(tok)) ? Number(tok) : 0;
43
67
  return n >= 1 && n <= state.count ? { ...state, index: n - 1 } : state;
@@ -57,6 +81,28 @@ function lines(spec, index, columns) {
57
81
  })];
58
82
  }
59
83
 
84
+ // The wizard screen: the stepper, a blank line, the title, the rows, a blank line, the highlighted
85
+ // option's description and the help line. Cut to the terminal's width like lines().
86
+ function wizardLines(spec, state, columns, color) {
87
+ const fit = (t) => (columns > 1 && [...t].length > columns - 1 ? [...t].slice(0, columns - 2).join('') + '…' : t);
88
+ const out = [];
89
+ if (spec.step) {
90
+ const names = spec.step.names.map((nm, i) => (i + 1 === spec.step.n ? paint(color, 'bold', `[${nm}]`) : nm));
91
+ out.push(fit(` step ${spec.step.n} of ${spec.step.names.length} · ${names.join(' › ')}`), '');
92
+ }
93
+ out.push(fit(` ${paint(color, 'bold', spec.title)}${spec.why ? ` (${spec.why})` : ''}`));
94
+ const width = Math.max(...spec.items.map((it) => String(it.label).length));
95
+ spec.items.forEach((it, i) => {
96
+ const box = state.checked ? `[${state.checked[i] ? 'x' : ' '}] ` : '';
97
+ const row = fit(` ${i === state.index ? '❯' : ' '} ${box}${i + 1}. ${String(it.label).padEnd(width)}${it.note ? ` ${it.note}` : ''}`);
98
+ out.push(i === state.index ? paint(color, 'inverse', row) : row);
99
+ });
100
+ const desc = spec.items[state.index].desc;
101
+ const keys = spec.help || `↑/↓ move ${spec.multi ? 'Space toggle ' : ''}${spec.back ? '← back ' : ''}Enter next Esc cancel`;
102
+ out.push('', ...(desc ? [fit(` ${paint(color, 'dim', desc)}`)] : []), fit(` ${paint(color, 'dim', keys)}`));
103
+ return out;
104
+ }
105
+
60
106
  function main() {
61
107
  let spec;
62
108
  try { spec = JSON.parse(process.env.RISKRAILS_MENU_SPEC || ''); } catch (e) { process.exit(3); }
@@ -68,9 +114,12 @@ function main() {
68
114
  const write = (s) => process.stderr.write(s);
69
115
  let drawn = 0;
70
116
  const erase = () => { if (drawn) write(`\x1b[${drawn}A\r\x1b[J`); drawn = 0; };
71
- const draw = (index) => {
117
+ const wizard = isWizard(spec);
118
+ const color = spec.color === true && useColor(process.stderr.isTTY, process.env);
119
+ const draw = () => {
72
120
  erase();
73
- const shown = lines(spec, index, process.stderr.columns || 0);
121
+ const shown = wizard ? wizardLines(spec, state, process.stderr.columns || 0, color)
122
+ : lines(spec, state.index, process.stderr.columns || 0);
74
123
  write(shown.join('\n') + '\n');
75
124
  drawn = shown.length;
76
125
  };
@@ -83,8 +132,8 @@ function main() {
83
132
  erase();
84
133
  restore();
85
134
  if (code === 0) {
86
- const why = state.index === initial && spec.why ? ` (${spec.why})` : '';
87
- write(`${spec.title}: ${value}${why}\n`);
135
+ const why = !spec.multi && state.index === initial && spec.why ? ` (${spec.why})` : '';
136
+ write(`${spec.title}: ${spec.multi ? value.split(',').join(', ') : value}${why}\n`);
88
137
  process.stdout.write(value, () => process.exit(0));
89
138
  } else {
90
139
  process.exit(code);
@@ -95,22 +144,32 @@ function main() {
95
144
  const initial = Number.isInteger(spec.initial) && spec.initial >= 0 && spec.initial < spec.items.length
96
145
  ? spec.initial : 0;
97
146
  let state = { index: initial, count: spec.items.length, done: null };
147
+ if (spec.multi) {
148
+ const on = new Set((Array.isArray(spec.checked) ? spec.checked : []).map(String));
149
+ state.checked = spec.items.map((it) => on.has(String(it.value)));
150
+ }
98
151
  // A lone Esc: emitKeypressEvents without a readline interface has no escape timeout and would
99
152
  // wait for the rest of a sequence. An arrow key arrives as one chunk (ESC [ B), never as ESC alone.
100
153
  stdin.on('data', (chunk) => { if (chunk.length === 1 && chunk[0] === 0x1b) finish(130); });
101
154
  require('readline').emitKeypressEvents(stdin);
102
155
  write('\x1b[?25l');
103
- draw(state.index);
156
+ draw();
104
157
  stdin.on('keypress', (_str, key) => {
105
- const tok = token(key);
106
- if (tok === null) return;
158
+ const tok = token(key, wizard);
159
+ if (tok === null || (tok === 'back' && !spec.back)) return;
107
160
  const next = reduce(state, tok);
108
- if (next.done === 'pick') { state = next; finish(0, String(spec.items[state.index].value)); return; }
161
+ if (next.done === 'pick') {
162
+ state = next;
163
+ const set = spec.items.filter((_it, i) => state.checked && state.checked[i]).map((it) => String(it.value));
164
+ finish(0, state.checked ? set.join(',') : String(spec.items[state.index].value));
165
+ return;
166
+ }
109
167
  if (next.done === 'abort') { finish(130); return; }
110
- if (next.index !== state.index) { state = next; draw(state.index); }
168
+ if (next.done === 'back') { finish(4); return; }
169
+ if (next.index !== state.index || next.checked !== state.checked) { state = next; draw(); }
111
170
  });
112
171
  stdin.resume();
113
172
  }
114
173
 
115
- module.exports = { reduce, token, lines };
174
+ module.exports = { reduce, token, lines, wizardLines };
116
175
  if (require.main === module) main();