Skip to content

qargi 0.2.10

Choose a tag to compare

@github-actions github-actions released this 10 Nov 22:58
· 278 commits to master since this release

Added

  • qargi local report RUN [--findings] [--from FILE] [--force]: a read-only member's own way to put its report into OUT when its runtime refuses the file tool for report.md. The member writes the text under another name in OUT with its file tool and runs the verb with --from NAME, or gives it on standard input; the verb writes OUT/report.md (--findings: OUT/findings.md, which review check does not read as a report) and, when OUT has none, a minimal manifest.json that names it. It reads only standard input or a regular file under OUT (a link, a path that leaves OUT, a file over one megabyte and an empty report are refused), writes only in the OUT of a read-only run that the lead's registry names, keeps a report that is there unless --force, refuses a build run and, with the member mark set, any run but the member's own. It is not review save (the lead's, which stores a final message): the guard needs no change, because local report is on none of its lists of the lead's verbs. The member contract says to use it when the file tool is refused for report.md.
  • The CI scan of qargi release verify can be told which jobs are not unmatched. A gate may carry ci_jobs, a list of CI job names or ids that run its check under another name: the scan reports each as a note, covered by gate NAME, and does not warn. The new answer ci.not_gates is a list of job names or ids that are aggregate or publish jobs and not gates; the scan leaves them out. A name listed in either that no workflow has is a warning, unless a workflow or a job name could not be read. --strict counts neither covered nor listed jobs. project validate and project set check both lists (non-empty names, each once).
  • A feature gate may carry covered_by: NAME, NAME being a release-tier gate whose command runs the same check. qargi release verify lists the gate as covered by NAME and does not run it; its result in the run record and the gates: line is covered by NAME when NAME passed, and not run when NAME failed or was not reached. qargi feature verify still runs it. project validate and project set refuse a covered_by that names no release gate (or that is on a release gate), naming the entry.
  • qargi project adopt NAME --show prints, with the plan, everything the plan token covers: the refs it starts from, the branches to create, and the full content of each file to write (the sha256 in the basis is that of the content shown). It changes nothing and takes neither --plan nor --approved.
  • qargi project adopt lists the repository's own agent instructions (CLAUDE.md, AGENTS.md) and what it does about each, in the plan (instructions in the plan JSON): it edits neither; with a CLAUDE.md and no AGENTS.md it writes an AGENTS.md that holds the Development environment section only, and says that the @AGENTS.md import line in CLAUDE.md is for the owner to add.
  • The gitflow scaffold has the parameters unreleased_heading and changelog_entry.
  • The answer methodology_accepted, a list of {gap, reason, date} (the gap written as attach prints it, for example file: CONTRIBUTING.md), records the deviations from the methodology the lead accepts. The lead sets it with qargi project set; it is validated, and project set refuses it in a member run. project attach, project adopt and marshal handoff (a == methodology deviations section, and deviations in --json) print the accepted deviations apart from the open gaps; adopt writes no accepted file and creates no accepted branch; an accepted deviation whose gap is gone reads "accepted, no longer applies". (The key is top-level because methodology is a plain string.)
  • qargi release plan VERSION [--hotfix] [--date D] [--keep] [--allow-empty-notes] [--again]: one plan and one approval for the whole release cut. It prints every step with what moves (start, push when the repository has an origin, finish, site when site.enabled) and ONE plan token (publish is not in the cut: it has its own plan); the start step is planned by release start itself, so its refusals come before a token does. release plan VERSION --plan TOKEN --approved "<words>" records the approval once (the words, the plan, the date, an audit line release:cut) in STATE/release/PROJECT-VERSION.cut.json, lead only. Then release start, push, finish and site each take --plan TOKEN alone: the words are the record's and are on that step's audit line, every step records that it ran (plan again and release status show the steps and the next command, so a cut resumes after a refusal or a death), and each step runs only while what the plan fixed holds: start finds its own plan as it was frozen, push the branch where start left it, finish the branch where push left it (the head the checks ran on) with master, the second target and --keep as planned, site the tag on the commit finish tagged. release publish refuses the cut's token (run it with its own plan and approval): the notes and the release files it creates, whose hashes exist only after the tag, are shown by its own plan. A step under the cut is the lead's as approving it is, and is refused in a member run and when QARGI_NESTED is set; a --plan that is not the recorded cut's token, without --approved, is refused naming that a cut is recorded (it does not print a plan of its own). A step that no longer matches is refused before anything moves and is run with its own plan and approval, which the cut records; the single verbs and their own tokens work as before. finish stays the push and tag gate: the approval words are the owner's, and --ignore-checks WORDS at the call is recorded as before. The qargi-flow skill, docs/flow/git-flow.md, the help and the manual page describe it.
  • qargi project unset NAME PATH removes one answer of a project. It refuses what project set refuses (the marshal backend; models.extensions and models.accept_no_barrier, also inside a whole models object, with exit 2 and the verb that takes a plan token), refuses a key that is not set, refuses the removal of an answer the project needs (validate runs first and the file is not touched), and is refused in a member run (the run directory or the member mark in the environment) with exit 2.
  • The answers backlog.section (a heading of the backlog file: only the list under it, up to the next heading of the same or a higher level, holds items) and backlog.archive (a directory that receives NAME-archive-YYYY-MM.md, or a path ending in .md for one archive file; by default the archive is beside the file) shape qargi backlog list and done. A heading the file lacks is an error. lib/backlog.py takes them as --section and --archive.
  • qargi backlog add ID "text" [--detail "..."] appends the item - [ID] text (the detail as indented lines) at the end of the list, of the backlog.section list when that is set, with an audit line. It refuses an id that is not a slug and an id already used anywhere in the file, and then leaves the file alone. qargi backlog done ID [ID...] --evidence TEXT takes several ids as one batch: one plan, one token, the same evidence archived with each id, applied together (the archive is written once, then the backlog); a missing, ambiguous or repeated id refuses the whole batch before a plan is printed. Both are the lead's, as done was: the guard refuses every backlog verb but list.
  • The answer review.reject_trailers, a list of trailer keys (letters, digits and hyphens, * for any run of them) refused besides the built-in Co-Authored-By and Generated-with under review.trailer_policy reject; project validate refuses it while the policy is not reject, and a member's guard refuses setting it. The scrub verdict for a rejected trailer names the commit, the line of its message and what matched (review.trailer_policy is reject or review.reject_trailers names KEY), and an entry that is no trailer key fails the scrub. The scrub reads the list from the run's copy of the answers in the lead's registry, as it does review.scrub.
  • qargi doctor has a project-env check: last env check <verdict> at <time> from the record qargi env check keeps in state/env/PROJECT.json (a warning when the verdict is not ENV OK), or unknown never run when none was recorded. It only reads the record and never runs the project's code; the help of doctor and of env check name each other.
  • qargi memory init repo --worktree DIR (and memory move with the repo tier at one end) writes the repo tier into DIR, another worktree of the project's repository, at the same path inside it, instead of the main checkout. DIR must be the top of a work tree of that repository; the option is refused for any other tier and for a move that has no repo tier. The plan (and its token) are of that worktree.
  • Tab completion of qargi project set offers the answers ci.not_gates, methodology_accepted, review.reject_trailers, backlog.archive and backlog.section.
  • qargi doctor has a tools check: flock (the locks of the finishing verbs and the ref journal) and timeout (the bound on a question put to a remote) must be on PATH, and a missing one warns, naming what it is for and the package that has it. flock and timeout are in the Makefile TOOLS list (make doctor has a line for each), in the bootstrap lines (util-linux, coreutils) and in the tools AGENTS.md lists.
  • The answer release.hotfix_preflight, a command line or a command as a list of words ({version} replaced, QARGI_RELEASE_VERSION set): qargi release push runs it for a hotfix, in a copy of the branch before anything is pushed, instead of release.preflight, for a project whose stored preflight needs content only the fix has or does not suit a hotfix. The plans of release start --hotfix and release push name the answer that runs. A release keeps release.preflight; a member may not set the answer (project set refuses every release.* answer).
  • The answer release.finish_check, a command line or a command as a list of words ({version} replaced, QARGI_RELEASE_VERSION set): qargi release finish runs it in a copy of the branch once its plan is approved and before any merge, tag or push, and a non-zero exit refuses with the tail of its output and nothing moved. The finish plan and the release plan cut plan list it. Off unless set; a member may not set it (project set refuses every release.* answer).

Changed

  • With workflows and no gate declared, the CI scan of qargi release verify prints one note naming all the jobs (no gate is declared, so the CI jobs are not compared with gate names (jobs: ...)) instead of a warning for each job. --strict still refuses, its message saying that no gate is declared.
  • qargi help feature verify names the exact lines a gate or test command may print for the failure report that ends a failed feature verify or release verify: ^FAILED: for a failing step, ^FAIL( |:) and now ^--- FAIL: (a Go test run's line) for a failing test, and ^logs of the failed steps: for where the runner kept its logs. Before, a gate that printed --- FAIL: TestX was reported as having no failing test name.
  • The refusal of a restricted environment variable under the read-only class (PATH, TMPDIR, HOME, GIT_*, LD_*, PAGER, BASH_ENV ...) says what it is for: a read-only member may not assign the name (inline, with env, export or declare) and runs with the one it was given; a build, vet or test command with that environment is not refused. It read "TMPDIR is not set", which read as if the variable were missing from the environment of a command that only uses it. The rule is the same; only the wording changed.
  • qargi help guard (and the GUARD section of the manual page) and the review role text tell a read-only member what it may run (build, vet and test commands with the environment it was given; only an assignment of PATH, TMPDIR, GIT_* and the like is refused), that its TMPDIR is the run's TMP (a Go test writes there; the class writes only OUT and TMP), and that a piece after a separator that starts with git push, git tag NAME, ssh, scp, sftp or gh is refused even in text, while the same words in the middle of a sentence are not read. The topic stays within its 400 words, so a few of its sentences are shorter. The guard's reading of a command is unchanged. The member definition (agents/member.md) is unchanged, so qargi agent refresh is not needed; the prompt of a cloud agent of the review role carries the role text, so qargi instances refresh-prompt brings it in line, and a run prepared earlier keeps the old role text in its task.md.
  • qargi branch prune counts the remote-tracking branch origin/BRANCH of each repo.detached branch as a place a branch is merged into (with the same --no-ff rule), because a detached branch that origin changes only through a pull request lags behind origin locally; --remote now fetches before it reads the history, not after. docs/flow/git-flow.md (Detached branches) and the qargi-flow skill describe that flow: a short-lived agent/pr-SLUG branch cut from origin/BRANCH in a worktree, local prepare --base on it, review apply, a fast-forward, a push, and a pull request merged with a merge commit; a squash or rebase merge leaves no merge commit and its branches are deleted by hand.
  • The changelog sentence of the CLAUDE.md and CONTRIBUTING.md that project adopt writes, and the heading of a new CHANGELOG.md, follow the project's release.changelog.unreleased heading and release.changelog.numbered ("as the next numbered item under Next"), where the scaffold said [Unreleased] always. The files of a project without release answers are as before.
  • The AGENTS.md that project adopt writes names only the bootstrap, dry-run and doctor commands the repository has or declares (env.* in the answers); a line for a command it lacks (a task runner without the conventional targets) is left out.
  • qargi project attach and qargi project adopt print a declared setting, or a state that is not a problem, apart from the gaps, under "noted, not gaps (nothing to close)": release.version_file none (no VERSION is planned, and it is no longer listed under "left for you to judge"), and a changelog whose unreleased heading carries the version (release.changelog.unreleased with {version}) that has none right after a release ("no unreleased section yet; the last release is X"; before, a format gap). The plan JSON of adopt has an info list for them, and so has the detection JSON of attach.
  • qargi marshal claim and qargi marshal check refuse when the remote of the shared records cannot be reached (exit 3, nothing changed), where they went on: a claim stayed provisional and a check answered from the last fetched copy, with a warning. The message says what could not be reached (the records of the project, refs/qargi/features/NAME on the remote) and why, and how to go on. The new --assume-unreachable-ok lets the lead go on (the old behaviour) and records marshal:unreachable-ok in the audit log (project, feature, verb, remote, the reason). The other changes of a shared record (set, renew, release, take, takeover, finish, resign) already exited 3 without the remote; their message now names what was unreachable too, and says there is no override. marshal status still shows the cache with its notice. The flow verbs that ask the marshal first (feature start, sync, merge, verify and finish) refuse the same way (exit 3, nothing changed; feature start before it makes the branch or the record), where they warned and went on, and the message names the flag. They take the same --assume-unreachable-ok, which goes on and writes the same audit line marshal:unreachable-ok (verb start or check). A project whose marshal is local or off, an explicit refusal by another lead (exit 1), and a marshal that is missing or fails in some other way (a warning) behave as before; local prepare is not changed.
  • The warning that memory init repo and a move into the repo tier print says on which branch the files must be committed (the branch checked out where they are written), that a member's clone has them only once committed, and that --worktree DIR writes them into another worktree. The lead's session-start warning for an uncommitted repo tier says the same (the branch and the directory to commit in).
  • qargi project set prints was: OLD after its line (the JSON the answer held, or (not set) for an absent key) and, like project unset, writes an audit line (project:set and project:unset, with the path and the old value, cut at 200 characters). set wrote none before.
  • A null stored for an optional answer is the same as not set, for every optional answer: project validate accepted it for gates and git.author and refused it for review.scrub, review.trailer_policy, review.keep_author and backlog.file. It now accepts it for all of them (the readers already treated a null as unset); qargi project unset removes the key itself. The help of project set says so.
  • qargi import: an archive's project answers never add or change ci.not_gates, methodology_accepted, review.reject_trailers, backlog.archive or backlog.section either, which a member's project set is refused, as they never add or change models.extensions and models.accept_no_barrier: the stored value stays (an entry may be dropped, none is added), or the key is left out, and a note names the keys. The answers were imported as the archive held them.
  • The providers page says that the guard fails closed on a provider definition in the library that does not load (a draft made with provider new counts): every call of a member that writes or reads a protected place is refused until provider check NAME is clean or the pack is removed, and that a pack that may be broken for a while is better edited outside the library. Tests keep the help topics apart from the command names (qargi help NAME shows the topic first) and CONTRIBUTING.md in step with the rules of the hermetic lint and the legacy lint.
  • The qargi-verify skill names qargi local report (the member's own writer of a read-only report) beside qargi review save (the lead stores a final message) for a read-only run with no report in OUT.
  • The requirements line of the README names timeout (GNU coreutils) and says flock is for the locks.
  • The skills page of the documentation (docs/skills-tools-and-memory) has a section on the answers and verbs the lead reaches for that no page of its own listed: project unset, methodology_accepted, project adopt --show, backlog.section, backlog.archive, backlog add, backlog done with several ids, review.reject_trailers, ci.not_gates, release plan and local report, each with a line and a link to the page that holds the detail.
  • qargi role list|show|prompt|check is one Python process (lib/role.py, imported by the dispatcher as the guard is, so its bytecode is cached) where lib/role.sh started jq a dozen times. The output and the exit statuses of every verb are those of the script, also for a role.json that is an array, null, empty, holds several values or a description that is not text; only what jq printed to standard error about a role.json that does not parse is worded by qargi now. role list takes about 85 ms where it took about 120 ms (the four built-in roles, the tool compiled); the startup ceiling of role list in tests/startup-budget.txt is 260 ms (twice the 130 ms the test measures, with no bytecode cache and the test shim in front of python3), down from 350.
  • A read of the shared marshal records (marshal status, claim, check, renew and every other verb that loads the cache of the remote's refs/qargi/) lists the refs with one git for-each-ref and reads their commits, trees and record.json files with two git cat-file processes, instead of three git processes per ref (14 refs: 43 git processes became 3). The records, the warnings and their order, and every verb's output are the same, a record.json over 64 KiB is still never read, every signature is still verified on every read, and a push is still made with a lease on the oid that was read; when git cannot answer in a batch the read goes back to one ref at a time.
  • The listing of qargi marshal status (every feature, or one) asks git once for what is the same for all features: the work trees, the lists of feature and agent branches, whether develop exists. It asked again for each feature (14 features: 100 git processes became 40). The output is the same; the answers are kept only while one listing runs, so a later command asks git again.
  • With integrity advisory or strict, the signatures of one read of the shared marshal records are verified at the same time (up to 8 ssh-keygen -Y runs at once) instead of one after the other. Every signature is still verified on every read, a record that fails is refused or tagged exactly as before, and nothing is remembered between reads; with 14 signed records a read is about 150 ms faster.
  • qargi local prepare --task-file F --done COMMAND checks the command word of every simple command of the DONE line, not only of a line with no operator: the word that starts the line and each word after ;, &, &&, ||, |, ( or $( (past NAME=value words and redirections) must be an executable on PATH, a shell builtin or keyword, or a script. Prose that merely contains an operator (coverage > 80%, tests pass & lint is clean) used to pass because bash parses it; it is now refused with exit 2 and no run is made. The words of a [[ ]] test are not read, and a line that holds a case, function or select command, (( (an arithmetic command or a for (( header) or an array assignment (NAME=() is checked only up to that point and the rest is accepted on syntax alone (bash -n), so [[ -f out.txt ]] && echo yes, case $x in a) make a;; esac, for ((i=0;i<3;i++)); do echo $i; done, function g { make; }; g and arr=(a b c); echo "${arr[@]}" are DONE lines; the lookup of a builtin or keyword compares the word as a fixed string, so [[ is no longer read as a pattern. A path is looked up at BASE or under --scope only as the first command; a later one may follow a cd. Arguments are never checked.
  • qargi review check fails a manifest whose done.command is not the DONE: line of RUN/task.md (the check the lead set; the line before PROFILE: or BASE:, so a DONE: line in the task text does not stand for it), naming both commands. A manifest with no done.command passes with a warning that names the DONE. A run with no task.md is unchanged.
  • qargi local prepare exits 2, not 1, for every call that cannot be right, as its other usage errors already did: neither --repo nor --repo-url (the message was --repo must be a git working tree, about a path never given; it now says prepare needs --repo DIR ... or --repo-url URL), a --repo that is no working tree, a --repo-url that is not https or carries a user name, a --credential-file that is not there, an unknown --role, a bad --slug, and a --task-file that is a link, missing, empty, over 1 MiB, holds a NUL byte or has a SCOPE: line. qargi local clean RUN --yes and --scratch DIR (options of other forms of clean) exit 2 too. A failure of the work itself (a clone that fails, an unreadable answer) still exits 1; no run is made in either case.
  • qargi skill check, tool check, packs check and skill|tool list do the work for the packs of a role in one process (lib/packs_check.py) instead of about 15 python3, 34 jq and 18 find calls per pack: the content hash, the drift and review lines, the compatibility with the project's provider, the render inputs and the library facts of the skills a session would load. Their output and exit status are the same, byte for byte, and so is every content hash (the approvals stored with the packs stay valid); the order of the names in the hash follows the locale's collation, as sort did. One difference in skill|tool list PROJECT ROLE: a pack is marked [attached to ...] when its name equals an entry of the role's packs, where the shell matched the name as a pattern (a name with a . also matched other characters).
  • lib/procenv.py holds the helpers that lib/env_check.py, lib/env_apply.py, lib/local_run.py, lib/tools.py, lib/supervise.py and lib/discover.py each defined for themselves: the PATH of a child process without empty and relative entries (clean_path), the base environment of a child (child_env) the time stamp of a record (now), the plan token hash (plan_token) the line of the audit log (audit, same fields and order as before), die and the runner of the git commands of env check and the tool checks (git). The output of every verb is unchanged, with one exception: when the caller's PATH has no absolute entry, the doctor that qargi env check runs and the tool lookups of qargi env check and qargi env plan fell back to /usr/bin:/bin (two of the three copies); every copy now falls back to /usr/local/bin:/usr/bin:/bin, as qargi env apply and qargi local run already did.
  • The message of a conflict in the second merge of qargi release finish (the tagged master merge commit into develop, or into the open release branch) names the tag and not a bare sha: merging tag VERSION (the master merge commit abc1234) conflicts. The refusal of --resolved for a commit that is not that merge names it the same way.
  • The prompt texts (contract/*.md, roles/*/ROLE.md, agents/member.md and agents/member.body.md, packs/skills/*/SKILL.md, skills/*/*.md, provision/templates/*.md, lib/providers/*/*.md) are one paragraph per line, a list item on one line, where they were hard-wrapped at about 80 to 100 columns (some lines were far longer after later edits) and showed as ragged fragments in a viewer narrower than the lines. Only line breaks changed: not a word is added, removed or reordered, so the word ceilings of tests/skill-budget.txt stand and the golden task blocks differ by line joins. tests/test_prose_lint.py (fast tier) keeps it so: outside front matter and fenced code a line that continues a paragraph or a list item, or ends in spaces, fails. CONTRIBUTING.md names the rule. Rendered members are STALE after this release because their prompt text changed in form: qargi agent refresh renders them again, and a cloud agent takes it with qargi instances refresh-prompt.
  • qargi help project (set) lists everything a member's guard refuses to set (also upstream.*, ci.*, methodology_accepted, git.* and review.*), and qargi help import names the five lead answers besides models.extensions and models.accept_no_barrier that an archive may not add or change (ci.not_gates, methodology_accepted, review.reject_trailers, backlog.archive, backlog.section).
  • The wording about a memory store that cannot be listed no longer names the cloud command-line tool outside the cloud runtime's own code: the help of qargi canon says "the cloud tool missing, not logged in, no access", and the reason canon prints for a list that failed with no error text of its own is the store list failed (status N) (it was ant failed (status N)). tests/test_neutral.sh found the three lines (it is in the fast tier and covers every file), and the allowlist of tests/neutral-allowlist.txt only shrinks, so the lines were reworded and not listed. The manual page is regenerated.
  • lib/provider.py, lib/marshal_store.py, lib/help.py, lib/gates.py and lib/lead_usage.py are imported by bin/qargi and by the scripts of lib/ instead of run as scripts, so python reuses the compiled form that install.sh --user and qargi upgrade leave (a script is compiled on every run, a module is cached). The scripts call the one helper pymod MODULE ARGS... of lib/lib.sh (bin/qargi, which does not source lib/lib.sh, holds the same text and execs); it runs the module's main() as the script's __main__ block does and answers with its exit status, and sys.argv is what the script had. Every verb prints what it printed and exits with the status it had (the modules still run as scripts too, python3 -I lib/provider.py list). With the tool compiled, agent for build takes about 420 ms where it took 525, marshal status --brief --no-fetch 235 ms (275), provider list 117 ms (153), help role 46 ms (58) and a completion 36 ms (49). tests/test_startup_modules.py checks that each module answers as its script did, that its bytecode is cached, and that bin/qargi and pymod hold the same text.
  • tests/startup-budget.txt has ceilings for qargi agent for build (850 ms) and qargi marshal status --brief --no-fetch (470 ms), twice the median of 421 and 235 ms that tests/test_startup_modules.py measures with the tool compiled, a project, a member and an open feature (the same verbs took 525 and 275 ms before the five large modules were imported). Like the one of role list they warn and do not fail.
  • The test budgets (tests/suite.txt, fourth column) and tests/durations.txt are re-recorded from one whole-suite run (134 steps, 829 s, eight CPUs, in parallel): a budget is the last recorded duration of the step on the reference machine, rounded up by a tenth, and the run reports a step that takes more than 1.5x its budget. The rule that a budget only shrinks is gone from the manifest header, tests/run.sh and CONTRIBUTING.md; tests/test_ci.py checks instead that every step is well formed (a budget of at least a second, a timeout of at least 1.5x the budget). The timeout of tests/test_review.sh is raised to 1356 s with its budget, and the CI jobs' timeout-minutes from 35 to 55, which the new budgets ask for (a shard plans 1756 s).

Removed

  • lib/role.sh, replaced by lib/role.py (see Changed). The shell helpers role_dir, role_names and role_class of lib/lib.sh, which other scripts use, stay.

Fixed

  • qargi release start VERSION --hotfix works on a master whose CHANGELOG.md has no unreleased heading when release.changelog.unreleased carries {version} (such as ## {version} (unreleased): the heading is renamed at a release, so master keeps none); with any other unreleased heading, the default ## [Unreleased] included, a master without it is refused for a hotfix as for a release ("add one"). It used to stop with has no '...' section to release, with or without --allow-empty-notes. The hotfix branch now gets the heading for VERSION (the released template with the version and the date), empty, above the first heading that follows the released template (any version and date; else the first heading of the unreleased heading's level, ignoring fenced code; at the end when there is none), so a ## Format note above the version sections is not where it goes, and a file with CRLF line endings keeps them (it is read and written without newline translation); the plan says it was added because master had none, and release finish of a hotfix refuses (before anything is merged, tagged or pushed, at its plan as well) while the CHANGELOG.md of the branch has no entries under the heading of VERSION, whatever the layout (a branch with no CHANGELOG.md is not checked); the refusal names the file and the heading and says to record the entries under it on the branch. release push runs only the preflight the project set, as before. A release (not a hotfix) is still refused, and a master that has the unreleased heading is handled as before.
  • qargi canon plan and canon apply on a project that records no memory store (or no reference store, with --kind) stop with the project has no memory store recorded and a non-zero status. Before, the die inside the plan step ended only the command substitution it ran in, so the verb went on: it printed jq: error: Could not open file three times and a plan token (that of an empty plan), and exited 0.
  • qargi canon plan and canon apply build the plan of each store once (they built it twice, once for the token and once to show or run it, and read the store again each time) and leave no temporary directory behind: every temporary file of a canon verb is made under one directory that a trap removes when the verb ends, whether it succeeds or dies. Before, each plan left a mktemp -d directory in TMPDIR.
  • qargi canon check|plan|apply|seed-role|promote no longer take a store that cannot be listed (ant missing, not logged in, no access) for an empty one. A failing list is an error: check prints unreachable: <reason> under each such store, as memory tiers does, and exits 1 without advising canon plan; plan and apply refuse and make no plan; seed-role and promote write nothing. Before, the list failure was discarded, so check said the store was empty and told you to plan, and plan offered to create the whole seed over a store that already held notes. A project with no store recorded still checks clean. mem_dump returns 1 and the first line of the error on standard error when the list fails.
  • A write by a read-only member to its run's temporary directory (TMP, usually below /dev/shm) is matched to its run by the path, through the lead's registry of local runs, when the call's working directory is in no run. It was refused as "no run directory is found from this directory" (a Go test run from elsewhere, a redirection after a cd). Every other refusal is kept: a path outside OUT and TMP, a registry line that names no run, a relative or missing directory, a path beside or above the registered directory.
  • The environment gaps of project attach and project adopt are measured against the effective environment: a command the project's answers declare (env.bootstrap_command, env.doctor_command) is not reported as a missing target, and the files adopt writes name the declared commands. project attach of a stored project reads its answers (attach.py detect --answers FILE), so its release answers (version file, changelog headings) apply too.
  • The lock that release finish (and every finishing verb) takes checks the exit status of flock: a flock that fails or is not installed ends the verb with "could not take the lock", naming the cause (the command missing, or the lock directory's file system), and nothing is changed; before, the verb went on as if it held the lock, so two runs of it were not kept apart. tests/test_release_cut.sh is the new test of the release ceremony.
  • qargi release start removes the bookkeeping files (STATE/conflicts/PROJECT-release-VERSION-into-*.master and .done, or hotfix- for a hotfix) that an earlier finish of the same version parked and that were never used again when that release was given up; before, they stayed for good, and a file in that directory is what the lead's compact check reads as a merge waiting to be resolved. Only files are removed, never a merge waiting in a worktree. A plan alone removes nothing.
  • qargi release start VERSION --hotfix is no longer refused by a stored release.preflight that needs content only the hotfix brings (a file, a changelog entry): a hotfix is cut from master, which cannot have it, so hand-cutting the branch was the only way. For a hotfix start does not run the preflight and its plan says so, naming the command, where it runs and when ("It runs in a copy of hotfix/VERSION when you push"); release push runs it in a copy of the branch before anything is pushed, as it always did, and refuses until the fix is committed. A release (not a hotfix) is checked at start as before, and its plan now says the preflight passed and that it runs again at push.
  • The member's guard matches a write to a temporary directory (TMP) to a run by the path only for the member's own run when the hook's environment carries QARGI_MEMBER_RUN: a read-only member of one run could write the TMP of another run by its path. Without the mark the path match is as before. The text of the guard that said input that is not JSON is let through now says it is refused, as it has been.
  • A member's guard refuses qargi project set of ci (ci.not_gates takes a CI job out of the comparison with the gates) and of methodology_accepted (the deviations the lead accepts), as it refuses the other answers the lead owns; review.reject_trailers, backlog.archive and backlog.section were already refused with their parents. A project unset whose verb or path a variable, a substitution, a brace, a glob or xargs builds is refused like the same form of project set; a plain project unset is still refused in a member run by the verb itself.
  • qargi provider accept without --no-barrier or without --asked exits 2 (a usage error), as the manual page promises for a usage error; it exited 1.
  • The home page that lib/site.py generates when a site has no hand-written home page no longer says the agents are sandboxed; its description says the guard is a best-effort text check, not a sandbox (the README says the same).
  • qargi local prepare copies review.reject_trailers into the run's line of the lead's registry, with review.keep_author, review.scrub and review.trailer_policy. It copied only those three, and the scrub reads the list from that copy, so the answer was not enforced for a prepared run (a commit with Signed-off-by passed under the policy reject). A run prepared before this change has no list in its copy and is not held to it; the list taken at prepare stays the run's, as the other review answers do.
  • qargi lead compact-check no longer reports a merge waits to be resolved or applied for the .master file of a release finish (STATE/conflicts/PROJECT-KIND-VERSION-into-BRANCH.master, the master merge commit the second merge takes, left by a finish that stopped after writing it) nor for anything in finish-intent/: only a parked worktree (a directory under conflicts/) and the .done mark of a resolved merge count.
  • qargi local clean RUN (and so --applied and --stale, which clean each run by the same rules) refuses a run while its qargi review apply is running, --discard or not: the apply holds a pid lock in the lead's state (apply/RUN-ID.json.run), and removing the run under it left the apply writing into a directory that was gone. A lock whose process is dead, or whose pid another process now has, does not block; clean removes it with the run.
  • The bookkeeping file STATE/conflicts/PROJECT-KIND-VERSION-into-TARGET.master, which keeps the master merge commit of a finish that parked a conflict in its second merge, is used again only when the commit it names is a merge of exactly the master and the release branch head the finish now has (two parents, in that order). A file that names any other commit (edited, left by another run, a commit that is not that merge) is removed and the merge is made anew; before, any commit that existed was tagged and merged as if it were the master merge.
  • The record a finishing verb writes before it tags or pushes (the intent record) is written under a lock that is checked like the verb's own: a flock that fails or is not installed ends the verb with "could not take the lock for the intent record", naming the cause, and no record is written; before, the write went on as if the lock were held. The ref journal says the same in its warning (the flock command (util-linux) is not installed, or flock on FILE failed) when its line could not be written for that reason.
  • The plan line backlog items named in these commits of feature finish and release finish reads only the list under backlog.section when that answer is set, as the backlog verbs do; before, it looked in the whole file and named items outside the section. A heading the file lacks names nothing.
  • The key completion offers release.finish_check and release.hotfix_preflight, and the text project validate prints for a release answer that is not an object lists every key the release answers accept (it is built from the list the check uses, which had grown past the copy of it).
  • qargi lead compact-check counts a parked or resolved merge as the project's only when its entry in the conflicts directory is PROJECT-KIND-... with the kind feature, detached, release or hotfix; before, any entry starting with PROJECT- counted, so the merges of another project whose name continued after a hyphen (app-web for app) made the check unsafe.
  • The audit log (state/audit.log) is appended to under an exclusive lock. audit_log in the shell library and the python writers (procenv.audit, which the supervisor and env apply use, the audit of export and import including the lines import adds from an archive, and the audit of the provider verbs) each take flock on audit.log.lock beside the log and write whole lines under it, so lines of verbs running at the same time no longer run into each other (a long line was written in several pieces and another writer's could land between them). The line format is unchanged. A lock that cannot be had (no flock command, no fcntl, a lock file that cannot be opened, a refused lock) ends the verb with a message naming the cause and writes nothing, as the pid lock does.
  • tests/test_canon_reach.sh asserts the absence of a jq error with jq: (the prefix of every jq error message) and not with the bare jq, which a random temporary path in the output can hold (the absent rule of the hermetic lint, tests/hermetic_lint.py). tests/test_hermetic.sh failed on it.
  • Three more findings of the hermetic lint on the same head, which kept tests/test_hermetic.sh failing, are fixed: the shim of tests/test_marshal_batch.sh that makes a verification take 0.3 s on purpose says so (hermetic: allow sleep, nothing waits for it), tests/test_procenv.py builds the run and temporary names of its child_env call under the test's own temporary directory, and tests/test_release_cut.sh gives journal_line the test's own state directory as the repository instead of /tmp.
  • The member contract's sentence on qargi local report names the run as the parent directory of OUT, not as RUN_DIR. RUN_DIR is a name only a local task block has (a cloud member's prompt gives OUT alone), and tests/test_prompt.sh keeps the contract from naming it as a place; the sentence failed that check. The word ceiling of contract/contract.md is 1010.
  • A missing argument or option that the message itself asks for ends with exit 2, a usage error, not exit 1 (a refusal): qargi packs load with no project to infer (name the project with --project P), qargi skill import and tool import whose source gives no usable name (pass --name) and qargi instances init --location cloud whose deployment gives no usable name (pass --name). The manual page promises 1 for a refusal and 2 for a usage error.
  • A value an option cannot take ends with exit 2, a usage error, not exit 1: --kind of canon, --path of canon promote, --roles, --model and --effort of agent add, --profile and --surface of agent for, --model, --effort and --budget of instances init, --budget and --networking of provision, --scope of skill|tool link|unlink, --role of packs load and of start on a base, --why of mark, --warn of watch and --date of release start|plan. skill link and unlink with a bad --scope stop at once with that status: the check ran in a command substitution whose exit ended only the substitution, so the verb went on to a later failure.
  • A member's guard refuses qargi project unset in its plain form, whatever the path, with a reason of its own (it removes an answer of the project the lead runs on, has no plan token, and the removal is to be proposed in the report), as it refuses project link and project unlink. Only the unreadable forms were refused before; the refusal of the plain form was left to lib/project.sh, which asks for a member's run directory or mark, so a member started some other way removed gates, review.scrub, release.finish_check or any other answer whose set the guard refuses for it after a cd /. The change is in the guard's table of verbs, not in how it reads shell text.
  • qargi project unset refuses (exit 2) when its check for a member run cannot answer. The check's failure (python missing, marshal_refs unreadable, a traceback) was sent to /dev/null and read as "not a member", so the removal went on; it now stops with a message that says the check failed, and python's own error stays on stderr.