mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 15:20:11 +02:00
Merge origin/main into garrytan/upgrade-gstack-gbrain-v1
Catch up to main (1.52.0.0, plan-tune cathedral + browse memory work). Branch bumps to 1.52.1.0 — PATCH above main. Conflict resolutions: - VERSION / package.json → 1.52.1.0 (monotonic above main's 1.52.0.0) - CHANGELOG.md → reconstructed reverse-chronological: this branch's brain-aware-planning + save-results entry renumbered 1.51.1.0 → 1.52.1.0 on top, then main's 1.52.0.0 / 1.51.0.0 / 1.49.0.0 entries, then shared history. No entries dropped or orphaned. - setup → kept both endgame blocks (my gbrain detection + main's plan-tune cathedral hook install); they're independent. - SKILL.md files → regenerated from merged templates via bun run gen:skill-docs (canonical no-gbrain), not accepted from either merge side, per CLAUDE.md. Idempotent (0 STALE on re-run). - bin/gstack-config → both sides' additions present (main's GSTACK_STATE_ROOT support + this branch's gbrain-refresh subcommand). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Declared-profile annotation helper (plan-tune cathedral T7).
|
||||
*
|
||||
* Given a kebab signal_key from scripts/question-registry.ts, returns a
|
||||
* one-line plain-English annotation when the user's declared profile is in
|
||||
* a strong band on the matching dimension, else null. Read-only — never
|
||||
* mutates the profile.
|
||||
*
|
||||
* Signature uses kebab signal_key per D2/Codex correction. Internally maps
|
||||
* to the underscore Dimension key by consulting SIGNAL_MAP and picking the
|
||||
* dimension this signal influences most strongly.
|
||||
*
|
||||
* Used by:
|
||||
* - hosts/claude/hooks/question-preference-hook (Layer 3 injection path,
|
||||
* when AUQ mutation lands)
|
||||
* - scripts/resolvers/question-tuning.ts preamble (Layer 9 fallback,
|
||||
* host-portable path on Codex / older Claude Code)
|
||||
*
|
||||
* NOT used for AUTO_DECIDE. Annotation is advisory only — declared-only
|
||||
* per TODOS.md E1 substrate-risk guidance. Inferred-driven AUTO_DECIDE
|
||||
* remains v2.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
import { SIGNAL_MAP, type Dimension, ALL_DIMENSIONS } from './psychographic-signals';
|
||||
|
||||
const STRONG_HIGH = 0.7;
|
||||
const STRONG_LOW = 0.3;
|
||||
|
||||
/**
|
||||
* Plain-English phrasing per dimension + band. Keep one sentence each.
|
||||
* Used directly in question prose, so phrasing matters.
|
||||
*/
|
||||
const DIMENSION_PHRASING: Record<Dimension, { high: string; low: string }> = {
|
||||
scope_appetite: {
|
||||
high: 'Your declared profile leans complete-implementation (boil the ocean).',
|
||||
low: 'Your declared profile leans ship-small-fast.',
|
||||
},
|
||||
risk_tolerance: {
|
||||
high: 'Your declared profile leans move-fast.',
|
||||
low: 'Your declared profile leans check-carefully.',
|
||||
},
|
||||
detail_preference: {
|
||||
high: 'Your declared profile leans verbose-with-tradeoffs.',
|
||||
low: 'Your declared profile leans terse, just-do-it.',
|
||||
},
|
||||
autonomy: {
|
||||
high: 'Your declared profile leans delegate-and-trust.',
|
||||
low: 'Your declared profile leans consult-me-first.',
|
||||
},
|
||||
architecture_care: {
|
||||
high: 'Your declared profile leans get-the-design-right.',
|
||||
low: 'Your declared profile leans pragmatic-ship-it.',
|
||||
},
|
||||
};
|
||||
|
||||
interface DeveloperProfile {
|
||||
declared?: Partial<Record<Dimension, number>>;
|
||||
}
|
||||
|
||||
function stateRoot(): string {
|
||||
return (
|
||||
process.env.GSTACK_STATE_ROOT ||
|
||||
process.env.GSTACK_HOME ||
|
||||
path.join(os.homedir(), '.gstack')
|
||||
);
|
||||
}
|
||||
|
||||
function readProfile(): DeveloperProfile | null {
|
||||
try {
|
||||
const p = path.join(stateRoot(), 'developer-profile.json');
|
||||
if (!fs.existsSync(p)) return null;
|
||||
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which dimension a signal_key influences most strongly.
|
||||
* Sums |delta| across all user_choice → DimensionDelta[] entries for that
|
||||
* signal, returns the dimension with the largest total influence.
|
||||
* Returns null if the signal_key isn't in the map.
|
||||
*/
|
||||
export function primaryDimensionFor(signalKey: string): Dimension | null {
|
||||
const entry = SIGNAL_MAP[signalKey];
|
||||
if (!entry) return null;
|
||||
const totals: Partial<Record<Dimension, number>> = {};
|
||||
for (const choice of Object.keys(entry)) {
|
||||
for (const dd of entry[choice]) {
|
||||
totals[dd.dim] = (totals[dd.dim] ?? 0) + Math.abs(dd.delta);
|
||||
}
|
||||
}
|
||||
let best: Dimension | null = null;
|
||||
let bestVal = -Infinity;
|
||||
for (const d of ALL_DIMENSIONS) {
|
||||
const v = totals[d] ?? 0;
|
||||
if (v > bestVal) {
|
||||
bestVal = v;
|
||||
best = d;
|
||||
}
|
||||
}
|
||||
return bestVal > 0 ? best : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a signal_key, return a one-line plain-English annotation when
|
||||
* the user's declared profile is in a strong band on the primary dim,
|
||||
* else null.
|
||||
*/
|
||||
export function getDeclaredAnnotation(signalKey: string): string | null {
|
||||
if (!signalKey || typeof signalKey !== 'string') return null;
|
||||
const dim = primaryDimensionFor(signalKey);
|
||||
if (!dim) return null;
|
||||
|
||||
const profile = readProfile();
|
||||
const declared = profile?.declared?.[dim];
|
||||
if (typeof declared !== 'number') return null;
|
||||
|
||||
if (declared >= STRONG_HIGH) return DIMENSION_PHRASING[dim].high;
|
||||
if (declared <= STRONG_LOW) return DIMENSION_PHRASING[dim].low;
|
||||
return null;
|
||||
}
|
||||
@@ -187,6 +187,23 @@ export const SIGNAL_MAP: Record<string, Record<string, DimensionDelta[]>> = {
|
||||
skip: [{ dim: 'architecture_care', delta: -0.04 }],
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// decision-autonomy — does the user trust the agent to apply decisions
|
||||
// without checking back? (Cathedral T7: was the missing signal for the
|
||||
// 'autonomy' dimension; added so /plan-tune annotations can render
|
||||
// 'consult me' vs 'delegate' guidance on merge/rollback questions.)
|
||||
// -----------------------------------------------------------------------
|
||||
'decision-autonomy': {
|
||||
accept: [{ dim: 'autonomy', delta: +0.04 }],
|
||||
reject: [{ dim: 'autonomy', delta: -0.04 }],
|
||||
// common option keys for "I'll review first" vs "go ahead":
|
||||
'review-first': [{ dim: 'autonomy', delta: -0.05 }],
|
||||
proceed: [{ dim: 'autonomy', delta: +0.05 }],
|
||||
// /investigate-style: "agent applies fix" vs "show me the diff first"
|
||||
'apply-fix': [{ dim: 'autonomy', delta: +0.04 }],
|
||||
'show-diff': [{ dim: 'autonomy', delta: -0.04 }],
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// session-mode — office-hours goal selection
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -455,6 +455,7 @@ export const QUESTIONS = {
|
||||
category: 'approval',
|
||||
door_type: 'one-way',
|
||||
options: ['accept', 'reject'],
|
||||
signal_key: 'decision-autonomy',
|
||||
description: "Merge this PR to base branch?",
|
||||
},
|
||||
'land-and-deploy-rollback': {
|
||||
@@ -463,6 +464,7 @@ export const QUESTIONS = {
|
||||
category: 'approval',
|
||||
door_type: 'one-way',
|
||||
options: ['accept', 'reject'],
|
||||
signal_key: 'decision-autonomy',
|
||||
description: "Canary detected regressions — roll back the deploy?",
|
||||
},
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ export function generateQuestionTuning(ctx: TemplateContext): string {
|
||||
|
||||
Before each AskUserQuestion, choose \`question_id\` from \`scripts/question-registry.ts\` or \`{skill}-{slug}\`, then run \`${bin}/gstack-question-preference --check "<id>"\`. \`AUTO_DECIDE\` means choose the recommended option and say "Auto-decided [summary] → [option] (your preference). Change with /plan-tune." \`ASK_NORMALLY\` means ask.
|
||||
|
||||
After answer, log best-effort:
|
||||
**Embed the question_id as a marker in the question text** so hooks can identify it deterministically (plan-tune cathedral T14 / D18 progressive markers). Append \`<gstack-qid:{question_id}>\` somewhere in the rendered question (the leading line or trailing line is fine; the marker doesn't render visibly to the user when wrapped in HTML-style angle brackets, but the hook strips it). Without the marker the PreToolUse enforcement hook treats the AUQ as observed-only and never auto-decides — so always include it when the question matches a registered \`question_id\`.
|
||||
|
||||
**Embed the option recommendation via the \`(recommended)\` label suffix** on exactly one option per AUQ. The PreToolUse hook parses \`(recommended)\` first, falls back to "Recommendation: X" prose, and refuses to auto-decide if ambiguous. Two \`(recommended)\` labels = refuse.
|
||||
|
||||
After answer, log best-effort (PostToolUse hook also captures deterministically when installed; dedup on (source, tool_use_id) handles double-writes):
|
||||
\`\`\`bash
|
||||
${bin}/gstack-question-log '{"skill":"${ctx.skillName}","question_id":"<id>","question_summary":"<short>","category":"<approval|clarification|routing|cherry-pick|feedback-loop>","door_type":"<one-way|two-way>","options_count":N,"user_choice":"<key>","recommended":"<key>","session_id":"'"$_SESSION_ID"'"}' 2>/dev/null || true
|
||||
\`\`\`
|
||||
|
||||
Reference in New Issue
Block a user