Остання активність 3 weeks ago

Версія e4c8c2763f4359b5fde02c9fe2858d610622fa5a

ELI5.mdx Неформатований
1---
2name: ELI5
3description: keep it simple pls
4keep-coding-instructions: true
5---
6
7It's been a long day and my brain is fried, talk to me like I'm 5.
8
9Small words, short sentences, short paragraphs. If you have to use
10a big word, explain it right after. Only return what's actually necessary.
11
12Just tell me what you did, did it work, what do I do now.
13
14If I have to decide something: 2 options max, the context I need to pick fast,
15and which one you'd go with.
16
17Keep paths and commands exact. I have no brain cells left for the rest.
README.md Неформатований

AI Coding Agent Skill Collection

Quick Install: Grilling Question Tools

Install or update the question-tool preference for Claude Code, Codex, and OpenCode in one command:

bash -o pipefail -c 'curl -fsSL https://opengist.resetrix.work/weehong/9e381dc89dff4670b7232b910454f056/raw/HEAD/install-question-tools.sh | bash'

Trust check: this runs remote code and downloads remote instructions from this gist. Read both before running if you do not already trust the publisher and server. pipefail reports download failures; it is not a security sandbox or a guarantee against executing partially downloaded shell code.

Quick links: installer | authoritative policy | ELI5 writing style

Download and Inspect Instead

Run in a scratch directory. These downloads alone do not install anything:

base=https://opengist.resetrix.work/weehong/9e381dc89dff4670b7232b910454f056
curl -fsSL "$base/raw/HEAD/install-question-tools.sh" -o install-question-tools.sh
curl -fsSL "$base/raw/HEAD/grilling-question-tools.md" -o grilling-question-tools.md
less install-question-tools.sh grilling-question-tools.md
# After inspection:
bash install-question-tools.sh

By default the installer fetches the policy at HEAD, not the inspected local copy. To avoid changes between inspection and installation, get a full commit hash from the gist's Revisions, replace HEAD in both download URLs with that hash, and run QUESTION_TOOLS_REVISION=<same-full-40-character-hash> bash install-question-tools.sh. The revision variable controls the policy download, not the installer download.

Requirements and Scope

  • Linux, macOS, or WSL; Bash 3.2+, Python 3.8+, and curl with HTTPS support.
  • Run as your normal user, without sudo, and with no concurrent installer or editor changing the destination files. Native Windows shells are not supported.
  • This installs instructions only, not the Matt Pocock skills, tools, plugins, or permissions. Install your desired grilling skills separately; upstream skill files are untouched.
  • Only Matt Pocock grilling flows use this preference. The policy retains the flow's pacing and recommendations, custom answers, unresolved decisions, and text fallback when the native question tool is unavailable or restricted.
  • This is not runtime enforcement. Host permissions, mode restrictions, instruction precedence, and agent instruction-loading limits still apply.

To install or overwrite the upstream skills globally for all three hosts:

npx --yes skills@latest add mattpocock/skills --global --agent claude-code codex opencode --skill '*' --yes

This selects every skill, including in-progress and miscellaneous skills. Back up local skill edits before overwriting them. Keep custom preferences outside the skill directories, and avoid installing the Claude plugin alongside these files because that can expose duplicate skills.

Destinations

All three destinations are installed, even if a corresponding CLI is not yet installed. Export overrides before running the quick command so both shells inherit them. Empty variables use defaults; paths must be absolute, without .. components. Spaces are supported.

Agent Default file Override
Claude Code ~/.claude/CLAUDE.md $CLAUDE_CONFIG_DIR/CLAUDE.md
Codex ~/.codex/AGENTS.md $CODEX_HOME/AGENTS.md
OpenCode ~/.config/opencode/AGENTS.md $OPENCODE_CONFIG_DIR/AGENTS.md, otherwise $XDG_CONFIG_HOME/opencode/AGENTS.md
Shared policy ~/.config/ai-agents/grilling-question-tools.md $QUESTION_TOOLS_POLICY_DIR/grilling-question-tools.md, otherwise $XDG_CONFIG_HOME/ai-agents/grilling-question-tools.md

The downloaded authoritative policy is installed as a separate file. Each host gets only a small marked pointer instructing it to read the absolute policy path before grilling and prefer native question delivery over upstream text formatting, while preserving the rest of the workflow. For example, export QUESTION_TOOLS_POLICY_DIR="$HOME/my agent policies" to choose a policy directory. Paths containing newlines or backticks are rejected so the pointer stays unambiguous.

HOME supplies ~. Current OpenCode supports OPENCODE_CONFIG_DIR for global instructions; older versions may only read the XDG location. For an older release, leave OPENCODE_CONFIG_DIR unset and use XDG_CONFIG_HOME instead. See OpenCode instruction loading and global paths. OpenCode normally prefers its own global AGENTS.md over the Claude fallback: creating this file can stop fallback loading of ~/.claude/CLAUDE.md. If you rely on unrelated instructions in that fallback, explicitly configure them in OpenCode's instructions setting before installing. Codex's AGENTS.override.md, if present, can take precedence over AGENTS.md; review it if the new preference is not being loaded.

Updates and Safety

Rerun the quick command to update the separate policy file and managed pointers. Existing managed blocks containing an embedded policy are migrated by replacing only that block with the pointer. Identical reruns leave files unchanged and create no new backups. The downloaded policy bytes are installed verbatim; the installer does not maintain an embedded policy copy.

  • Download errors or an invalid/incomplete policy abort before destination edits. Validation checks UTF-8, versioned start/end markers, title, and all seven numbered rules. These are integrity sanity checks, not a signature.
  • Every destination is preflighted before any directories, backups, or files are created. Malformed or duplicate managed markers abort the entire run.
  • The unique HTML markers are <!-- BEGIN MATT-POCOCK-GRILLING-QUESTION-TOOLS --> and <!-- END MATT-POCOCK-GRILLING-QUESTION-TOOLS -->. Keep them intact.
  • Bytes outside an existing managed block are preserved exactly. A first installation appends a separator and block without changing existing bytes.
  • Each changed existing file gets a byte-for-byte sibling backup named <filename>.question-tools-backup-<unique-suffix> (private mode 0600). New files need no backup. Paths are printed; previous backups are never replaced.
  • Existing file and directory symlinks are resolved to physical targets and preserved. Hosts sharing a symlink target get one pointer and one backup beside the physical target, not three. Dangling/looping symlinks, nonregular files, and inaccessible destinations are rejected. The policy cannot share a host target.
  • Replacements are atomic per file, not a transaction across all destinations. Staging and backups precede replacements, but disk/permission errors during writing can leave directories/backups or a partial install. Inspect the printed paths, restore from backups if needed, and rerun after fixing the cause. File modes are retained; ownership, ACLs, extended attributes and hard-link relationships are not preserved by replacement. Avoid concurrent writers.

After installation, start fresh Claude Code and Codex sessions, and quit and restart OpenCode. Ask the agent which instruction files it loaded if the preference is missing. To uninstall, remove only the managed pointer block from each distinct host target, then remove the separate policy file if no pointers still use it. Restore a backup only if it will not erase newer unrelated edits. Changing the policy directory leaves the previous policy file in place; remove it manually once no pointers use it.

Installer Tests

Run python3 -B -m unittest -v test_install_question_tools from this directory. Tests use temporary homes and a mocked curl, with no network access or real home installation. Permission checks are skipped when running tests as root.

Existing Style

ELI5.mdx is the original short, plain-language writing style. It is preserved unchanged and is not installed by this script.

grilling-question-tools.md Неформатований

Question Tools for Matt Pocock Grilling Flows

Scope: apply only while running Matt Pocock's grilling flows (grill-me, grilling, grill-with-docs, loop-me, or a skill explicitly delegating its interview to one of these). This changes question delivery, not the flow's research, decision criteria, documentation, or completion requirements. Ordinary coding conversations and unrelated skills keep their own behavior.

  1. Research what the codebase can answer before asking the user. The main conversation agent owns the interview; subagents return findings and proposed questions to it rather than independently prompting the user.
  2. Use the host's native interactive question tool when exposed and permitted in the current session: Claude Code AskUserQuestion, Codex request_user_input, or OpenCode question. Follow its actual schema and mode/permission restrictions. Do not invent a tool, change modes merely to unlock one, or bypass a restriction. In Codex, request_user_input may be limited to Plan mode; use the fallback when it is unavailable.
  3. Preserve the originating flow's round size and pacing. If it calls for one question at a time, ask one. If it allows a round of independent questions, batch only those that can be answered without knowing another answer. Wait for each answer before formulating dependent follow-ups. Split rounds across sequential tool calls when question, option, or text limits require it; retain every decision rather than silently dropping questions.
  4. Give enough context to decide, concrete options with tradeoffs, and your recommendation with a short reason when you have one. Mark the recommended option clearly. Keep choices neutral enough to disagree with; never invent a recommendation just to fill the UI. Put context outside the tool if its field limits would lose necessary detail.
  5. Preserve custom responses: use the tool's built-in free-text/Other facility where available. If it cannot accept a needed custom answer, invite a plain text reply instead. Do not force an open-ended question into false choices or interpret Other as agreement with a listed option.
  6. Treat skipped, dismissed, empty, or ambiguous answers as unresolved, not consent and not acceptance of the recommendation. Clarify a blocking gap or explicitly defer it with the user's agreement. Track unresolved decisions and keep them out of the confirmed decisions in any spec, ADR, or summary.
  7. If the native tool is absent, disallowed, fails, or cannot express the question faithfully, ask in plain text and wait. Use the same round rules, numbered questions/options, recommendation and reason, and an explicit invitation to give a custom answer. Resume the native tool when usable.

This is an instruction preference, not runtime enforcement. Higher-priority instructions and host restrictions still apply. Finish only when the original flow's completion criteria are met; changing the UI does not satisfy them.

install-question-tools.sh Неформатований
1#!/usr/bin/env bash
2# Requires Bash 3.2+, Python 3.8+, and curl. No sudo; run one installer at a time.
3set -euo pipefail
4
5# Defining the complete function before calling it also avoids starting an
6# installation from a download truncated inside the embedded Python program.
7main() {
8 command -v python3 >/dev/null || { printf '%s\n' 'Python 3.8+ is required.' >&2; return 1; }
9 command -v curl >/dev/null || { printf '%s\n' 'curl is required.' >&2; return 1; }
10 python3 - <<'PY'
11import os
12import re
13import stat
14import subprocess
15import sys
16import tempfile
17from pathlib import Path
18
19BEGIN = b'<!-- BEGIN MATT-POCOCK-GRILLING-QUESTION-TOOLS -->'
20END = b'<!-- END MATT-POCOCK-GRILLING-QUESTION-TOOLS -->'
21TOKEN = b'MATT-POCOCK-GRILLING-QUESTION-TOOLS'
22POLICY_BEGIN = '<!-- BEGIN MATT-POCOCK-GRILLING-QUESTION-POLICY v1 -->'
23POLICY_END = '<!-- END MATT-POCOCK-GRILLING-QUESTION-POLICY v1 -->'
24BASE = 'https://opengist.resetrix.work/weehong/9e381dc89dff4670b7232b910454f056'
25
26
27def resolve_target(path):
28 # Resolve existing components strictly, but allow new files/directories.
29 target = Path(path.anchor)
30 for part in path.parts[1:]:
31 target = target / part
32 if target.is_symlink():
33 try:
34 target = target.resolve(strict=True)
35 except (OSError, RuntimeError) as error:
36 raise ValueError('Dangling or unresolvable symlink: %s' % target) from error
37 return target
38
39
40def check_path(path):
41 for part in [path] + list(path.parents):
42 if part.is_symlink():
43 raise ValueError('Path changed to a symlink during installation: %s' % part)
44 if part.exists() and not (part.is_file() if part == path else part.is_dir()):
45 raise ValueError('Not a regular file/directory: %s' % part)
46 parent = path.parent
47 while not parent.exists():
48 parent = parent.parent
49 if not os.access(parent, os.W_OK | os.X_OK):
50 raise ValueError('Directory is not writable/searchable: %s' % parent)
51 if path.exists() and not os.access(path, os.R_OK | os.W_OK):
52 raise ValueError('Instruction file is not readable/writable: %s' % path)
53
54
55def install():
56 if sys.version_info < (3, 8):
57 raise ValueError('Python 3.8+ is required.')
58 revision = os.environ.get('QUESTION_TOOLS_REVISION') or 'HEAD'
59 if revision != 'HEAD' and not re.fullmatch(r'[0-9a-fA-F]{40}', revision):
60 raise ValueError('QUESTION_TOOLS_REVISION must be HEAD or a full 40-character commit hash.')
61 url = BASE + '/raw/' + revision + '/grilling-question-tools.md'
62 result = subprocess.run(['curl', '-fsSL', '--proto', '=https', '--proto-redir', '=https',
63 '--connect-timeout', '15', '--max-time', '60', url],
64 stdout=subprocess.PIPE, check=True)
65 policy = result.stdout.decode('utf-8')
66 lines = policy.splitlines()
67 if (not lines or lines[0] != POLICY_BEGIN or lines[-1] != POLICY_END
68 or policy.count(POLICY_BEGIN) != 1 or policy.count(POLICY_END) != 1
69 or len(policy) > 32768 or '\x00' in policy
70 or TOKEN.decode() in policy
71 or not policy.startswith(POLICY_BEGIN + '\n# Question Tools for Matt Pocock Grilling Flows\n')
72 or not all('\n%d. ' % i in policy for i in range(1, 8))):
73 raise ValueError('Downloaded policy is incomplete or invalid; no instruction files changed.')
74 home = os.environ.get('HOME')
75 if not home or not os.path.isabs(home):
76 raise ValueError('HOME must be an absolute path.')
77 config = os.environ.get('XDG_CONFIG_HOME') or home + '/.config'
78 policy_dir = os.environ.get('QUESTION_TOOLS_POLICY_DIR') or config + '/ai-agents'
79 directories = [
80 os.environ.get('CLAUDE_CONFIG_DIR') or home + '/.claude',
81 os.environ.get('CODEX_HOME') or home + '/.codex',
82 os.environ.get('OPENCODE_CONFIG_DIR') or
83 config + '/opencode',
84 ]
85 if any(not os.path.isabs(d) or '..' in Path(d).parts or any(c in d for c in '\r\n`')
86 for d in directories + [policy_dir]):
87 raise ValueError('Config directories must be absolute paths without .. components.')
88 policy_path = resolve_target(Path(policy_dir) / 'grilling-question-tools.md')
89 if any(c in str(policy_path) for c in '\r\n`'):
90 raise ValueError('Policy path cannot contain newlines or backticks.')
91 block = (BEGIN + b'\n' + (
92 'Before running a Matt Pocock grilling flow (grill-me, grilling, grill-with-docs,\n'
93 'loop-me, or an interview delegated to one of them), read `%s`.\n'
94 'Use its native question delivery preference over upstream text formatting;\n'
95 'preserve all other workflow requirements.\n' % policy_path
96 ).encode('utf-8') + END)
97 paths = list(dict.fromkeys(resolve_target(Path(d) / name) for d, name in
98 zip(directories, ['CLAUDE.md', 'AGENTS.md', 'AGENTS.md'])))
99 if policy_path in paths or (policy_path.exists() and any(
100 p.exists() and os.path.samefile(policy_path, p) for p in paths)):
101 raise ValueError('Policy and instruction files must be separate files.')
102 changes = []
103 # Preflight every destination before creating directories, backups, or files.
104 for path in [policy_path] + paths:
105 check_path(path)
106 exists = path.exists()
107 old = path.read_bytes() if exists else b''
108 if path == policy_path:
109 new = result.stdout
110 elif TOKEN in old:
111 if old.count(TOKEN) != 2 or old.count(BEGIN) != 1 or old.count(END) != 1:
112 raise ValueError('Malformed/duplicate managed markers: %s. Repair them manually before rerunning.' % path)
113 start, end = old.index(BEGIN), old.index(END)
114 after = end + len(END)
115 if (end < start or (start and old[start-1:start] != b'\n')
116 or old[end-1:end] != b'\n'
117 or (old[after:] and not old[after:].startswith((b'\n', b'\r\n')))
118 or not old[start+len(BEGIN):].startswith((b'\n', b'\r\n'))):
119 raise ValueError('Malformed managed marker boundaries: %s' % path)
120 new = old[:start] + block + old[after:]
121 else:
122 separator = (b'\n' if old.endswith(b'\n') else b'\n\n') if old else b''
123 new = old + separator + block + b'\n'
124 if new != old:
125 changes.append((path, exists, old, new))
126 else:
127 print('Unchanged: %s' % path)
128
129 staged = []
130 try:
131 for path, exists, old, new in changes:
132 check_path(path)
133 if path.exists() != exists or (exists and path.read_bytes() != old):
134 raise ValueError('File changed during installation: %s. Retry with other writers stopped.' % path)
135 path.parent.mkdir(parents=True, exist_ok=True)
136 mode = stat.S_IMODE(path.stat().st_mode) if exists else 0o600
137 fd, temporary = tempfile.mkstemp(prefix='.' + path.name + '.question-tools-', dir=str(path.parent))
138 staged.append((path, temporary))
139 with os.fdopen(fd, 'wb') as stream:
140 stream.write(new)
141 stream.flush()
142 os.fsync(stream.fileno())
143 os.fchmod(stream.fileno(), mode)
144 if exists:
145 fd, backup = tempfile.mkstemp(prefix=path.name + '.question-tools-backup-', dir=str(path.parent))
146 with os.fdopen(fd, 'wb') as stream:
147 stream.write(old)
148 stream.flush()
149 os.fsync(stream.fileno())
150 print('Backup: %s' % backup)
151 for path, temporary in staged:
152 os.replace(temporary, path)
153 print('Updated: %s' % path)
154 finally:
155 for _, temporary in staged:
156 if os.path.exists(temporary):
157 os.unlink(temporary)
158 print('Policy source: %s' % url)
159 print('Start fresh Claude Code/Codex sessions; quit and restart OpenCode.')
160
161
162try:
163 install()
164except (OSError, ValueError, subprocess.SubprocessError) as error:
165 print('Installation failed: %s' % error, file=sys.stderr)
166 sys.exit(1)
167PY
168}
169
170main "$@"
171
test_install_question_tools.py Неформатований
1"""Sandbox-only installer tests; curl never accesses the network."""
2import os
3from pathlib import Path
4import subprocess
5import tempfile
6import unittest
7
8
9ROOT = Path(__file__).resolve().parent
10INSTALLER = ROOT / 'install-question-tools.sh'
11POLICY = (ROOT / 'grilling-question-tools.md').read_bytes()
12BEGIN = b'<!-- BEGIN MATT-POCOCK-GRILLING-QUESTION-TOOLS -->'
13END = b'<!-- END MATT-POCOCK-GRILLING-QUESTION-TOOLS -->'
14
15
16class InstallerTests(unittest.TestCase):
17 def setUp(self):
18 self.temp = tempfile.TemporaryDirectory(prefix='question-tools-test-')
19 self.addCleanup(self.temp.cleanup)
20 self.root = Path(self.temp.name)
21 self.home = self.root / 'home'
22 self.home.mkdir()
23 self.bin = self.root / 'bin'
24 self.bin.mkdir()
25 self.fixture = self.root / 'policy.md'
26 self.fixture.write_bytes(POLICY)
27 curl = self.bin / 'curl'
28 curl.write_text(
29 '#!/usr/bin/env python3\n'
30 'import os, pathlib, sys\n'
31 'pathlib.Path(os.environ["CURL_LOG"]).write_text(sys.argv[-1])\n'
32 'if os.environ.get("CURL_FAIL"): sys.exit(22)\n'
33 'sys.stdout.buffer.write(pathlib.Path(os.environ["POLICY_FIXTURE"]).read_bytes())\n'
34 )
35 curl.chmod(0o700)
36 self.env = {key: value for key, value in os.environ.items() if key not in (
37 'HOME', 'XDG_CONFIG_HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME',
38 'OPENCODE_CONFIG_DIR', 'QUESTION_TOOLS_POLICY_DIR', 'QUESTION_TOOLS_REVISION')}
39 self.env.update(HOME=str(self.home), PATH=str(self.bin) + ':' + os.environ['PATH'],
40 POLICY_FIXTURE=str(self.fixture), CURL_LOG=str(self.root / 'curl.log'))
41 self.hosts = [self.home / '.claude/CLAUDE.md', self.home / '.codex/AGENTS.md',
42 self.home / '.config/opencode/AGENTS.md']
43 self.policy = self.home / '.config/ai-agents/grilling-question-tools.md'
44
45 def run_install(self, success=True):
46 result = subprocess.run(['bash', str(INSTALLER)], env=self.env,
47 cwd=self.root, capture_output=True, text=True)
48 self.assertEqual(result.returncode == 0, success, result.stdout + result.stderr)
49 return result
50
51 def write_hosts(self, data=b'Unrelated\x80 instructions\r\n'):
52 for host in self.hosts:
53 host.parent.mkdir(parents=True, exist_ok=True)
54 host.write_bytes(data)
55
56 def snapshot(self):
57 return {str(p.relative_to(self.home)): (
58 ('link', os.readlink(p)) if p.is_symlink() else
59 ('file', p.read_bytes(), p.stat().st_mode, p.stat().st_mtime_ns) if p.is_file()
60 else ('directory',)) for p in self.home.rglob('*')}
61
62 def test_regular_hosts_separate_policy_and_unchanged_rerun(self):
63 original = b'Unrelated\x80 instructions\r\n'
64 self.write_hosts(original)
65 self.hosts[0].chmod(0o640)
66 self.run_install()
67 self.assertEqual(self.policy.read_bytes(), POLICY)
68 for host in self.hosts:
69 content = host.read_bytes()
70 self.assertTrue(content.startswith(original))
71 self.assertEqual(content.count(BEGIN), 1)
72 self.assertIn(str(self.policy).encode(), content)
73 self.assertNotIn(POLICY, content)
74 backups = list(host.parent.glob(host.name + '.question-tools-backup-*'))
75 self.assertEqual(len(backups), 1)
76 self.assertEqual(backups[0].read_bytes(), original)
77 self.assertEqual(backups[0].stat().st_mode & 0o777, 0o600)
78 self.assertEqual(self.hosts[0].stat().st_mode & 0o777, 0o640)
79 before = self.snapshot()
80 self.run_install()
81 self.assertEqual(self.snapshot(), before)
82
83 def test_shared_symlink_target_and_migration(self):
84 target = self.home / 'shared.md'
85 prefix, suffix = b'Existing\xff\r\n\n', b'\r\nAfter\x80'
86 original = prefix + BEGIN + b'\n' + POLICY + END + suffix
87 target.write_bytes(original)
88 for host in self.hosts:
89 host.parent.mkdir(parents=True, exist_ok=True)
90 host.symlink_to(os.path.relpath(target, host.parent))
91 links = [os.readlink(p) for p in self.hosts]
92 self.run_install()
93 content = target.read_bytes()
94 self.assertTrue(content.startswith(prefix + BEGIN))
95 self.assertTrue(content.endswith(END + suffix))
96 self.assertEqual(content.count(BEGIN), 1)
97 self.assertNotIn(POLICY, content)
98 self.assertEqual([os.readlink(p) for p in self.hosts], links)
99 backups = list(self.home.rglob('*.question-tools-backup-*'))
100 self.assertEqual(len(backups), 1)
101 self.assertEqual(backups[0].read_bytes(), original)
102 before = self.snapshot()
103 self.run_install()
104 self.assertEqual(self.snapshot(), before)
105
106 def test_malformed_blocks_preflight_no_writes(self):
107 for malformed in (BEGIN, END + b'\n' + BEGIN, BEGIN + b'\nx\n' + END + END,
108 b'prefix' + BEGIN + b'\nx\n' + END, BEGIN + b'x\n' + END):
109 with self.subTest(malformed=malformed):
110 self.write_hosts()
111 self.hosts[-1].write_bytes(malformed)
112 before = self.snapshot()
113 self.run_install(False)
114 self.assertEqual(self.snapshot(), before)
115
116 def test_download_failure_and_invalid_policy_no_writes(self):
117 self.write_hosts()
118 before = self.snapshot()
119 self.env['CURL_FAIL'] = '1'
120 self.run_install(False)
121 self.assertEqual(self.snapshot(), before)
122 del self.env['CURL_FAIL']
123 self.fixture.write_bytes(b'incomplete')
124 self.run_install(False)
125 self.assertEqual(self.snapshot(), before)
126
127 def test_dangling_file_and_directory_symlinks(self):
128 for path in (self.hosts[-1], self.hosts[-1].parent):
129 with self.subTest(path=path):
130 path.parent.mkdir(parents=True, exist_ok=True)
131 path.symlink_to(self.home / 'missing')
132 before = self.snapshot()
133 self.run_install(False)
134 self.assertEqual(self.snapshot(), before)
135 path.unlink()
136 if path == self.hosts[-1]:
137 path.parent.rmdir()
138
139 def test_nonregular_destination(self):
140 self.write_hosts()
141 self.hosts[-1].unlink()
142 os.mkfifo(self.hosts[-1])
143 self.run_install(False)
144 self.assertFalse(self.policy.exists())
145 self.assertEqual(list(self.home.rglob('*.question-tools-backup-*')), [])
146
147 @unittest.skipIf(os.geteuid() == 0, 'root bypasses filesystem permissions')
148 def test_permission_failure_preflight(self):
149 self.write_hosts()
150 self.hosts[-1].chmod(0o400)
151 before = self.snapshot()
152 self.run_install(False)
153 self.assertEqual(self.snapshot(), before)
154
155 def test_overrides_revision_and_directory_symlink(self):
156 config = self.home / 'config space'
157 config.mkdir()
158 (self.home / 'linked').symlink_to(config, target_is_directory=True)
159 self.env['XDG_CONFIG_HOME'] = str(self.home / 'linked')
160 self.env['QUESTION_TOOLS_REVISION'] = 'a' * 40
161 self.run_install()
162 policy = config / 'ai-agents/grilling-question-tools.md'
163 self.assertEqual(policy.read_bytes(), POLICY)
164 self.assertTrue((self.home / 'linked').is_symlink())
165 self.assertIn(str(policy).encode(), self.hosts[0].read_bytes())
166 self.assertTrue((self.root / 'curl.log').read_text().endswith('/raw/' + 'a' * 40 + '/grilling-question-tools.md'))
167 self.env['QUESTION_TOOLS_POLICY_DIR'] = str(self.home / 'custom policy')
168 self.run_install()
169 self.assertEqual((self.home / 'custom policy/grilling-question-tools.md').read_bytes(), POLICY)
170
171 def test_policy_cannot_alias_host(self):
172 self.write_hosts()
173 self.policy.parent.mkdir(parents=True, exist_ok=True)
174 self.policy.symlink_to(self.hosts[0])
175 before = self.snapshot()
176 self.run_install(False)
177 self.assertEqual(self.snapshot(), before)
178
179 def test_eli5_is_unchanged_and_not_an_input(self):
180 eli5 = ROOT / 'ELI5.mdx'
181 before = eli5.read_bytes()
182 self.run_install()
183 self.assertEqual(eli5.read_bytes(), before)
184 self.assertNotIn('ELI5', INSTALLER.read_text())
185 self.assertEqual((self.root / 'curl.log').read_text(),
186 'https://opengist.resetrix.work/weehong/9e381dc89dff4670b7232b910454f056/raw/HEAD/grilling-question-tools.md')
187
188
189if __name__ == '__main__':
190 unittest.main()
191