@sabaiway/agent-workflow-kit 5.9.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "5.2.0",
6
+ "version": "5.3.0",
7
7
  "provides": ["review", "probe"],
8
8
  "posture": { "model": "Gemini 3.7 Flash (High)" },
9
9
  "roles": {
@@ -33,6 +33,7 @@
33
33
  ],
34
34
  "receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
35
35
  "notes": [
36
+ "transport: every review dispatch drives the CLI in --output-format json (plus --disable-slash-commands) and the returned envelope is parsed in node (bin/agy-envelope.mjs) — the operator-facing invocations and flags above do NOT change, and on a ZERO exit the wrapper still PRINTS the review text, never JSON. A missing or unreadable envelope on a zero exit is a loud failure with NO receipt, never a downgraded verdict and never a fallback to raw-stdout parsing; a non-zero CLI exit keeps its own code and message, and publishes the captured stdout unchanged from the SINGLE dispatch or the FINAL fed turn (which may therefore be a JSON or partial payload — the envelope is parsed only on a zero exit); an INTERMEDIATE feed turn is the exception, its output stays private (Invariant E) and its failure prints only a named error. Enforced by a PRE-SPEND capability probe, not a version floor: agy --help must advertise --output-format and --disable-slash-commands, node must be >= 22, and bin/agy-envelope.mjs must be present — otherwise the review refuses before any run is spent and names the missing capability",
36
37
  "pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt",
37
38
  "the review posture banner appends a banner-only timeout=<duration> field — exactly the duration agy-run hands to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity",
38
39
  "quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
@@ -229,7 +230,7 @@
229
230
  "max": 100000000,
230
231
  "default": "240000",
231
232
  "appliesTo": ["agy-review"],
232
- "effect": "agy-review code: the ceiling on the SUM of all outgoing prompt bytes the chunked feed may send (every envelope, every body, the final turn). Checked BEFORE the first turn is spent; past it the fed review refuses. An economy guard, not the correctness guard — correctness is the per-part delivery echo."
233
+ "effect": "agy-review code: the ceiling on the SUM of all outgoing prompt bytes the chunked feed may send (every feed-turn framing block, every body, the final turn). Checked BEFORE the first turn is spent; past it the fed review refuses. An economy guard, not the correctness guard — correctness is the per-part delivery echo."
233
234
  }
234
235
  ],
235
236
  "networkHosts": ["*.googleapis.com", "accounts.google.com", "antigravity-unleash.goog", "lh3.googleusercontent.com"],
@@ -6,13 +6,25 @@ were captured from **v1.1.13**; if the binary disagrees, the binary wins. The wr
6
6
 
7
7
  ## Headless behaviour
8
8
 
9
- Use `-p`, `--print`, or `--prompt` to run one non-interactive prompt and print the text response. The
10
- wrapper always uses headless `-p`. v1.1.13 adds `--output-format text|json|stream-json` and
11
- `--json-schema` (structured output); **text stays the wrapper default** — raw passthrough
12
- (`agy-run "…" -- --output-format json`) is possible but rides with NO first-class parsing or schema
13
- validation (first-class adoption is a separate backlog item). When the caller needs structure
14
- through the wrapper, ask for Markdown, bullets, tables, or fenced blocks, then validate the text
15
- yourself.
9
+ Use `-p`, `--print`, or `--prompt` to run one non-interactive prompt and print the text response.
10
+ Both wrappers run headless `-p`, and they differ in **transport**:
11
+
12
+ - **`agy-run` stays text.** It prints the CLI's text response unchanged. JSON can ride passthrough
13
+ (`agy-run "…" -- --output-format json`) with NO parsing or validation if you need structure here,
14
+ ask for Markdown, bullets, tables or fenced blocks and validate the text yourself.
15
+ - **`agy-review` drives every dispatch in `--output-format json`** and reads the returned envelope
16
+ (`bin/agy-envelope.mjs`), so the answer TEXT and the conversation id come from **named fields**
17
+ instead of a guess at raw stdout. The **review contract stays prose-shaped**: the verdict is still
18
+ parsed out of that text's `### Verdict` section, exactly as before. It also passes
19
+ `--disable-slash-commands`, so a delivered change-set line that begins with a slash command stays
20
+ BODY rather than being expanded by the CLI. **On a zero exit** what it PRINTS is the review text,
21
+ never JSON — the envelope is wire format, not output format. When the **single dispatch or the
22
+ FINAL fed turn** exits non-zero the captured stdout is published as-is (the CLI's own failure wins,
23
+ and the envelope is parsed only on a zero exit), so a failing run may print a JSON or partial
24
+ payload. An **intermediate** feed turn is the exception: its output stays private and its failure
25
+ prints only a named error.
26
+
27
+ `--json-schema` is **not** adopted and was measured and rejected; see the flags table below.
16
28
 
17
29
  ## Wrapper contract
18
30
 
@@ -70,6 +82,15 @@ agy-review --continue | --conversation <id> [--decided @f] [--focus "…"] #
70
82
  passthrough (it owns the posture). The service can still **stall on large/substantive prompts**
71
83
  (Issue-001), so keep reviews **focused**; the hard timeout is the guard.
72
84
 
85
+ **Host requirements, enforced BEFORE any run is spent.** Because the review reads agy's JSON
86
+ envelope in node, `agy-review` needs **Node ≥ 22** on `PATH`; and because it passes flags the CLI
87
+ must honour, it probes `agy --help` for **`--output-format`** and **`--disable-slash-commands`**
88
+ first. A missing flag, an unreadable `agy --help`, a missing or too-old node, or a missing
89
+ `bin/agy-envelope.mjs` each refuse with a named cause and spend nothing. This is a **capability
90
+ probe, not a version floor** — the release that introduced `--output-format` is not measurable from
91
+ one installed build, so a guessed floor would refuse working installs. A failed probe is never read
92
+ as "capability present".
93
+
73
94
  ## Models
74
95
 
75
96
  Pass the **exact display string** from `agy models`, or set `AGY_MODEL`.
@@ -112,19 +133,31 @@ AGY_MODEL="Claude Sonnet 4.6 (Thinking)" AGY_TIMEOUT=10m agy-run @review-prompt.
112
133
  | `--dangerously-skip-permissions` | auto-approve all tool permissions | avoid by default; use only with explicit user approval |
113
134
  | `--sandbox` | run with terminal restrictions enabled | prefer when delegating a prompt that might trigger tool/terminal work |
114
135
  | `--log-file <path>` | override the CLI log-file path | keep logs secret-free and out of committed artifacts |
115
- | `--output-format <fmt>` | print-mode output: `text` (default), `json`, `stream-json` | NEW in 1.1.x; not wrapper-adopted (backlog) |
116
- | `--json-schema <s\|path>` | enforce structured output (stream-json final result) | NEW in 1.1.x; not wrapper-adopted (backlog) |
136
+ | `--output-format <fmt>` | print-mode output: `text` (default), `json`, `stream-json` | **ADOPTED by `agy-review`** — every dispatch runs `json` and the envelope is parsed (`bin/agy-envelope.mjs`). `agy-run` stays `text`; JSON there rides passthrough, unparsed. `stream-json` is adopted by neither (see below) |
137
+ | `--json-schema <s\|path>` | enforce structured output (stream-json final result) | **REJECTED, and the reason is a measurement.** It is not a constrained decode: the model answers in prose and the CLI then spends a **second turn** asking it to restate that answer in schema shape (`num_turns: 2`). Matched control — same prompt, same model, schema off vs on: **16,585 vs 33,446 total tokens**. So a schema does not remove prose parsing; it replaces a free, deterministic, anchored regex with a billed non-deterministic re-read (the structured `reason` came back reworded, not quoted) and adds a failure mode — a run dying between the two turns has prose but no `structured_output` |
117
138
  | `--effort <low\|medium\|high>` | reasoning effort for the session | NEW in 1.1.x; the display strings already carry an effort tier — the wrapper keeps model selection in ONE place (`AGY_MODEL`) |
118
139
  | `--mode <m>` | agent execution mode (`accept-edits`, `plan`) | NEW in 1.1.x; not used by the wrapper |
119
140
  | `--agent` / `--project <id>` / `--new-project` | agent + project selection for the session | NEW in 1.1.x; not used by the wrapper |
120
- | `--disable-slash-commands` | disable slash command/skill expansion in print mode | NEW in 1.1.x; not used by the wrapper |
141
+ | `--disable-slash-commands` | disable slash command/skill expansion in print mode | **ADOPTED by `agy-review`** on every dispatch — a delivered change-set line beginning with a slash command stays BODY instead of being expanded, so the model reviews the delivered bytes. Not used by `agy-run` |
121
142
 
122
143
  ## Subcommands (v1.1.13)
123
144
 
124
145
  `agent` / `agents`, `changelog`, `help`, `install`, `models`, `plugin` / `plugins`, `update`.
125
146
 
126
- **Still not available in v1.1.13:** any `agy inspect`. Wrapper output stays plain text (the
127
- `--output-format` lane is not adopted heresee the backlog row).
147
+ **Still not available in v1.1.13:** any `agy inspect`. On output format the two wrappers differ:
148
+ `agy-run` is a **text-by-default passthrough**it hands back the CLI's stdout unchanged, so
149
+ `agy-run "…" -- --output-format json` really does print JSON; `agy-review` prints the review text it
150
+ read out of the JSON envelope on a **successful** run. (When its single dispatch or FINAL fed turn
151
+ exits non-zero, `agy-review` publishes that captured stdout unchanged — never an intermediate feed
152
+ turn's; see the transport bullet above.)
153
+
154
+ **`--output-format stream-json` is deliberately NOT adopted.** Its two measured gains are queued as
155
+ follow-ups rather than built: its `init` event names the **resolved** model (neither the plain `json`
156
+ envelope nor the stream's own `result` event carries one), which would let the receipt record what
157
+ actually ran; and its live `step_update` events would make a stalled run visible (Issue-001). The
158
+ seam built here makes the FLAG easy to switch, but neither gain is free: `bin/agy-envelope.mjs`
159
+ parses ONE JSON object and refuses anything else, so NDJSON needs a stream-aware reader, and live
160
+ progress needs an event relay rather than a captured file.
128
161
 
129
162
  ## Project-context flags
130
163
 
@@ -89,6 +89,12 @@ itself can never reveal the very text it asks for, and the wrapper needs no seco
89
89
  construction) or `fed` (proven by echo). The kit's review-state gate requires the field present and
90
90
  well-formed, never a particular value, so a receipt minted before this lane existed no longer
91
91
  attests. The recovery is stated: re-run the review.
92
+ - **Later turns are routed by a NAMED field.** Turn 1's `--output-format json` envelope carries
93
+ `conversation_id`; the wrapper validates it against the UUID grammar and dispatches every later
94
+ turn with `--conversation <id>`. There is no `--continue` fallback in this lane: an id that is
95
+ absent, wrong-typed or malformed stops the run **before turn 2 is spent**, with NO receipt. (It was
96
+ formerly scraped from `agy`'s own run log — a format that is `agy`'s to change, so the pin could
97
+ rot silently; a named field fails loudly instead.)
92
98
 
93
99
  ### Honest residuals (recorded, not engineered away)
94
100
 
@@ -99,9 +105,6 @@ itself can never reveal the very text it asks for, and the wrapper needs no seco
99
105
  - A change set whose parts carry **no unique interior line** in the 24..200-byte window (a single
100
106
  huge minified line, for instance) cannot be proven delivered, so the wrapper **refuses** rather
101
107
  than reviewing unprovably. Split the review, or exclude the blob.
102
- - The conversation id is parsed from `agy`'s own run log, whose format is `agy`'s to change. An
103
- unparseable log **degrades loudly** to `--continue`; correctness still rests on the echo proof,
104
- which fails closed when the wrong conversation answers.
105
108
 
106
109
  ## agy's own permission ask — surfaced, never applied
107
110
 
@@ -9,11 +9,18 @@ works in any project.
9
9
  ```bash
10
10
  curl -fsSL https://antigravity.google/cli/install.sh | bash
11
11
  export PATH="$HOME/.local/bin:$PATH" # add to ~/.bashrc / ~/.zshrc to persist
12
- agy --version # expect 1.1.13 or newer
12
+ agy --version # captured here from 1.1.13 see the capability note below
13
13
  ```
14
14
 
15
15
  - The binary is **`agy`** (not `antigravity`); it installs to `~/.local/bin/agy`.
16
16
  - Keep `$HOME/.local/bin` on `PATH` (the wrapper also prepends it defensively).
17
+ - **There is no version floor.** The docs in this skill were captured from **v1.1.13**, but the
18
+ release that introduced each flag is not measurable from one installed build, so a guessed floor
19
+ would refuse working installs. What actually gates a review is a **capability probe** —
20
+ `agy-review` reads `agy --help` and refuses BEFORE spending a run unless the install advertises
21
+ both **`--output-format`** and **`--disable-slash-commands`**, plus **Node ≥ 22** on `PATH` (the
22
+ review parses agy's JSON envelope in node) and the skill's own `bin/agy-envelope.mjs`. Each
23
+ refusal names the missing capability and the recovery. `agy-run` needs none of these.
17
24
 
18
25
  ## 2. Sign in once (subscription only)
19
26
 
@@ -59,13 +66,19 @@ agy --version
59
66
  echo "say OK" | agy-run -
60
67
  ```
61
68
 
62
- Expected: the version prints (`1.1.13` or newer), then a short reply containing `OK`. If `agy-run`
63
- reports `'agy' not found`, fix your `PATH` (step 1). If it asks you to sign in, complete step 2.
69
+ Expected: a version prints (any — `agy-review` probes its required capabilities, not a version
70
+ floor; `agy-run` probes nothing), then a short reply containing `OK`. If `agy-run` reports
71
+ `'agy' not found`, fix your `PATH` (step 1). If it asks you to sign in, complete step 2. If
72
+ `agy-review` later refuses naming a missing flag or Node, that is the capability probe from step 1,
73
+ not a broken install — follow the recovery it prints.
64
74
 
65
75
  ## Notes
66
76
 
67
- - `agy-run` is headless and text by default; the CLI's 1.1.x `--output-format json` can ride
68
- passthrough (`-- --output-format json`) but the wrapper adds no parsing/validation.
77
+ - `agy-run` is headless and text: `--output-format json` can ride passthrough
78
+ (`-- --output-format json`) but that wrapper adds no parsing/validation. `agy-review` is the one
79
+ that drives `--output-format json` first-class and reads the envelope (a successful run still
80
+ prints text; when its single dispatch or FINAL fed turn exits non-zero it publishes that captured
81
+ stdout unchanged, never an intermediate feed turn's).
69
82
  - `AGY_MODEL` selects the exact model display string; `AGY_TIMEOUT` controls `--print-timeout`.
70
83
  - Extra `agy` flags go after `--`, e.g. `agy-run @prompt.md -- --add-dir .`.
71
84
  - Re-run interactive `agy` only when the OAuth token expires or the account changes.
@@ -1198,7 +1198,7 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
1198
1198
  // The grammar ENUMERATES the ASCII set (no ranges): a locale-collated [A-Za-z] could admit a
1199
1199
  // non-ASCII letter the kit's JS reader then refuses, breaking correlation after a paid run.
1200
1200
  const utf8 = makeSandbox();
1201
- const r2 = run(utf8, { env: { AW_REVIEW_NONCE: 'ré1', LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8' } });
1201
+ const r2 = run(utf8, { env: { AW_REVIEW_NONCE: 'r\u00e91', LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8' } });
1202
1202
  rmSync(utf8.root, { recursive: true, force: true });
1203
1203
  assert.equal(r2.status, 2, 'a non-ASCII nonce letter refuses whatever the locale collation says');
1204
1204
  assert.match(r2.stderr, /safe nonce grammar/);
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "5.9.0",
6
+ "version": "5.10.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -7,9 +7,9 @@
7
7
  //
8
8
  // At Stop time the turn is ENDING. That single fact turns two closing shapes from "discouraged" into
9
9
  // FALSE, so they can be judged mechanically:
10
- // the «what I need from you» slot answering "nothing" — a stopped turn always needs a resume, so
10
+ // * the "what I need from you" slot answering "nothing" — a stopped turn always needs a resume, so
11
11
  // the slot must NAME it. "Nothing needed" is honest only while work is actually running;
12
- // a first-person promise of imminent work in the closing block — the turn is over, so the work
12
+ // * a first-person promise of imminent work in the closing block — the turn is over, so the work
13
13
  // is not starting. This is the announce-and-stop shape.
14
14
  // A promise GATED on something named (after your yes, when CI finishes) is honest and passes.
15
15
  //
@@ -19,6 +19,12 @@
19
19
  // required a second classifier to decide what counts as a real ask, the rule was DELETED and its
20
20
  // residual written down instead — twice. The mode contract lists every residual by name.
21
21
  //
22
+ // ONE VOCABULARY, ENGLISH. The kit ships English markers and enumerates no other language: a
23
+ // language list would be a shipped guess about somebody else's dialogue, and it never stops growing.
24
+ // A deployment whose dialogue language is not English extends the arrays in ITS OWN placed copy —
25
+ // this file is copied into the project and self-contained, so that edit is local and survives an
26
+ // uninstalled kit. Until then such a deployment gets no detection, which the mode contract states.
27
+ //
22
28
  // HONEST LIMIT, stated because it bounds what this can claim: a Stop hook cannot un-send the turn it
23
29
  // judges. This is DETECTION — it converts a silent recurrence into a loud one. It never blocks, and
24
30
  // every anomaly path exits 0: a hook that fires on every turn must not become the blocker or the
@@ -39,18 +45,17 @@ const EXIT_OK = 0;
39
45
  // The three slots, matched on their LABEL and ANCHORED TO A LINE START. The anchor is not cosmetic:
40
46
  // a message that DISCUSSES this contract — which the sessions this guard was built for do constantly
41
47
  // — mentions the labels inline, and an unanchored match would judge that prose instead of the real
42
- // closing block. Russian is the dialogue language this contract was written for; the English twins
43
- // keep the guard usable in an English-dialogue deployment.
48
+ // closing block.
44
49
  const LINE_LEAD = '^[ \\t\\-•]*';
45
50
  const SLOT_PATTERNS = Object.freeze({
46
- now: new RegExp(`${LINE_LEAD}(?:что сейчас|where we are|state now)\\s*[::]`, 'gim'),
47
- fromYou: new RegExp(`${LINE_LEAD}(?:что нужно от вас|что от вас|what i need from you|from you)\\s*[::]`, 'gim'),
48
- next: new RegExp(`${LINE_LEAD}(?:что дальше|what(?:'s| is)? next|next)\\s*[::]`, 'gim'),
51
+ now: new RegExp(`${LINE_LEAD}(?:now|where we are|state now)\\s*[::]`, 'gim'),
52
+ fromYou: new RegExp(`${LINE_LEAD}(?:what i need from you|from you)\\s*[::]`, 'gim'),
53
+ next: new RegExp(`${LINE_LEAD}(?:what(?:'s| is)? next|next)\\s*[::]`, 'gim'),
49
54
  });
50
55
 
51
56
  // Quoted and fenced material is EXAMPLE text, not the turn's own closing block. Stripped before any
52
57
  // label search — otherwise pasting the very specimen this guard catches would make the guard judge
53
- // the paste. Emphasis marks are stripped after, so `**Что сейчас:**` still parses.
58
+ // the paste. Emphasis marks are stripped after, so `**Now:**` still parses.
54
59
  // Scanned line by line rather than matched as one regex: an UNCLOSED fence must run to the end of
55
60
  // the message. A pattern that only removes CLOSED fences would take an unfinished example's labels
56
61
  // for a real block — judging a demonstration as if it were the turn, which is precisely the mistake
@@ -84,9 +89,11 @@ const stripFences = (text) => {
84
89
 
85
90
  const BLOCKQUOTE_LINE = /^[ \t]*>.*$/gm;
86
91
  const EMPHASIS_MARKS = /[*_`]/g;
87
- // A model writes «Whats next» and «Ill» with a typographic apostrophe far more often than with the
88
- // ASCII one, so matching only ASCII made the English half fail silently on ordinary output.
89
- const APOSTROPHES = /[’‘ʼ‛]/g;
92
+ // A model writes "What's next" and "I'll" with a typographic apostrophe far more often than with the
93
+ // ASCII one, so matching only ASCII made every label and every promise marker fail silently on
94
+ // ordinary output. U+02BC rides as an escape because it is a modifier LETTER, and this family's
95
+ // sources carry no non-ASCII letter; the escaped class matches exactly the same characters.
96
+ const APOSTROPHES = /[’‘\u02bc‛]/g;
90
97
  const toProse = (text) => stripFences(text)
91
98
  .replace(BLOCKQUOTE_LINE, '')
92
99
  .replace(EMPHASIS_MARKS, '')
@@ -103,69 +110,93 @@ const openingAnswer = (slot) => {
103
110
  const at = slot.search(CLAUSE_END);
104
111
  return at === -1 ? slot : slot.slice(0, at);
105
112
  };
106
- // A rule excusing «…, ничего другого не нужно» was built, tightened twice, and then DELETED. Every
113
+ // A rule excusing "…, nothing else is needed" was built, tightened twice, and then DELETED. Every
107
114
  // version let a banned answer through behind some harmless prefix, and the next version would have
108
115
  // needed an "is this a real ask" recogniser — a second lexical classifier with its own edge set. The
109
116
  // residual is stated instead of coded: a comma-joined qualifier IS flagged, and the writer's fix is
110
117
  // a clause break, which the first-clause rule already passes.
111
118
 
112
119
  const NOTHING_FORMS = Object.freeze([
113
- 'ничего',
114
- 'ничем',
115
- 'не требуется',
116
- 'не нужно',
117
- 'не нужен',
118
- 'нет необходимости',
119
120
  'nothing',
120
121
  'none',
121
122
  'n/a',
122
123
  'no action',
124
+ 'no need',
125
+ 'not needed',
126
+ 'not required',
127
+ ]);
128
+
129
+ // -- the "what is happening NOW" slot is not a report of what was DONE ----------------------------
130
+ // The third judgeable shape. The three slots answer three different questions; when the now-slot
131
+ // opens with finished work, the reader's one question — what is running — is buried behind a
132
+ // summary they can already read in the message body above.
133
+ // Judged by TOKEN ORDER, the technique the promise/gate rule uses: a COMPLETION marker standing
134
+ // BEFORE the first CURRENCY marker means the running fact was buried. Two consequences are
135
+ // deliberate — a slot with NO currency marker is never judged here (nothing running is a legitimate
136
+ // state, and whether that is honest is the from-you slot's question), and a slot that LEADS with
137
+ // what is running passes whatever it adds afterwards.
138
+ // Stated residual, in this file's own style: a CURRENT state phrased with a COMPLETION MARKER
139
+ // ("the work is closed, waiting on your call" — `closed` is in the list below) is flagged, and the
140
+ // writer's fix is to phrase the state rather than the transition. Telling that from a real
141
+ // completion report needs a classifier this layer lacks. The specimen is deliberately one the list
142
+ // really carries: a residual written with a word the code does not match would document a false
143
+ // positive that cannot happen, which is worse than no residual at all.
144
+ const COMPLETION_FORMS = Object.freeze([
145
+ 'done', 'completed', 'finished', 'closed', 'landed', 'shipped', 'merged', 'published',
146
+ 'committed', 'fixed', 'added', 'removed', 'updated', 'recorded', 'measured', 'marked',
147
+ 'verified', 'ran', 'wrote',
148
+ "i've", 'i have',
149
+ ]);
150
+
151
+ // Running STATE only, never a time-deictic. "now" is the slot's own label, so accepting it as a
152
+ // currency marker would put one at position 0 of nearly every slot and every completion after it
153
+ // would read as honest — one word defeating the whole rule. A marker that names WHEN rather than
154
+ // WHAT-IS-HAPPENING cannot carry this judgement.
155
+ const CURRENCY_FORMS = Object.freeze([
156
+ 'running', 'in progress', 'waiting', 'awaiting', 'pending', 'is up', 'under way', 'underway',
157
+ 'in flight', 'blocked on',
123
158
  ]);
124
159
 
125
160
  // First-person promises of imminent work. Present tense counts: at Stop it describes work that is
126
161
  // NOT running.
127
- // Action verbs only. «считаю» and «смотрю» were dropped rather than qualified: they are as often
128
- // stative («считаю задачу завершённой», «смотрю на вопрос как на закрытый») as they are promises, and
129
- // a form that cannot tell the two apart buys detection with false flags.
130
- // The English markers are pronoun+modal rather than action verbs, so they cannot tell «I'll start» from
131
- // «I'll wait». Waiting is not work: it is what a turn that ENDS actually does, and it needs no gate to
132
- // be honest. So a modal followed by a waiting verb is excluded rather than the modal being dropped —
133
- // dropping it would need an open-ended list of every action verb English can put after it.
162
+ // The markers are pronoun+modal or pronoun+verb, so they cannot tell "I'll start" from "I'll wait".
163
+ // Waiting is not work: it is what a turn that ENDS actually does, and it needs no gate to be honest.
164
+ // So a modal followed by a waiting verb is excluded rather than the modal being dropped — dropping
165
+ // it would need an open-ended list of every action verb English can put after it.
134
166
  const WAITING_CONTINUATIONS = Object.freeze([
135
167
  "i'll wait", 'i will wait', "i'll be waiting", "i'll stand by", 'i will stand by',
136
168
  "i'll hold", 'i will hold', "i'll stay", 'i will stay', "i'll remain", 'i will remain',
137
169
  ]);
138
170
 
139
171
  const PROMISE_FORMS = Object.freeze([
140
- 'беру', 'начинаю', 'перехожу', 'сажусь', 'продолжаю', 'иду', 'проверяю', 'пишу', 'строю',
141
- 'планирую', 'разбираюсь',
142
- "i'll", 'i will', "i'm going to", 'i am going to', 'i start', 'i begin', 'next i',
172
+ "i'll", 'i will', "i'm going to", 'i am going to', "i'm starting", "i'm taking",
173
+ 'i start', 'i begin', 'i take', 'i move on', 'next i',
143
174
  ]);
144
175
 
145
176
  // A gate the promise may depend on. What is actually checked is TOKEN ORDER inside one segment: a
146
177
  // gate is treated as excusing a promise when it appears in the same segment and earlier in it. That
147
178
  // is a lexical approximation of dependency, not dependency itself, and it carries two named
148
- // residuals — an honest TRAILING gate («беру, когда вы скажете») is flagged, and a gate belonging to
149
- // an earlier comma-clause («если тест упадёт, сообщу, а сейчас начинаю…») wrongly excuses a promise
150
- // that follows it. Both are accepted limits of a lexical layer, documented in the mode contract.
179
+ // residuals — an honest TRAILING gate ("I'll take it when you say so") is flagged, and a gate
180
+ // belonging to an earlier comma-clause ("if the test fails I'll report, and now I start the next
181
+ // class") wrongly excuses a promise that follows it. Both are accepted limits of a lexical layer,
182
+ // documented in the mode contract.
151
183
  const CONDITIONAL_FORMS = Object.freeze([
152
- 'после', 'когда', 'если', 'как только',
153
- 'after', 'when', 'if', 'as soon as', 'once', 'pending',
184
+ 'after', 'when', 'if', 'as soon as', 'once', 'upon', 'pending',
154
185
  ]);
155
186
 
156
- // Segment boundaries for the promise/gate rule. Deliberately NOT the comma: «если вы согласны, беру
157
- // класс» is one honest thought, and splitting it would flag the shape the rule exists to permit.
187
+ // Segment boundaries for the promise/gate rule. Deliberately NOT the comma: "if you agree, I'll take
188
+ // the class" is one honest thought, and splitting it would flag the shape the rule exists to permit.
158
189
  const PROMISE_SEGMENT_BREAK = /;|\n|[.!?](?=\s|$)/;
159
190
 
160
- // Word-bounded matching, Unicode-aware. JavaScript's `\b` is ASCII-only and therefore useless here:
161
- // with raw substring matching «не нужно» hides inside the honest «мне нужно ваше подтверждение»
162
- // («м|не нужно»), so the most natural way to name a real ask was read as answering "nothing".
163
- // The trailing exclusion covers the interrogative particle: «не нужно ЛИ добавить тест» asks a
164
- // question, it does not decline help.
191
+ // Word-bounded matching, Unicode-aware. The markers are English but the TEXT is the project's
192
+ // dialogue language, which need not be ASCII, and JavaScript's `\b` is ASCII-only: under it every
193
+ // non-ASCII letter reads as a word break, so an English marker sitting inside a non-ASCII word would
194
+ // match as if it stood alone. The Unicode class costs nothing and keeps that door shut; on the
195
+ // English side it is what stops `none` matching inside `nonexistent`.
165
196
  const WORD_CHAR = '\\p{L}\\p{N}';
166
197
  const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
167
198
  const boundedPattern = (phrase) => new RegExp(
168
- `(?<![${WORD_CHAR}])${escapeForRegExp(phrase)}(?![${WORD_CHAR}])(?!\\s+ли(?![${WORD_CHAR}]))`,
199
+ `(?<![${WORD_CHAR}])${escapeForRegExp(phrase)}(?![${WORD_CHAR}])`,
169
200
  'iu',
170
201
  );
171
202
  const firstMatchAt = (haystack, needles) => needles.reduce((best, needle) => {
@@ -175,6 +206,25 @@ const firstMatchAt = (haystack, needles) => needles.reduce((best, needle) => {
175
206
  }, -1);
176
207
  const containsAny = (haystack, needles) => firstMatchAt(haystack, needles) !== -1;
177
208
 
209
+ // Nearly every English completion marker doubles as an ADJECTIVE, which the Russian verbs this rule
210
+ // was first written against did not: "the updated test suite is running" is an honest now-slot that
211
+ // leads with the running fact, and reading `updated` there as a completion report would flag it. A
212
+ // marker sitting directly after a DETERMINER is attributive — it modifies the noun after it instead
213
+ // of reporting a transition — so it is not counted.
214
+ // Residual, stated: this also loses a completion phrased attributively ("the finished council, the
215
+ // matrix is running" passes). That trade is deliberate. A false flag on a correct slot is noise from
216
+ // a hook that fires on EVERY turn, and noise is the failure this file is most careful to avoid; a
217
+ // missed marginal shape is not.
218
+ const DETERMINERS = Object.freeze(['the', 'a', 'an', 'this', 'that', 'these', 'those', 'its', 'my', 'our', 'your', 'their']);
219
+ const AFTER_DETERMINER = new RegExp(`(?:^|[^${WORD_CHAR}])(?:${DETERMINERS.join('|')})[ \\t]+$`, 'iu');
220
+ const matchesIn = (haystack, needle) => [...haystack.matchAll(new RegExp(boundedPattern(needle).source, 'giu'))]
221
+ .map((match) => match.index);
222
+ const firstReportedAt = (haystack, needles) => needles.reduce((best, needle) => {
223
+ const at = matchesIn(haystack, needle).find((index) => !AFTER_DETERMINER.test(haystack.slice(0, index)));
224
+ if (at === undefined) return best;
225
+ return best === -1 ? at : Math.min(best, at);
226
+ }, -1);
227
+
178
228
  const normalise = (value) => (typeof value === 'string'
179
229
  ? value.replace(EMPHASIS_MARKS, '').replace(APOSTROPHES, "'").toLowerCase()
180
230
  : '');
@@ -197,7 +247,7 @@ const groupLabels = (labels) => labels.reduce((state, label) => {
197
247
  : { current: candidate, completed: state.completed };
198
248
  }, { current: [], completed: null });
199
249
 
200
- // findStateBlock(text) { now, fromYou, next } or null. The LAST STARTED candidate decides: if the
250
+ // findStateBlock(text) -> { now, fromYou, next } or null. The LAST STARTED candidate decides: if the
201
251
  // message ends mid-block, the turn did not end on a block at all, and falling back to an earlier
202
252
  // complete one would judge text the turn already moved past.
203
253
  export const findStateBlock = (text) => {
@@ -213,7 +263,7 @@ export const findStateBlock = (text) => {
213
263
  return block;
214
264
  };
215
265
 
216
- // decideStop({ closingText, requireBlock }) { ok, reasons }. Never throws: junk input decides
266
+ // decideStop({ closingText, requireBlock }) -> { ok, reasons }. Never throws: junk input decides
217
267
  // "no block", which is a reportable state rather than a crash. An ABSENT block is reported only
218
268
  // under `requireBlock`: this kit does not mandate the three-part block, so warning about its absence
219
269
  // by default would fire on nearly every turn of a project that never adopted it — noise, from a hook
@@ -225,7 +275,7 @@ export const decideStop = (options) => {
225
275
  return requireBlock
226
276
  ? {
227
277
  ok: false,
228
- reasons: ['no state block: the closing message must end with «что сейчас · что нужно от вас · что дальше»'],
278
+ reasons: ['no state block: the closing message must end with "now / what I need from you / what\'s next"'],
229
279
  }
230
280
  : { ok: true, reasons: [] };
231
281
  }
@@ -236,6 +286,11 @@ export const decideStop = (options) => {
236
286
  'from-you slot answers "nothing": the turn is ENDING, so a resume from the maintainer IS required — name the real unblocker instead',
237
287
  );
238
288
  }
289
+ if (buriesTheRunningFact(normalise(block.now))) {
290
+ reasons.push(
291
+ 'the now-slot reports finished work before it names what is running — "now" is the state at this instant; move what you completed into the message body above the block',
292
+ );
293
+ }
239
294
  if (hasUngatedPromise(normalise(block.next))) {
240
295
  reasons.push(
241
296
  'announce-and-stop: the what-next slot promises imminent first-person work while the turn ends — either do it in this turn, or state what the work waits on',
@@ -244,6 +299,13 @@ export const decideStop = (options) => {
244
299
  return { ok: reasons.length === 0, reasons };
245
300
  };
246
301
 
302
+ const buriesTheRunningFact = (nowSlot) => {
303
+ const currencyAt = firstMatchAt(nowSlot, CURRENCY_FORMS);
304
+ if (currencyAt === -1) return false;
305
+ const completionAt = firstReportedAt(nowSlot, COMPLETION_FORMS);
306
+ return completionAt !== -1 && completionAt < currencyAt;
307
+ };
308
+
247
309
  const hasUngatedPromise = (nextSlot) => nextSlot.split(PROMISE_SEGMENT_BREAK).some((segment) => {
248
310
  const promiseAt = firstMatchAt(segment, PROMISE_FORMS);
249
311
  if (promiseAt === -1) return false;
@@ -285,7 +347,7 @@ const parsePayload = (rawInput) => {
285
347
  //
286
348
  // An EMPTY string is delivered text, not an absent one: a turn that ended with no prose really did
287
349
  // end without a closing block, and `--require-block` should be able to say so.
288
- // runHook(raw, {requireBlock}) a decision, or null when there is genuinely nothing to judge.
350
+ // runHook(raw, {requireBlock}) -> a decision, or null when there is genuinely nothing to judge.
289
351
  // Never throws.
290
352
  export const runHook = (rawInput, deps) => {
291
353
  const requireBlock = (deps ?? {}).requireBlock === true;
@@ -7,12 +7,15 @@ The **config writer** for `docs/ai/orchestration.json` — the answer to *"set m
7
7
 
8
8
  **Map the user's plain language → explicit ops** (the kit ships no NL parser; it performs no `all`-magic, so you expand scope explicitly, asking when unclear):
9
9
 
10
- | user says (RU/EN) | op | scope |
10
+ The intents are listed in English; a user speaking any other language expresses the same four, and
11
+ mapping their wording onto them is your job, not a shipped phrase list.
12
+
13
+ | user means | op | scope |
11
14
  |---|---|---|
12
- | "оба ревьюят" / "both review" | `--set <activity>.review=council` | **disambiguate**: which activity? If both, pass `--set plan-authoring.review=council --set plan-execution.review=council`. |
13
- | "один ревьюер" / "one reviewer" | `--set <activity>.review=reviewed` | per the named activity, else ask |
14
- | "делегируй исполнение" / "delegate execution" | `--set plan-execution.execute=delegated` | execution only |
15
- | "верни как было / сам" / "revert / do it myself" | `--unset <activity>.<slot>` | the named slot → its computed default |
15
+ | "both review" | `--set <activity>.review=council` | **disambiguate**: which activity? If both, pass `--set plan-authoring.review=council --set plan-execution.review=council`. |
16
+ | "one reviewer" | `--set <activity>.review=reviewed` | per the named activity, else ask |
17
+ | "delegate execution" | `--set plan-execution.execute=delegated` | execution only |
18
+ | "revert / do it myself" | `--unset <activity>.<slot>` | the named slot → its computed default |
16
19
 
17
20
  Run **`node ${CLAUDE_SKILL_DIR}/tools/set-recipe.mjs [--set <activity>.<slot>=<recipe>]… [--unset <activity>.<slot>]… [--write] [--json]`**:
18
21