riskrails 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +22 -13
- package/bin/menu.js +116 -0
- package/bin/riskrails.js +119 -20
- package/hooks/AGENTS.md +0 -6
- package/hooks/CLAUDE.md +0 -6
- package/hooks/guard/dialog.py +14 -2
- package/hooks/guard/paths.py +48 -1406
- package/hooks/guard/paths_base.py +59 -0
- package/hooks/guard/paths_bash.py +176 -0
- package/hooks/guard/paths_classify.py +205 -0
- package/hooks/guard/paths_fs.py +137 -0
- package/hooks/guard/paths_human.py +105 -0
- package/hooks/guard/paths_interp.py +162 -0
- package/hooks/guard/paths_movers.py +393 -0
- package/hooks/guard/paths_shell.py +288 -0
- package/package.json +1 -1
- package/skills/AGENTS.md +0 -6
- package/skills/CLAUDE.md +0 -6
- package/skills/ai-init/scaffold-ai.sh +2 -1
- package/skills/ai-init/templates/.ai/policies/safety.md +4 -3
- package/skills/ai-init/templates/gitignore.snippet +2 -0
- package/skills/ai-task/memory_cleanup.py +213 -0
- package/skills/ai-task/memory_compact.py +25 -0
- package/skills/ai-task/memory_config.py +120 -0
- package/skills/ai-task/memory_export.py +246 -0
- package/skills/ai-task/memory_format.py +7 -3
- package/skills/ai-task/memory_hook.py +172 -0
- package/skills/ai-task/memory_io.py +122 -0
- package/skills/ai-task/memory_migrate.py +187 -0
- package/skills/ai-task/memory_packages.py +83 -0
- package/skills/ai-task/memory_risks.py +84 -0
- package/skills/ai-task/memory_status.py +76 -0
- package/skills/ai-task/memory_sync.py +20 -1481
- package/skills/ai-task/memory_verify.py +316 -0
- package/skills/ai-task/rr_state/cli.py +2 -1
- package/skills/ai-task/rr_state/close.py +2 -2
- package/skills/ai-task/rr_state/dialog_cmd.py +183 -6
- package/skills/ai-task/rr_state/labels.py +14 -2
- package/skills/ai-task/rr_state/memory_cmd.py +91 -68
- package/skills/project-update/history/blobs/8ec171f0fbdf38cc6996d496f01491fd1f21f04a435a118ad979b127a8be06f1 +85 -0
- package/skills/project-update/history/blobs/fa0cb370b024e1a30e1355912032657b08f609e47773a603c193b1e609b0f997 +8 -0
- package/skills/project-update/history/index.json +4 -2
- package/skills/project-update/rr_update/__init__.py +1 -0
- package/skills/project-update/rr_update/apply.py +188 -0
- package/skills/project-update/rr_update/blocks.py +38 -0
- package/skills/project-update/rr_update/build.py +96 -0
- package/skills/project-update/rr_update/cleanup.py +194 -0
- package/skills/project-update/rr_update/cli.py +102 -0
- package/skills/project-update/rr_update/files.py +106 -0
- package/skills/project-update/rr_update/gate.py +60 -0
- package/skills/project-update/rr_update/hints.py +128 -0
- package/skills/project-update/rr_update/history.py +117 -0
- package/skills/project-update/rr_update/kept.py +120 -0
- package/skills/project-update/rr_update/paths.py +108 -0
- package/skills/project-update/rr_update/plan.py +123 -0
- package/skills/project-update/rr_update/report.py +51 -0
- package/skills/project-update/rr_update/rules.py +178 -0
- package/skills/project-update/rr_update/shared.py +74 -0
- package/skills/project-update/rr_update/siblings.py +37 -0
- package/skills/project-update/rr_update/weights.py +171 -0
- package/skills/project-update/update.py +8 -1682
- package/skills/rr-setup/SKILL.md +79 -21
- package/skills/rr-setup/scaffold_ai.py +8 -2
- package/skills/rr-setup/setup.py +371 -4
package/skills/rr-setup/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rr-setup
|
|
3
|
-
description: Set up and maintain the .ai/ layer of an existing production codebase. With no flag, scaffold docs/sdlc/, the runtime directory and .ai/, survey the code into .ai/project/ (light or full), write the Verification commands, the weight table's project entries and the constitution — read-only towards application code. --update regenerates a project's files from the installed plugin (a copy that is a shipped version is replaced, an edited one waits for the human, the project's own files are kept); run it right after installing a new riskrails; --audit scores the repository against the twelve plays and writes docs/ai-sdlc-adoption-plan.md. Use on "/rr-setup", "set up this project", "analyse this project for AI work", "update the rules", "обнови правилата", "audit our AI setup", or when /rr-status says the project is behind.
|
|
4
|
-
argument-hint: [--survey light|full] | --update [--apply] | --audit [context]
|
|
3
|
+
description: Set up and maintain the .ai/ layer of an existing production codebase. With no flag, scaffold docs/sdlc/, the runtime directory and .ai/, ask the open options (memory, runtimes, survey depth, verify command) in one dialog, survey the code into .ai/project/ (light or full), write the Verification commands, the weight table's project entries and the constitution — read-only towards application code. --options asks them all again; --update regenerates a project's files from the installed plugin (a copy that is a shipped version is replaced, an edited one waits for the human, the project's own files are kept); run it right after installing a new riskrails; --audit scores the repository against the twelve plays and writes docs/ai-sdlc-adoption-plan.md. Use on "/rr-setup", "set up this project", "analyse this project for AI work", "update the rules", "обнови правилата", "audit our AI setup", or when /rr-status says the project is behind.
|
|
4
|
+
argument-hint: [--survey light|full] | --options | --update [--apply] | --audit [context]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# /rr-setup $ARGUMENTS
|
|
@@ -17,29 +17,74 @@ UPDATE="python3 $AI_HOME/skills/project-update/update.py $PWD"
|
|
|
17
17
|
SETUP="python3 $AI_HOME/skills/rr-setup/setup.py"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`--update` and `--audit` go to their sections below; anything else
|
|
20
|
+
`--options`, `--update` and `--audit` go to their sections below; anything else
|
|
21
21
|
is **Setup**. In every mode: no application code changes — not a rename, not a
|
|
22
22
|
formatting fix. Problems you find are written down, never fixed. Never read
|
|
23
23
|
`.env`, secrets, dumps or production logs. Do not commit; suggest the `git add`.
|
|
24
24
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
|
27
|
+
0. **Options.** Project memory, the runtimes, the survey depth and `verify_command`
|
|
28
|
+
are asked once, in one dialog, before any work; the picks feed steps 1, 3 and 5.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
"$AI_HOME/skills/ai-init/scaffold-ai.sh" "$PWD" --runtime none # 0a
|
|
32
|
+
$SETUP options --open # 0b; --options adds --all, a given --survey adds --skip survey
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**0a** makes `.ai/` (so `.ai/policies/dialog.json`) and its `.gitignore` lines, with no
|
|
36
|
+
instruction file and no runtime directory; only this script takes `none`
|
|
37
|
+
(`project-scaffold.sh` refuses it). If its output does **not** list
|
|
38
|
+
`.ai/project/overview.md`, the project was set up before.
|
|
39
|
+
|
|
40
|
+
**0b** prints one of three first lines:
|
|
41
|
+
- `OPTIONS_DIALOG D<n> …`, then state.py's `DIALOG_PENDING` line and a JSON: send that
|
|
42
|
+
JSON exactly as printed with the dialog tool the line names (AskUserQuestion; Codex:
|
|
43
|
+
request_user_input).
|
|
44
|
+
- `OPTIONS_FALLBACK <why>`: nothing was opened. Ask the numbered questions after it in
|
|
45
|
+
chat as printed, the answers being the picks; show its `memory is the human's` line
|
|
46
|
+
as printed and never run it.
|
|
47
|
+
- `OPTIONS_NONE`: nothing to ask.
|
|
48
|
+
|
|
49
|
+
Exit 6 (no first line, the reason on stderr): `DECISION_LIMIT` or `DECISION_DUPLICATE`
|
|
50
|
+
means a decision is already open — settle every one first (`$STATE decision show` and
|
|
51
|
+
send it, or `$STATE decision fallback --id <Dn> --why "<why>"`), then run 0b once more.
|
|
52
|
+
Any other refusal, or a second one: ask the questions of steps 1, 3 and 5 in prose and,
|
|
53
|
+
in a git work tree, give the human for their own terminal, with `$AI_HOME` and `$PWD`
|
|
54
|
+
substituted: `python3 "<home>/skills/ai-task/state.py" --root "<project>" memory enable
|
|
55
|
+
--period quarter [--index sqlite]`.
|
|
56
|
+
|
|
57
|
+
**0c** — the human answers. After a dialog, a memory command they picked is run first,
|
|
58
|
+
at once, verbatim and alone; it prints `memory: via dialog (consent C<n>, decision D<n>)`
|
|
59
|
+
and its pick settles its decision. Then `$STATE decision settle --id <Dn>` for each other
|
|
60
|
+
decision: it prints `D<n> settled: <label> (set)` and `SETUP_PICK <key>=<value>`, after
|
|
61
|
+
a decline `SETUP_PICK <key>=`. Apply each pick where its step needs it:
|
|
62
|
+
- `runtime=<v>` → step 1's two scaffolds with `--runtime <v>`;
|
|
63
|
+
- `verify=<cmd>` → `$SETUP verify --set "<cmd>"` after step 2; `keep` and `edit` → nothing;
|
|
64
|
+
- `survey=light` or `full` → step 3's depth; `keep` → no new survey;
|
|
65
|
+
- `memory=off` or `keep` → nothing: a picked command already ran.
|
|
66
|
+
|
|
67
|
+
An empty value (`verify=` is "Not now"; any key after a decline) or `other` → the step
|
|
68
|
+
as it was without the dialog: step 1 passes `auto`, step 3 surveys `light`, step 5 asks
|
|
69
|
+
in chat.
|
|
27
70
|
1. **Scaffold.** Both scripts are idempotent, never overwrite, and print what
|
|
28
|
-
they created; note it. They take `--runtime auto|claude|codex|both`
|
|
29
|
-
follows `CLAUDE.md`/`.claude/` and `AGENTS.md`/`.codex/`); pass
|
|
30
|
-
the repository is worked on from
|
|
71
|
+
they created; note it, 0a's too. They take `--runtime auto|claude|codex|both`
|
|
72
|
+
(`auto` follows `CLAUDE.md`/`.claude/` and `AGENTS.md`/`.codex/`); pass the
|
|
73
|
+
`runtime` pick, else `auto` — `both` when the repository is worked on from
|
|
74
|
+
both runtimes.
|
|
31
75
|
|
|
32
76
|
```bash
|
|
33
|
-
"$AI_HOME/hooks/project-scaffold.sh" "$PWD"
|
|
34
|
-
"$AI_HOME/skills/ai-init/scaffold-ai.sh" "$PWD"
|
|
77
|
+
"$AI_HOME/hooks/project-scaffold.sh" "$PWD" --runtime <pick> # docs/sdlc/, the runtime dir, .gitignore
|
|
78
|
+
"$AI_HOME/skills/ai-init/scaffold-ai.sh" "$PWD" --runtime <pick> # the instruction file; .ai/ is 0a's
|
|
35
79
|
```
|
|
36
80
|
|
|
37
|
-
If `.ai/project/overview.md
|
|
81
|
+
If 0a did **not** create `.ai/project/overview.md`, the project was set up
|
|
38
82
|
before: run **--update** below, then ask whether the survey should be
|
|
39
83
|
refreshed; a refresh never overwrites a `.ai/project/` file a human edited
|
|
40
|
-
without saying so first. If
|
|
41
|
-
wanted), fill its Commands / Conventions /
|
|
42
|
-
one line per command, five sentences of
|
|
84
|
+
without saying so first. If 0a and this step together created only an
|
|
85
|
+
instruction file (no `.ai/` wanted), fill its Commands / Conventions /
|
|
86
|
+
Architecture from the manifests — one line per command, five sentences of
|
|
87
|
+
architecture — and stop.
|
|
43
88
|
2. **Stack.** `$SETUP survey` first: the project's own toolbox
|
|
44
89
|
(`.claude/{skills,commands,rules,agents,hooks}`, `.codex/*`, `bin/*.sh`,
|
|
45
90
|
Makefile targets) with each item's purpose, the guards a project hook
|
|
@@ -51,13 +96,15 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
51
96
|
(`phpunit.xml*`, `phpstan.neon*`, `playwright.config.*` …). For Symfony or
|
|
52
97
|
Sylius also: bundles, entities, state machines, Messenger, event
|
|
53
98
|
subscribers, payment, shipping, tax, promotions, ERP or fiscal integrations.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
99
|
+
Then write the `verify=<cmd>` pick, if any (0c).
|
|
100
|
+
3. **Survey.** The depth is the `survey` pick or `--survey light|full`. `light`,
|
|
101
|
+
the default, is for a developer who knows the project: ask for a ten-minute
|
|
102
|
+
brain dump (entry points, modules that matter, integrations,
|
|
103
|
+
unwritten business rules, areas nobody dares touch), confirm
|
|
57
104
|
each claim with `grep -n`, then fan out exactly **two** `ai-discovery` agents
|
|
58
105
|
in one message: (1) legacy, risks and code that looks unused; (2) tests,
|
|
59
106
|
CI/CD, data and configuration — how secrets are supplied, never their
|
|
60
|
-
values.
|
|
107
|
+
values. `full`, for a codebase new to you: one `Explore` for the
|
|
61
108
|
inventory, then six `ai-discovery` agents in one message — entry points;
|
|
62
109
|
domain modules; integrations and their failure handling; business rules and
|
|
63
110
|
legacy; tests and CI/CD; data and configuration. Hand each the slice of the
|
|
@@ -68,14 +115,15 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
68
115
|
write `Documented behaviour / Observed behaviour / Evidence / Risk` and do
|
|
69
116
|
not resolve it.
|
|
70
117
|
5. **Policies.** Fill the project sections of `.ai/policies/coding.md` and
|
|
71
|
-
`testing.md` only if
|
|
118
|
+
`testing.md` only if 0a created them. The **Verification** block of
|
|
72
119
|
`testing.md` is the feedback loop every task runs:
|
|
73
120
|
- `verify_command` — the fast suite to the end, the e2e group **excluded**;
|
|
74
121
|
- `step_test_command` — the same runner on given paths (`{files}`);
|
|
75
122
|
- `e2e_command` — the end-to-end suite, or `none` when there is none;
|
|
76
123
|
- two or three lines of what healthy output looks like.
|
|
77
124
|
|
|
78
|
-
`verify_command` comes from the human:
|
|
125
|
+
`verify_command` comes from the human: the `verify=<cmd>` pick is their yes,
|
|
126
|
+
written at the end of step 2; without one, show the one `$SETUP survey`
|
|
79
127
|
proposed, ask, and only after a yes in chat write it with
|
|
80
128
|
`$SETUP verify --set "<command>"` (exit 3: one is already set — the human
|
|
81
129
|
edits it). `$SETUP check` then exits 0; `VERIFY_COMMAND_MISSING` (exit 1)
|
|
@@ -109,14 +157,24 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
109
157
|
casually, the weight-table entries you chose, recommended next improvements —
|
|
110
158
|
recommendations only, none implemented. When the survey found a toolbox,
|
|
111
159
|
name each toolbox item the project keeps using; they stay where they are.
|
|
112
|
-
9. **Report.** Files created (a tree from the scaffold output), surprising
|
|
160
|
+
9. **Report.** Files created (a tree from the scaffold output, 0a's too), surprising
|
|
113
161
|
findings, the modes and what puts a change in each, the safety restrictions
|
|
114
162
|
in force (hooks versus policy), the tooling the workflows reuse, assumptions,
|
|
115
163
|
unresolved questions and who can answer them, the survey's superpowers
|
|
116
164
|
line as printed (the `enabledPlugins` line is the human's to add — never
|
|
117
165
|
write it), and the suggested first task:
|
|
118
166
|
`/rr-plan <it>`. Claim nothing was validated that was not run. Suggest
|
|
119
|
-
`git add .ai .gitignore docs/sdlc` plus the instruction files created
|
|
167
|
+
`git add .ai .gitignore docs/sdlc` plus the instruction files created
|
|
168
|
+
(`.ai/memory/` and `.ai/reports/` are git-ignored; memory stays local unless the human runs
|
|
169
|
+
`memory enable --share git`).
|
|
170
|
+
|
|
171
|
+
## --options
|
|
172
|
+
|
|
173
|
+
Reopens every option on a set-up project: 0a, then `$SETUP options --open --all`, then
|
|
174
|
+
send, settle and apply as in step 0 — a `survey` pick runs steps 3–4 at that depth, as a
|
|
175
|
+
refresh (step 1), and a `verify` command is written at once. Say what changed and stop;
|
|
176
|
+
no other step reruns. A project 0a just scaffolded (it listed
|
|
177
|
+
`.ai/project/overview.md`) continues as the plain **Setup**.
|
|
120
178
|
|
|
121
179
|
## --update
|
|
122
180
|
|
|
@@ -187,7 +245,7 @@ riskrails is installed: until then the project's old shapes are not read.
|
|
|
187
245
|
second machine runs the same before its next pull, else a modify/delete
|
|
188
246
|
conflict); never run it yourself.
|
|
189
247
|
7. `$UPDATE --check` and `git diff --stat`; say what changed, what you merged
|
|
190
|
-
and what is the human's. Suggest `git add -A .ai docs/sdlc .gitignore` plus
|
|
248
|
+
and what is the human's. Suggest `git add -A .ai docs/sdlc .gitignore` (the record and task logs are git-ignored) plus
|
|
191
249
|
the runtime directories and instruction files.
|
|
192
250
|
|
|
193
251
|
## --audit
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Idempotent .ai/ scaffold for RiskRails.
|
|
3
|
-
scaffold_ai.py [project-dir] [--runtime auto|claude|codex|both] (default: $PWD, auto)
|
|
3
|
+
scaffold_ai.py [project-dir] [--runtime auto|claude|codex|both|none] (default: $PWD, auto)
|
|
4
4
|
skills/ai-init/scaffold-ai.sh execs it; the templates stay in skills/ai-init/.
|
|
5
5
|
|
|
6
6
|
Never overwrites an existing file. Run it as often as you like: it creates what
|
|
@@ -14,6 +14,10 @@ that points at it differs — CLAUDE.md for Claude Code, AGENTS.md for Codex.
|
|
|
14
14
|
A port of the shell script it replaced, kept to its bytes: the same files and
|
|
15
15
|
modes, the same listing in the same order, the same messages and exit codes
|
|
16
16
|
(tests/test_scaffold_differential.py runs the frozen shell beside it).
|
|
17
|
+
|
|
18
|
+
One value is new, and the shell refuses it: `--runtime none` (or AI_RUNTIMES=none) scaffolds .ai/ and its
|
|
19
|
+
.gitignore lines and no instruction file, so /rr-setup can open .ai/ before the project's runtimes are chosen.
|
|
20
|
+
Like `both` and `auto` it is a whole value: inside a list it is an unknown runtime, as in the shell.
|
|
17
21
|
"""
|
|
18
22
|
import errno
|
|
19
23
|
import json
|
|
@@ -143,7 +147,9 @@ def logical_cd(root):
|
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
def runtimes(runtime, root):
|
|
146
|
-
"""The runtimes in scope: what the project declares (auto),
|
|
150
|
+
"""The runtimes in scope: what the project declares (auto), the list given, or none at all."""
|
|
151
|
+
if runtime == "none":
|
|
152
|
+
return set()
|
|
147
153
|
if runtime == "both":
|
|
148
154
|
runtime = "claude,codex"
|
|
149
155
|
if runtime == "auto":
|
package/skills/rr-setup/setup.py
CHANGED
|
@@ -5,21 +5,33 @@ and whether superpowers is on (spec R9, R28, SQ5, SQ12 variant B).
|
|
|
5
5
|
setup.py survey [--root DIR] the toolbox, the proposed verify_command, the superpowers line
|
|
6
6
|
setup.py check [--root DIR] exit 1 VERIFY_COMMAND_MISSING unless verification.md names one
|
|
7
7
|
setup.py verify [--root DIR] --set CMD write verify_command into verification.md (after the human confirmed)
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
setup.py options [--root DIR] [--all] [--skip KEY]… --json
|
|
9
|
+
what each of the four setup options is now, as one JSON object
|
|
10
|
+
setup.py options [--root DIR] [--all] [--skip KEY]… --open
|
|
11
|
+
the unset ones as one dialog. First line: OPTIONS_DIALOG D<n> …
|
|
12
|
+
(then state.py's two lines), OPTIONS_FALLBACK <why> (the questions
|
|
13
|
+
as prose) or OPTIONS_NONE
|
|
14
|
+
|
|
15
|
+
--skip KEY (memory|runtime|survey|verify, repeatable) leaves that option out of what is asked, for both outputs and
|
|
16
|
+
with --all; its entry in the --json facts stays. survey, check and options --json write nothing; options --open
|
|
17
|
+
writes only the decisions state.py opens, none in the fallback. The toolbox is production behaviour (C1): it is
|
|
10
18
|
inventoried in place, never copied or moved; what to inventory is the constants
|
|
11
19
|
FOREIGN_ROOTS and OWN_* below.
|
|
12
20
|
The enabledPlugins line is printed, never written: .claude/settings.json is the
|
|
13
21
|
human's.
|
|
14
22
|
|
|
15
23
|
Exit codes: 0 done; 1 VERIFY_COMMAND_MISSING; 2 usage;
|
|
16
|
-
3 refused (the line is already set).
|
|
24
|
+
3 refused (the line is already set); 6 state.py refused to open the dialog (its message is passed through).
|
|
17
25
|
"""
|
|
18
26
|
import argparse
|
|
27
|
+
import hashlib
|
|
28
|
+
import importlib
|
|
19
29
|
import json
|
|
20
30
|
import os
|
|
21
31
|
import re
|
|
32
|
+
import shlex
|
|
22
33
|
import shutil
|
|
34
|
+
import subprocess
|
|
23
35
|
import sys
|
|
24
36
|
|
|
25
37
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -31,6 +43,20 @@ UNKNOWN = "UNKNOWN — no description and no leading comment"
|
|
|
31
43
|
VERIFY_RE = re.compile(r"(?m)^verify_command:[ \t]*(.*)$")
|
|
32
44
|
MAKE_TARGET_RE = re.compile(r"^([A-Za-z0-9][A-Za-z0-9_. \t-]*?)\s*:(?![=:])(.*)$")
|
|
33
45
|
BLOCK_SCALAR_RE = re.compile(r"^[|>][-+]?$")
|
|
46
|
+
PENDING_RE = re.compile(r"DIALOG_PENDING ((?:D\d+ )*D\d+) — ") # state.py's first line: the decisions it opened
|
|
47
|
+
# `options`: the sibling skills, the state.py of this install (the one the dialog's commands name), the files the
|
|
48
|
+
# survey signal compares, and the signals that scaffold_ai.runtimes() reads for `--runtime auto` as (instruction
|
|
49
|
+
# file, runtime directory).
|
|
50
|
+
SKILLS = os.path.dirname(HERE)
|
|
51
|
+
AI_TASK = os.path.join(SKILLS, "ai-task")
|
|
52
|
+
STATE_PY = os.path.join(AI_TASK, "state.py")
|
|
53
|
+
OPTION_KEYS = ("memory", "runtime", "survey", "verify") # the four options, in the order they are asked
|
|
54
|
+
OVERVIEW = os.path.join(".ai", "project", "overview.md")
|
|
55
|
+
OVERVIEW_TEMPLATE = os.path.join(SKILLS, "ai-init", "templates", OVERVIEW)
|
|
56
|
+
HISTORY_INDEX = os.path.join(SKILLS, "project-update", "history", "index.json")
|
|
57
|
+
HISTORY_KEY = "ai-init/.ai/project/overview.md"
|
|
58
|
+
RUNTIME_SIGNALS = {"claude": ("CLAUDE.md", ".claude"), "codex": ("AGENTS.md", ".codex"),
|
|
59
|
+
"gemini": ("GEMINI.md", ".gemini"), "junie": (None, ".junie")}
|
|
34
60
|
# Paths a foreign AI tool owns (Spec Kit, Kiro, AI-DLC, Cursor, Copilot): no part of the project's own toolbox.
|
|
35
61
|
FOREIGN_ROOTS = [
|
|
36
62
|
".specify/",
|
|
@@ -113,6 +139,14 @@ def read_text(path):
|
|
|
113
139
|
return None
|
|
114
140
|
|
|
115
141
|
|
|
142
|
+
def read_bytes(path):
|
|
143
|
+
try:
|
|
144
|
+
with open(path, "rb") as fh:
|
|
145
|
+
return fh.read()
|
|
146
|
+
except OSError:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
116
150
|
def is_crlf(path):
|
|
117
151
|
try:
|
|
118
152
|
with open(path, "rb") as fh:
|
|
@@ -402,6 +436,305 @@ def write(path, text):
|
|
|
402
436
|
os.remove(tmp)
|
|
403
437
|
|
|
404
438
|
|
|
439
|
+
# ------------------------------------------------------------------ options
|
|
440
|
+
|
|
441
|
+
def sibling(directory, name):
|
|
442
|
+
"""A module of this skill or of another, imported once its directory leads sys.path: `python3 -I` puts none
|
|
443
|
+
there, and the module's own imports are by name (memory_export reaches memory_compact that way). Skills are
|
|
444
|
+
installed independently, so a missing module is an ImportError for the caller to answer."""
|
|
445
|
+
if directory not in sys.path:
|
|
446
|
+
sys.path.insert(0, directory)
|
|
447
|
+
sys.dont_write_bytecode = True # a read-only verb leaves no __pycache__ in the install
|
|
448
|
+
return importlib.import_module(name)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def digest(data):
|
|
452
|
+
"""sha256 with LF line ends, as update.py's History.shipped reads a file a CRLF checkout changed."""
|
|
453
|
+
return hashlib.sha256(data.replace(b"\r\n", b"\n")).hexdigest()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def in_git_tree(root):
|
|
457
|
+
try:
|
|
458
|
+
done = subprocess.run(["git", "-C", root, "rev-parse", "--is-inside-work-tree"],
|
|
459
|
+
capture_output=True, text=True, check=False)
|
|
460
|
+
except OSError: # no git binary
|
|
461
|
+
return False
|
|
462
|
+
return done.returncode == 0 and done.stdout.strip() == "true"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def dialog_on(root):
|
|
466
|
+
"""state.py's own answer (`consent --json`): the project's dialog.json on, the runtime's dialog tool verified and
|
|
467
|
+
a session id in this environment. False without dialog.json and when state.py cannot answer."""
|
|
468
|
+
if not os.path.isfile(os.path.join(root, ".ai", "policies", "dialog.json")):
|
|
469
|
+
return False
|
|
470
|
+
try:
|
|
471
|
+
done = subprocess.run([sys.executable, "-I", "-B", os.path.join(AI_TASK, "state.py"), "--root", root,
|
|
472
|
+
"consent", "--json"], capture_output=True, text=True, check=False)
|
|
473
|
+
data = json.loads(done.stdout) if done.returncode == 0 else None
|
|
474
|
+
except (OSError, ValueError):
|
|
475
|
+
return False
|
|
476
|
+
return isinstance(data, dict) and data.get("dialog") is True
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def author_source(cfg):
|
|
480
|
+
"""Where `cfg.author` comes from, in the order memory_config.MemoryConfig.author tries them."""
|
|
481
|
+
if cfg.local.get("author"):
|
|
482
|
+
return "local config"
|
|
483
|
+
return next(("$" + name for name in ("USER", "USERNAME") if os.environ.get(name)), "default")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def fts5():
|
|
487
|
+
"""FTS5 in this Python's sqlite3, by memory_store's own probe (AI_MEMORY_NO_FTS=1 turns it off);
|
|
488
|
+
False without the sqlite3 module."""
|
|
489
|
+
try:
|
|
490
|
+
return sibling(AI_TASK, "memory_store").fts5_available()
|
|
491
|
+
except ImportError:
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def memory_option(root, git):
|
|
496
|
+
"""Packages-mode project memory: set once memory.json exists, its mode being `current`. `author` is what
|
|
497
|
+
`memory enable` writes without --author (the local config, else $USER): memory_config's own answer, never git's."""
|
|
498
|
+
def unavailable(why):
|
|
499
|
+
return {"key": "memory", "state": "unavailable", "why": why}
|
|
500
|
+
if not git:
|
|
501
|
+
return unavailable("not a git work tree")
|
|
502
|
+
try:
|
|
503
|
+
memory_config = sibling(AI_TASK, "memory_config")
|
|
504
|
+
memory_io = sibling(AI_TASK, "memory_io")
|
|
505
|
+
except ImportError as exc:
|
|
506
|
+
return unavailable("the memory modules are not installed beside state.py (%s)" % exc)
|
|
507
|
+
try:
|
|
508
|
+
cfg = memory_config.load_config(root)
|
|
509
|
+
except memory_io.MemoryError_ as exc:
|
|
510
|
+
return unavailable(str(exc))
|
|
511
|
+
return {"key": "memory", "state": "set" if cfg.present else "unset", "current": cfg.mode if cfg.present else None,
|
|
512
|
+
"author": cfg.author, "author_from": author_source(cfg),
|
|
513
|
+
"period": None if cfg.present else memory_config.DEFAULTS["period"], "fts5": fts5()}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def runtime_option(root):
|
|
517
|
+
"""`declared`: the runtimes the repository already names, by the signals scaffold_ai.runtimes() reads.
|
|
518
|
+
`auto`: what `--runtime auto` would scaffold now, as that function resolves it, in the spelling --runtime takes."""
|
|
519
|
+
declared = [name for name, (instruction, directory) in RUNTIME_SIGNALS.items()
|
|
520
|
+
if (instruction and os.path.exists(os.path.join(root, instruction)))
|
|
521
|
+
or os.path.isdir(os.path.join(root, directory))]
|
|
522
|
+
scaffold = sibling(HERE, "scaffold_ai")
|
|
523
|
+
chosen = scaffold.runtimes("auto", root)
|
|
524
|
+
return {"key": "runtime", "state": "set" if declared else "unset", "declared": declared,
|
|
525
|
+
"auto": ",".join(name for name in scaffold.RUNTIMES if name in chosen)}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def surveyed(root):
|
|
529
|
+
"""No depth is recorded anywhere. A survey fills .ai/project/overview.md, so a copy that is none of the files
|
|
530
|
+
the plugin shipped for it (today's template, or a version in project-update's history index: update.py's
|
|
531
|
+
own test for an unedited file) has been written into."""
|
|
532
|
+
mine = read_bytes(os.path.join(root, OVERVIEW))
|
|
533
|
+
if mine is None:
|
|
534
|
+
return False
|
|
535
|
+
shipped = set()
|
|
536
|
+
template = read_bytes(OVERVIEW_TEMPLATE)
|
|
537
|
+
if template is not None:
|
|
538
|
+
shipped.add(digest(template))
|
|
539
|
+
try:
|
|
540
|
+
shipped.update(json.loads(read_text(HISTORY_INDEX) or "{}")[HISTORY_KEY])
|
|
541
|
+
except (ValueError, KeyError, TypeError):
|
|
542
|
+
pass
|
|
543
|
+
return digest(mine) not in shipped
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def verify_option(root):
|
|
547
|
+
"""Set by check's own rule (a value that is not `none`); else the proposal `survey` makes, and the others."""
|
|
548
|
+
current = verify_value(root)
|
|
549
|
+
if current and current.lower() != "none":
|
|
550
|
+
return {"key": "verify", "state": "set", "current": current}
|
|
551
|
+
found = [{"command": command, "from": source} for command, source in verify_candidates(root)]
|
|
552
|
+
return {"key": "verify", "state": "unset", "proposal": found[0] if found else None, "also": found[1:]}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def option_facts(root, show_all=False, skip=()):
|
|
556
|
+
"""What each of the four setup options is now, read-only (spec interactive-setup-options R9, R16). `ask` lists
|
|
557
|
+
the unset ones in the order memory, runtime, survey, verify; `show_all` lists every one not unavailable; `skip`
|
|
558
|
+
leaves keys out of it that the caller has answered another way (/rr-setup --survey). The facts of a skipped
|
|
559
|
+
option stay as computed: they are still true, and only what is asked changes."""
|
|
560
|
+
git = in_git_tree(root)
|
|
561
|
+
options = [memory_option(root, git), runtime_option(root),
|
|
562
|
+
{"key": "survey", "state": "set" if surveyed(root) else "unset"}, verify_option(root)]
|
|
563
|
+
return {"schema": 1, "root": root, "git": git, "dialog": dialog_on(root),
|
|
564
|
+
"ask": [o["key"] for o in options if o["key"] not in skip
|
|
565
|
+
and (o["state"] == "unset" or (show_all and o["state"] == "set"))],
|
|
566
|
+
"options": options}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ------------------------------------------------------------ options --open
|
|
570
|
+
|
|
571
|
+
def state_label(root, *words):
|
|
572
|
+
"""The command that runs this install's state.py with `words` on this project, absolute and quoted as
|
|
573
|
+
rr_state.labels.state_label writes it: a pick and the call the agent makes are then one string. A copy, not an
|
|
574
|
+
import: rr_state.fs.ENTRY is only set by state.py's own entry."""
|
|
575
|
+
return "python3 %s %s" % (shlex.quote(STATE_PY), shlex.join(["--root", root, *words]))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def choice_option(label, value):
|
|
579
|
+
return {"kind": "choice", "label": label, "value": value}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def command_option(label, description):
|
|
583
|
+
return {"kind": "command", "label": label, "description": description}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def shown(text):
|
|
587
|
+
"""`text` inside backticks, as a label shows a command, or None when the opener would refuse it: a backtick of its
|
|
588
|
+
own ends the span early, and a choice value is at most 200 printable characters (dialog_cmd.SETUP_VALUE_MAX), so
|
|
589
|
+
that "Use `…`" stays inside the labels' 400."""
|
|
590
|
+
return "`%s`" % text if text and "`" not in text and len(text) <= 200 and text.isprintable() else None
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def decision(key, question, options):
|
|
594
|
+
return {"key": key, "header": key.capitalize(), "question": question, "options": options}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def memory_decision(option, root, name):
|
|
598
|
+
"""Memory is asked first, so that its pick is the earliest the gate passes (spec B3). Its commands are `memory
|
|
599
|
+
enable` of this install's state.py: --period only while memory.json is absent (`period` is then set), never
|
|
600
|
+
--author, because enable writes the author the question names (D1): the local config's, else $USER's."""
|
|
601
|
+
period = ["--period", option["period"]] if option["period"] else []
|
|
602
|
+
who = "Author %s (from %s)" % (option["author"], option["author_from"])
|
|
603
|
+
if option["state"] == "set":
|
|
604
|
+
first = choice_option("Keep — %s mode" % option["current"], "keep")
|
|
605
|
+
question = ("Project memory for %s is in %s mode: off keeps closed tasks only in .ai/reports/. packages also writes "
|
|
606
|
+
"one compact file per developer per period under .ai/memory/; both stay local (git-ignored) unless you "
|
|
607
|
+
"run 'state.py memory enable --share git' in your own terminal. %s. Keep it, or run memory enable?" % (name, option["current"], who))
|
|
608
|
+
else:
|
|
609
|
+
first = choice_option("Off — keep files mode", "off")
|
|
610
|
+
question = ("Project memory for %s: off keeps closed tasks only in .ai/reports/. packages also writes one compact "
|
|
611
|
+
"file per developer per %s under .ai/memory/; both stay local (git-ignored) unless you run "
|
|
612
|
+
"'state.py memory enable --share git' in your own terminal. %s, period %s. Turn it on?" % (name, option["period"], who, option["period"]))
|
|
613
|
+
return decision("memory", question, [
|
|
614
|
+
first,
|
|
615
|
+
command_option(state_label(root, "memory", "enable", *period),
|
|
616
|
+
"packages; local, git-ignored record under .ai/memory/"),
|
|
617
|
+
command_option(state_label(root, "memory", "enable", *period, "--index", "sqlite"),
|
|
618
|
+
"plus a local, git-ignored SQLite index; FTS5 %s — without it search scans: same results, slower"
|
|
619
|
+
% ("yes" if option["fts5"] else "no"))])
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def runtime_decision(option, name):
|
|
623
|
+
"""No "Not now": the four runtimes use all the options a question has. `auto` names what it resolves to: the
|
|
624
|
+
runtime this copy is installed for while the repository declares none, else what it declares."""
|
|
625
|
+
auto = "Auto — what the repository declares"
|
|
626
|
+
auto += " (%s)" % option["auto"] if option["declared"] else ", else %s" % option["auto"]
|
|
627
|
+
return decision("runtime", "Which agent runtimes work on %s? The scaffold adds their instruction files and directories "
|
|
628
|
+
"and never removes one." % name,
|
|
629
|
+
[choice_option(auto, "auto"), choice_option("Claude Code only", "claude"),
|
|
630
|
+
choice_option("Codex only", "codex"), choice_option("Claude Code and Codex", "both")])
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def survey_decision(option, name):
|
|
634
|
+
options = [choice_option("Light — your 10-minute brain dump + 2 discovery agents", "light"),
|
|
635
|
+
choice_option("Full — inventory + 6 discovery agents", "full")]
|
|
636
|
+
if option["state"] == "set":
|
|
637
|
+
options.append(choice_option("Keep the current survey", "keep"))
|
|
638
|
+
return decision("survey", "How deep should the first survey of %s go?" % name, options)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def verify_decision(option):
|
|
642
|
+
"""Unset: not now, the proposal, at most one other candidate, or typed in chat (a free-text answer is a decline,
|
|
643
|
+
so a command of the human's own is typed after the dialog: spec A3). A command the opener would refuse is not
|
|
644
|
+
offered. Set (--all): keep it, or edit it by hand — verify_command is the human's once written."""
|
|
645
|
+
intro = "verify_command — the fast suite every task runs, e2e excluded."
|
|
646
|
+
if option["state"] == "set":
|
|
647
|
+
current = shown(option["current"])
|
|
648
|
+
return decision("verify", "%s Current: %s." % (intro, current or "set in .ai/policies/verification.md"),
|
|
649
|
+
[choice_option("Keep %s" % current if current else "Keep the current command", "keep"),
|
|
650
|
+
choice_option("Change — I edit it myself", "edit")])
|
|
651
|
+
proposal = [option["proposal"]] if option["proposal"] and shown(option["proposal"]["command"]) else []
|
|
652
|
+
also = [c for c in option["also"] if shown(c["command"])][:1]
|
|
653
|
+
found = proposal + also
|
|
654
|
+
options = [choice_option("Not now — ask me in chat", "")]
|
|
655
|
+
options += [choice_option("Use %s" % shown(c["command"]), c["command"]) for c in found]
|
|
656
|
+
options.append(choice_option("Another — I type it in chat", "other"))
|
|
657
|
+
proposed = ("Proposed: %s (from %s)." % (shown(found[0]["command"]), found[0]["from"]) if found
|
|
658
|
+
else "No proposal: nothing found in the project's files.")
|
|
659
|
+
return decision("verify", "%s %s" % (intro, proposed), options)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def decision_for(option, root, name):
|
|
663
|
+
if option["key"] == "memory":
|
|
664
|
+
return memory_decision(option, root, name)
|
|
665
|
+
if option["key"] == "runtime":
|
|
666
|
+
return runtime_decision(option, name)
|
|
667
|
+
if option["key"] == "survey":
|
|
668
|
+
return survey_decision(option, name)
|
|
669
|
+
return verify_decision(option)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def dialog_off_why(root):
|
|
673
|
+
"""Why state.py's switch is off here, as the first cause found in the order it tests them: the project's
|
|
674
|
+
dialog.json, an unattended run or session, then the two it cannot be told apart by from here (the runtime's dialog
|
|
675
|
+
tool the installer verified, a session id)."""
|
|
676
|
+
try:
|
|
677
|
+
with open(os.path.join(root, ".ai", "policies", "dialog.json"), encoding="utf-8") as fh:
|
|
678
|
+
enabled = (json.load(fh) or {}).get("enabled") is True
|
|
679
|
+
except (OSError, ValueError, AttributeError):
|
|
680
|
+
enabled = False
|
|
681
|
+
if not enabled:
|
|
682
|
+
return "no enabled .ai/policies/dialog.json"
|
|
683
|
+
if os.environ.get("AI_UNATTENDED", "").strip().lower() not in ("", "0", "false", "no"):
|
|
684
|
+
return "AI_UNATTENDED is set (an unattended run)"
|
|
685
|
+
if os.environ.get("CLAUDE_CODE_SESSION_ATTENDED") == "0":
|
|
686
|
+
return "CLAUDE_CODE_SESSION_ATTENDED=0 (an unattended session)"
|
|
687
|
+
return "the runtime's dialog tool is not verified (riskrails/profile.json dialog.tool) or this session has no id"
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def open_dialog(root, decisions):
|
|
691
|
+
"""The decisions piped to this install's state.py, which opens all of them or none. Returns (exit code, None) once
|
|
692
|
+
it answered: 0 with OPTIONS_DIALOG and its two lines unchanged, 6 with its refusal passed through (the skill
|
|
693
|
+
settles what is open first, or falls back); (None, why) when it found the dialog off after all, for the fallback."""
|
|
694
|
+
spec = json.dumps({"schema": 1, "decisions": decisions}) # ASCII only: no locale decides how it is read
|
|
695
|
+
try:
|
|
696
|
+
done = subprocess.run([sys.executable, "-I", "-B", "-X", "utf8", STATE_PY, "--root", root, "decision", "open",
|
|
697
|
+
"--kind", "setup", "--spec", "-"],
|
|
698
|
+
input=spec, capture_output=True, encoding="utf-8", errors="replace", check=False)
|
|
699
|
+
except OSError as exc:
|
|
700
|
+
sys.stderr.write("setup.py: cannot run %s (%s)\n" % (STATE_PY, exc))
|
|
701
|
+
return 6, None
|
|
702
|
+
if done.returncode == 1: # its die() default: the dialog off after all, or a decision.json it cannot read
|
|
703
|
+
last = ([line for line in done.stderr.splitlines() if line.strip()] or ["no message"])[-1]
|
|
704
|
+
text = " ".join(last.replace(root + os.sep, "").replace("state.py: ", "", 1).split())
|
|
705
|
+
return None, "state.py could not open the dialog: %s" % (text if len(text) <= 200 else text[:199] + "…")
|
|
706
|
+
if done.returncode != 0:
|
|
707
|
+
sys.stderr.write(done.stderr or "setup.py: state.py exited %d without a message\n" % done.returncode)
|
|
708
|
+
return 6, None
|
|
709
|
+
ids = PENDING_RE.match(done.stdout.split("\n", 1)[0])
|
|
710
|
+
if not ids:
|
|
711
|
+
sys.stderr.write("setup.py: state.py answered with no DIALOG_PENDING line; 'state.py decision show' lists "
|
|
712
|
+
"what it opened\n")
|
|
713
|
+
return 6, None
|
|
714
|
+
sys.stderr.write(done.stderr)
|
|
715
|
+
print("OPTIONS_DIALOG %s" % ids.group(1))
|
|
716
|
+
sys.stdout.write(done.stdout)
|
|
717
|
+
return 0, None
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def fallback_lines(root, why, decisions):
|
|
721
|
+
"""The questions as numbered prose, in the dialog's words (a command is its arguments after the project's root, and
|
|
722
|
+
its description), then, when memory is asked, the command the human runs: nothing is enabled from here, and
|
|
723
|
+
nothing is written."""
|
|
724
|
+
prefix = state_label(root) + " "
|
|
725
|
+
lines = ["OPTIONS_FALLBACK %s" % why]
|
|
726
|
+
for number, item in enumerate(decisions, 1):
|
|
727
|
+
lines.append("%d. %s: %s" % (number, item["header"], item["question"]))
|
|
728
|
+
lines += [" %s) %s" % (letter, o["label"] if o["kind"] == "choice"
|
|
729
|
+
else "%s — %s" % (o["label"][len(prefix):], o["description"]))
|
|
730
|
+
for letter, o in zip("abcd", item["options"])]
|
|
731
|
+
memory = next((d for d in decisions if d["key"] == "memory"), None)
|
|
732
|
+
if memory:
|
|
733
|
+
enable = next(o["label"] for o in memory["options"] if o["kind"] == "command")
|
|
734
|
+
lines.append("memory is the human's, in their own terminal: %s [--index sqlite]" % enable)
|
|
735
|
+
return lines
|
|
736
|
+
|
|
737
|
+
|
|
405
738
|
# ----------------------------------------------------------------- commands
|
|
406
739
|
|
|
407
740
|
def cmd_survey(root):
|
|
@@ -456,14 +789,45 @@ def cmd_verify(root, command):
|
|
|
456
789
|
return 0
|
|
457
790
|
|
|
458
791
|
|
|
792
|
+
def cmd_options(root, show_all, skip):
|
|
793
|
+
print(json.dumps(option_facts(root, show_all, skip), ensure_ascii=False))
|
|
794
|
+
return 0
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def cmd_options_open(root, show_all, skip):
|
|
798
|
+
"""R10, R17: nothing to ask → OPTIONS_NONE; the dialog on → the questions opened through state.py; else the
|
|
799
|
+
questions as prose. Memory is only ever in `ask` inside a git work tree (R16)."""
|
|
800
|
+
facts = option_facts(root, show_all, skip)
|
|
801
|
+
if not facts["ask"]:
|
|
802
|
+
print("OPTIONS_NONE")
|
|
803
|
+
return 0
|
|
804
|
+
name = os.path.basename(root) or root
|
|
805
|
+
by_key = {o["key"]: o for o in facts["options"]}
|
|
806
|
+
decisions = [decision_for(by_key[key], root, name) for key in facts["ask"]]
|
|
807
|
+
if facts["dialog"]:
|
|
808
|
+
code, why = open_dialog(root, decisions)
|
|
809
|
+
if code is not None:
|
|
810
|
+
return code
|
|
811
|
+
else:
|
|
812
|
+
why = dialog_off_why(root)
|
|
813
|
+
print("\n".join(fallback_lines(root, why, decisions)))
|
|
814
|
+
return 0
|
|
815
|
+
|
|
816
|
+
|
|
459
817
|
def main(argv=None):
|
|
460
818
|
parser = argparse.ArgumentParser(prog="setup.py", description=__doc__.split("\n\n", 1)[0])
|
|
461
819
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
462
|
-
for name in ("survey", "check", "verify"):
|
|
820
|
+
for name in ("survey", "check", "verify", "options"):
|
|
463
821
|
p = sub.add_parser(name)
|
|
464
822
|
p.add_argument("--root", default=".")
|
|
465
823
|
if name == "verify":
|
|
466
824
|
p.add_argument("--set", dest="command", required=True)
|
|
825
|
+
if name == "options":
|
|
826
|
+
p.add_argument("--all", action="store_true")
|
|
827
|
+
p.add_argument("--skip", action="append", default=[], choices=OPTION_KEYS)
|
|
828
|
+
output = p.add_mutually_exclusive_group(required=True)
|
|
829
|
+
output.add_argument("--json", action="store_true")
|
|
830
|
+
output.add_argument("--open", action="store_true")
|
|
467
831
|
args = parser.parse_args(argv)
|
|
468
832
|
root = os.path.abspath(args.root)
|
|
469
833
|
if not os.path.isdir(root):
|
|
@@ -472,6 +836,9 @@ def main(argv=None):
|
|
|
472
836
|
return cmd_survey(root)
|
|
473
837
|
if args.cmd == "check":
|
|
474
838
|
return cmd_check(root)
|
|
839
|
+
if args.cmd == "options":
|
|
840
|
+
run = cmd_options if args.json else cmd_options_open
|
|
841
|
+
return run(root, args.all, args.skip)
|
|
475
842
|
return cmd_verify(root, args.command)
|
|
476
843
|
|
|
477
844
|
|