mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 23:37:53 +01:00
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design, ios-qa daemon, lib and scripts resolve the state root through bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the twin and stop with a reinstall message when it is missing; hooks source it and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts delegate to resolveStateRoot. Analytics writers and readers move together so the usage log stays one file. gstack-uninstall deletes state only at ~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor, the checkout or the git root, and leaves any other resolved root in place with the removal command. Fixtures that copy single bins now copy the twin.
535 lines
20 KiB
Bash
Executable File
535 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-developer-profile — unified developer profile access and derivation.
|
|
#
|
|
# Supersedes bin/gstack-builder-profile. The old binary remains as a legacy
|
|
# shim that delegates to `gstack-developer-profile --read`.
|
|
#
|
|
# Subcommands:
|
|
# --read (default) emit KEY: VALUE pairs in builder-profile format
|
|
# for /office-hours compatibility.
|
|
# --derive recompute inferred dimensions from question events;
|
|
# write updated ~/.gstack/developer-profile.json.
|
|
# --profile emit the full profile as JSON (all fields).
|
|
# --gap emit declared-vs-inferred gap as JSON.
|
|
# --trace <dim> show events that contributed to a dimension.
|
|
# --narrative (v2 stub) output a coach bio paragraph.
|
|
# --vibe (v2 stub) output the one-word archetype.
|
|
# --check-mismatch detect meaningful gaps between declared and observed.
|
|
# --migrate migrate builder-profile.jsonl → developer-profile.json.
|
|
# Idempotent; archives the source file on success.
|
|
# --log-session append a session entry (from /office-hours) to
|
|
# sessions[] and update aggregates. Required fields:
|
|
# date, mode. Silent skip on invalid input.
|
|
#
|
|
# Profile file: ~/.gstack/developer-profile.json (unified schema — see
|
|
# docs/designs/PLAN_TUNING_V0.md). Event file: ~/.gstack/projects/{SLUG}/
|
|
# question-events.jsonl.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
# GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16).
|
|
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
|
gstack_state_root_select
|
|
GSTACK_HOME="$_gstack_sr_root"
|
|
# Windows git-bash: GSTACK_HOME resolves to an MSYS path (/c/Users/...), which
|
|
# Bun on Windows cannot open as a filesystem path (bites --derive). Normalize
|
|
# once, here, before PROFILE_FILE/LEGACY_FILE are derived from it below —
|
|
# doing it after would leave those two on the stale MSYS form.
|
|
case "$(uname -s)" in
|
|
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && GSTACK_HOME="$(cygpath -m "$GSTACK_HOME")" ;;
|
|
esac
|
|
PROFILE_FILE="$GSTACK_HOME/developer-profile.json"
|
|
LEGACY_FILE="$GSTACK_HOME/builder-profile.jsonl"
|
|
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
|
|
SLUG="${SLUG:-unknown}"
|
|
|
|
CMD="${1:---read}"
|
|
shift || true
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Migration: builder-profile.jsonl → developer-profile.json
|
|
# -----------------------------------------------------------------------
|
|
do_migrate() {
|
|
if [ ! -f "$LEGACY_FILE" ]; then
|
|
echo "MIGRATE: no legacy file to migrate"
|
|
return 0
|
|
fi
|
|
|
|
if [ -f "$PROFILE_FILE" ]; then
|
|
# Already migrated — no-op (idempotent).
|
|
echo "MIGRATE: already migrated (developer-profile.json exists)"
|
|
return 0
|
|
fi
|
|
|
|
# Run migration in a temp file, then atomic rename.
|
|
local TMPOUT
|
|
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
|
|
trap 'rm -f "$TMPOUT"' EXIT
|
|
|
|
cat "$LEGACY_FILE" | bun -e "
|
|
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
|
const sessions = [];
|
|
const signalsAcc = {};
|
|
const resources = new Set();
|
|
const topics = new Set();
|
|
for (const line of lines) {
|
|
try {
|
|
const e = JSON.parse(line);
|
|
sessions.push(e);
|
|
for (const s of (e.signals || [])) {
|
|
signalsAcc[s] = (signalsAcc[s] || 0) + 1;
|
|
}
|
|
for (const r of (e.resources_shown || [])) resources.add(r);
|
|
for (const t of (e.topics || [])) topics.add(t);
|
|
} catch {}
|
|
}
|
|
const profile = {
|
|
identity: {},
|
|
declared: {},
|
|
inferred: {
|
|
values: {
|
|
scope_appetite: 0.5,
|
|
risk_tolerance: 0.5,
|
|
detail_preference: 0.5,
|
|
autonomy: 0.5,
|
|
architecture_care: 0.5,
|
|
},
|
|
sample_size: 0,
|
|
diversity: { skills_covered: 0, question_ids_covered: 0, days_span: 0 },
|
|
},
|
|
gap: {},
|
|
overrides: {},
|
|
sessions,
|
|
signals_accumulated: signalsAcc,
|
|
resources_shown: Array.from(resources),
|
|
topics: Array.from(topics),
|
|
migrated_at: new Date().toISOString(),
|
|
schema_version: 1,
|
|
};
|
|
console.log(JSON.stringify(profile, null, 2));
|
|
" > "$TMPOUT"
|
|
|
|
# Atomic rename.
|
|
mv "$TMPOUT" "$PROFILE_FILE"
|
|
trap - EXIT
|
|
|
|
# gbrain-sync: enqueue the migrated file for cross-machine sync (no-op if off).
|
|
SCRIPT_DIR_E="$(cd "$(dirname "$0")" && pwd)"
|
|
"$SCRIPT_DIR_E/gstack-brain-enqueue" "developer-profile.json" 2>/dev/null &
|
|
|
|
# Archive the legacy file.
|
|
local TS
|
|
TS="$(date +%Y-%m-%d-%H%M%S)"
|
|
mv "$LEGACY_FILE" "$LEGACY_FILE.migrated-$TS"
|
|
|
|
local COUNT
|
|
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
|
|
echo "MIGRATE: ok — migrated $COUNT sessions from builder-profile.jsonl"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Load-or-migrate helper: ensure developer-profile.json exists.
|
|
# Auto-migrates from builder-profile.jsonl if present.
|
|
# Returns path to profile file via stdout. Creates a minimal stub if nothing exists.
|
|
# -----------------------------------------------------------------------
|
|
ensure_profile() {
|
|
if [ -f "$PROFILE_FILE" ]; then
|
|
return 0
|
|
fi
|
|
if [ -f "$LEGACY_FILE" ]; then
|
|
do_migrate >/dev/null
|
|
return 0
|
|
fi
|
|
# Nothing yet — create a stub.
|
|
mkdir -p "$GSTACK_HOME"
|
|
cat > "$PROFILE_FILE" <<EOF
|
|
{
|
|
"identity": {},
|
|
"declared": {},
|
|
"inferred": {
|
|
"values": {
|
|
"scope_appetite": 0.5,
|
|
"risk_tolerance": 0.5,
|
|
"detail_preference": 0.5,
|
|
"autonomy": 0.5,
|
|
"architecture_care": 0.5
|
|
},
|
|
"sample_size": 0,
|
|
"diversity": { "skills_covered": 0, "question_ids_covered": 0, "days_span": 0 }
|
|
},
|
|
"gap": {},
|
|
"overrides": {},
|
|
"sessions": [],
|
|
"signals_accumulated": {},
|
|
"schema_version": 1
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Record session: append a session entry from /office-hours to sessions[]
|
|
# and update aggregates (signals_accumulated, resources_shown, topics).
|
|
# Fix for #1671: the writer side of the v1.0.0.0 migration. Reader and
|
|
# writer now share the same file.
|
|
# Silent skip on invalid input (matches gstack-timeline-log:22-26 pattern).
|
|
# -----------------------------------------------------------------------
|
|
do_log_session() {
|
|
local INPUT="${1:-}"
|
|
if [ -z "$INPUT" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Validate: input must be parseable JSON with required fields (date, mode).
|
|
if ! printf '%s' "$INPUT" | bun -e "
|
|
const j = JSON.parse(await Bun.stdin.text());
|
|
if (!j.date || !j.mode) process.exit(1);
|
|
" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
ensure_profile
|
|
|
|
local TMPOUT
|
|
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
|
|
trap 'rm -f "$TMPOUT"' EXIT
|
|
|
|
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun -e "
|
|
const fs = require('fs');
|
|
const entry = JSON.parse(process.env.RECORD_INPUT);
|
|
if (!entry.ts) entry.ts = new Date().toISOString();
|
|
|
|
const profile = JSON.parse(fs.readFileSync(process.env.PROFILE_FILE_PATH, 'utf-8'));
|
|
profile.sessions = profile.sessions || [];
|
|
profile.sessions.push(entry);
|
|
|
|
profile.signals_accumulated = profile.signals_accumulated || {};
|
|
for (const s of (entry.signals || [])) {
|
|
profile.signals_accumulated[s] = (profile.signals_accumulated[s] || 0) + 1;
|
|
}
|
|
|
|
profile.resources_shown = profile.resources_shown || [];
|
|
const resSet = new Set(profile.resources_shown);
|
|
for (const r of (entry.resources_shown || [])) resSet.add(r);
|
|
profile.resources_shown = Array.from(resSet);
|
|
|
|
profile.topics = profile.topics || [];
|
|
const topicSet = new Set(profile.topics);
|
|
for (const t of (entry.topics || [])) topicSet.add(t);
|
|
profile.topics = Array.from(topicSet);
|
|
|
|
fs.writeFileSync(process.env.TMPOUT_PATH, JSON.stringify(profile, null, 2));
|
|
"
|
|
|
|
mv "$TMPOUT" "$PROFILE_FILE"
|
|
trap - EXIT
|
|
"$SCRIPT_DIR/gstack-brain-enqueue" "developer-profile.json" 2>/dev/null &
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Read: emit legacy KEY: VALUE output for /office-hours compat.
|
|
# -----------------------------------------------------------------------
|
|
do_read() {
|
|
ensure_profile
|
|
cat "$PROFILE_FILE" | bun -e "
|
|
const p = JSON.parse(await Bun.stdin.text());
|
|
const sessions = p.sessions || [];
|
|
|
|
// SESSION_COUNT / TIER / CROSS_PROJECT / NUDGE must reflect real sessions, not
|
|
// resource-tracking events (the Phase 6 auto-append). Without this filter, a
|
|
// session's resources entry written immediately after the real session inflates
|
|
// the count (bumping TIER), clobbers LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE,
|
|
// and pushes NUDGE_ELIGIBLE over its threshold from bookkeeping alone.
|
|
const realSessions = sessions.filter(e => e.mode !== 'resources');
|
|
|
|
const count = realSessions.length;
|
|
let tier = 'introduction';
|
|
if (count >= 8) tier = 'inner_circle';
|
|
else if (count >= 4) tier = 'regular';
|
|
else if (count >= 1) tier = 'welcome_back';
|
|
const last = realSessions[realSessions.length - 1] || {};
|
|
const prev = realSessions[realSessions.length - 2] || {};
|
|
const crossProject = prev.project_slug && last.project_slug
|
|
? prev.project_slug !== last.project_slug
|
|
: false;
|
|
|
|
const designs = realSessions.map(e => e.design_doc || '').filter(Boolean);
|
|
const designTitles = realSessions
|
|
.map(e => (e.design_doc ? (e.project_slug || 'unknown') : ''))
|
|
.filter(Boolean);
|
|
|
|
const signalCounts = p.signals_accumulated || {};
|
|
let totalSignals = 0;
|
|
for (const v of Object.values(signalCounts)) totalSignals += v;
|
|
const signalStr = Object.entries(signalCounts).map(([k,v]) => k + ':' + v).join(',');
|
|
|
|
// Builder-mode design sessions only — the builder->founder nudge is about
|
|
// someone who keeps returning to *build*, not startup-mode diagnostics and not
|
|
// resources bookkeeping. (Was \`e.mode !== 'startup'\`, which counted resources
|
|
// entries and excluded real startup sessions.)
|
|
const builderSessions = realSessions.filter(e => e.mode === 'builder').length;
|
|
const nudgeEligible = builderSessions >= 3 && totalSignals >= 5;
|
|
|
|
const resources = p.resources_shown || [];
|
|
const topics = p.topics || [];
|
|
|
|
console.log('SESSION_COUNT: ' + count);
|
|
console.log('TIER: ' + tier);
|
|
console.log('LAST_PROJECT: ' + (last.project_slug || ''));
|
|
console.log('LAST_ASSIGNMENT: ' + (last.assignment || ''));
|
|
console.log('LAST_DESIGN_TITLE: ' + (last.design_doc || ''));
|
|
console.log('DESIGN_COUNT: ' + designs.length);
|
|
console.log('DESIGN_TITLES: ' + JSON.stringify(designTitles));
|
|
console.log('ACCUMULATED_SIGNALS: ' + signalStr);
|
|
console.log('TOTAL_SIGNAL_COUNT: ' + totalSignals);
|
|
console.log('CROSS_PROJECT: ' + crossProject);
|
|
console.log('NUDGE_ELIGIBLE: ' + nudgeEligible);
|
|
console.log('RESOURCES_SHOWN: ' + resources.join(','));
|
|
console.log('RESOURCES_SHOWN_COUNT: ' + resources.length);
|
|
console.log('TOPICS: ' + topics.join(','));
|
|
"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Profile: emit the full JSON
|
|
# -----------------------------------------------------------------------
|
|
do_profile() {
|
|
ensure_profile
|
|
cat "$PROFILE_FILE"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Gap: declared vs inferred diff
|
|
# -----------------------------------------------------------------------
|
|
do_gap() {
|
|
ensure_profile
|
|
cat "$PROFILE_FILE" | bun -e "
|
|
const p = JSON.parse(await Bun.stdin.text());
|
|
const declared = p.declared || {};
|
|
const inferred = (p.inferred && p.inferred.values) || {};
|
|
const dims = ['scope_appetite','risk_tolerance','detail_preference','autonomy','architecture_care'];
|
|
const gap = {};
|
|
for (const d of dims) {
|
|
if (declared[d] !== undefined && inferred[d] !== undefined) {
|
|
gap[d] = +(Math.abs(declared[d] - inferred[d])).toFixed(3);
|
|
}
|
|
}
|
|
console.log(JSON.stringify({ declared, inferred, gap }, null, 2));
|
|
"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Derive: recompute inferred dimensions from question-events.jsonl
|
|
# -----------------------------------------------------------------------
|
|
do_derive() {
|
|
ensure_profile
|
|
local EVENTS="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
|
|
local REGISTRY="$ROOT_DIR/scripts/question-registry.ts"
|
|
local SIGNALS="$ROOT_DIR/scripts/psychographic-signals.ts"
|
|
if [ ! -f "$REGISTRY" ] || [ ! -f "$SIGNALS" ]; then
|
|
echo "DERIVE: registry or signals file missing, cannot derive" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cd "$ROOT_DIR"
|
|
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun -e "
|
|
import('./scripts/question-registry.ts').then(async (regmod) => {
|
|
const sigmod = await import('./scripts/psychographic-signals.ts');
|
|
const fs = require('fs');
|
|
const { QUESTIONS } = regmod;
|
|
const { SIGNAL_MAP, applySignal, newDimensionTotals, normalizeToDimensionValue } = sigmod;
|
|
|
|
const profilePath = process.env.PROFILE_FILE_PATH;
|
|
const eventsPath = process.env.EVENTS_PATH;
|
|
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
|
|
let lines = [];
|
|
if (fs.existsSync(eventsPath)) {
|
|
lines = fs.readFileSync(eventsPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
}
|
|
|
|
const totals = newDimensionTotals();
|
|
const skills = new Set();
|
|
const qids = new Set();
|
|
const days = new Set();
|
|
let count = 0;
|
|
for (const line of lines) {
|
|
let e;
|
|
try { e = JSON.parse(line); } catch { continue; }
|
|
if (!e.question_id || !e.user_choice) continue;
|
|
count++;
|
|
skills.add(e.skill);
|
|
qids.add(e.question_id);
|
|
if (e.ts) days.add(String(e.ts).slice(0,10));
|
|
const def = QUESTIONS[e.question_id];
|
|
if (def && def.signal_key) {
|
|
applySignal(totals, def.signal_key, e.user_choice);
|
|
}
|
|
}
|
|
|
|
const values = {};
|
|
for (const [dim, total] of Object.entries(totals)) {
|
|
values[dim] = +normalizeToDimensionValue(total).toFixed(3);
|
|
}
|
|
|
|
profile.inferred = {
|
|
values,
|
|
sample_size: count,
|
|
diversity: {
|
|
skills_covered: skills.size,
|
|
question_ids_covered: qids.size,
|
|
days_span: days.size,
|
|
},
|
|
};
|
|
|
|
// Recompute gap.
|
|
const gap = {};
|
|
for (const d of Object.keys(values)) {
|
|
if (profile.declared && profile.declared[d] !== undefined) {
|
|
gap[d] = +(Math.abs(profile.declared[d] - values[d])).toFixed(3);
|
|
}
|
|
}
|
|
profile.gap = gap;
|
|
profile.derived_at = new Date().toISOString();
|
|
|
|
const tmp = profilePath + '.tmp';
|
|
fs.writeFileSync(tmp, JSON.stringify(profile, null, 2));
|
|
fs.renameSync(tmp, profilePath);
|
|
console.log('DERIVE: ok — ' + count + ' events, ' + skills.size + ' skills, ' + qids.size + ' questions');
|
|
}).catch(err => { console.error('DERIVE:', err.message); process.exit(1); });
|
|
"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Trace: show events contributing to a dimension
|
|
# -----------------------------------------------------------------------
|
|
do_trace() {
|
|
local DIM="${1:-}"
|
|
if [ -z "$DIM" ]; then
|
|
echo "TRACE: missing dimension argument" >&2
|
|
exit 1
|
|
fi
|
|
local EVENTS="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
|
|
if [ ! -f "$EVENTS" ]; then
|
|
echo "TRACE: no events for this project"
|
|
return 0
|
|
fi
|
|
cd "$ROOT_DIR"
|
|
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun -e "
|
|
import('./scripts/question-registry.ts').then(async (regmod) => {
|
|
const sigmod = await import('./scripts/psychographic-signals.ts');
|
|
const fs = require('fs');
|
|
const { QUESTIONS } = regmod;
|
|
const { SIGNAL_MAP } = sigmod;
|
|
const target = process.env.TRACE_DIM;
|
|
const lines = fs.readFileSync(process.env.EVENTS_PATH, 'utf-8').trim().split('\n').filter(Boolean);
|
|
const rows = [];
|
|
for (const line of lines) {
|
|
let e;
|
|
try { e = JSON.parse(line); } catch { continue; }
|
|
const def = QUESTIONS[e.question_id];
|
|
if (!def || !def.signal_key) continue;
|
|
const deltas = SIGNAL_MAP[def.signal_key]?.[e.user_choice] || [];
|
|
for (const d of deltas) {
|
|
if (d.dim === target) {
|
|
rows.push({ ts: e.ts, question_id: e.question_id, choice: e.user_choice, delta: d.delta });
|
|
}
|
|
}
|
|
}
|
|
if (rows.length === 0) {
|
|
console.log('TRACE: no events contribute to ' + target);
|
|
} else {
|
|
console.log('TRACE: ' + rows.length + ' events for ' + target);
|
|
for (const r of rows) {
|
|
console.log(' ' + (r.ts || '').slice(0,19) + ' ' + r.question_id + ' → ' + r.choice + ' (' + (r.delta > 0 ? '+' : '') + r.delta + ')');
|
|
}
|
|
}
|
|
});
|
|
"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Check mismatch: flag when declared ≠ inferred by > threshold
|
|
# -----------------------------------------------------------------------
|
|
do_check_mismatch() {
|
|
ensure_profile
|
|
cat "$PROFILE_FILE" | bun -e "
|
|
const p = JSON.parse(await Bun.stdin.text());
|
|
const declared = p.declared || {};
|
|
const inferred = (p.inferred && p.inferred.values) || {};
|
|
const sampleSize = (p.inferred && p.inferred.sample_size) || 0;
|
|
const diversity = (p.inferred && p.inferred.diversity) || {};
|
|
|
|
// Require enough data before reporting mismatch.
|
|
if (sampleSize < 10) {
|
|
console.log('MISMATCH: not enough data (' + sampleSize + ' events; need 10+)');
|
|
process.exit(0);
|
|
}
|
|
|
|
const THRESHOLD = 0.3;
|
|
const flagged = [];
|
|
for (const d of Object.keys(declared)) {
|
|
if (inferred[d] === undefined) continue;
|
|
const gap = Math.abs(declared[d] - inferred[d]);
|
|
if (gap > THRESHOLD) {
|
|
flagged.push({ dim: d, declared: declared[d], inferred: inferred[d], gap: +gap.toFixed(3) });
|
|
}
|
|
}
|
|
|
|
if (flagged.length === 0) {
|
|
console.log('MISMATCH: none');
|
|
} else {
|
|
console.log('MISMATCH: ' + flagged.length + ' dimension(s) disagree (gap > ' + THRESHOLD + ')');
|
|
for (const f of flagged) {
|
|
console.log(' ' + f.dim + ': declared ' + f.declared + ' vs inferred ' + f.inferred + ' (gap ' + f.gap + ')');
|
|
}
|
|
}
|
|
"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Narrative + Vibe (v2 stubs)
|
|
# -----------------------------------------------------------------------
|
|
do_narrative() {
|
|
echo "NARRATIVE: (v2 — not yet implemented; use /plan-tune profile for now)"
|
|
}
|
|
|
|
do_vibe() {
|
|
ensure_profile
|
|
cd "$ROOT_DIR"
|
|
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun -e "
|
|
import('./scripts/archetypes.ts').then(async (mod) => {
|
|
const p = JSON.parse(process.env.PROFILE_DATA);
|
|
const dims = (p.inferred && p.inferred.values) || {
|
|
scope_appetite: 0.5, risk_tolerance: 0.5, detail_preference: 0.5,
|
|
autonomy: 0.5, architecture_care: 0.5,
|
|
};
|
|
const arch = mod.matchArchetype(dims);
|
|
console.log(arch.name);
|
|
console.log(arch.description);
|
|
});
|
|
"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Dispatch
|
|
# -----------------------------------------------------------------------
|
|
case "$CMD" in
|
|
--read) do_read ;;
|
|
--profile) do_profile ;;
|
|
--gap) do_gap ;;
|
|
--derive) do_derive ;;
|
|
--trace) do_trace "$@" ;;
|
|
--narrative) do_narrative ;;
|
|
--vibe) do_vibe ;;
|
|
--check-mismatch) do_check_mismatch ;;
|
|
--migrate) do_migrate ;;
|
|
--log-session) do_log_session "$@" ;;
|
|
--help|-h) sed -n '1,/^set -euo/p' "$0" | sed 's|^# \?||' ;;
|
|
*)
|
|
echo "gstack-developer-profile: unknown subcommand '$CMD'" >&2
|
|
echo "run --help for usage" >&2
|
|
exit 1
|
|
;;
|
|
esac
|