@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.
@@ -19,12 +19,12 @@ this as preventing the defect is describing it wrongly.
19
19
  - **The «what I need from you» slot answering "nothing".** A turn that ends is by definition idle, so
20
20
  a resume from the reader IS required; a slot saying otherwise states something untrue. Judged over
21
21
  the slot's **first clause only** — a slot that names a real ask and then, after a `;` or a full
22
- stop, adds «ничего другого не нужно» is honest and passes. A COMMA does not open a new clause, so
23
- the comma-joined version of that sentence is flagged; see the residuals. Hedged forms count: «не
24
- требуется», «ваше присутствие не требуется прямо сейчас», «nothing right now», «n/a».
25
- - **Announce-and-stop.** The «what next» slot promising first-person imminent work («беру…»,
26
- «начинаю…», «I'll start…») while the turn ends. A promise **gated on something named** («после
27
- вашего да беру…», «once CI finishes…») states a dependency instead of a false start, and passes.
22
+ stop, adds «nothing else is needed» is honest and passes. A COMMA does not open a new clause, so
23
+ the comma-joined version of that sentence is flagged; see the residuals. Hedged forms count: «not
24
+ required», «your presence is not required right now», «nothing right now», «n/a».
25
+ - **Announce-and-stop.** The «what next» slot promising first-person imminent work («I take the
26
+ class…», «I'll start…») while the turn ends. A promise **gated on something named** («after your
27
+ yesI publish», «once CI finishes…») states a dependency instead of a false start, and passes.
28
28
 
29
29
  **A message with no recognisable block at all is SILENT by default.** This kit does not mandate the
30
30
  three-part closing block — it is a per-project dialogue contract — so warning whenever a block is
@@ -75,10 +75,13 @@ gets no detection at all**, silently — not a warning on every turn, which is w
75
75
  condition would produce. An EMPTY delivered message is different: it is text, so a turn that ended
76
76
  with no prose is judged as having no block, which `--require-block` will report.
77
77
 
78
- **Language.** The slot labels and both banned sets carry Russian and English twins, because the
79
- contract this enforces was written for a Russian-dialogue deployment. A deployment in another
80
- dialogue language gets no detection until its labels are added that is a real limit, not a
81
- configuration you can set today.
78
+ **Language one vocabulary, English, and no list of others.** The slot labels and every phrase set
79
+ are English. The kit enumerates no second language: a shipped language list is a guess about somebody
80
+ else's dialogue, and it never stops growing. A deployment whose dialogue language is not English
81
+ extends the arrays **in its own placed copy** — the runtime is copied into the project and
82
+ self-contained, so that edit is local and survives an uninstalled kit. Until it is made, such a
83
+ deployment gets **no detection**, silently. That is a real limit, stated rather than hidden, and it is
84
+ not a configuration you can set today.
82
85
 
83
86
  **No writer yet — and why, plainly.** Every other placed thing in this kit arrives through a
84
87
  consent-gated writer. This one does not, deliberately:
@@ -162,28 +165,33 @@ the warning text. It is not a sandbox and not a permission control.
162
165
 
163
166
  - **Detection, not prevention** — restated because it is the one thing that must not blur.
164
167
  - **The judgement is LEXICAL, and that is a layer with limits, not a temporary weakness.** It matches
165
- slot labels and phrase sets on Unicode-aware word boundaries, so an honest «мне нужно ваше
166
- подтверждение» is not read as «не нужно» and an interrogative «не нужно ли…» is not read as a
167
- refusal. It cannot parse a sentence, and it cannot recognise a wording it has never been told
168
- about. Two rules that tried to close an edge here were **deleted rather than tightened a third
169
- time**, because each next version needed a second classifier the residuals below are what
170
- replaced them. A named specimen of the cost: «подтвердите, что ничего не упущено» is a real ask
171
- and is FLAGGED, because the banned word sits inside it. Phrase the ask without the word.
172
- - **A comma-joined qualifier is flagged.** «одно да, ничего другого не нужно» warns; the same
173
- sentence with a `;` or a full stop passes. The rule that tried to exempt the comma form kept
174
- letting a real "nothing" through behind a harmless prefix, so it was removed and the false flag is
175
- accepted instead. It costs one line and names its own fix.
168
+ slot labels and phrase sets on **Unicode-aware** word boundaries the markers are English but the
169
+ message is the project's dialogue language, which need not be ASCII, and JavaScript's `\b` would
170
+ read every non-ASCII letter as a word break and match a marker sitting inside a longer word. On the
171
+ English side the same class is what stops «none» matching inside «nonexistent». It cannot parse a
172
+ sentence, and it cannot recognise a wording it has never been told about. Two rules that tried to
173
+ close an edge here were **deleted rather than tightened a third time**, because each next version
174
+ needed a second classifier the residuals below are what replaced them. A named specimen of the
175
+ cost: «confirm that nothing was missed» is a real ask and is FLAGGED, because the banned word sits
176
+ inside it. Phrase the ask without the word.
177
+ - **A comma-joined qualifier is flagged.** «one yes, nothing else is needed» warns; the same sentence
178
+ with a `;` or a full stop passes. The rule that tried to exempt the comma form kept letting a real
179
+ "nothing" through behind a harmless prefix, so it was removed and the false flag is accepted
180
+ instead. It costs one line and names its own fix.
176
181
  - **A condition is bound to a promise by TOKEN ORDER inside one segment, which is an approximation.**
177
- «после вашего даберу класс» passes; «беру класс, а если тест упадёт, сообщу» is flagged. Two
178
- known misreadings follow from the approximation, both accepted: an honest TRAILING gate («беру,
179
- когда вы скажете») is flagged, and a gate belonging to an earlier comma-clause («если тест упадёт,
180
- сообщу, а сейчас начинаю…») wrongly excuses the promise after it. Leading with the gate avoids the
181
- first; the second is a miss this layer cannot close without parsing.
182
- - **The English side is weaker than the Russian side, structurally.** Russian promises are action
183
- verbs («беру», «начинаю»); English ones are pronoun+modal («I'll», «I will»), which cannot tell
184
- starting from waiting. Waiting is explicitly excluded «I'll wait for your approval» passes,
185
- because waiting is what a turn that ends actually does but the exclusion is a list, and an
186
- unusual way of saying "I am waiting" will be flagged.
182
+ «after your yesI take the class» passes; «I take the class, and if the test fails I'll report»
183
+ is flagged. Two known misreadings follow from the approximation, both accepted: an honest TRAILING
184
+ gate («I take it when you say so») is flagged, and a gate belonging to an earlier comma-clause («if
185
+ the test fails I'll report, and now I start the next class») wrongly excuses the promise after it.
186
+ Leading with the gate avoids the first; the second is a miss this layer cannot close without
187
+ parsing.
188
+ - **The promise markers are a MIXED set, and each half misses differently.** Some are pronoun+modal
189
+ («I'll», «I will», «I'm going to»), which name no verb of their own and so cannot tell starting
190
+ from waiting; waiting is therefore excluded by an explicit list«I'll wait for your approval»
191
+ passes, because waiting is what a turn that ends actually does — and an unusual way of saying "I am
192
+ waiting" is flagged. The others are pronoun+verb («I take», «I start», «I begin», «I move on»),
193
+ which name the action but only in the exact wording listed: a synonym nobody wrote down is missed
194
+ entirely, and a stative use of a listed verb («I take that as settled») is a false flag.
187
195
  - **A host that does not deliver `last_assistant_message` gets no detection**, silently. There is no
188
196
  transcript fallback, on purpose — see above.
189
197
  - **Without `--require-block`, a turn that drops the block entirely is not detected.** That is the
@@ -1,6 +1,6 @@
1
1
  // check-docs-size-cli.test.mjs — runCli branch pins the subprocess smokes cannot reach
2
2
  // in-process (Phase-5 coverage fill; the main spec file is parity-frozen, so these ride a
3
- // colocated file): the unknown-argument refusal and the written-empty-index guard.
3
+ // colocated file): the unknown-argument refusal and the pre-write symlink refusal on the index path.
4
4
  import { describe, it } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
@@ -20,7 +20,7 @@ describe('check-docs-size runCli — refusal branches', () => {
20
20
  assert.match(stderr, /Unknown argument: --bogus/);
21
21
  });
22
22
 
23
- it('--write-index landing on a sink path (index stat size 0) is the loud written-empty refusal', async () => {
23
+ it('--write-index refuses a symlinked index path BEFORE writing, naming the path', async () => {
24
24
  const root = mkdtempSync(join(tmpdir(), 'cds-cli-'));
25
25
  try {
26
26
  mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
@@ -51,8 +51,8 @@ the project's internal `docs/ai` structure version, the stamp filename, or the i
51
51
  vocabulary — that number is inert here and only confuses; it belongs to *Version disclosure* (below).
52
52
  Frame the success itself plainly, in the **user's conversational language** (never hardcode a phrase):
53
53
  - a **zero-diff no-op `upgrade`** (step 4) → **settings already current — no update is required**
54
- (illustrative tone for a Russian-speaking user, an example of the meaning, not a literal string to
55
- embed: *«Настройки уже актуальны — обновление не требуется»*);
54
+ (that is the MEANING to convey, not a literal string to embed say it in the user's conversational
55
+ language, in your own words);
56
56
  - a **fresh `bootstrap`** → its normal "deployed and ready" success, minus the number.
57
57
 
58
58
  **Version block — the installed package versions, fed from `--json`** (the `docs/ai` structure version
@@ -71,6 +71,7 @@ Apply this as part of §2 before any user-facing summary:
71
71
  - **No condescension, no filler.** Own a miss plainly and fix it in the same message.
72
72
  - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
73
73
  - **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.
74
+ - **The closing state block answers three DIFFERENT questions.** Close a user-facing message with three labelled slots — *now* · *what I need from you* · *what's next*. The slot LABELS stay ENGLISH — an English label is what lets a state-block checker FIND the block and its slots at all; everything written INTO a slot is in the project's dialogue language; when that language is not English, the checker's English phrase sets do not judge those values. **Now** = the state at this instant: what is RUNNING, or what the work is stopped on. It is **never a report of finished work** — what you completed goes in the message BODY, above the block. **From you** = the real unblocker, named; a turn that is ENDING always has one. **Next** = what follows. A *now* slot that opens with what was completed buries the one fact the reader opened the message for, and the three slots collapse into one restatement.
74
75
 
75
76
  ### 2.6. Planning, review & process-fidelity invariants
76
77
  Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
@@ -141,6 +141,7 @@ const RAW_BACKENDS = [
141
141
  ],
142
142
  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",
143
143
  notes: [
144
+ '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',
144
145
  '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',
145
146
  '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',
146
147
  'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
@@ -8,8 +8,8 @@
8
8
  // index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact)
9
9
  //
10
10
  // Each was prose in references/modes/upgrade.md that an agent performed by hand. One command instead
11
- // of four is deliberate: four independent runs would be four chances to skip one, and the mode doc now
12
- // has a single invocation point whose four outcome lines it relays.
11
+ // of five is deliberate: five independent runs would be five chances to skip one, and the mode doc now
12
+ // has a single invocation point whose five outcome lines it relays.
13
13
  //
14
14
  // The contract (pinned by this module's tests):
15
15
  // • --reconcile is REQUIRED. A bare run is a usage error, so nothing writes by accident.
@@ -17,7 +17,7 @@
17
17
  // • The ops run in a FIXED order and one op's failure NEVER skips the rest: every op reports its own
18
18
  // token, and the exit is non-zero when any of them failed.
19
19
  // • The deployment gate runs ONCE, before any op: an absent/symlinked docs/ai stops the whole run
20
- // with the gate's own message rather than four copies of it.
20
+ // with the gate's own message rather than five copies of it.
21
21
  //
22
22
  // Output is ENGLISH/structured (repo-artifact Hard Constraint); the agent localizes when narrating.
23
23
  // Exit codes: 0 every op fine · 1 an op failed, or the deployment gate stopped the run · 2 usage.
@@ -138,7 +138,7 @@ export const main = (argv = [], ctx = {}) => {
138
138
  const cwd = resolve(args.cwd ?? ctx.cwd ?? process.cwd());
139
139
  const deps = ctx.deps ?? {};
140
140
  // ONE deployment gate for the whole run (see the header): with no docs/ai there is nothing to
141
- // reconcile, and four identical STOPs would read as four separate problems.
141
+ // reconcile, and five identical STOPs would read as five separate problems.
142
142
  assertDocsAiDeployment(cwd, deps, { noun: 'the project configuration', rel: 'under docs/ai' });
143
143
  const outcomes = runEnsures({ cwd, kitRoot: ctx.kitRoot ?? KIT_ROOT, dryRun: args.dryRun, deps, only: args.only });
144
144
  return {
@@ -49,7 +49,19 @@ Apply this as part of §2 before any user-facing summary:
49
49
  - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.`;
50
50
  const COMMS_PRIOR_AD054 = `${COMMS_PRIOR_PRE_AD054}
51
51
  - **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.`;
52
- export const COMMS_PRIORS = [COMMS_PRIOR_PRE_AD054, COMMS_PRIOR_AD054];
52
+ // The canon that shipped between the plain-language bullet and the closing-state-block contract.
53
+ // Written out in full rather than composed from the constant above: the plain-language bullet LEADS
54
+ // the block, so an append-based composition would produce a body that never shipped and would then
55
+ // match no deployed file at all.
56
+ const COMMS_PRIOR_PLAIN_LANGUAGE = `### 2.x. Communication (user-facing messages)
57
+ Apply this as part of §2 before any user-facing summary:
58
+ - **Plain language.** User-facing narration is short, clear, plain words of the dialogue language; when the dialogue language is not English, transliterated English jargon is banned — an English term survives only as the NAME of a thing (a flag / command / file / test), glossed in plain words when helpful; plain English stays plain for English-dialogue users.
59
+ - **Deliver the artifact IN the message** — paste the prompt / diff / version / command inline; never "see §X / open the file / run it and you'll see" as a *substitute* for showing what was asked.
60
+ - **Lead with the result**, then the details; show exactly what was asked — no deflection, no "almost done" when the ask was the finished thing.
61
+ - **No condescension, no filler.** Own a miss plainly and fix it in the same message.
62
+ - **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
63
+ - **Live host/session facts are tool-composed only.** Any claim about the current host or session state (prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts) must trace to **live tool output** from **this session**; a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection.`;
64
+ export const COMMS_PRIORS = [COMMS_PRIOR_PRE_AD054, COMMS_PRIOR_AD054, COMMS_PRIOR_PLAIN_LANGUAGE];
53
65
 
54
66
  const stripCr = (line) => (line.endsWith('\r') ? line.slice(0, -1) : line);
55
67
  const isBoundary = (bareLine) => bareLine === '---' || /^#{2,3} /.test(bareLine);
@@ -31,6 +31,8 @@ const GITLINK_MODE = '160000';
31
31
  // Raw bytes in, raw bytes out: `ls-files -s -z` emits mode, object and stage, then a TAB, then the
32
32
  // path, then a NUL — and the path half is never decoded before it has been matched, so a name
33
33
  // carrying a tab or a newline survives intact (splitting on lines would mangle it).
34
+ // The object id is carried through rather than dropped: the record already holds it, and a consumer
35
+ // that must read what the index HOLDS (not what the worktree happens to show) needs exactly that.
34
36
  const parseIndexEntries = (buf) => {
35
37
  const entries = [];
36
38
  let start = 0;
@@ -41,7 +43,7 @@ const parseIndexEntries = (buf) => {
41
43
  const tab = record.indexOf(TAB);
42
44
  if (tab !== -1) {
43
45
  const head = record.subarray(0, tab).toString('utf8').split(' ');
44
- entries.push({ mode: head[0], stage: Number(head[2]), path: record.subarray(tab + 1) });
46
+ entries.push({ mode: head[0], sha: head[1], stage: Number(head[2]), path: record.subarray(tab + 1) });
45
47
  }
46
48
  start = end + 1;
47
49
  }