Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ From Bash, bootstrap the profile with one command:
```

The bootstrap is safe to rerun. It installs the unpinned top-level Pi package sources,
merges only SecStack-managed package entries and shell-path configuration into the
SecStack profile's `settings.json` and creates a profile-local Python environment. It links the
merges only SecStack-managed package entries, shell-path configuration, and `quietStartup`
preference into the SecStack profile's `settings.json` and creates a profile-local Python environment. It links the
profile's `SYSTEM.md` to the installed SecStack package, so `pi update --extensions` updates the
research-agent identity and prompt envelope. It does not install coding-task guidance or link
global `AGENTS.md` or `APPEND_SYSTEM.md` files into the profile.
Expand Down
16 changes: 8 additions & 8 deletions context/MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ The bootstrap configures the isolated profile at `~/.pi/secstack-agent`. Its
- `agent-browser`.

Pi installs and updates each source independently. The bootstrap manages only those package
entries, the profile's Pi-managed shell path, the profile-local Python environment, and a
relative `SYSTEM.md` symlink into the installed SecStack package. The system-prompt-viewer
package is the sole owner of the `/system-context` overlay. SecStack's
entries, the profile's Pi-managed shell path, the `quietStartup` preference, the profile-local
Python environment, and a relative `SYSTEM.md` symlink into the installed SecStack package. The
system-prompt-viewer package is the sole owner of the `/system-context` overlay. SecStack's
`restore-tool-envelope` extension restores Pi's live tool snippets and guidelines after
`SYSTEM.md` replaces Pi's coding-agent identity. Its `startup-header` extension replaces
Pi's built-in header with a responsive, theme-adaptive SecStack research mark. Coding-task
guidance files are not linked, and the filtered `pi-setup` package contributes no skills,
including `repo-nav` and `bootstrap-docs`. Research runs in the primary Pi context;
no sub-agent package is installed. Authentication, model selection, provider configuration,
sessions, and unrelated settings remain profile-local and untouched.
Pi's built-in header with a responsive, theme-adaptive SecStack research mark and interactive
research pipeline. Coding-task guidance files are not linked, and the filtered `pi-setup` package
contributes no skills, including `repo-nav` and `bootstrap-docs`. Research runs in the primary
Pi context; no sub-agent package is installed. Authentication, model selection, provider
configuration, sessions, and unrelated settings remain profile-local and untouched.

## Skill internals

Expand Down
186 changes: 132 additions & 54 deletions extensions/startup-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,85 +4,163 @@ import type {
} from "@earendil-works/pi-coding-agent";
import { truncateToWidth } from "@earendil-works/pi-tui";

const WIDE_HEADER_WIDTH = 50;
const COMPACT_HEADER_WIDTH = 38;
const WIDE_HEADER_WIDTH = 42;
const COMPACT_HEADER_WIDTH = 34;

function fit(lines: string[], width: number): string[] {
return lines.map((line) => truncateToWidth(line, width, ""));
}

function renderWide(theme: Theme, width: number): string[] {
const border = (text: string) => theme.fg("borderMuted", text);
const ink = (text: string) => theme.fg("text", text);
const copy = (text: string) => theme.fg("muted", text);
function renderWide(theme: Theme, width: number, showPipeline: boolean): string[] {
const g = (text: string) => theme.fg("success", text);
const gold = (text: string) => theme.fg("warning", text);
const mark = (text: string) => theme.fg("accent", text);
const ink = (text: string) => theme.bold(theme.fg("text", text));
const tree = (text: string) => theme.fg("dim", text);
const copy = (text: string) => theme.fg("muted", text);

return fit(
[
"",
border("┌─────────────────────┐"),
`${border("│")} ${ink(theme.bold("FORM 10-K · ITEM 7"))} ${border("◢│")}`,
`${border("│")} ${copy("━━━━━━━━━━━━━━━ ")} ${border("│")} ${mark("┏")}`,
`${border("│")} ${copy("━━━━━━━━━━ ")} ${border("│")} ${mark("┃")} ${ink(theme.bold("S E C S T A C K"))}`,
`${border("│")} ${mark("███████████████")} ${border("│")}${mark("━━┫━━")} ${mark(theme.bold("EVIDENCE → JUDGMENT"))}`,
`${border("│")} ${copy("━━━━━━━━━━━━━━━━ ")} ${border("│")} ${mark("┃")}`,
`${border("│")} ${copy("━━━━━━━━━━━ ")} ${border("│")} ${mark("┗")}`,
border("└─────────────────────┘"),
"",
],
Math.min(width, WIDE_HEADER_WIDTH),
);
const corner = `${g("[")}${gold("$")}${g("]")}`;
const billTop = `${g("╔═")}${corner}${g("════════════════")}${corner}${g("═╗")}`;
const billMid = `${g("║")} ${ink("SECSTACK")} ${g("║")}`;
const billBot = `${g("╚═")}${corner}${g("════════════════")}${corner}${g("═╝")}`;

const lines = [
"",
` ${mark("▄█▄")}`,
` ${mark("┌─────┐")} ${billTop}`,
` ${mark("│ ")}${gold("█ █")}${mark(" │")} ${billMid}`,
` ${mark("┌┴─────┴┐")} ${billBot}`,
` ${mark("▀▀▀▀▀▀▀▀▀▀▀")}`,
];

if (showPipeline) {
if (width >= 77) {
lines.push(
"",
` ${tree("Research Pipeline:")}`,
` ${tree("├─ ")}${gold("[SIGNALS]")} ${copy("Insider clusters · 13D blocks · Market screens · Themes")}`,
` ${tree("├─ ")}${theme.fg("mdCode", "[EDGAR]")} ${copy("10-K/Q/8-K · XBRL financials · 13F holders · Proxies")}`,
` ${tree("├─ ")}${mark("[MARKET]")} ${copy("Live quotes · Peer comps · Trailing returns · Transcripts")}`,
` ${tree("├─ ")}${theme.fg("mdHeading", "[ANALYSIS]")} ${copy("Normalized FCFF · Reverse DCF / EPV · Moats · Pre-mortem")}`,
` ${tree("└─ ")}${theme.fg("syntaxType", "[VIC PITCH]")} ${copy("Norbert Lou VIC style · Hard arithmetic · Falsification")}`,
);
} else {
lines.push(
"",
` ${tree("Research Pipeline:")}`,
` ${tree("├─ ")}${gold("[SIGNALS]")} ${copy("Insider clusters · 13Ds · Screens")}`,
` ${tree("├─ ")}${theme.fg("mdCode", "[EDGAR]")} ${copy("10-K/Q/8-K · XBRL · 13F holders")}`,
` ${tree("├─ ")}${mark("[MARKET]")} ${copy("Quotes · Peer comps · Transcripts")}`,
` ${tree("├─ ")}${theme.fg("mdHeading", "[ANALYSIS]")} ${copy("Normalized FCFF · DCF · Pre-mortem")}`,
` ${tree("└─ ")}${theme.fg("syntaxType", "[VIC PITCH]")} ${copy("Norbert Lou VIC pitch · Math")}`,
);
}
}

lines.push("");
return fit(lines, width);
}

function renderCompact(theme: Theme, width: number): string[] {
const border = (text: string) => theme.fg("borderMuted", text);
const ink = (text: string) => theme.fg("text", text);
const copy = (text: string) => theme.fg("muted", text);
function renderCompact(theme: Theme, width: number, showPipeline: boolean): string[] {
const g = (text: string) => theme.fg("success", text);
const gold = (text: string) => theme.fg("warning", text);
const mark = (text: string) => theme.fg("accent", text);
const ink = (text: string) => theme.bold(theme.fg("text", text));
const tree = (text: string) => theme.fg("dim", text);
const copy = (text: string) => theme.fg("muted", text);

return fit(
[
"",
`${border("┌───────────────┐")} ${ink(theme.bold("SECSTACK"))}`,
`${border("│")} ${ink(theme.bold("10-K · ITEM 7"))} ${border("◢│")}`,
`${border("│")} ${copy("━━━━━━━━━━ ")} ${border("│")} ${mark("┓")}`,
`${border("│")} ${mark("██████████")} ${border("│")}${mark("━━┫")}`,
`${border("│")} ${copy("━━━━━━━━━━━━ ")} ${border("│")} ${mark("┛")}`,
`${border("└───────────────┘")} ${mark("EVIDENCE → JUDGMENT")}`,
const corner = `${g("[")}${gold("$")}${g("]")}`;
const billTop = `${g("╔")}${corner}${g("═══════════")}${corner}${g("╗")}`;
const billMid = `${g("║")} ${ink("SECSTACK")} ${g("║")}`;
const billBot = `${g("╚")}${corner}${g("═══════════")}${corner}${g("╝")}`;

const lines = [
"",
` ${mark("▄█▄")}`,
` ${mark("┌─────┐")} ${billTop}`,
` ${mark("│ ")}${gold("█ █")}${mark(" │")} ${billMid}`,
` ${mark("┌┴─────┴┐")} ${billBot}`,
` ${mark("▀▀▀▀▀▀▀▀▀▀▀")}`,
];

if (showPipeline) {
lines.push(
"",
],
Math.min(width, COMPACT_HEADER_WIDTH),
);
` ${tree("Research Pipeline:")}`,
` ${tree("├─ ")}${gold("[SIGNALS]")} ${copy("Insiders · 13Ds · Screens")}`,
` ${tree("├─ ")}${theme.fg("mdCode", "[EDGAR]")} ${copy("10-K/Q · XBRL · 13F")}`,
` ${tree("├─ ")}${mark("[MARKET]")} ${copy("Quotes · Comps · Calls")}`,
` ${tree("├─ ")}${theme.fg("mdHeading", "[ANALYSIS]")} ${copy("FCFF · DCF · Moats")}`,
` ${tree("└─ ")}${theme.fg("syntaxType", "[VIC PITCH]")} ${copy("Norbert Lou pitch")}`,
);
}

lines.push("");
return fit(lines, width);
}

function renderNarrow(theme: Theme, width: number): string[] {
const ink = (text: string) => theme.fg("text", text);
const mark = (text: string) => theme.fg("accent", text);
function renderNarrow(theme: Theme, width: number, showPipeline: boolean): string[] {
const g = (text: string) => theme.fg("success", text);
const gold = (text: string) => theme.fg("warning", text);
const ink = (text: string) => theme.bold(theme.fg("text", text));
const copy = (text: string) => theme.fg("muted", text);
const tree = (text: string) => theme.fg("dim", text);

return fit(
[
"",
ink(theme.bold("SECSTACK")),
`${mark("▰")} ${ink("10-K · ITEM 7")} ${mark("→")} ${mark(theme.bold("JUDGMENT"))}`,
const corner = `${g("[")}${gold("$")}${g("]")}`;
const lines = [
"",
`${corner} ${ink("SECSTACK")} ${corner}`,
copy("Bottom-Up Equity Research"),
];

if (showPipeline) {
lines.push(
"",
],
width,
);
tree("• Signals & Discovery"),
tree("• EDGAR Filings & 13F"),
tree("• Market & Transcripts"),
tree("• Bottom-Up Valuation"),
tree("• Norbert Lou VIC Pitch"),
);
}

lines.push("");
return fit(lines, width);
}

export function renderStartupHeader(theme: Theme, width: number): string[] {
if (width >= WIDE_HEADER_WIDTH) return renderWide(theme, width);
if (width >= COMPACT_HEADER_WIDTH) return renderCompact(theme, width);
return renderNarrow(theme, width);
export function renderStartupHeader(
theme: Theme,
width: number,
showPipeline: boolean = true,
): string[] {
if (width >= WIDE_HEADER_WIDTH) return renderWide(theme, width, showPipeline);
if (width >= COMPACT_HEADER_WIDTH) return renderCompact(theme, width, showPipeline);
return renderNarrow(theme, width, showPipeline);
}

export default function startupHeaderExtension(pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
if (ctx.mode !== "tui") return;

ctx.ui.setHeader((_tui, theme) => ({
render: (width) => renderStartupHeader(theme, width),
let showPipeline = true;

ctx.ui.setHeader((tui, theme) => ({
render: (width) => renderStartupHeader(theme, width, showPipeline),
invalidate() {},
setExpanded(toolsExpanded: boolean) {
// Invert: when tools are expanded for deeper inspection, collapse the
// header to yield vertical screen space. When tools are collapsed,
// show the full research capability pipeline.
const next = !toolsExpanded;
if (showPipeline === next) return;
showPipeline = next;
tui.requestRender();
},
}));
});

pi.on("session_shutdown", (_event, ctx) => {
if (ctx.mode !== "tui") return;
ctx.ui.setHeader(undefined);
});
}
6 changes: 5 additions & 1 deletion scripts/bootstrap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SecStack profile.
*
* This script deliberately changes only the SecStack profile's package list,
* shell command prefix, and optional Bash launcher.
* shell command prefix, quietStartup setting, and optional Bash launcher.
*/
import { execFileSync } from "node:child_process";
import {
Expand Down Expand Up @@ -151,6 +151,10 @@ function mergeSettings() {
: pathCommand;
}

// Silence Pi's default startup banner and raw resource listing so SecStack's
// custom startup header and interactive capability pipeline take center stage.
settings.quietStartup = true;

const temp = join(agentDir, `.settings.${process.pid}.tmp`);
writeFileSync(temp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
renameSync(temp, settingsPath);
Expand Down
Loading