Orchestrata Ultimate › Plans › briefs › qwen › Q2a-snapshot-argv-capture
Q2a — The AST snapshot records literal program names at process-spawn sites
Source: plans/briefs/qwen/Q2a-snapshot-argv-capture.md at 8a2cb42ad · last changed 2026-10-11 · first committed 2026-10-11 · 490 lines.
Prose pending.
Plan text
Q2a — The AST snapshot records literal program names at process-spawn sites
| Field | Value |
|---|---|
| Executor | Qwen3.8-max (openrouter/qwen/qwen3.8-max-0902) via opencode run |
| Worktree | /home/mohamed/workspace/wt-ARGV-GATE — work ONLY inside this directory |
| Branch | feat/bare-toolchain-gate, base origin/master @ 762935ab6 |
| Spec author | orchestrator, 2026-10-11 |
| Follow-on tasks (NOT yours) | Q2b: Prolog facts + bare_toolchain rule + INVALID guard. Q3: fix every site Q2b reports. |
| Your deliverable | code + tests + regenerated snapshot, uncommitted but staged, plus plans/briefs/qwen/Q2a-report.md |
1. Directive and why this task exists
Mohamed's directive (verbatim): "fix bare python make it prolog and atoms".
A bare interpreter or compiler name in a spawned command (subprocess.run(["python", "x.py"]),
os.system("gcc -c a.c")) is resolved from PATH at runtime. Measured on this host on 2026-10-11:
- The agent session's
PATHbegins with/home/mohamed/workspace/orchestrata-ultimate-architecture/.venv/bin(a stale checkout ~530 commits behind master). Runningpython -c 'import orchestrata_ultimate as o; print(o.__file__)'from inside a worktree printed/home/mohamed/workspace/orchestrata-ultimate-architecture/src/..., so a barepythonin any spawned command silently executes another checkout's stale code. gcconPATHis Guix GCC 16.2.0 (~/.guix-profile/bin/gcc), which fails the janus-swi native build. The system compiler is/usr/bin/gcc(GCC 14.2.1).
The fix is enforced in Prolog: the gate (scripts/gate.py run) derives violations from facts extracted from
the AST snapshot (plans/ast-ultimate.json). Rule REQ-GATE-001: the fact extractor never opens a .py file;
every structural fact comes from the snapshot. Today the snapshot records calls only as bare callee strings,
without arguments, so the program literal is unrecoverable. This is the same gap EnvAccessInfo closed for
environment keys (read it in §3.1; you are building its sibling).
Your job: the snapshot records facts; it never classifies them. Do NOT write any list of "bad" program
names (python, gcc, …) in Python. Which programs count as bare toolchain invocations is decided by Prolog
atoms in Q2b. You record every literal program name at a spawn site, verbatim.
2. Rules you must follow (project law — violations are rejected)
- Interpreter. Never run bare
python,python3,pytestorpip. Always run, from the worktree root:env -u PYTHONPATH .venv/bin/python .... (Barepythonresolves to the stale checkout described above.) - uv only, no new dependencies, no edits to
pyproject.toml/uv.lock. - Tests: no
unittest.mock,Mock,MagicMock,patch,monkeypatch, no Fake/Stub/Dummy classes, noskip/skipif/xfail, nopytest-xdist. Function-scope fixtures only; usetmp_pathfor files. Parse real source text with the realCodebaseAnalyzer. - Absolute imports only. Never
from . import x. - Language: never write the word "should" in code, comments or docstrings. State facts.
- Environment access: never
os.environ["X"]; useos.environ.get("X", default)(you do not need env access here). - Git: do NOT commit, push, merge, rebase, reset, stash, or check out another branch. In step 8 you
git addthe files you changed so the review gate can run; the orchestrator reviews and commits. - Scope: change ONLY the files in §5. If another file must change, STOP and report why.
- Isolation: never read or write outside
/home/mohamed/workspace/wt-ARGV-GATEexcept reading/home/mohamed/.cache/uv. Other worktrees hold other agents' live work. - Infrastructure: never connect to
10.53.1.x(production). Tests pin themselves to the test fleet10.53.2.xviatests/conftest.py; do not override any endpoint. - Test runs: never run the full suite. Never run
scripts/run_tests.pywithout explicit test paths. Never use-n/xdist. Use exactly the commands in §7 and §8. - Disagreement: if the code contradicts this spec, or a design choice in §4 looks wrong, STOP and write the reason in your report. Do not redesign.
3. Context you need (filtered; verbatim from origin/master @ 762935ab6)
To read anything else, use the AST toolkit (a warm daemon is already running for this worktree):
env -u PYTHONPATH .venv/bin/python scripts/serialize_codebase_ast.py source <Name or module.Class.method>.
Opening the files you edit is fine.
3.1 The precedent you mirror — src/orchestrata_ast/models.py:136-163
@dataclass
class EnvAccessInfo:
"""One read of the process environment, located.
WHY THIS IS A SNAPSHOT FACT. `os.environ.get("X", default)` appears in the
`calls` list as the bare string `os.environ.get`, usually without its
arguments, and bare `os.environ["X"]` is a SUBSCRIPT that never appears in
`calls` at all. So neither the KEY nor the FORM is recoverable from the
snapshot as it stood, while RUL-003 is stated entirely in terms of both:
...
`key` is the literal env var name, or "" when the subscript/argument is a
computed expression rather than a literal — an unknown key is reported as
unknown rather than guessed.
"""
file_path: str
line: int
key: str
form: str
has_default: bool = False
3.2 ModuleInfo (models.py:166-182) and CodebaseAST (models.py:185-203)
@dataclass
class ModuleInfo:
path: str
file_path: str
line_count: int = 0
classes: list[ClassInfo] = field(default_factory=list)
functions: list[FunctionInfo] = field(default_factory=list)
imports: list[ImportInfo] = field(default_factory=list)
module_vars: list[ClassVarInfo] = field(default_factory=list)
docstring: str | None = None
all_exports: list[str] = field(default_factory=list) # __all__ contents
# Every environment read in the module, at any nesting depth — see
# EnvAccessInfo for why `calls` cannot answer this.
env_accesses: list[EnvAccessInfo] = field(default_factory=list)
residue_hash: str = ""
@dataclass
class CodebaseAST:
name: str
root_path: str
modules: list[ModuleInfo] = field(default_factory=list)
inheritance_graph: dict[str, list[str]] = field(default_factory=dict)
dependency_graph: dict[str, list[str]] = field(default_factory=dict)
total_classes: int = 0
total_functions: int = 0
total_methods: int = 0
total_loc: int = 0
extra_roots: list[str] = field(default_factory=list)
3.3 Where the precedent is wired — src/orchestrata_ast/analyzer.py
In CodebaseAnalyzer._analyze_file (analyzer.py:273-330), after the top-level loop:
# Whole-tree, not the top-level loop above: an env read inside a
# function body is exactly as much a RUL-003 subject as one at module
# scope, and the loop above only walks direct children of the Module.
module.env_accesses = self._analyze_env_accesses(tree, str(file_path))
CodebaseAnalyzer._analyze_env_accesses (analyzer.py:667-761) is a @staticmethod that walks the WHOLE tree
with ast.walk, records only literal keys, and returns a list. Read it in full before writing your function;
match its style (docstring explaining WHY, inline comments with measured reasons).
_DATACLASS_LIST_FIELDS (analyzer.py:~983). Its entries rebuild nested dataclass lists on deserialize:
_DATACLASS_LIST_FIELDS: dict[type, dict[str, type]] = {
FunctionInfo: {"parameters": ParamInfo},
ClassInfo: {"methods": FunctionInfo, "class_vars": ClassVarInfo},
ModuleInfo: {
"classes": ClassInfo,
"functions": FunctionInfo,
"imports": ImportInfo,
"module_vars": ClassVarInfo,
"env_accesses": EnvAccessInfo,
},
}
_dataclass_from_dict (analyzer.py:988-1007) drops unknown keys and lets missing keys take their defaults.
That is the hazard in §4.4: a cached module dict without argv_literals deserializes as argv_literals=[],
indistinguishable from "captured, found none".
3.4 Warm start — load_or_analyze (analyzer.py:1115-1232)
The snapshot is regenerated INCREMENTALLY: unchanged files reuse their cached module dict verbatim. Identity checks that force a full reparse today:
if cached.get("name") != name or not same_root:
return _full_fallback()
# The set of trees is part of the cache's identity, not a detail of how it
# was built. ...
if list(cached.get("extra_roots", [])) != [str(extra) for extra in extra_roots]:
return _full_fallback()
file_meta = cached.get("file_meta")
if not isinstance(file_meta, dict):
return _full_fallback()
...
if (
isinstance(prior, dict)
and prior.get("sha1") == sha1
and prior_dict is not None
):
live_modules.append(module_info_from_dict(prior_dict))
stats["reused"].append(module_path)
stats["cached_modules"][module_path] = prior_dict
...
analyzer.result = CodebaseAST(
name=name,
root_path=str(root),
modules=live_modules,
extra_roots=[str(extra) for extra in extra_roots],
)
And scripts/serialize_codebase_ast.py::_cmd_serialize (:12343-12426) writes the cached raw dict for every
reused module (out_modules.append(cached_modules[mpath])). So after your model change, an incremental run with no
identity change would write every unchanged module WITHOUT the argv_literals key. §4.4 closes this.
3.5 The tests projection — derive_subtree (analyzer.py:1016-1112)
plans/ast-tests.json is derived from the wide snapshot. It builds a NEW CodebaseAST(name=..., root_path=..., modules=[...]), serializes it, then replaces derived["modules"] with the projected dicts. Any new top-level field
on CodebaseAST is therefore reset to its default in the projection unless copied from the source snapshot.
3.6 How the snapshot is produced
The entry point is the ast-sync console script (pyproject.toml: ast-sync = "orchestrata_ultimate.tools.ast_pipeline:main");
run env -u PYTHONPATH uv run --frozen ast-sync --snapshot-only from the worktree root. It calls
src/orchestrata_ultimate/tools/ast_pipeline.py::main, which runs
scripts/serialize_codebase_ast.py serialize --source <6 trees> --name orchestrata-ultimate --output plans/ast-ultimate.json --incremental
and then derives plans/ast-tests.json with derive_subtree. Trees (ast_pipeline.EXTRA_SOURCE_TREES):
src/orchestrata_ultimate (primary), tests, scripts, src/orchestrata_ast, src/orchestrata_tools, claude.
The run prints Incremental: fallback (full reparse) or Incremental: warm — reused N, reparsed M, removed K.
3.7 Consumers of the snapshot's module dicts (must keep working; their tests are in §8)
src/orchestrata_ultimate/gates/fact_extractor.py::FactExtractor._module_level_facts(readsmod.get("env_accesses", [])).tests/gates/test_gate_rules.py:214,tests/gates/test_gate_explain.py:68build module dicts by hand.src/orchestrata_tools/parser/json_loader.py::JSONASTLoader._parse_modulepicks named keys (tolerates new keys — verified).scripts/ast_daemon.py,scripts/ast_live_sync.py,scripts/code_intel.pyread the snapshot.
3.8 Test precedents to read before writing tests
tests/test_orchestrata_ast_env_access.py— header docstring (WHY + "Test Standards Compliance" block), a module-levelSAMPLEsource string, parsed through the real analyzer on atmp_pathfile. Copy this shape.tests/scripts/test_ast_warm_cache.py::test_load_or_analyze_fallback_reasons(line 412) and::test_incremental_serialize_matches_full_on_unchanged_tree(line 465) — how warm-start fallbacks are tested.
4. Design (decided — implement exactly)
4.1 Model — src/orchestrata_ast/models.py
Add directly after EnvAccessInfo:
@dataclass
class ArgvLiteralInfo:
"""<docstring: WHY THIS IS A SNAPSHOT FACT — calls carry no arguments, so the
program a spawn runs is unrecoverable; Q2b's Prolog rule decides which
programs are bare toolchain names; this records every literal program
verbatim and classifies nothing. Include the forms table from §4.2.>"""
file_path: str
line: int # line of the LITERAL that names the program (where a fix edits)
site: str # dotted callee text of the spawning call, e.g. "subprocess.run", "_run", "os.system"
form: str # "argv" | "exec" | "shell"
program: str # the literal program token, verbatim: "python3", "gcc", "/usr/bin/gcc", "uv", "env"
Add to ModuleInfo, directly after env_accesses, with a two-line comment pointing at ArgvLiteralInfo:
argv_literals: list[ArgvLiteralInfo] = field(default_factory=list)
Add to CodebaseAST, after extra_roots, with a comment explaining §4.4:
captures: list[str] = field(default_factory=list)
4.2 Capture — CodebaseAnalyzer._analyze_argv_literals(tree, file_path) -> list[ArgvLiteralInfo]
A @staticmethod, placed directly after _analyze_env_accesses. Literals only: a head that is not an
ast.Constant of type str is never recorded (computed heads such as sys.executable, PY, [*PY, ...],
argv parameters are skipped, not guessed).
Callee text (site): render call.func as a dotted chain: Name gives its id; Attribute gives
<render(value)>.<attr>; any other node in the chain renders as ? (e.g. factory().run gives ?.run).
The last segment is the text after the final ..
| Form | AST shape that is captured | program |
line |
|---|---|---|---|
argv (direct) |
any Call (any callee) whose first positional argument — or, when it has no positional arguments, its keyword args, cmd, command or argv — is a List/Tuple whose element 0 is a str Constant, OR a BinOp with op=Add whose left is such a List/Tuple |
element 0, verbatim | the List/Tuple node's lineno |
argv (named) |
inside ONE scope: an Assign with a single Name target (or AnnAssign with a Name target and a value) whose value is a List/Tuple (or BinOp Add with such a left) with a str Constant element 0, AND a Call in the same scope whose first positional argument (or keyword args/cmd/command/argv) is a Name with that id |
element 0, verbatim | the assigned List/Tuple node's lineno |
exec |
a Call whose last segment is create_subprocess_exec, or whose dotted text is os.execv, os.execve, os.execvp, os.execvpe, os.execl, os.execle, os.execlp, os.execlpe — first positional arg is a str Constant. For os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe the program is the SECOND positional arg (the first is the mode). |
that string, verbatim | the string Constant's lineno |
shell |
a Call whose dotted text is os.system or os.popen, or whose last segment is getoutput, getstatusoutput, create_subprocess_shell, run, Popen, call, check_call or check_output and whose base text is subprocess or asyncio (create_subprocess_shell only) — and whose first positional argument is a str Constant, or a JoinedStr whose values[0] is a str Constant containing whitespace |
the first whitespace-separated token of that string (s.strip().split()[0]), verbatim; skip if empty |
the string node's lineno |
Scope for the named form: the module body, or one function body (FunctionDef/AsyncFunctionDef); a nested
function or class body is its own scope and is NOT searched from the outer one. Walk each scope's statements with
ast.walk but do not descend into nested FunctionDef/AsyncFunctionDef/ClassDef/Lambda nodes (they are
scopes of their own; Lambda bodies are expressions and are visited as part of their own walk). Emit ONE record
per assigned literal that at least one call in the same scope consumes; site is the callee of the FIRST consuming
call in source order (lineno, then col_offset).
Not captured (by design): a literal list that no call consumes (e.g. LANGS = ("python", "bash") never
passed to a call — a REPL language table is not a spawn); a str first argument to a call outside the shell
row; any non-literal head; a Starred element 0.
Over-capture is intended. The argv rows match ANY callee, so sorted(["python", "b"]) or
frozenset(("gcc", "cc")) IS recorded (site="sorted" / "frozenset"). That is deliberate: a project wrapper
such as _run([...]) or self._spawn([...]) is a spawn that no fixed callee list can name, so the snapshot
records every literal-headed argument list together with its callee, and Q2b's Prolog rule decides which
callees spawn processes. Do NOT add a callee allowlist or denylist for the argv rows.
Determinism: return records sorted by (line, site, form, program) and de-duplicated on that 4-tuple.
The snapshot is committed; unstable ordering creates churn.
Known limitation to state in the docstring: a shell string prefixed by env ("env -u X python y") records
env. Looking past env is a later refinement (Q2b may handle it in Prolog); do not do it here.
4.3 Wiring — src/orchestrata_ast/analyzer.py
- Import
ArgvLiteralInfoin the existingfrom orchestrata_ast.models import (...)block. - In
_analyze_file, directly aftermodule.env_accesses = ..., addmodule.argv_literals = self._analyze_argv_literals(tree, str(file_path))with a comment of the same kind. - In
_DATACLASS_LIST_FIELDS[ModuleInfo]add"argv_literals": ArgvLiteralInfo.
4.4 Capture identity (closes the half-captured-snapshot hole)
- In
analyzer.py, near_DATACLASS_LIST_FIELDS, add a documented module constant:SNAPSHOT_CAPTURES: tuple[str, ...] = ("env_accesses", "argv_literals")— the per-module capture fields this analyzer emits. The comment states: adding a capture field means adding its name here; the name is part of the warm-start cache identity, so an old snapshot forces one full reparse instead of reusing module dicts that lack the field (which would deserialize as[]and read as "captured, none found"). - Every
CodebaseASTthe analyzer produces carriescaptures=list(SNAPSHOT_CAPTURES): inCodebaseAnalyzer.analyze()(find where it builds the result) and inload_or_analyze(theCodebaseAST(...)shown in §3.4). - In
load_or_analyze, directly after theextra_rootsidentity check, add:if list(cached.get("captures", [])) != list(SNAPSHOT_CAPTURES): return _full_fallback()with a comment stating the hazard and the date 2026-10-11. - In
derive_subtree, afterderived = cast(...serialize(analyzer.result)), setderived["captures"] = list(snapshot.get("captures", []))— the projection reports what the source snapshot captured, never more.
4.5 What you do NOT change
src/orchestrata_ultimate/gates/*, src/orchestrata_ultimate/prolog/*, scripts/gate.py, any fact or rule —
that is Q2b. No site fixes — that is Q3.
5. Files you may change or create
| File | Change |
|---|---|
src/orchestrata_ast/models.py |
§4.1 |
src/orchestrata_ast/analyzer.py |
§4.2–§4.4 |
tests/test_orchestrata_ast_argv_literals.py |
NEW — §6 |
plans/ast-ultimate.json, plans/ast-tests.json |
regenerated by §7 commands only; never hand-edited |
plans/briefs/qwen/Q2a-report.md |
NEW — your report (§9) |
6. Tests — tests/test_orchestrata_ast_argv_literals.py
Shape: copy tests/test_orchestrata_ast_env_access.py — a header docstring (WHY these facts exist; the Test
Standards Compliance block: "T2 no os.environ mutation — sources are PARSED, never executed; T3 function-scope
fixtures — tmp_path only; no mocking / no stubs / no monkeypatch / no skips"). Each test writes source text to a
file under tmp_path, runs the real CodebaseAnalyzer over it, and asserts on module.argv_literals.
Assert the FULL record (file_path suffix, line, site, form, program), not just the count.
Required tests (one function each):
subprocess.run(["python3", "x.py"])gives(site="subprocess.run", form="argv", program="python3")on the list's line.- Tuple literal, and keyword-only
subprocess.Popen(args=("gcc", "-c", "a.c")). - Wrapper callee:
_run(["python", "scripts/x.py"])givessite="_run";self.runner.run(["uv", "sync"])givessite="self.runner.run";factory().run(["gcc"])givessite="?.run". - Named form:
cmd = ["gcc", "-c", "x.c"]thensubprocess.check_call(cmd)in the same function: one record at the ASSIGNMENT line,site="subprocess.check_call". Annotatedcmd: list[str] = ["python", "-m", "x"]is captured the same way. - Named form scope boundary: an assignment in function
fand a call passing the same name in a nested functionginsidefgives NO record; the same at module scope consumed at module scope gives one. BinOp:subprocess.run(["python"] + extra)captured;cmd = ["python3", "-c"] + [code]thensubprocess.run(cmd)captured.- NOT captured:
[sys.executable, "x"],[*PY, "x"],[interpreter, "x"],subprocess.run(argv)withargva parameter,LANGS = ("python", "bash")never passed to a call,print("python x"). execform:asyncio.create_subprocess_exec("python3", "-c", "...")andos.execvp("gcc", ["gcc", "a.c"])(the second arg is a list: assert theexecrecord AND the separateargvrecord for that list, since both shapes match);os.spawnv(os.P_WAIT, "/usr/bin/gcc", ["gcc"])givesprogram="/usr/bin/gcc".shellform:os.system("python x.py"),subprocess.run("gcc -o a a.c", shell=True),subprocess.getoutput("clang --version"), f-stringos.system(f"python {s}")gives"python";os.system(f"{py} x")gives NO record;os.system("")gives NO record.- Absolute and non-toolchain heads are recorded verbatim (
"/usr/bin/gcc","uv","git","env"), and a non-spawning callee is recorded too:sorted(["python", "b"])gives(site="sorted", form="argv", program="python")— classification is not this layer's job (§4.2 "Over-capture is intended"). - Determinism: two calls on one line in reverse alphabetical callee order come back sorted by
(line, site, form, program); analysing the same file twice gives identical lists. - Round trip:
serialize(module)→module_info_from_dict(...)gives equalargv_literals(list ofArgvLiteralInfo, not dicts). - Capture identity:
CodebaseAnalyzer(...).analyze().captures == ["env_accesses", "argv_literals"]. - Warm-start fallback: write a 2-file tree; produce a cache JSON with the real
serializepath used bytest_incremental_serialize_matches_full_on_unchanged_tree(read it), then rewrite that JSON's top-level"captures"to["env_accesses"]and delete"argv_literals"from one module dict.load_or_analyze(...)then reportsstats["fallback"] is Trueand every module in the result carries a populatedargv_literalswhere the source has a spawn. With an untouched cache, the same call reportsfallback is False(warm) — proves the identity is stable, so routine refreshes stay incremental. derive_subtreecarriescapturesfrom the source snapshot dict (build a minimal snapshot dict withcapturesset and onetests.module; assertderived["captures"]).
7. Build — exact commands, in order, from /home/mohamed/workspace/wt-ARGV-GATE
- Baseline — already taken by the orchestrator; do not redo it. The snapshot committed on
origin/masterwas produced in a different worktree (its module paths are absolute paths under/home/mohamed/workspace/wt-GRAPH-DEFECTS/), so against this worktree the gate reportedgate: INVALID … fact base does not describe the working tree: 6 file(s)(rc 2). To get a real baseline the orchestrator regenerated the snapshot HERE with the UNMODIFIED base code, under your exact environment:- regen 1:
Incremental: fallback (full reparse) — reused 0, reparsed 3327, removed 0, 93 s - regen 2:
Incremental: warm — reused 3327, reparsed 0, removed 0, 10 s plans/ast-tests.jsonderived: 2078 modulesscripts/gate.py run:gate: PASS— 5 rule clause(s), no violations; 426,176 facts across 23 predicates; rc 0- sizes:
plans/ast-ultimate.json89,569,413 bytes;plans/ast-tests.json60,514,609 bytes That is whygit statusshows both snapshot files asMbefore you start: they are the base-code regeneration, and you regenerate them again in steps 4–5. Before step 1, confirm the base state withgit status --porcelain(expect exactlyM plans/ast-tests.json,M plans/ast-ultimate.json,?? plans/briefs/qwen/) and paste it into the report.
- regen 1:
- Implement §4.1–§4.4.
- Write §6 tests. Iterate with ONLY your module:
env -u PYTHONPATH .venv/bin/python -m pytest -q -p no:cacheprovider tests/test_orchestrata_ast_argv_literals.py(this module needs no infrastructure; it parses files intmp_path). - Lint/format your changed
.pyfiles:env -u PYTHONPATH .venv/bin/python -m ruff check --fix <files>thenenv -u PYTHONPATH .venv/bin/python -m black <files>. - Regenerate the snapshot — FIRST run:
env -u PYTHONPATH uv run --frozen ast-sync --snapshot-only. It MUST printIncremental: fallback (full reparse). The base snapshot already has this worktree's root and was warm before your change (step 0), so the ONLY thing that can force this fallback is your §4.4 captures check — this run is the end-to-end proof that the check fires. If it printswarm, §4.4 is not wired: STOP, fix, and repeat. Expect ~90 s. Record its wall time. - Regenerate — SECOND run, no edits in between:
env -u PYTHONPATH uv run --frozen ast-sync --snapshot-only. It MUST printIncremental: warm — reused 3328, reparsed 0, removed 0(3327 base modules + your new test module; ~10 s). If it falls back, the captures identity is unstable between runs: STOP and report. - Measure, and paste every number into the report:
Required:env -u PYTHONPATH .venv/bin/python - <<'EOF' import json, collections for p in ("plans/ast-ultimate.json", "plans/ast-tests.json"): d = json.load(open(p)) mods = d["modules"] missing = [m["path"] for m in mods if "argv_literals" not in m] recs = [r for m in mods for r in m.get("argv_literals", [])] print(p, "captures=", d.get("captures"), "modules=", len(mods), "missing_key=", len(missing), "records=", len(recs)) print(" by form:", collections.Counter(r["form"] for r in recs)) print(" top programs:", collections.Counter(r["program"] for r in recs).most_common(30)) EOFcaptures == ['env_accesses', 'argv_literals']in BOTH files andmissing_key == 0in BOTH. env -u PYTHONPATH .venv/bin/python scripts/gate.py run; echo "gate rc=$?"— MUST printgate: PASS,5 rule clause(s), no violations, rc 0, exactly as the step-0 baseline. The fact count changes only by the facts of your new test module (this task adds snapshot fields, not facts or rules — Q2b adds the facts); paste the new count. Any other verdict means you broke an existing consumer: STOP and report.
8. Verify — the checks that prove the task is complete
Run in this order; each must pass before the next.
- Focused tests (no infrastructure, fast):
All pass, 0 skipped. (Measured by the orchestrator on 2026-10-11 under your exact environment:env -u PYTHONPATH .venv/bin/python -m pytest -q -p no:cacheprovider \ tests/test_orchestrata_ast_argv_literals.py tests/test_orchestrata_ast_env_access.py \ tests/scripts/test_ast_warm_cache.py tests/test_orchestrata_ast_derive_subtree.py \ tests/test_orchestrata_ast_analyzer.py tests/test_orchestrata_ast_scope.pytests/test_orchestrata_ast_env_access.pyalone = 21 passed in 0.7 s, 7.4 s wall including startup.) - Stage your files (exactly the §5 list, nothing else):
git add <files>; thengit status --porcelainmust show only those paths as staged (A/M). At dispatch the worktree was clean exceptplans/briefs/qwen/(untracked; this spec and your report live there; do NOT stage it). Any other unstaged change you did not make is NOT yours: leave it unstaged and untouched, and name it in your report. - The pre-commit review, exactly as the hook runs it:
env -u PYTHONPATH .venv/bin/python scripts/gate.py review-staged; echo "review rc=$?"— must be0. rc1= blocking findings: fix them and repeat. rc2= no verdict: report the reasons, do not work around it. - The bounded run over the blast radius — ONE run, last, after everything above is green.
First resolve the modules your change can affect (the resolver reads the snapshot's import/reference edges;
it runs in ~1 s and runs no tests):
Required:env -u PYTHONPATH .venv/bin/python scripts/affected_tests.py --json --files \ src/orchestrata_ast/models.py src/orchestrata_ast/analyzer.py \ tests/test_orchestrata_ast_argv_literals.py plans/ast-ultimate.json plans/ast-tests.json \ > /home/mohamed/workspace/wt-ARGV-GATE/.q2a-affected.json"escalate": falseand"untested": []. Ifescalateis true, STOP and paste thereasons(an escalation means a full-suite run, which is not yours to start). Expect ~33 modules; the orchestrator resolved the same file set minus your new test before dispatch and got these 32:tests/factory/test_mutation.py,tests/gates/{test_differential_vs_legacy,test_fact_extractor,test_freshness_inference,test_gate_explain,test_gate_rules}.py,tests/generated_recovery/test_verification_spec_completeness.py,tests/infrastructure,tests/scripts/{test_affected_manifest_rules,test_ast_branches,test_ast_live_sync,test_ast_live_sync_main,test_ast_warm_cache,test_run_ast_pipeline_cli,test_scan_implementation}.py,tests/test_orchestrata_ast_{analyzer,derive_subtree,env_access,inheritance,scope}.py,tests/test_orchestrata_tools_testgen_{adapter_coverage,generator_coverage,ranking_coverage,structural_coverage,units}.py,tests/tools/{test_ast_pipeline,test_ast_pipeline_coverage}.py,tests/verification/{test_implementation_scanner,test_non_python_scan_coverage,test_spec_completeness,test_spec_completeness_extended,test_spec_completeness_js_evidence}.py. Then run exactly those paths through the project runner (it runs the infrastructure preflight against the test fleet10.53.2.xwith the kernel required, the safety gates, then the modules on 8 threads):
Then deleteenv -u PYTHONPATH .venv/bin/python scripts/run_tests.py \ $(env -u PYTHONPATH .venv/bin/python -c 'import json; print(" ".join(json.load(open(".q2a-affected.json"))["tests"]))'); \ echo "run rc=$?".q2a-affected.json(rm .q2a-affected.json). Do not start a second run while one is in progress. It must end with 0 failed, 0 errors, rc0. Paste its final summary lines. Do NOT usescripts/run_tests.py --affected origin/masterand do NOT runscripts/run_tests.pywith no paths (the second is the full ~30-minute suite). If it reports an infrastructure failure (graph/kernel/redis unreachable), do NOT retry in a loop: runenv -u PYTHONPATH .venv/bin/python scripts/start_test_services.py --statusonce, paste the output, stop.
9. Done when — all true, then write plans/briefs/qwen/Q2a-report.md
- [ ] §6 tests 1–15 exist and pass; §8.1 module set passes with 0 skipped.
- [ ] Snapshot:
capturescorrect andmissing_key == 0in both JSON files; first regen = full reparse, second = warm. - [ ]
gate.py run=gate: PASS, 5 rule clauses, no violations, rc 0 (the step-0 baseline). - [ ]
gate.py review-stagedrc 0 with only §5 files staged. - [ ] §8.4 resolver:
escalatefalse,untestedempty;run_tests.py <resolved paths>rc 0, 0 failed, 0 errors.
Report contents (plain facts, no "should"): files changed with a one-line purpose each; the step-0 baseline and step-7 gate output; both regen outputs with wall times; the §7.6 numbers (record count, by form, top 30 programs, both JSON sizes before/after); the §8 outputs (test counts, review rc, affected run summary); any STOP condition you hit and why; anything in this spec you found wrong.