mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
feat(config): memorable_recall consent key (on|off, default off, reject-and-preserve)
The gstack-side gate for the Memorable UserPromptSubmit bridge. `on` lets a Claude Code hook hand every prompt to a third-party binary, so the key follows the codex_reviews rule: an invalid value is rejected and the stored value kept, never coerced in either direction. Registered in all four places gstack-config keeps in sync (annotated header, DEFAULTS table, the set validator, and both enumeration loops). Memorable's own capture consent (`memorable enable`) is a separate thing gstack never sets. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6259b37e48
commit
3034769813
+22
-2
@@ -111,6 +111,18 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
|
||||
# # Override per-run: ./setup --plan-tune-hooks /
|
||||
# # --no-plan-tune-hooks, or env GSTACK_PLAN_TUNE_HOOKS.
|
||||
#
|
||||
# ─── Memorable recall bridge (opt-in, third party) ──────────────────
|
||||
# memorable_recall: off # The gstack-side consent gate for the Memorable
|
||||
# # UserPromptSubmit bridge (bin/gstack-memorable).
|
||||
# # off — the hook does nothing, spawns nothing (default)
|
||||
# # on — the hook hands each prompt to the local
|
||||
# # `memorable` CLI, receipted as memorable-recall
|
||||
# # Written by `gstack-memorable enable|disable`. An
|
||||
# # invalid value is REJECTED and the stored value kept:
|
||||
# # a typo must never flip a third-party consent.
|
||||
# # The vendor capture consent (`memorable enable`)
|
||||
# # is separate; gstack never sets it.
|
||||
#
|
||||
# ─── Advanced ────────────────────────────────────────────────────────
|
||||
# codex_reviews: enabled # Master switch for Codex cross-model review. enabled =
|
||||
# # Codex runs as a standard step in /review, /ship,
|
||||
@@ -161,6 +173,7 @@ lookup_default() {
|
||||
redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection
|
||||
redact_prepush_hook) echo "false" ;;
|
||||
pair_agent) echo "off" ;; # remote tunnel consent — fail-closed until /pair-agent asks
|
||||
memorable_recall) echo "off" ;; # on | off — Memorable bridge gate, fail-closed until `gstack-memorable enable`
|
||||
founder_resources) echo "true" ;; # office-hours resource pitch — #538 permanent opt-out sets false
|
||||
# Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline:
|
||||
# brain_trust_policy@<endpoint-id> — unset on fresh install; setup-gbrain
|
||||
@@ -424,6 +437,13 @@ case "${1:-}" in
|
||||
echo "Error: cross_project_learnings '$VALUE' not recognized. Valid values: true, false. Existing value left unchanged." >&2
|
||||
exit 1
|
||||
fi
|
||||
# memorable_recall is a CONSENT key: `on` lets a Claude Code hook hand every
|
||||
# prompt to a third-party binary. Reject like codex_reviews -- a typo must
|
||||
# never flip consent in either direction, so nothing is coerced or stored.
|
||||
if [ "$KEY" = "memorable_recall" ] && [ "$VALUE" != "on" ] && [ "$VALUE" != "off" ]; then
|
||||
echo "Error: memorable_recall '$VALUE' not recognized. Valid values: on, off. Existing value left unchanged." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$STATE_DIR"
|
||||
# Write annotated header on first creation
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
@@ -455,7 +475,7 @@ case "${1:-}" in
|
||||
skill_prefix checkpoint_mode checkpoint_push explain_level \
|
||||
codex_reviews gstack_contributor skip_eng_review workspace_root \
|
||||
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks \
|
||||
timeline_stop_hook; do
|
||||
timeline_stop_hook memorable_recall; do
|
||||
VALUE=$(read_config_value "$KEY" || true)
|
||||
SOURCE="default"
|
||||
if [ -n "$VALUE" ]; then
|
||||
@@ -472,7 +492,7 @@ case "${1:-}" in
|
||||
skill_prefix checkpoint_mode checkpoint_push explain_level \
|
||||
codex_reviews gstack_contributor skip_eng_review workspace_root \
|
||||
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks \
|
||||
timeline_stop_hook; do
|
||||
timeline_stop_hook memorable_recall; do
|
||||
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
|
||||
done
|
||||
;;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* memorable_recall — gstack's own consent gate for the Memorable
|
||||
* UserPromptSubmit bridge (bin/gstack-memorable, hosts/claude/hooks/
|
||||
* memorable-user-prompt-hook). `on` lets a hook hand every prompt to a
|
||||
* third-party binary, so the key follows the codex_reviews rule: an invalid
|
||||
* value is REJECTED and the stored value left alone. A consent key that
|
||||
* coerces a typo into a default is a consent key that lies in one direction
|
||||
* or the other.
|
||||
*/
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const CONFIG_BIN = path.join(ROOT, 'bin', 'gstack-config');
|
||||
let state: string;
|
||||
|
||||
function cfg(args: string[]): { code: number; out: string; err: string } {
|
||||
const r = spawnSync('bash', [CONFIG_BIN, ...args], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 30_000,
|
||||
env: { ...process.env, GSTACK_STATE_ROOT: state, GSTACK_HOME: state },
|
||||
});
|
||||
return { code: r.status ?? -1, out: (r.stdout ?? '').trim(), err: r.stderr ?? '' };
|
||||
}
|
||||
|
||||
beforeEach(() => { state = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-cfg-memo-')); });
|
||||
afterEach(() => { fs.rmSync(state, { recursive: true, force: true }); });
|
||||
|
||||
describe('memorable_recall config key', () => {
|
||||
test('defaults to off and exits 0 (a fresh install can never recall)', () => {
|
||||
const r = cfg(['get', 'memorable_recall']);
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.out).toBe('off');
|
||||
});
|
||||
|
||||
test('set on / set off round-trip', () => {
|
||||
expect(cfg(['set', 'memorable_recall', 'on']).code).toBe(0);
|
||||
expect(cfg(['get', 'memorable_recall']).out).toBe('on');
|
||||
expect(cfg(['set', 'memorable_recall', 'off']).code).toBe(0);
|
||||
expect(cfg(['get', 'memorable_recall']).out).toBe('off');
|
||||
});
|
||||
|
||||
test('an invalid value is REJECTED (exit 1) and the stored value is preserved, in both directions', () => {
|
||||
let r = cfg(['set', 'memorable_recall', 'yes']);
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.err).toContain('Existing value left unchanged');
|
||||
expect(cfg(['get', 'memorable_recall']).out).toBe('off'); // never coerced to on
|
||||
cfg(['set', 'memorable_recall', 'on']);
|
||||
r = cfg(['set', 'memorable_recall', 'maybe']);
|
||||
expect(r.code).toBe(1);
|
||||
expect(cfg(['get', 'memorable_recall']).out).toBe('on'); // never coerced to off either
|
||||
});
|
||||
|
||||
test('appears in `list` and `defaults` (the two hand-synced enumerations)', () => {
|
||||
expect(cfg(['list']).out).toMatch(/memorable_recall:\s+off \(default\)/);
|
||||
expect(cfg(['defaults']).out).toMatch(/memorable_recall:\s+off/);
|
||||
});
|
||||
|
||||
test('the annotated header documents the key next to the other consent keys', () => {
|
||||
cfg(['set', 'telemetry', 'off']); // first set writes the header
|
||||
const yaml = fs.readFileSync(path.join(state, 'config.yaml'), 'utf-8');
|
||||
expect(yaml).toContain('memorable_recall: off');
|
||||
expect(yaml).toContain('gstack never sets it');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user