paqad-ai 1.76.1 → 1.76.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # paqad-ai
2
2
 
3
+ ## 1.76.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 7ce4c34: fix: stop the `Stop says:` narration leak — silence Stop-hook `{systemMessage}` prose
8
+
9
+ Claude Code changed how it renders Stop-hook output: a Stop-hook `{systemMessage}` now renders on Desktop as literal `Stop says:` lines, inverting the premise the #368/#409 narration design was built on (that the channel was invisible, so the agent speaks the receipt and the hook echo is a silent backchannel). With the host inverted, paqad's three Stop-event emitters poured user-facing prose into that channel on every turn, across every onboarded repo and machine — duplicating the receipt the agent already speaks and leaking the model-only narration advisory into the developer's chat.
10
+
11
+ The Stop hooks no longer emit user-facing `{systemMessage}` prose:
12
+
13
+ - `runtime/scripts/verify-backstop.mjs` (`hook-completion`): writes nothing on a pass/inconclusive turn and only a model-only `{decision:'block'}` reason on a hard failure. Enforcement is unchanged — the block reason reaches the model (never rendered on either host) and the git/CI backstop still hard-fails with exit 2.
14
+ - `runtime/hooks/capability-gate.mjs` (`completion` seam): no longer echoes narration on the allow path; the `pre-mutation` (PreToolUse) seam is unchanged.
15
+ - `runtime/hooks/stage-marker-parse.mjs`: still records every parsed marker to the ledger; only the chat echo is removed.
16
+
17
+ The developer-facing channel is the agent's own final message (the #409 contract), which was always the intended primary channel. The narration contract docs and the verification module doc are corrected to state the new host behavior. Decision `D-01KZV9HFGDXZ03J0S6P9BTQ53Q` records the approach.
18
+
3
19
  ## 1.76.1
4
20
 
5
21
  ### Patch Changes
package/dist/cli/index.js CHANGED
@@ -36251,7 +36251,7 @@ init_cancelled_error();
36251
36251
  init_events();
36252
36252
 
36253
36253
  // src/index.ts
36254
- var VERSION = "1.76.1";
36254
+ var VERSION = "1.76.2";
36255
36255
 
36256
36256
  // src/cli/commands/audit.ts
36257
36257
  init_esm_shims();
package/dist/index.js CHANGED
@@ -24632,7 +24632,7 @@ var memoizedReport;
24632
24632
  function getEngineVersionReport() {
24633
24633
  if (memoizedReport === void 0) {
24634
24634
  memoizedReport = Object.freeze({
24635
- engineVersion: normalizeEngineVersion("1.76.1"),
24635
+ engineVersion: normalizeEngineVersion("1.76.2"),
24636
24636
  minConsumerVersion: MIN_CONSUMER_VERSION,
24637
24637
  deprecatedAsOf: DEPRECATED_AS_OF
24638
24638
  });
@@ -46151,7 +46151,7 @@ function formatVerdictSummary(input2) {
46151
46151
 
46152
46152
  // src/verification/repository/run-repository-verification.ts
46153
46153
  function verifierVersion() {
46154
- return true ? "1.76.1" : "0.0.0-dev";
46154
+ return true ? "1.76.2" : "0.0.0-dev";
46155
46155
  }
46156
46156
  function backstopGates() {
46157
46157
  return [
@@ -49085,7 +49085,7 @@ var WorkflowEngine = class {
49085
49085
  };
49086
49086
 
49087
49087
  // src/index.ts
49088
- var VERSION = "1.76.1";
49088
+ var VERSION = "1.76.2";
49089
49089
  function getFrameworkName() {
49090
49090
  return "paqad-ai";
49091
49091
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paqad-ai",
3
- "version": "1.76.1",
3
+ "version": "1.76.2",
4
4
  "description": "Spec-driven development framework — AI agents that think before they type",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -155,12 +155,15 @@ Emit the `start` marker as you begin the stage and the `end` marker as you finis
155
155
 
156
156
  | Channel | Claude Code CLI | Claude Code Desktop |
157
157
  | --- | --- | --- |
158
- | Hook `{systemMessage}` (PreToolUse / Stop) | rendered | **not rendered** — recorded as a hook attachment |
158
+ | Hook `{systemMessage}` — PreToolUse | rendered | **not rendered** — recorded as a hook attachment |
159
+ | Hook `{systemMessage}` — Stop | rendered | **rendered as literal `Stop says:` lines** |
159
160
  | Hook `{decision:'block'}` `reason` | reaches the model only | reaches the model only |
160
161
  | CLI verb stdout (`stage start`, `plan compile`) | inside the tool-result block | collapsed — invisible unless expanded |
161
162
  | Your assistant text, final message of the turn | **rendered** | **rendered** |
162
163
 
163
- **Per host — who speaks.** On **Claude Code** YOU speak. The stage hooks do fire on your edits and at turn end, and they do write the ledger — but their narration rides `{systemMessage}`, so on Desktop it is recorded and never shown (issue #409: a full six-stage run emitted eleven stage lines and the developer saw none of them, while the evidence bundle was complete). Never treat a hook as having spoken for you. On **Codex** and **Gemini** the record hook is deliberately record-only — it writes the ledger at turn end but says nothing in chat — so there too YOU must narrate your own `▸ paqad` stage lines and speak the end-of-change verdict in prose. On **advisory hosts** (JetBrains "Claude Agent" / AI Assistant, Cursor, Windsurf, Copilot, Continue, Aider, Antigravity) **no native hook fires at all** — nothing blocks an edit, nothing writes the ledger for you, nothing speaks a verdict. Never rely on a hook-spoken line or a hook-written ledger row on an advisory host. Narrating alone is **not enough**: a narrated-but-unrecorded stage leaves the ledger empty, exactly the JetBrains Claude Agent gap (issue #389) — "framework followed" but "no evidence".
164
+ A Stop-hook `{systemMessage}` used to be invisible on Desktop, so paqad kept a receipt echo there as belt-and-braces. Claude Code changed that: a Stop `{systemMessage}` now renders verbatim as `Stop says:` lines, which duplicated the receipt the agent already speaks and leaked model-only advisories into the chat. So paqad's Stop hooks no longer emit user-facing `{systemMessage}` prose at all — they write the ledger and, on a hard failure, ride the model-only `{decision:'block'}` `reason`. The developer-facing channel is your own final message, full stop.
165
+
166
+ **Per host — who speaks.** On **Claude Code** YOU speak. The stage hooks do fire on your edits and at turn end, and they do write the ledger — but they no longer narrate in chat: their `{systemMessage}` echo was removed once Claude Code began rendering a Stop `{systemMessage}` as `Stop says:` lines, because it duplicated the receipt you already speak and leaked model-only advisories to the developer. Before that it was the opposite failure — the echo was recorded and never shown on Desktop (issue #409: a full six-stage run emitted eleven stage lines and the developer saw none of them, while the evidence bundle was complete). Either way the lesson holds. Never treat a hook as having spoken for you. On **Codex** and **Gemini** the record hook is deliberately record-only — it writes the ledger at turn end but says nothing in chat — so there too YOU must narrate your own `▸ paqad` stage lines and speak the end-of-change verdict in prose. On **advisory hosts** (JetBrains "Claude Agent" / AI Assistant, Cursor, Windsurf, Copilot, Continue, Aider, Antigravity) **no native hook fires at all** — nothing blocks an edit, nothing writes the ledger for you, nothing speaks a verdict. Never rely on a hook-spoken line or a hook-written ledger row on an advisory host. Narrating alone is **not enough**: a narrated-but-unrecorded stage leaves the ledger empty, exactly the JetBrains Claude Agent gap (issue #389) — "framework followed" but "no evidence".
164
167
 
165
168
  **Advisory-host stage protocol — run the CLI, do not just narrate.** On an advisory host, for any **feature-development** change you MUST populate the ledger yourself by running the host-independent CLI (plain `npx paqad-ai …` in the IDE's shell — no hook required), in this order, and narrate each `▸ paqad` stage line as you go:
166
169
 
@@ -130,12 +130,20 @@ export async function main(input, seam = SEAM) {
130
130
  }
131
131
  return 2;
132
132
  }
133
- // Allow path: surface narration + advisory findings through the host's
134
- // user-message channel (`systemMessage`), not bare stdout — bare stdout is
133
+ // Allow path: on the pre-mutation (PreToolUse) seam, surface narration + advisory
134
+ // findings through the host's user-message channel (`systemMessage`) — bare stdout is
135
135
  // only visible in verbose mode, and narration is non-negotiable (issue #307).
136
- const visible = [result.narration, result.summary].filter(Boolean).join('\n');
137
- if (visible) {
138
- process.stdout.write(`${JSON.stringify({ systemMessage: visible })}\n`);
136
+ //
137
+ // The completion (Stop) seam no longer emits here. Claude Code changed: a Stop-hook
138
+ // `{systemMessage}` now RENDERS on Desktop as literal "Stop says:" lines, and the agent
139
+ // already speaks its end-of-change narration in its final message (issue #409), so a
140
+ // Stop echo would duplicate that and leak into the developer's chat (the "Stop says:"
141
+ // narration leak). A PreToolUse `{systemMessage}` is still not rendered, so it stays.
142
+ if (seam !== 'completion') {
143
+ const visible = [result.narration, result.summary].filter(Boolean).join('\n');
144
+ if (visible) {
145
+ process.stdout.write(`${JSON.stringify({ systemMessage: visible })}\n`);
146
+ }
139
147
  }
140
148
  return 0;
141
149
  } catch {
@@ -31,22 +31,17 @@ async function main(input) {
31
31
  if (!transcriptText) return 0;
32
32
 
33
33
  const distUrl = new URL('../../dist/stage-evidence/marker-parse.js', import.meta.url);
34
- const narrationUrl = new URL('../../dist/stage-evidence/narration.js', import.meta.url);
35
- const [{ parseAndRecordMarkers }, { markerBatchNarration }] = await Promise.all([
36
- import(distUrl.href),
37
- import(narrationUrl.href),
38
- ]);
39
- const recorded = parseAndRecordMarkers({
34
+ const { parseAndRecordMarkers } = await import(distUrl.href);
35
+ // Record every parsed marker to the ledger (issue #307 — the ledger write is
36
+ // non-negotiable). The chat echo that used to ride `{systemMessage}` is gone: this is a
37
+ // Stop hook, and Claude Code now RENDERS a Stop-hook `{systemMessage}` on Desktop as
38
+ // literal "Stop says:" lines, so echoing here duplicated the stage lines the agent
39
+ // already speaks itself (issue #409) and leaked into the developer's chat.
40
+ parseAndRecordMarkers({
40
41
  projectRoot,
41
42
  transcriptText,
42
43
  sessionId: payload?.session_id ?? null,
43
44
  });
44
- // Narration and ledger are both non-negotiable (issue #307): every row this
45
- // parse just minted is shown to the user via the host's user-message channel.
46
- const narration = markerBatchNarration(recorded);
47
- if (narration) {
48
- process.stdout.write(`${JSON.stringify({ systemMessage: narration })}\n`);
49
- }
50
45
  return 0;
51
46
  } catch {
52
47
  /* v8 ignore next */
@@ -128,33 +128,34 @@ export async function runVerificationBackstop({
128
128
  // hook (it is a PreToolUse mechanism), so the old `return 2` was both invisible AND
129
129
  // a no-op. git/CI keep the exit-code-gated path below.
130
130
  if (origin === 'hook-completion') {
131
- // Issue #409 — the voice backstop. `{systemMessage}` is exactly backwards for a
132
- // receipt (the model reads it, the Desktop developer never sees it), which is why
133
- // the agent must speak the receipt itself. But that same property makes it the
134
- // right channel for an advisory ABOUT the narration: the audience is the model.
135
- // So when this change recorded stages the agent never said out loud, the advisory
136
- // rides along here, and is folded into the block reason below when a block is
137
- // already firing for a real gate failure. It never causes a block of its own — a
138
- // silent turn is a voice defect, not a broken change (INV-1).
139
- const payload = {
140
- systemMessage: verdict.narrationAdvisory
141
- ? `${receipt}\n\n${verdict.narrationAdvisory}`
142
- : receipt,
143
- };
131
+ // The Stop hook no longer emits a `{systemMessage}`. Claude Code changed: a
132
+ // Stop-hook `{systemMessage}` now RENDERS on Desktop as literal "Stop says:" lines,
133
+ // inverting the #368/#409 premise that this channel was invisible. Routing the
134
+ // receipt through it duplicated the narration the agent already speaks (the #409
135
+ // contract) and leaked the model-only narration advisory into the developer's chat
136
+ // (the "Stop says:" narration leak). So the developer-facing channel is the agent's
137
+ // own final message; enforcement rides the `{decision:'block'}` reason below, which
138
+ // is NOT rendered on either host (it reaches the model only), plus the git/CI
139
+ // backstop.
140
+ const payload = {};
144
141
  // Give the gate real teeth on a HARD failure (a gate reported `fail`, e.g. a
145
142
  // mandatory stage missing or a red checks report): tell the model to keep working
146
143
  // and resolve it before the turn ends. An Inconclusive verdict (no failing gate —
147
- // only unproven signals) is surfaced but never blocks: "do not over-trust", not
148
- // "you must fix". The loop guard (`loopActive`, from Claude's `stop_hook_active`)
149
- // means the gate already bit once this turn, so a second block would loop — step
150
- // aside and let the session end (git/CI remains the hard gate).
144
+ // only unproven signals) never blocks: "do not over-trust", not "you must fix". The
145
+ // loop guard (`loopActive`, from Claude's `stop_hook_active`) means the gate already
146
+ // bit once this turn, so a second block would loop — step aside and let the session
147
+ // end (git/CI remains the hard gate). The narration advisory (issue #409) still rides
148
+ // the model-only reason when a block is already firing; on a clean turn there is no
149
+ // model-facing decision to convey, so the hook stays silent.
151
150
  if (verdictHasHardFailure(verdict) && !loopActive) {
152
151
  payload.decision = 'block';
153
152
  payload.reason = verdict.narrationAdvisory
154
153
  ? `${blockReason(verdict)}\n\n${verdict.narrationAdvisory}`
155
154
  : blockReason(verdict);
156
155
  }
157
- out.write(`${JSON.stringify(payload)}\n`);
156
+ if (payload.decision) {
157
+ out.write(`${JSON.stringify(payload)}\n`);
158
+ }
158
159
  return 0;
159
160
  }
160
161