ruvnet-brain 4.3.28 → 4.3.29

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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +74 -7
  3. package/console/app.js +7 -2
  4. package/kb/corpus-release-identity.mjs +73 -0
  5. package/package.json +4 -3
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/mcp/managed-cli-interface.mjs +83 -9
  9. package/plugin/scripts/advocacy-route.mjs +4 -1
  10. package/plugin/scripts/capacity-aware-parallel-work.mjs +4 -0
  11. package/plugin/scripts/continuation-gate.mjs +71 -0
  12. package/plugin/scripts/decision-gate.mjs +29 -57
  13. package/plugin/scripts/ground-ruvnet.sh +77 -4
  14. package/plugin/scripts/grounding-stamp.sh +34 -5
  15. package/plugin/scripts/grounding-turn-gate.mjs +8 -2
  16. package/plugin/scripts/grounding-turn-mark.mjs +6 -1
  17. package/plugin/scripts/hook-input.mjs +54 -0
  18. package/plugin/scripts/hook-shim.mjs +10 -0
  19. package/plugin/scripts/memory-doctor.mjs +43 -0
  20. package/plugin/scripts/nightly-controller.mjs +6 -1
  21. package/plugin/scripts/project-progression-contract.mjs +17 -0
  22. package/plugin/scripts/project-progression-hook.mjs +18 -5
  23. package/plugin/scripts/project-progression-producer.mjs +16 -12
  24. package/plugin/scripts/ruvnet-gate1-pattern.mjs +17 -0
  25. package/plugin/scripts/session-start-core.mjs +11 -5
  26. package/plugin/scripts/session-start-update-plane.mjs +5 -1
  27. package/plugin/scripts/unprompted-runtime.mjs +29 -8
  28. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +1 -1
  29. package/scripts/adr-072-completion.mjs +2 -1
  30. package/scripts/calibrate-router.mjs +6 -6
  31. package/scripts/corpus-reconcile.mjs +32 -3
  32. package/scripts/correction-detect.mjs +10 -11
  33. package/scripts/dispatch-receipt.mjs +2 -2
  34. package/scripts/dual-host-deliberation.mjs +3 -2
  35. package/scripts/execution-policy.mjs +10 -2
  36. package/scripts/gen-console-images.mjs +0 -1
  37. package/scripts/gen-images.mjs +0 -4
  38. package/scripts/host-install-matrix.mjs +7 -4
  39. package/scripts/independent-review-receipt.mjs +7 -8
  40. package/scripts/ingest-repo.mjs +45 -1
  41. package/scripts/learning-replay-cli.mjs +2 -1
  42. package/scripts/learning-replay-fixture.mjs +2 -1
  43. package/scripts/learnings.mjs +19 -5
  44. package/scripts/lesson-migrate-agentdb.mjs +635 -0
  45. package/scripts/loop-checkpoint.mjs +37 -1
  46. package/scripts/metaharness-receipts.mjs +3 -2
  47. package/scripts/nightly-two-run-proof.mjs +6 -6
  48. package/scripts/nightly-watchdog.mjs +10 -2
  49. package/scripts/onboarding-console.mjs +98 -49
  50. package/scripts/oracle/producer-hosts.mjs +2 -1
  51. package/scripts/private-overlay.mjs +31 -9
  52. package/scripts/public-verification-aggregate.mjs +4 -3
  53. package/scripts/publication-receipt.mjs +9 -0
  54. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  55. package/scripts/rebuild-gists-from-receipts.mjs +1 -1
  56. package/scripts/reconcile-project.mjs +0 -0
  57. package/scripts/release.mjs +34 -82
  58. package/scripts/retrieval-canary.mjs +3 -2
  59. package/scripts/review-model-defaults.mjs +26 -0
  60. package/scripts/route-cheap.mjs +20 -7
  61. package/scripts/router-utilization.mjs +5 -5
  62. package/scripts/rvf-generation.mjs +68 -2
  63. package/scripts/single-source-check.mjs +270 -0
  64. package/scripts/subscription-hosts.mjs +4 -0
  65. package/scripts/sync-version.mjs +22 -0
  66. package/scripts/trismart.mjs +3 -3
  67. package/scripts/wired-check.mjs +40 -4
  68. package/console/assets/memory.webp +0 -0
  69. package/plugin/scripts/version-bump-gate.sh +0 -124
  70. package/scripts/qe/aggregate-4.3.mjs +0 -42
  71. package/scripts/release-convergence-watchdog.mjs +0 -112
  72. package/scripts/stamp-existing-rvf-generations.mjs +0 -53
  73. package/scripts/verify-channels.mjs +0 -196
@@ -92,34 +92,30 @@ export const MIN_HEADROOM_MS = 3000;
92
92
  * hijack-ruvnet the managed-memory boundary (ADR-063): a correctness rule about where data
93
93
  * goes, ahead of anything about process.
94
94
  * ground-before-write don't write RuvNet-product code ungrounded (ADR-0012).
95
- * design-wall don't ship a visual surface nobody looked at.
96
95
  *
97
96
  * `unprompted-speech` is LAST and is not really a peer: it is the speech chokepoint, which refuses
98
- * only for a lesson the user personally opted into blocking. It is included so that Write/Edit and
99
- * Bash have exactly ONE process that can refuse them — which is the entire invariant — and its
100
- * allow-path stdout envelope is forwarded untouched.
97
+ * only for a lesson the user personally opted into blocking. It is included so that Write/Edit have
98
+ * exactly ONE process that can refuse them — which is the entire invariant — and its allow-path
99
+ * stdout envelope is forwarded untouched.
100
+ *
101
+ * H5 (this repo's own dead-code audit): a 'bash' route used to sit alongside 'write' here, gating
102
+ * identifier-preflight, spend-guard, degradation-watch, hijack-ruvnet (a SECOND use, alongside its
103
+ * 'write' one) and design-wall on a PreToolUse-Bash event. It was never registered in
104
+ * plugin/hooks/hooks.json or codex-hooks.json — continuity-hook-policy.mjs's own header names this
105
+ * explicitly: "decision-gate's BASH route ... remains reachable through hook-shim's dispatch table
106
+ * by explicit invocation" only, never wired into the automatic plane. Removed as dead routing, not
107
+ * as a verdict on the four now-orphaned policies' worth: identifier-preflight.mjs, spend-guard.mjs,
108
+ * degradation-watch.mjs and design-wall.sh all remain in the tree with their own passing tests
109
+ * (each exports/exposes pure, independently-tested logic — `check`/`identifierIn`, `dependentEvent`,
110
+ * etc. — used elsewhere, e.g. tests/unit/lesson-gate.test.mjs imports degradation-watch.mjs's
111
+ * `dependentEvent` directly to cross-check lesson-hooks.sh's own pattern). Only their SELECTION by
112
+ * this gate's dead 'bash' route is removed here.
101
113
  */
102
114
  const POLICY = (id, file, interpreter = 'bash') => ({ id, file, interpreter });
103
115
  const REFUSAL_POLICIES = [
104
116
  POLICY('protect-state', 'protect-brain-state.sh'),
105
- // degradation-watch sits second because it decides whether ANY record this system keeps is real.
106
- // Measured 2026-08-13: better_sqlite3.node was built for NODE_MODULE_VERSION 141 against a node
107
- // needing 137, ruflo fell back to sql.js, and nothing persisted for three days while every write
108
- // printed `[OK] Data stored successfully`. A warning was printed on every one of those writes and
109
- // read. It could not stop anything, because a warning is text and text is skimmable — so this is
110
- // a refusal instead. It probes only for commands whose truth DEPENDS on durable memory (a lesson
111
- // store, a ship), so ordinary Bash pays nothing.
112
- POLICY('degradation-watch', 'degradation-watch.mjs', 'node'),
113
- // identifier-preflight is FIRST among the cheap checks and costs one file read: it refuses a
114
- // command that names a model this machine's CLI does not accept. `codex exec --model gpt-5.6`
115
- // (correct: gpt-5.6-sol, in ~/.codex/config.toml) printed a 400 and EXITED 0 into a redirected
116
- // file on 2026-08-13, so a 50-minute audit produced nothing and there was no exit code to catch.
117
- // It refuses ONLY a positively-known-wrong value and allows every unknown, because a wall that
118
- // fabricates a reason is one people learn to route around.
119
- POLICY('identifier-preflight', 'identifier-preflight.mjs', 'node'),
120
117
  POLICY('hijack-ruvnet', 'hijack-ruvnet.sh'),
121
118
  POLICY('ground-before-write', 'ground-before-write.sh'),
122
- POLICY('design-wall', 'design-wall.sh'),
123
119
  // adr-currency-gate fires on the EDIT, where the pre-push gate fires on the push. Same rule, same
124
120
  // machinery (it calls doc-currency.mjs, never a second copy of the logic) — moved to the earliest
125
121
  // moment it has enough information. On 2026-08-13 four ADRs went stale together and were caught
@@ -128,22 +124,12 @@ const REFUSAL_POLICIES = [
128
124
  // DEBT, not change: you may edit governed code freely, but not while a document governing it is
129
125
  // still unreconciled from the last round.
130
126
  POLICY('adr-currency', 'adr-currency-gate.mjs', 'node'),
131
- // spend-guard refuses an agent FLEET that would inherit metered API keys. The $1,600 of
132
- // agentic-qe#557: ~374 headless agents billed api.anthropic.com per-token for 11 hours while the
133
- // Claude Max subscription sat unused. The rule was stored, ratified and severity:high — and
134
- // delivered as advisory text, which is what gets skimmed. `claude` and `codex` are the seats and
135
- // are never touched; OPENROUTER is metered and deliberately allowed, because cost-optimal routing
136
- // exists to spend it and a gate that fires on the feature you configured is the gate you disable.
137
- POLICY('spend-guard', 'spend-guard.mjs', 'node'),
138
127
  ];
139
128
  const SPEECH = { id: 'unprompted-speech', file: 'unprompted-runtime.mjs', interpreter: 'node' };
140
129
 
141
130
  /** Which policies apply to which PreToolUse sub-event, mirroring the matchers they replaced. */
142
131
  const REGISTRY = {
143
132
  'write': ['protect-state', 'hijack-ruvnet', 'ground-before-write', 'adr-currency'],
144
- // degradation-watch is bash-only on purpose: the acts it guards — `ruflo memory store`, `git
145
- // push` — are commands, so the dependency is observable there and nowhere else.
146
- 'bash': ['protect-state', 'identifier-preflight', 'spend-guard', 'degradation-watch', 'hijack-ruvnet', 'design-wall'],
147
133
  };
148
134
 
149
135
  export function policiesFor(event, registry = REGISTRY, all = REFUSAL_POLICIES) {
@@ -156,33 +142,22 @@ export function policiesFor(event, registry = REGISTRY, all = REFUSAL_POLICIES)
156
142
  /**
157
143
  * ── APPLICABILITY: THE CHEAPEST POLICY IS THE ONE NEVER SPAWNED ──────────────────────────────────
158
144
  *
159
- * degradation-watch was spawned for EVERY Bash call and then exited 0 on its own second line — its
160
- * `dependentEvent()` returns null for anything that is not a ship or a memory store, which is nearly
161
- * everything. Measured here on 2026-08-14: 63-65ms of node boot bought to learn that `ls -la` is not
162
- * `git push`, on every single Bash tool call, on the machine where a node boot costs 60ms. On the
163
- * audit's machine that same boot is ~300ms.
164
- *
165
- * The predicate is IMPORTED, never restated. Copying the DEPENDENT_COMMANDS regexes up here would
166
- * make two answers to one question and guarantee they drift — the same reason adr-currency-gate calls
167
- * doc-currency.mjs instead of carrying a second copy of the logic.
168
- *
169
- * FAIL TOWARD RUNNING THE POLICY. If the import fails, or the predicate throws, the policy is
170
- * spawned exactly as before: this is a latency optimisation and it may never become a way to silently
171
- * disable a guard.
145
+ * H5: this table's one entry (degradation-watch, applicable only to the now-removed 'bash' route)
146
+ * was removed along with that route — see the REGISTRY comment above. The mechanism itself stays:
147
+ * an empty table costs nothing (skipReason below returns null immediately for every policy, so every
148
+ * currently-registered policy is consulted exactly as if this file did not exist), and it is the
149
+ * correct extension point for a future policy that only applies to SOME invocations of its event.
172
150
  */
173
- let dependentEvent = null; // set from degradation-watch.mjs at startup; null → spawn it as before
174
151
  /**
175
152
  * Resolved once per invocation by the runtime block below; null means no bash on this host.
176
153
  *
177
- * Declared HERE, above that block, and not next to runPolicy() where it reads more naturally: the
178
- * `if (isMain())` block runs during module evaluation, so a `let` declared after it sits in the
179
- * temporal dead zone and the assignment throws — the identical mistake `speechEventFor` was already
180
- * a hoisted `function` to avoid, recorded a few lines further down.
154
+ * Declared HERE, and not next to runPolicy() where it reads more naturally: the `if (isMain())`
155
+ * block runs during module evaluation, so a `let` declared after it sits in the temporal dead zone
156
+ * and the assignment throws — the identical mistake `speechEventFor` was already a hoisted
157
+ * `function` to avoid, recorded a few lines further down.
181
158
  */
182
159
  let BASH = null;
183
- const APPLICABILITY = {
184
- 'degradation-watch': (input) => (dependentEvent ? Boolean(dependentEvent(input.command)) : true),
185
- };
160
+ const APPLICABILITY = {};
186
161
 
187
162
  /** Returns a skip reason, or null if the policy must be consulted. */
188
163
  export function skipReason(policy, toolInput, table = APPLICABILITY) {
@@ -239,18 +214,15 @@ if (isMain()) {
239
214
  const payload = readPayload();
240
215
  const selected = policiesFor(EVENT);
241
216
  // An unknown event is not an occasion to refuse anything. Same rule as unprompted-runtime's
242
- // "never speak on a guess", pointed at the other decision.
243
- if (!selected.length && EVENT !== 'write' && EVENT !== 'bash') process.exit(ALLOW);
217
+ // "never speak on a guess", pointed at the other decision. 'write' is the only registered route
218
+ // (H5 removed the dead 'bash' one — see the REGISTRY comment above), so it is the only exception.
219
+ if (!selected.length && EVENT !== 'write') process.exit(ALLOW);
244
220
 
245
221
  const budgetMs = Number(process.env.RUVNET_DECISION_BUDGET_MS) || DEFAULT_BUDGET_MS;
246
222
  const deadline = started + budgetMs;
247
223
  // Resolved ONCE. On win32 resolveBash() can shell out to `where.exe`; four bash policies meant up
248
224
  // to four of those per tool call, for an answer that cannot change mid-invocation.
249
225
  BASH = resolveBash();
250
- // Best-effort, and deliberately not a static import: a missing or broken degradation-watch.mjs
251
- // must cost us the optimisation, not the whole gate. `runPolicy` already tolerates a missing
252
- // policy file; a top-level `import` of it would have made that tolerance a lie.
253
- try { ({ dependentEvent } = await import('./degradation-watch.mjs')); } catch { dependentEvent = null; }
254
226
 
255
227
  const trace = []; // one row per policy — surfaced by RUVNET_DECISION_TRACE=1
256
228
  const unconsulted = []; // policies the budget cost us. NEVER silent; see reportBudget().
@@ -50,6 +50,23 @@ fi
50
50
  TEXT=$(printf '%s' "$INPUT" | jq -r '.prompt // .user_prompt // .input // empty' 2>/dev/null)
51
51
  [ -z "$TEXT" ] && TEXT="$INPUT"
52
52
 
53
+ # ── H2: HARNESS-GENERATED MESSAGES ARE NOT A USER'S PROMPT. ──────────────────────────────────────
54
+ # Background task notifications, slash-command scaffolding, and other harness-authored bookkeeping
55
+ # arrive on UserPromptSubmit exactly like real user text, wrapped in tags such as
56
+ # <task-notification>, <local-command-caveat>, <command-name>, <local-command-stdout>, and
57
+ # <system-reminder>. None of that is something a human typed, so none of Gates 1-4 below should ever
58
+ # fire on it — injecting a grounding directive in response to the harness's own bookkeeping message
59
+ # is noise on every background-task turn. This is a literal, byte-identical copy of
60
+ # plugin/scripts/hook-input.mjs's HARNESS_GENERATED_SHELL_PATTERN (SOURCE OF TRUTH there), kept as a
61
+ # copy here (not a `node hook-input.mjs` call) for the same reason Gate 1's own pattern below is a
62
+ # copy: this is a hot, every-prompt hook (see the bounded-read comment above), and a process spawn
63
+ # per prompt is exactly the kind of non-surgical cost this file's header warns against.
64
+ # tests/unit/hook-input-harness.test.mjs proves this copy is byte-identical to
65
+ # HARNESS_GENERATED_SHELL_PATTERN and that both agree with isHarnessGenerated() behaviorally.
66
+ if printf '%s' "$TEXT" | grep -qiE '\[Your previous response|\[Request interrupted|</?system-reminder>|</?(command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|task-notification|function_results|function_calls|budget)\b|^[[:space:]]*Caveat:|Base directory for this skill:|This session is being continued from a previous conversation|^[[:space:]]*#[[:space:]]*claudeMd\b|\[INTELLIGENCE\]'; then
67
+ exit 0
68
+ fi
69
+
53
70
  # ── QUIET-PROMPT FAST PATH. ────────────────────────────────────────────────────────────────────
54
71
  # A hook whose output contract is silence must not pay the full stack-currency/project-state scan
55
72
  # before discovering that nothing can fire. This mattered on a packed Windows install: immediately
@@ -403,8 +420,19 @@ fi
403
420
  # The bug this kills: line "5. CLEARED TO GO" below ends every build response with "Want me to
404
421
  # build it now?" — a question asked to an EMPTY ROOM inside a /loop. That is what a real user's
405
422
  # "it wouldn't run autonomously" looked like from the outside.
423
+ #
424
+ # H3 (fixed): the ORIGINAL fix over-corrected into a NEW bug in the opposite direction. Matching
425
+ # conversational phrases — "autonomous", "unattended", "don't stop", "keep going/working", "soak
426
+ # run" — meant an ATTENDED human saying ordinary things ("please keep working on this", "don't stop
427
+ # until the tests pass") got the full AUTONOMOUS MODE block injected: "no human is watching", "NEVER
428
+ # halt to ask", ignore the CLEARED-TO-GO checkpoint question. A human who is plainly IN the
429
+ # conversation is not an empty room; inferring "nobody is watching" from prose a watching human just
430
+ # typed is exactly backwards. AUTON is now restricted to signals a HUMAN does not type by hand: the
431
+ # `<<autonomous-loop` sentinel a real unattended harness wraps its own prompts in, a `/loop` slash
432
+ # command LEADING the text (the actual mechanism that starts a loop — see the `loop` skill — not the
433
+ # bare word "loop" appearing anywhere), or the explicit RUVNET_AUTONOMOUS=1 environment signal.
406
434
  AUTON=0
407
- if printf '%s' "$TEXT" | grep -qiE '/loop|\bautonomous(ly)?\b|\bunattended\b|do(n.t| not) stop|keep (working|going)( until| on)?|\bsoak run\b|<<autonomous-loop'; then
435
+ if printf '%s' "$TEXT" | grep -qiE '<<autonomous-loop|^[[:space:]]*/loop\b'; then
408
436
  AUTON=1
409
437
  fi
410
438
  [ "${RUVNET_AUTONOMOUS:-0}" = "1" ] && AUTON=1
@@ -492,9 +520,54 @@ if [ "$AUTON" -eq 1 ]; then
492
520
  and a wait.
493
521
  EOF
494
522
  if [ -f "$CP_FILE" ]; then
495
- echo "[RuvNet Brain — RESUME: your prior checkpoint. Continue from 'next'; do not repeat done work.]"
496
- cat "$CP_FILE" 2>/dev/null
497
- echo ""
523
+ # H3: a checkpoint from a loop that ended days or weeks ago used to be injected UNCONDITIONALLY —
524
+ # resumed as though it were this session's own live state, with no age check at all.
525
+ #
526
+ # NOT resolved via scripts/loop-checkpoint.mjs (a first version of this fix did, and shipped
527
+ # tests/unit/payload-self-contained.test.mjs RED: loop-checkpoint.mjs lives only at repo-root
528
+ # scripts/, never inside plugin/scripts/, and only `plugin/` reaches a real install — every
529
+ # shipped layout flattens it, so that reference resolved to nothing everywhere except this
530
+ # repo's own dogfood checkout, and the feature silently degraded to "always inject" for every
531
+ # real user). The 24h threshold is instead inlined here as `node -e`, no file reference at all —
532
+ # payload-self-contained by construction. scripts/loop-checkpoint.mjs's own exported
533
+ # CHECKPOINT_STALE_MS (used by scripts/single-source-check.mjs's E1 audit) is the SOURCE OF
534
+ # TRUTH for the number; tests/unit/ground-ruvnet-staleness-inline.test.mjs asserts this literal
535
+ # matches it, same drift-test idiom as Gate 1's own bash/JS pattern copy.
536
+ STALE_RC=9
537
+ STALE_AGE=""
538
+ if command -v node >/dev/null 2>&1; then
539
+ UPDATED_AT=$(node -e '
540
+ let raw = ""; process.stdin.on("data", (c) => { raw += c; });
541
+ process.stdin.on("end", () => {
542
+ try { const cp = JSON.parse(raw); process.stdout.write(typeof cp.updatedAt === "string" ? cp.updatedAt : ""); }
543
+ catch { /* empty stdout: not a valid checkpoint */ }
544
+ });
545
+ ' < "$CP_FILE" 2>/dev/null)
546
+ if [ -n "$UPDATED_AT" ]; then
547
+ STALE_AGE=$(node -e '
548
+ const STALE_MS = 24 * 60 * 60 * 1000;
549
+ const ms = Date.parse(process.argv[1]);
550
+ if (!Number.isFinite(ms)) process.exit(2);
551
+ const ageMs = Date.now() - ms;
552
+ if (ageMs < STALE_MS) process.exit(0);
553
+ process.stdout.write(String(Math.floor(ageMs / 86_400_000)));
554
+ process.exit(1);
555
+ ' "$UPDATED_AT" 2>/dev/null)
556
+ STALE_RC=$?
557
+ else
558
+ STALE_RC=2
559
+ fi
560
+ fi
561
+ if [ "$STALE_RC" -eq 1 ]; then
562
+ echo "[RuvNet Brain — a stale checkpoint (age ${STALE_AGE} days) was ignored; starting fresh rather than resuming a plan that old.]"
563
+ else
564
+ # RC 0 (fresh) — inject as intended. RC 2 (no parseable `updatedAt`) or RC 9 (no node on this
565
+ # host) both mean "unknown age", which must never be treated as PROVEN stale — fall back to
566
+ # the pre-fix behavior of injecting rather than silently dropping a checkpoint we cannot judge.
567
+ echo "[RuvNet Brain — RESUME: your prior checkpoint. Continue from 'next'; do not repeat done work.]"
568
+ cat "$CP_FILE" 2>/dev/null
569
+ echo ""
570
+ fi
498
571
  fi
499
572
  fi
500
573
 
@@ -72,6 +72,21 @@ case "$INPUT" in
72
72
  *) exit 0 ;;
73
73
  esac
74
74
 
75
+ DIR="$HOME/.cache/ruvnet-brain/grounded"
76
+ mkdir -p "$DIR" 2>/dev/null || exit 0
77
+
78
+ # ── 2.5. THE ANY-SEARCH MARKER (H1 / GitHub #316). ──────────────────────────────────────────────
79
+ # grounding-turn-gate.mjs's Stop-time check must treat ANY successful search_ruvnet this turn as
80
+ # satisfying "a search happened" — independent of whether the query text below happens to contain
81
+ # one of the recognised product terms. A query like "how should agent handoffs stay consistent"
82
+ # grounds just as genuinely as one that names a product by name, and nothing should require the
83
+ # model to re-word a real search just to satisfy a keyword scan. This mints into the SAME
84
+ # directory grounding-turn-gate.mjs already scans for the newest mtime (newestGroundingStampMs),
85
+ # so no change is needed on that side. Written unconditionally now that the success banner (step 2)
86
+ # is confirmed, before the QUERY parse below — a search can succeed with a query this regex cannot
87
+ # extract, and that must not cost it this signal.
88
+ : > "$DIR/.any-search" 2>/dev/null || true
89
+
75
90
  # ── 3. WHICH terms — from the QUERY only, as it always was. The first raw "query" key in the JSON is
76
91
  # tool_input's; inside tool_response text the quotes are escaped (\"query\") so they cannot match.
77
92
  QUERY=""
@@ -79,12 +94,26 @@ re='"query"[[:space:]]*:[[:space:]]*"([^"]*)"'
79
94
  [[ $INPUT =~ $re ]] && QUERY="${BASH_REMATCH[1]}"
80
95
  [ -n "$QUERY" ] || exit 0
81
96
 
82
- DIR="$HOME/.cache/ruvnet-brain/grounded"
83
- mkdir -p "$DIR" 2>/dev/null || exit 0
84
-
85
- # Same product-term list as ground-before-write.sh — ONE list per concept, mirrored in both
97
+ # WRITE_GATE terms — same product-term list as ground-before-write.sh's own copy, mirrored in both
86
98
  # files on purpose (a shared sourced file would add a dependency a blocking hook must not have).
87
- for t in agentdb metaharness ruvector aidefence agentic-flow agentic-qe ruv-swarm rvf ruflo; do
99
+ # Do not change this list without mirroring ground-before-write.sh's copy — H1 keeps that gate's
100
+ # per-product WRITE semantics untouched (see decision-gate.mjs / ground-before-write.sh for why
101
+ # these 9 are scoped to code hand-rolling risk, not general rUv-ecosystem conversation).
102
+ WRITE_GATE_TERMS="agentdb metaharness ruvector aidefence agentic-flow agentic-qe ruv-swarm rvf ruflo"
103
+
104
+ # GATE-1-ONLY additions (H1 / GitHub #316): ruvnet-gate1-pattern.mjs's RUVNET_GATE1_PATTERN is the
105
+ # ONE owner of this vocabulary — grounding-turn-mark.mjs already arms the Stop-time turn gate from
106
+ # it. Before this fix, grounding-stamp.sh only recognised the 9 WRITE_GATE_TERMS above, so a search
107
+ # literally about "ruvnet" (or "sparc", "qudag", "claude-flow", ...) minted no per-term stamp, and
108
+ # combined with the missing any-search marker above, grounding-turn-gate.mjs wrongly reported "no
109
+ # successful search_ruvnet call this turn" even though one had just happened. Terms already covered
110
+ # by WRITE_GATE_TERMS are not repeated here. tests/unit/grounding-stamp-terms.test.mjs asserts
111
+ # WRITE_GATE_TERMS plus GATE1_ONLY_TERMS together cover every RUVNET_GATE1_PATTERN alternative, so a
112
+ # future addition to that pattern left unmirrored here goes red immediately (same idiom as
113
+ # tests/unit/ruvnet-gate1-pattern.test.mjs's byte-identity check against ground-ruvnet.sh).
114
+ GATE1_ONLY_TERMS="ruvnet agenticow rulake ruview rupixel ruv-fann synthlang dspy qudag safla cve-bench sparc swarm claude-flow ruv"
115
+
116
+ for t in $WRITE_GATE_TERMS $GATE1_ONLY_TERMS; do
88
117
  [[ $QUERY == *"$t"* ]] && { : > "$DIR/$t" 2>/dev/null || true; }
89
118
  done
90
119
 
@@ -35,8 +35,14 @@
35
35
  * result). ground-before-write.sh already trusts this exact directory's file mtimes for its own
36
36
  * 24h freshness check; this file trusts the SAME directory the SAME way, just against a
37
37
  * narrower window (since the marker's own mtime, not "20 hours ago", is the turn boundary).
38
- * No second "was it searched" signal is invented — a search_ruvnet call this turn mints a stamp
39
- * here exactly as it always has, for exactly the same reason (decision-gate's write gate).
38
+ * UPDATE (H1 / GitHub #316): per-product term files alone under-reported "was it searched" —
39
+ * grounding-stamp.sh used to recognise only a 9-term write-gate vocabulary that omitted `ruvnet`
40
+ * itself (and every other Gate-1 term), so a search literally about "ruvnet" minted nothing and
41
+ * this gate wrongly fired. grounding-stamp.sh now also writes a vocabulary-independent
42
+ * `.any-search` marker into this SAME directory on every successful search regardless of query
43
+ * content, so newestGroundingStampMs below (which already scans every file, by name-agnostic
44
+ * design) sees it with no code change needed here — a search_ruvnet call this turn always mints
45
+ * evidence here now, not only when its query happens to name a recognised product.
40
46
  * - The Stop block/continue contract: `{"hookSpecificOutput":{"hookEventName":"Stop",
41
47
  * "additionalContext":"..."}}` on stdout, exit 0. This is not a new discovery — it is the exact
42
48
  * contract continuation-gate.mjs already uses and this repo's own tests already prove works on
@@ -34,7 +34,7 @@ import fs from 'node:fs';
34
34
  import os from 'node:os';
35
35
  import path from 'node:path';
36
36
  import { fileURLToPath } from 'node:url';
37
- import { readStdinBounded } from './hook-input.mjs';
37
+ import { readStdinBounded, isHarnessGenerated } from './hook-input.mjs';
38
38
  import { ruvnetGate1Matches } from './ruvnet-gate1-pattern.mjs';
39
39
 
40
40
  const HOME = os.homedir();
@@ -54,6 +54,11 @@ export function shouldMark(hookInput) {
54
54
  if (!hookInput || hookInput.hook_event_name !== 'UserPromptSubmit') return false;
55
55
  if (!hookInput.session_id) return false;
56
56
  const text = String(hookInput.prompt ?? hookInput.user_prompt ?? hookInput.input ?? '');
57
+ // H2: a background task notification, slash-command scaffold, or other harness-authored message
58
+ // arrives on UserPromptSubmit exactly like real user text — arming the Stop-time grounding gate off
59
+ // one of these (because it happens to mention a rUv term) would demand a search_ruvnet call to
60
+ // close out a "turn" nobody had a hand in.
61
+ if (isHarnessGenerated(text)) return false;
57
62
  return ruvnetGate1Matches(text);
58
63
  }
59
64
 
@@ -127,6 +127,60 @@ export function rawToolResponse(ev) {
127
127
  return ev.tool_response;
128
128
  }
129
129
 
130
+ // ── HARNESS-GENERATED PROMPT DETECTION (H2) ─────────────────────────────────────────────────────
131
+ //
132
+ // WHY THIS LIVES HERE, and not only in scripts/correction-detect.mjs where it started. Background
133
+ // task notifications, slash-command scaffolding, and other harness-authored bookkeeping arrive on
134
+ // UserPromptSubmit exactly like real user text — wrapped in tags such as <task-notification>,
135
+ // <local-command-caveat>, <command-name>, <local-command-stdout>, and <system-reminder>. None of
136
+ // that is something a human typed, but before this fix every UserPromptSubmit consumer that reads
137
+ // `prompt`/`user_prompt`/`input` reacted to it as if it were a real prompt: grounding-turn-mark.mjs
138
+ // could arm the Stop-time grounding gate off a harness message that happens to mention a rUv term,
139
+ // unprompted-runtime.mjs's producers could fire advisories at a background notification, and
140
+ // capacity-aware-parallel-work.mjs could recommend a parallel-work fan-out for text nobody wrote.
141
+ // scripts/correction-detect.mjs already carried the exact regex list needed to recognise these
142
+ // (HARNESS_TEMPLATES, built from live corpus evidence — see that file's header for the measured
143
+ // 29%-of-holdout-pool finding that justified it) but as a private, unexported-to-this-purpose copy
144
+ // nothing else could reuse. This is now the ONE owner; correction-detect.mjs re-exports its old name
145
+ // from here instead of keeping its own literal array (tests/unit/hook-input-harness.test.mjs proves
146
+ // the two never drift).
147
+ export const HARNESS_GENERATED_PATTERNS = [
148
+ /\[Your previous response/i,
149
+ /\[Request interrupted/i,
150
+ /<\/?system-reminder>/i,
151
+ /<\/?(?:command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|task-notification|function_results|function_calls|budget)\b/i,
152
+ /^\s*Caveat:/i,
153
+ /Base directory for this skill:/i,
154
+ /This session is being continued from a previous conversation/i,
155
+ /^\s*#\s*claudeMd\b/im,
156
+ /\[INTELLIGENCE\]/i,
157
+ ];
158
+
159
+ /** True when `promptText` is harness-authored bookkeeping rather than something a user typed. */
160
+ export function isHarnessGenerated(promptText) {
161
+ const text = typeof promptText === 'string' ? promptText : String(promptText ?? '');
162
+ if (!text) return false;
163
+ return HARNESS_GENERATED_PATTERNS.some((re) => re.test(text));
164
+ }
165
+
166
+ /**
167
+ * The bash-portable EQUIVALENT of isHarnessGenerated(), for ground-ruvnet.sh — a hot, every-prompt
168
+ * hook (see its own header on the 38s-regression bounded-read fix) that must not pay a `node` spawn
169
+ * per prompt just for this check. This constant is exactly what ground-ruvnet.sh embeds as its own
170
+ * `grep -qiE` literal (SOURCE OF TRUTH here — tests/unit/hook-input-harness.test.mjs parses
171
+ * ground-ruvnet.sh's copy and asserts byte-identity, same idiom as ruvnet-gate1-pattern.mjs's own
172
+ * copy-with-a-drift-test for Gate 1). Its relationship to HARNESS_GENERATED_PATTERNS above is
173
+ * BEHAVIORAL parity only, not byte-identity: that is an array of independently-flagged regexes (one
174
+ * uses the 'm' flag) with no single source ERE it could be copied from verbatim, so the same test
175
+ * instead proves real `grep -qiE` against this string agrees with isHarnessGenerated() on
176
+ * representative inputs — the same idiom ruvnet-gate1-pattern.test.mjs uses for ITS third,
177
+ * non-identity assertion. POSIX `[[:space:]]` is used instead of `\s` and plain `(...)` instead of
178
+ * `(?:...)` — GNU/PCRE extensions with no POSIX ERE guarantee — because this string must run under
179
+ * whatever `grep` a user's shell resolves to, not only the one on the machine that wrote it.
180
+ */
181
+ export const HARNESS_GENERATED_SHELL_PATTERN =
182
+ '\\[Your previous response|\\[Request interrupted|</?system-reminder>|</?(command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|task-notification|function_results|function_calls|budget)\\b|^[[:space:]]*Caveat:|Base directory for this skill:|This session is being continued from a previous conversation|^[[:space:]]*#[[:space:]]*claudeMd\\b|\\[INTELLIGENCE\\]';
183
+
130
184
  /** Arbitrary dotted-path lookup (e.g. "tool_input.file_path"); "" if any segment is missing. */
131
185
  export function field(ev, dottedPath) {
132
186
  if (!ev || !dottedPath) return '';
@@ -108,6 +108,16 @@ const TABLE = {
108
108
  // one is the sole writer of unprompted BYTES, this one is the sole author of a REFUSAL. The four
109
109
  // policies it consults are unchanged and still individually tested; the gate only composes them.
110
110
  'decision-gate': { file: 'decision-gate.mjs', interpreter: 'node', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
111
+ // H5 CORRECTION (2026-09-26): a first pass removed these two TABLE entries as "retired dead code",
112
+ // since neither id is dispatched by plugin/hooks/hooks.json, plugin/hooks/codex-hooks.json, or this
113
+ // repo's .claude/settings.json. That broke a REAL test:
114
+ // tests/integration/codex-dispatch-cwd-divergence.test.mjs fires the genuine
115
+ // codex-hook.mjs -> codex-hook-adapter.mjs -> hook-shim.mjs 'learn-capture' chain to prove a real,
116
+ // shipped cross-host CWD-divergence fix (learn-capture.sh's project-containment check, #85/#107) —
117
+ // it needs the id to actually resolve through this TABLE, exactly as continuity-hook-policy.mjs's
118
+ // own header already says: "remains reachable through hook-shim's dispatch table by explicit
119
+ // invocation". Restored. wired-check.mjs's H6 fix does not depend on these keys existing either
120
+ // way — it stopped trusting hook-shim.mjs as a blind generic spawner, not their presence here.
111
121
  'learn-capture': { file: 'learn-capture.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
112
122
  'learn-flush': { file: 'learn-flush.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
113
123
  'session-snapshot': { file: 'session-snapshot-hook.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run', stdinBytes: 65536 },
@@ -45,6 +45,10 @@ import { canonicalPath, pathIdentity } from './project-identity.mjs';
45
45
 
46
46
  const HOME = os.homedir();
47
47
  const DEFAULT_SCAN_ROOTS = ['Code', 'code', 'src', 'source', 'projects', 'dev', 'work'];
48
+ const DEFAULT_PROJECT_VENDOR = Object.freeze([
49
+ '/clones/', '/node_modules/', '/vendor/', '/upstream/', '/ruvnet-repos/', '/ruvnet_repos/',
50
+ '/ruvnet-packages/', '/.targets/', '.claude-backup', '_snapshots',
51
+ ]);
48
52
 
49
53
  // Telemetry namespaces: high-volume, unembedded, zero-signal. Written by the npx hook calls.
50
54
  // Counted separately so "you have 11,000 memories" is never mistaken for "you have 11,000 lessons".
@@ -171,6 +175,45 @@ export function candidateRoots({
171
175
  return [...roots.values()].sort();
172
176
  }
173
177
 
178
+ /**
179
+ * Enumerate project directories once for wiring/reconciliation consumers. The result is canonical
180
+ * and de-duplicated by filesystem identity, so symlinked roots and case aliases cannot double-count
181
+ * a project. `purpose` is retained in the interface for callers that need to document a narrower
182
+ * scan without creating another traversal policy.
183
+ */
184
+ export function findProjects(root, { purpose = 'general', maxDepth = 4, vendor = [] } = {}) {
185
+ if (typeof root !== 'string' || !root.trim()) return [];
186
+ const exclusions = [...DEFAULT_PROJECT_VENDOR, ...vendor].map(String);
187
+ const found = new Map();
188
+ const canonicalRoot = canonicalPath(root);
189
+ if (!canonicalRoot) return [];
190
+ const walk = (dir, depth) => {
191
+ if (depth > maxDepth) return;
192
+ let entries;
193
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
194
+ for (const entry of entries) {
195
+ const candidate = path.join(dir, entry.name);
196
+ if (exclusions.some((marker) => `${candidate}/`.includes(marker))) continue;
197
+ if (entry.isDirectory()) {
198
+ if (entry.name === '.claude') {
199
+ const identity = pathIdentity(dir) ?? canonicalPath(dir) ?? dir;
200
+ found.set(identity, canonicalPath(dir) ?? dir);
201
+ continue;
202
+ }
203
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
204
+ walk(candidate, depth + 1);
205
+ } else if (entry.name === '.mcp.json') {
206
+ const identity = pathIdentity(dir) ?? canonicalPath(dir) ?? dir;
207
+ found.set(identity, canonicalPath(dir) ?? dir);
208
+ }
209
+ }
210
+ };
211
+ walk(canonicalRoot, 0);
212
+ // Keep purpose observable to profilers without changing the stable result shape.
213
+ void purpose;
214
+ return [...found.values()].sort();
215
+ }
216
+
174
217
  // Keyed by pathIdentity for the same reason candidateRoots is: one store reached by two names is
175
218
  // one store. Values are the canonical spelling, which is what every caller reads back.
176
219
  function storesBelow(root) {
@@ -55,7 +55,12 @@ function schedulerEnvironment(env) {
55
55
  throw new Error('RUVNET_CONSOLE_ROOT must be an absolute path');
56
56
  }
57
57
  const isolatedHome = path.resolve(fixtureRoot);
58
- if (isolatedHome === path.resolve(os.homedir())) {
58
+ // The real system home, NOT os.homedir() — os.homedir() reads process.env.HOME, which a caller
59
+ // isolating a fixture has typically ALREADY set to this same isolatedHome (consoleFixtureEnvironment
60
+ // does exactly this), so comparing against it made this guard trip on every correctly-isolated
61
+ // fixture instead of only on an accidental real-home leak. os.userInfo().homedir is the OS user
62
+ // database entry and ignores the HOME env var override, so it still names the real home here.
63
+ if (isolatedHome === path.resolve(os.userInfo().homedir)) {
59
64
  throw new Error('RUVNET_CONSOLE_ROOT must not be the real user home in test mode');
60
65
  }
61
66
  return { ...env, HOME: isolatedHome, USERPROFILE: isolatedHome };
@@ -40,6 +40,23 @@ const STATE_VALUE_FIELDS = Object.freeze([
40
40
  'currentGoal', 'acceptanceContract', 'activeProcess', 'activeStep', 'nextAction',
41
41
  ]);
42
42
 
43
+ // Canonical field ownership. Producers may fill a field only from the listed durable sources;
44
+ // readers/checkpoints preserve this provenance and surface conflicts instead of silently choosing.
45
+ export const PROGRESSION_FIELD_AUTHORITY = Object.freeze({
46
+ currentGoal: Object.freeze(['ledger', 'prior-head', 'owner-note', 'transcript-derived']),
47
+ nextAction: Object.freeze(['ledger', 'prior-head']),
48
+ acceptanceContract: Object.freeze(['prior-head', 'ledger']),
49
+ plan: Object.freeze(['ledger', 'prior-head']),
50
+ completed: Object.freeze(['ledger', 'prior-head']),
51
+ inProgress: Object.freeze(['ledger', 'prior-head']),
52
+ decisions: Object.freeze(['ledger', 'owner-note', 'prior-head']),
53
+ changedFiles: Object.freeze(['git']),
54
+ sourceIdentity: Object.freeze(['git']),
55
+ });
56
+ export function fieldAuthorityAllows(field, source) {
57
+ return PROGRESSION_FIELD_AUTHORITY[field]?.includes(source) === true;
58
+ }
59
+
43
60
  function eventKeyFor(value) {
44
61
  const identityDigest = digestCanonical({
45
62
  projectId: value.projectIdentity.id,
@@ -96,12 +96,24 @@ function toolAction(payload) {
96
96
  const responseRecord = response && typeof response === 'object' && !Array.isArray(response)
97
97
  ? response
98
98
  : null;
99
- const exitCode = responseRecord && [responseRecord.exit_code, responseRecord.exitCode, responseRecord.status]
99
+ const explicitCode = responseRecord && [responseRecord.exit_code, responseRecord.exitCode, responseRecord.status]
100
100
  .find((value) => Number.isSafeInteger(value));
101
- const interrupted = responseRecord?.interrupted === true;
102
- const failed = interrupted || (Number.isSafeInteger(exitCode) && exitCode !== 0);
101
+ const responseText = typeof response === 'string' ? response : '';
102
+ // Native host terminal envelopes may serialize an exact `Exit code: N` line. Do not
103
+ // scan arbitrary prose: tool output often quotes logs or examples containing `status: 0`.
104
+ const textualCode = responseText.match(/^\s*Exit code:\s*(-?\d+)\s*$/i);
105
+ const exitCode = Number.isSafeInteger(explicitCode)
106
+ ? explicitCode
107
+ : textualCode ? Number(textualCode[1]) : undefined;
108
+ const interrupted = responseRecord?.interrupted === true || responseRecord?.signal === 'SIGINT';
109
+ const explicitError = responseRecord?.isError === true || payload.is_error === true;
110
+ const declaredOutcome = ['success', 'failure', 'interrupted', 'unknown'].includes(responseRecord?.outcome)
111
+ ? responseRecord.outcome : null;
112
+ const failed = explicitError || (Number.isSafeInteger(exitCode) && exitCode !== 0);
113
+ const terminal = failed || Number.isSafeInteger(exitCode)
114
+ || responseRecord?.success === true || responseRecord?.ok === true;
103
115
  const outcome = payload.hook_event_name === 'PostToolUse'
104
- ? (failed ? 'failure' : response === undefined ? 'unknown' : 'success')
116
+ ? (interrupted ? 'interrupted' : failed ? 'failure' : declaredOutcome || (terminal ? 'success' : 'unknown'))
105
117
  : 'pending';
106
118
  const observation = {
107
119
  trigger: payload.hook_event_name,
@@ -111,6 +123,7 @@ function toolAction(payload) {
111
123
  outcome,
112
124
  ...(Number.isSafeInteger(exitCode) ? { exitCode } : {}),
113
125
  ...(interrupted ? { interrupted: true } : {}),
126
+ ...(explicitError ? { isError: true } : {}),
114
127
  };
115
128
  if (responseRecord) {
116
129
  const stdout = boundedText(responseRecord.stdout);
@@ -133,7 +146,7 @@ function enrichStateWithObservation(state, payload) {
133
146
  return {
134
147
  ...state,
135
148
  commands: [...commands, observation],
136
- ...(observation.outcome === 'failure' ? { failures: [...failures, observation] } : {}),
149
+ ...(observation.outcome === 'failure' || observation.outcome === 'interrupted' ? { failures: [...failures, observation] } : {}),
137
150
  };
138
151
  }
139
152
 
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import crypto from 'node:crypto';
32
32
  import path from 'node:path';
33
- import { digestCanonical, redactProgression, restoreProjectProgression } from './project-progression-contract.mjs';
33
+ import { digestCanonical, fieldAuthorityAllows, redactProgression, restoreProjectProgression } from './project-progression-contract.mjs';
34
34
  import { readOwnerNote, readSourceIdentity, readTranscriptReference, readWorkLedger } from './project-progression-sources.mjs';
35
35
  import { withProgressionReader } from './project-progression-reader.mjs';
36
36
 
@@ -132,30 +132,34 @@ export function buildProjectProgression({
132
132
  const priorState = heads.length === 1 ? heads[0].completeProjectState : null;
133
133
 
134
134
  const provenance = {};
135
- const record = (field, sourceName) => { provenance[field] = marker(sourceName); };
135
+ const record = (field, sourceName) => {
136
+ if (sourceName !== 'none' && !fieldAuthorityAllows(field === 'sourceIdentity' ? 'sourceIdentity' : field, sourceName)) {
137
+ throw new Error(`source ${sourceName} is not authoritative for progression field ${field}`);
138
+ }
139
+ provenance[field] = marker(sourceName);
140
+ };
136
141
 
137
- // GOAL — the ledger's oldest open item is what the user actually committed to; the owner note and
138
- // the prior head come next; the transcript is the last resort and is never authoritative.
142
+ // GOAL — the ledger's oldest open item is what the user actually committed to. A coherent prior
143
+ // head carries that commitment forward; an owner note or transcript can provide context only when
144
+ // no durable goal exists. Neither contextual source is an instruction.
139
145
  let currentGoal = ledger.open[0] ?? null;
140
146
  if (currentGoal) record('currentGoal', 'ledger');
141
147
  else if (typeof priorState?.currentGoal === 'string' && priorState.currentGoal) {
142
148
  currentGoal = priorState.currentGoal;
143
149
  record('currentGoal', 'prior-head');
144
- } else if (transcript.derivedGoal) {
145
- currentGoal = transcript.derivedGoal;
146
- record('currentGoal', 'transcript-derived');
147
150
  } else if (note?.excerpt) {
148
151
  currentGoal = note.excerpt.split('\n')[0].slice(0, 240);
149
152
  record('currentGoal', 'owner-note');
153
+ } else if (transcript.derivedGoal) {
154
+ currentGoal = transcript.derivedGoal;
155
+ record('currentGoal', 'transcript-derived');
150
156
  } else record('currentGoal', 'none');
151
157
 
152
- // NEXT ACTION — the next open ledger item, else the assistant's own last stated step (derived).
158
+ // NEXT ACTION — only a ledger commitment or coherent prior state may become a resumable action.
159
+ // Transcript text is evidence/context, never an invented structured action.
153
160
  let nextAction = ledger.open[1] ?? ledger.open[0] ?? null;
154
161
  if (nextAction) record('nextAction', 'ledger');
155
- else if (transcript.derivedNextAction) {
156
- nextAction = transcript.derivedNextAction;
157
- record('nextAction', 'transcript-derived');
158
- } else if (typeof priorState?.nextAction === 'string' && priorState.nextAction) {
162
+ else if (typeof priorState?.nextAction === 'string' && priorState.nextAction) {
159
163
  nextAction = priorState.nextAction;
160
164
  record('nextAction', 'prior-head');
161
165
  } else record('nextAction', 'none');