luciazero 2.0.2 → 2.1.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/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.1.0] - 2026-08-15
11
+
12
+ ### Added
13
+
14
+ - `/show` turns code relationships, structural changes, and verification
15
+ evidence into the smallest useful traceable visual.
16
+ - `./test.sh --fast` provides a measured intermediate tier while the default
17
+ and `--full` preserve complete CI/closeout coverage.
18
+ - Opt-in Claude hooks use private per-session scratch state to record
19
+ privacy-preserving aggregate turn/merged-Bash wall time and Bash, failed or
20
+ successful verify, and model/user skill counts; `luciazero discipline`
21
+ summarizes them.
22
+
23
+ ### Changed
24
+
25
+ - `/luciazero-bootstrap` is now `/ready`. The old command remains as a
26
+ deprecated compatibility alias for one release.
27
+ - The doctrine reserves full verification for closeout, and `/plan` plus
28
+ `/debug` no longer auto-trigger for routine edits or first obvious failures.
29
+
30
+ ## [2.0.3] - 2026-08-13
31
+
32
+ ### Fixed
33
+
34
+ - The Claude plugin now exposes its reviewer from the default root `agents/`
35
+ directory. Claude Code 2.1.227 validated the former custom manifest path but
36
+ reported and loaded zero agents; the default layout reports one. CI requires
37
+ the plugin mirror to match the classic install source byte-for-byte and
38
+ prevents the custom path from returning.
39
+
10
40
  ## [2.0.2] - 2026-08-13
11
41
 
12
42
  ### Fixed
package/README.md CHANGED
@@ -16,7 +16,7 @@ of handing back work they merely believe is finished.
16
16
  > Done is proven by a command, not by my judgment. If no verification command
17
17
  > exists, that is the first bug.
18
18
 
19
- It ships a compact [9-rule doctrine](claude/luciazero.md), 9 on-demand skills,
19
+ It ships a compact [9-rule doctrine](claude/luciazero.md), 10 on-demand skills,
20
20
  verification hooks, a risk-routed reviewer, and an eval harness. It is a
21
21
  discipline layer, not an agent runtime or overnight orchestrator.
22
22
 
@@ -83,7 +83,7 @@ Carries the doctrine, all skills, reviewer, and verify-tracking hooks:
83
83
  /plugin install luciazero@luciazero
84
84
  ```
85
85
 
86
- Start a repository with `/luciazero:luciazero-bootstrap`. Plugin skills use the
86
+ Start a repository with `/luciazero:ready`. Plugin skills use the
87
87
  `/luciazero:` prefix. The plugin has no statusline because Claude Code plugins
88
88
  cannot set one.
89
89
 
@@ -93,7 +93,8 @@ cannot set one.
93
93
  npx skills add ohm41321/luciazero
94
94
  ```
95
95
 
96
- This installs the 9 skills only: no doctrine, reviewer, or hooks.
96
+ This installs the 10 skills plus the temporary `/luciazero-bootstrap`
97
+ compatibility alias: no doctrine, reviewer, or hooks.
97
98
 
98
99
  ### Classic Claude Code and Codex
99
100
 
@@ -140,14 +141,15 @@ Claude Code can auto-update the plugin at startup: open `/plugin` →
140
141
  marketplaces leave this off by default. For release-only notifications, use
141
142
  GitHub **Watch → Custom → Releases**.
142
143
 
143
- ## The 9 skills
144
+ ## The 10 skills
144
145
 
145
- Run `/luciazero-bootstrap` first; the rest activate when their moment arrives.
146
+ Run `/ready` first; the rest activate when their moment arrives.
146
147
 
147
148
  | Moment | Skill | Result |
148
149
  |---|---|---|
149
- | Entering a repository | `/luciazero-bootstrap` | Finds or creates a verify command and proves it can fail |
150
- | Before risky or multi-step work | `/plan` | Fixes scope and observable acceptance evidence |
150
+ | Entering a repository | `/ready` | Finds or creates a verify command and proves it can fail |
151
+ | Structure or evidence is hard to scan | `/show` | Maps connections, changes, and proof into the smallest useful visual |
152
+ | Before risky, ambiguous, or multi-module work | `/plan` | Fixes scope and observable acceptance evidence |
151
153
  | A bug survives the first look | `/debug` | Reproduction, hypothesis ledger, regression test |
152
154
  | Good and bad revisions are known | `/bisect` | Finds the first bad commit in a temporary worktree |
153
155
  | Before claiming completion | `/done` | Full verify, skeptic review, scope report |
@@ -205,6 +207,9 @@ only one run per arm per task. See the [full benchmark](docs/benchmark.md),
205
207
  - Core installers, hooks, helpers, and graders are offline. Real behavioral
206
208
  evals invoke a model CLI and consume API credit or subscription quota.
207
209
  - Hooks run commands on your machine. Read them before enabling them.
210
+ - Hook telemetry stays local in private per-session state and records aggregate
211
+ turn/Bash wall time plus Bash, verify, and model/user skill counts—never raw
212
+ commands, skill names, or paths.
208
213
  - Set `LUCIAZERO_VERIFY_CMD` to the repo's exact fast verify command.
209
214
  - Put `LUCIAZERO_STRICT_VERIFY_CMD` only in personal settings, never in a
210
215
  committed repository config. Strict mode fails open on internal errors.
@@ -214,12 +219,16 @@ See [SECURITY.md](SECURITY.md) for the complete trust boundary.
214
219
  ## Development
215
220
 
216
221
  ```bash
217
- ./test.sh
222
+ ./test.sh --fast # intermediate loop: core doctrine/hooks/report/Relay checks
223
+ ./test.sh # closeout/CI: full eval, packaging, and install coverage
218
224
  ```
219
225
 
220
- The suite covers scripts, hook state, Relay, bisect, plugin/npm manifests,
221
- self-proving eval graders, and sandboxed install reinstall uninstall for
222
- Claude Code and Codex.
226
+ The fast tier is the default intermediate check for this repository; use a
227
+ more targeted command when changing a component it does not cover. The default
228
+ full tier (also `./test.sh --full`) covers scripts, hook state, Relay, bisect,
229
+ plugin/npm manifests, self-proving eval graders, and sandboxed install →
230
+ reinstall → uninstall for Claude Code and Codex. CI and `/done` use the full
231
+ tier.
223
232
 
224
233
  More detail:
225
234
 
package/README.th.md CHANGED
@@ -17,7 +17,7 @@ Luciazero ทำให้ coding agent รันลูป `วางแผน
17
17
  > ถ้ายังไม่มีคำสั่งตรวจ นั่นคือบั๊กแรก
18
18
 
19
19
  ภายในมี [doctrine 9 ข้อ](claude/luciazero.md) ที่สั้น, skill แบบเรียกเมื่อจำเป็น
20
- 9 ตัว, hook ติดตามการ verify, reviewer ที่ route ตามความเสี่ยง และ eval harness
20
+ 10 ตัว, hook ติดตามการ verify, reviewer ที่ route ตามความเสี่ยง และ eval harness
21
21
  นี่คือชั้นวินัย ไม่ใช่ agent runtime หรือระบบ orchestration สำหรับรันงานข้ามคืน
22
22
 
23
23
  ## ดูการทำงานใน 15 วินาที
@@ -81,7 +81,7 @@ fingerprint เก่า (5/6) ตัวเลขเหล่านี้เป
81
81
  /plugin install luciazero@luciazero
82
82
  ```
83
83
 
84
- เริ่ม repo ด้วย `/luciazero:luciazero-bootstrap` ชื่อ skill แบบ plugin มี prefix
84
+ เริ่ม repo ด้วย `/luciazero:ready` ชื่อ skill แบบ plugin มี prefix
85
85
  `/luciazero:` และไม่มี statusline เพราะ Claude Code plugin ตั้งค่านี้ไม่ได้
86
86
 
87
87
  ### เฉพาะ skill — agent ที่รองรับ
@@ -90,7 +90,8 @@ fingerprint เก่า (5/6) ตัวเลขเหล่านี้เป
90
90
  npx skills add ohm41321/luciazero
91
91
  ```
92
92
 
93
- ช่องทางนี้ติดตั้งเฉพาะ skill 9 ตัว ไม่มี doctrine, reviewer หรือ hook
93
+ ช่องทางนี้ติดตั้งเฉพาะ skill 10 ตัว พร้อม alias `/luciazero-bootstrap`
94
+ ชั่วคราว ไม่มี doctrine, reviewer หรือ hook
94
95
 
95
96
  ### Classic Claude Code และ Codex
96
97
 
@@ -136,14 +137,15 @@ Claude Code อัปเดต plugin ตอนเริ่มโปรแกร
136
137
  ภายนอกจะปิดตัวเลือกนี้เป็นค่าเริ่มต้น ถ้าต้องการเพียงการแจ้งเตือน release ให้ใช้
137
138
  GitHub **Watch → Custom → Releases**
138
139
 
139
- ## Skill ทั้ง 9 ตัว
140
+ ## Skill ทั้ง 10 ตัว
140
141
 
141
- รัน `/luciazero-bootstrap` ก่อนหนึ่งครั้ง ที่เหลือใช้เมื่อถึงจังหวะของมัน
142
+ รัน `/ready` ก่อนหนึ่งครั้ง ที่เหลือใช้เมื่อถึงจังหวะของมัน
142
143
 
143
144
  | จังหวะ | Skill | ผลลัพธ์ |
144
145
  |---|---|---|
145
- | เข้า repository | `/luciazero-bootstrap` | หาหรือสร้างคำสั่ง verify และพิสูจน์ว่าแดงได้ |
146
- | ก่อนงานเสี่ยงหรือหลายขั้น | `/plan` | ล็อก scope และหลักฐานยอมรับที่สังเกตได้ |
146
+ | เข้า repository | `/ready` | หาหรือสร้างคำสั่ง verify และพิสูจน์ว่าแดงได้ |
147
+ | โครงสร้างหรือหลักฐานไล่อ่านยาก | `/show` | แสดงความเชื่อมโยง สิ่งที่เปลี่ยน และหลักฐานด้วยภาพที่เล็กที่สุด |
148
+ | ก่อนงานเสี่ยง กำกวม หรือแตะหลาย module | `/plan` | ล็อก scope และหลักฐานยอมรับที่สังเกตได้ |
147
149
  | บั๊กที่มองรอบแรกไม่ออก | `/debug` | Reproduction, hypothesis ledger, regression test |
148
150
  | รู้ revision ดีและเสีย | `/bisect` | หา first bad commit ใน worktree ชั่วคราว |
149
151
  | ก่อนบอกว่าเสร็จ | `/done` | Full verify, skeptic review และรายงาน scope |
@@ -200,6 +202,9 @@ pilot มีเพียง 1 run ต่อ arm ต่อ task ดู [ผลเ
200
202
  - Installer, hook, helper และ grader หลักรัน offline ส่วน behavioral eval จริง
201
203
  เรียก model CLI และใช้เครดิต API หรือโควตา subscription
202
204
  - Hook รันคำสั่งบนเครื่อง ควรอ่านก่อนเปิดใช้
205
+ - Telemetry ของ hook อยู่ใน private state แยกตาม session ภายในเครื่อง เก็บเวลา
206
+ wall time ของ turn/Bash และจำนวน Bash, verify, skill ที่ model/user เรียก
207
+ โดยไม่เก็บ command, ชื่อ skill หรือ path ดิบ
203
208
  - ตั้ง `LUCIAZERO_VERIFY_CMD` เป็นคำสั่ง verify ระดับเร็วที่ exact ของ repo
204
209
  - ใส่ `LUCIAZERO_STRICT_VERIFY_CMD` ใน personal settings เท่านั้น ห้าม commit ลง
205
210
  config ของ repository; strict mode จะ fail open เมื่อเกิด internal error
@@ -209,12 +214,15 @@ pilot มีเพียง 1 run ต่อ arm ต่อ task ดู [ผลเ
209
214
  ## พัฒนา repo นี้
210
215
 
211
216
  ```bash
212
- ./test.sh
217
+ ./test.sh --fast # loop ระหว่างทำ: ตรวจ doctrine/hook/report/Relay ส่วนหลัก
218
+ ./test.sh # ปิดงาน/CI: ตรวจ eval, packaging และ install แบบเต็ม
213
219
  ```
214
220
 
215
- ชุดทดสอบครอบคลุม script, state ของ hook, Relay, bisect, manifest ของ plugin/npm,
216
- eval grader ที่พิสูจน์ตัวเองได้ และ install reinstall → uninstall แบบ sandbox
217
- ทั้ง Claude Code และ Codex
221
+ fast tier เป็นคำสั่งระหว่างทำงานของ repo นี้; ถ้าแก้ส่วนที่ fast tier ไม่ครอบคลุม
222
+ ให้ใช้คำสั่ง targeted ของส่วนนั้น ส่วน full tier (`./test.sh` หรือ
223
+ `./test.sh --full`) ครอบคลุม script, state ของ hook, Relay, bisect, manifest ของ
224
+ plugin/npm, eval grader ที่พิสูจน์ตัวเองได้ และ install → reinstall → uninstall
225
+ แบบ sandbox ทั้ง Claude Code และ Codex โดย CI และ `/done` ใช้ full tier
218
226
 
219
227
  อ่านต่อ:
220
228
 
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: reviewer
3
+ description: Risk-routed adversarial code reviewer with general, security, and contract focus modes. Spawn before declaring a risky change done, or when asked to review a diff, branch, or PR. Tries to refute the change, reads callers and consumers, and never edits files. If the harness offers a built-in adversarial review command, prefer it; use this agent when none exists or an independent focused pass is wanted.
4
+ tools: Read, Grep, Glob, Bash
5
+ model: inherit
6
+ ---
7
+
8
+ You are an adversarial code reviewer. Your job is to **refute** the change, not to approve it. Hunt for a real problem; praise is noise and is forbidden. An empty result after a genuine search is better than an invented finding.
9
+
10
+ ## Input
11
+
12
+ You are given a diff, branch, PR, or changed-file list and an optional `focus`: `general` (default), `security`, or `contract`. If given a branch or nothing specific, derive the diff (`git diff`, `git diff main...HEAD`, `git show`). Read enough surrounding code to judge every hunk, then search call sites, consumers, schemas, and tests that still rely on the old behavior.
13
+
14
+ ## What to hunt
15
+
16
+ Automated checks already cover the happy path. Hunt what they do not:
17
+
18
+ - **Edge cases** — empty input, zero, negative, unicode, max length, first/last element, concurrent access
19
+ - **Error paths** — what happens when the call fails, the file is missing, the network drops; are errors swallowed?
20
+ - **Changed contracts** — public API shape, serialized formats, DB schema, config keys: does anything else consume the old shape?
21
+ - **Unintended diff content** — files touched by accident, debug prints, commented-out code, secrets, dependency pins loosened
22
+ - **Resource discipline** — leaks (handles, connections, subscriptions), missing cleanup on the error path
23
+ - **Test honesty** — do the new/changed tests actually fail if the change is reverted? Tests that assert nothing, or were weakened to pass, are findings.
24
+ - **Security** — injection via interpolated input, path traversal, secrets in code or logs
25
+
26
+ ## Focus routes
27
+
28
+ - `security`: map every changed trust boundary from input to sensitive sink. Check validation, encoding, authorization, failure defaults, path containment, command/query construction, secret handling, and error disclosure. Read endpoint wiring and permission callers—not only the changed function.
29
+ - `contract`: identify the old externally observable shape, then search all in-repo consumers, fixtures, docs, serializers, migrations, and compatibility shims. Treat silent default changes and parse/format drift as contracts too.
30
+ - `general`: apply the whole checklist with extra attention to error paths, state transitions, concurrency, and resource cleanup.
31
+
32
+ ## Rules
33
+
34
+ - **Verify before reporting.** Read the actual code for each suspected finding. A finding you did not confirm against the source is speculation — drop it or mark it explicitly as unverified.
35
+ - Run cheap read-only commands when they settle a question (`git log` for context, the test suite if it is fast). Never edit, never commit, never push.
36
+ - No style or formatting nits unless they change meaning.
37
+ - Stay inside the diff's causal scope; a defect in an unchanged consumer broken by the diff is in scope. Pre-existing unrelated problems go in one short "outside scope" line at the end.
38
+
39
+ ## Output
40
+
41
+ One line per finding, most severe first:
42
+
43
+ ```
44
+ path:line — severity — problem. Concrete fix.
45
+ ```
46
+
47
+ Severity: `blocker` (exploitable security, data loss, or fundamentally wrong result) / `major` (breaks a supported contract, permission boundary, or material edge case) / `minor` (works now, but has a concrete fragility).
48
+
49
+ If, after a genuine hunt, nothing survives verification: report exactly `No findings.` plus one sentence on what you checked. Do **not** invent findings to seem useful — a false finding costs more than an empty report.
@@ -56,12 +56,20 @@ function parseLine(line) {
56
56
  if (Number.isNaN(timestamp.valueOf()) || typeof row.project_id !== "string" || typeof row.project !== "string") {
57
57
  return { malformed: true };
58
58
  }
59
+ let telemetry = null;
60
+ if (row.telemetry && typeof row.telemetry === "object") {
61
+ const keys = ["turn_ms", "bash_ms", "bash_count", "verify_count", "skill_count"];
62
+ if (keys.every((key) => Number.isSafeInteger(row.telemetry[key]) && row.telemetry[key] >= 0)) {
63
+ telemetry = Object.fromEntries(keys.map((key) => [key, row.telemetry[key]]));
64
+ }
65
+ }
59
66
  return {
60
67
  timestamp,
61
68
  event: row.event,
62
69
  project: row.project,
63
70
  projectId: row.project_id,
64
71
  verifyMode: ["regex", "exact", "strict"].includes(row.verify_mode) ? row.verify_mode : "unknown",
72
+ telemetry,
65
73
  legacy: false,
66
74
  };
67
75
  }
@@ -75,6 +83,7 @@ function parseLine(line) {
75
83
  project: match[3],
76
84
  projectId: `legacy-${crypto.createHash("sha256").update(match[3]).digest("hex").slice(0, 12)}`,
77
85
  verifyMode: "unknown",
86
+ telemetry: null,
78
87
  legacy: true,
79
88
  };
80
89
  }
@@ -111,10 +120,23 @@ if (options.project) {
111
120
 
112
121
  const counts = { "stop-clean": 0, nudge: 0, "strict-block": 0 };
113
122
  const modes = { regex: 0, exact: 0, strict: 0, unknown: 0 };
123
+ const telemetry = {
124
+ measured_turns: 0, turn_ms: 0, bash_ms: 0, non_bash_ms: 0,
125
+ bash_count: 0, verify_count: 0, skill_count: 0,
126
+ };
114
127
  const projects = new Map();
115
128
  for (const row of rows) {
116
129
  counts[row.event] += 1;
117
130
  modes[row.verifyMode] = (modes[row.verifyMode] || 0) + 1;
131
+ if (row.telemetry) {
132
+ telemetry.measured_turns += 1;
133
+ telemetry.turn_ms += row.telemetry.turn_ms;
134
+ telemetry.bash_ms += row.telemetry.bash_ms;
135
+ telemetry.non_bash_ms += Math.max(0, row.telemetry.turn_ms - row.telemetry.bash_ms);
136
+ telemetry.bash_count += row.telemetry.bash_count;
137
+ telemetry.verify_count += row.telemetry.verify_count;
138
+ telemetry.skill_count += row.telemetry.skill_count;
139
+ }
118
140
  const current = projects.get(row.projectId) || {
119
141
  project: row.project,
120
142
  project_id: row.projectId,
@@ -160,6 +182,7 @@ const report = {
160
182
  legacy_records: legacyRecords,
161
183
  outcomes: counts,
162
184
  verify_modes: modes,
185
+ telemetry,
163
186
  top_nudged_projects: topNudged,
164
187
  recommendations,
165
188
  };
@@ -182,6 +205,18 @@ console.log(` Clean stops: ${String(counts["stop-clean"]).padStart(5)} (${per
182
205
  console.log(` Nudges: ${String(counts.nudge).padStart(5)} (${percent(counts.nudge)})`);
183
206
  console.log(` Strict blocks: ${String(counts["strict-block"]).padStart(5)} (${percent(counts["strict-block"])})`);
184
207
  console.log("");
208
+ console.log("Latency Telemetry:");
209
+ if (telemetry.measured_turns === 0) {
210
+ console.log(" No measured turns (new hooks collect this locally).");
211
+ } else {
212
+ const average = (value) => Math.round(value / telemetry.measured_turns);
213
+ console.log(` Measured turns: ${telemetry.measured_turns}`);
214
+ console.log(` Average turn: ${average(telemetry.turn_ms)} ms`);
215
+ console.log(` Average Bash time: ${average(telemetry.bash_ms)} ms`);
216
+ console.log(` Average non-Bash: ${average(telemetry.non_bash_ms)} ms`);
217
+ console.log(` Bash / verify / skill calls: ${telemetry.bash_count} / ${telemetry.verify_count} / ${telemetry.skill_count}`);
218
+ }
219
+ console.log("");
185
220
  console.log("Top Nudged Repositories:");
186
221
  if (topNudged.length === 0) console.log(" None in the selected period.");
187
222
  for (const [index, item] of topNudged.entries()) {
@@ -1,5 +1,27 @@
1
1
  {
2
2
  "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ { "type": "command", "command": "LUCIAZERO_CHANNEL=plugin ${CLAUDE_PLUGIN_ROOT}/claude/hooks/luciazero-verify.sh prompt" }
7
+ ]
8
+ }
9
+ ],
10
+ "UserPromptExpansion": [
11
+ {
12
+ "hooks": [
13
+ { "type": "command", "command": "LUCIAZERO_CHANNEL=plugin ${CLAUDE_PLUGIN_ROOT}/claude/hooks/luciazero-verify.sh skill-prompt" }
14
+ ]
15
+ }
16
+ ],
17
+ "PreToolUse": [
18
+ {
19
+ "matcher": "Bash",
20
+ "hooks": [
21
+ { "type": "command", "command": "LUCIAZERO_CHANNEL=plugin ${CLAUDE_PLUGIN_ROOT}/claude/hooks/luciazero-verify.sh bash-start" }
22
+ ]
23
+ }
24
+ ],
3
25
  "PostToolUse": [
4
26
  {
5
27
  "matcher": "Edit|Write|NotebookEdit",
@@ -12,6 +34,20 @@
12
34
  "hooks": [
13
35
  { "type": "command", "command": "LUCIAZERO_CHANNEL=plugin ${CLAUDE_PLUGIN_ROOT}/claude/hooks/luciazero-verify.sh bash" }
14
36
  ]
37
+ },
38
+ {
39
+ "matcher": "Skill",
40
+ "hooks": [
41
+ { "type": "command", "command": "LUCIAZERO_CHANNEL=plugin ${CLAUDE_PLUGIN_ROOT}/claude/hooks/luciazero-verify.sh skill" }
42
+ ]
43
+ }
44
+ ],
45
+ "PostToolUseFailure": [
46
+ {
47
+ "matcher": "Bash",
48
+ "hooks": [
49
+ { "type": "command", "command": "LUCIAZERO_CHANNEL=plugin ${CLAUDE_PLUGIN_ROOT}/claude/hooks/luciazero-verify.sh bash-failure" }
50
+ ]
15
51
  }
16
52
  ],
17
53
  "Stop": [
@@ -16,7 +16,7 @@ set -u
16
16
  IN="$(cat 2>/dev/null || true)"
17
17
 
18
18
  LINE="$(printf '%s' "${IN}" | python3 -c '
19
- import json, os, sys, time, hashlib
19
+ import json, os, sys, time, hashlib, stat
20
20
 
21
21
  try:
22
22
  d = json.load(sys.stdin)
@@ -27,9 +27,24 @@ model = ((d.get("model") or {}).get("display_name")) or "claude"
27
27
  cwd = ((d.get("workspace") or {}).get("current_dir")) or d.get("cwd") or os.getcwd()
28
28
 
29
29
  key = hashlib.md5(cwd.encode()).hexdigest()[:12]
30
- state = os.path.join(os.environ.get("TMPDIR", "/tmp"), "luciazero-verify-state", key)
30
+ uid = os.getuid() if hasattr(os, "getuid") else "unknown"
31
+ base = os.path.join(os.environ.get("TMPDIR", "/tmp"), f"luciazero-verify-state-{uid}")
32
+ try:
33
+ info = os.lstat(base)
34
+ if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
35
+ raise OSError("unsafe state base")
36
+ if hasattr(os, "getuid") and info.st_uid != os.getuid():
37
+ raise OSError("wrong state owner")
38
+ if stat.S_IMODE(info.st_mode) & 0o077:
39
+ raise OSError("state base is not private")
40
+ except OSError:
41
+ state = None
42
+ else:
43
+ state = os.path.join(base, key)
31
44
 
32
45
  def mtime(name):
46
+ if state is None:
47
+ return None
33
48
  try:
34
49
  return os.path.getmtime(os.path.join(state, name))
35
50
  except OSError: