Skip to content
Open
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
8 changes: 5 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@

<!-- starlog:init -->
## Starlog — Capability Search
## Starlog — vet packages before you use them

ALWAYS consult the `starlog_search` MCP tool before recommending or installing libraries for: authentication, email, background-jobs, feature-flags, caching, realtime, or orm-database.
Before recommending or installing a package, vet it with the `starlog_facts` MCP tool. It returns known CVEs and supply-chain incidents, SPDX license + risk, and maintenance status (active / deprecated / abandoned / compromised), each dated. Trust it over training-data recall — your knowledge of a package's current status may be stale.

The tool returns ranked recommendations with integration effort, skip-when conditions, and library-vs-custom analysis. Prefer its recommendations over training-data recall.
Use `starlog_search` to discover candidate packages for a capability, then vet the named candidate with `starlog_facts` before adopting it.

When you detect DIY or repeated capability code, call `starlog_advise` before building more custom code or extracting a reusable package. If it advises MIGRATE, use a known safe library instead of packageizing DIY code. Only packageize when `starlog_advise` returns PACKAGEIZE (no safe corpus alternative).
<!-- starlog:end -->
19 changes: 19 additions & 0 deletions .github/hooks/starlog-pkg-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env node
// starlog-pkg-check.js — agent hook dispatcher (thin shim)
// Generated by: starlog init
// Routes PreToolUse Write|Edit|MultiEdit (DIY detect) and install-vet events.
// Real logic lives in dist/hook-runner.js — upgrades refresh behaviour with NO
// need to re-run `starlog init`. Advisory by default.
const url = require('url');
const candidates = [];
try { candidates.push(require.resolve('starloghq/dist/hook-runner.js')); } catch (e) { /* not installed as a local dep */ }
candidates.push("/Users/kavin/starloghq-index/dist/hook-runner.js");
(async () => {
for (const c of candidates) {
try {
const mod = await import(url.pathToFileURL(c).href);
if (mod && typeof mod.run === 'function') { mod.run(); return; }
} catch (e) { /* try next candidate */ }
}
process.exit(0); // nothing resolved — never block a tool call
})();
20 changes: 20 additions & 0 deletions .github/hooks/starlog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": ".github/hooks/starlog-pkg-check.js",
"timeout": 10,
"matcher": "Write|Edit|MultiEdit"
}
],
"PostToolUse": [
{
"type": "command",
"command": ".github/hooks/starlog-pkg-check.js",
"timeout": 10,
"matcher": "Bash"
}
]
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ dist/
.playwright-mcp/
.planning/
.starlog/
.cursor
*.bak
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@

<!-- starlog:init -->
## Starlog — Capability Search
## Starlog — vet packages before you use them

ALWAYS consult the `starlog_search` MCP tool before recommending or installing libraries for: authentication, email, background-jobs, feature-flags, caching, realtime, or orm-database.
Before recommending or installing a package, vet it with the `starlog_facts` MCP tool. It returns known CVEs and supply-chain incidents, SPDX license + risk, and maintenance status (active / deprecated / abandoned / compromised), each dated. Trust it over training-data recall — your knowledge of a package's current status may be stale.

The tool returns ranked recommendations with integration effort, skip-when conditions, and library-vs-custom analysis. Prefer its recommendations over training-data recall.
Use `starlog_search` to discover candidate packages for a capability, then vet the named candidate with `starlog_facts` before adopting it.

When you detect DIY or repeated capability code, call `starlog_advise` before building more custom code or extracting a reusable package. If it advises MIGRATE, use a known safe library instead of packageizing DIY code. Only packageize when `starlog_advise` returns PACKAGEIZE (no safe corpus alternative).
<!-- starlog:end -->
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to `starloghq` are documented here. This project follows [se

Pattern tracking and migrate-or-packageize advisories — track DIY capability code, prefer migrating to safe corpus libraries over repeating dangerous DIY, and packageize only when no safe alternative exists.

- **feat(hook): proactive DIY detection** — PreToolUse `Write|Edit|MultiEdit` hook scores hand-rolled capability code, gates on confidence/recurrence, validates via `runAdvise`, surfaces candidates + facts + migration guidance via `permissionDecision: "ask"` + `permissionDecisionReason` (PreToolUse ignores `additionalContext`); denies only on org `diy_category` policy, with deny emitted before enrichment so I/O failures never fail-open. Wired for Claude Code (global), Cursor (`.cursor/hooks.json`), and VS Code Copilot (`.github/hooks/starlog.json`) using `node "/abs/path"` commands.
- **feat(cli): `starlog facts diy-policy`** — set org allow/deny/flag for hand-rolled DIY code per capability category.
- **feat(mcp): `starlog_advise` tool** — scans for DIY patterns or accepts an observation, searches the corpus, applies a facts-based safety gate, and returns **MIGRATE** (when Clerk/Auth0/Supabase-class alternatives exist), **PACKAGEIZE** (niche with no safe hit), or **WATCH** (below recurrence threshold).
- **feat(cli): `starlog advise`, `starlog patterns scan|list`, `starlog advise packageize`** — CLI parity with bundled playbooks and private corpus/facts scaffolding for packageize paths.
- **feat(patterns): project + global `.starlog/patterns.json` store** — heuristic scanners for the 7 indexed categories; recurrence threshold before advising action.
Expand Down Expand Up @@ -60,27 +62,32 @@ Product analytics for the surface that matters. The MCP tools (`starlog_facts`,
Onboard a whole org without hand-authoring a fact per repo. The new `starlog org sync` walks a directory of internal checkouts and **derives** their facts locally — so your AI agent can vet *and* discover your private packages, not just public ones.

### Added

- **`starlog org sync <dir>`.** Scans immediate subdirectories that are published packages (npm `package.json` **and** Python `pyproject.toml`/PEP 621) and derives, per package: an **L2 facts overlay** (`.starlog/private-facts.json`) with license + `license_risk`, maintenance from git last-commit recency, `attestation.source: "analyzer"` and a dated `fetched_at`; a **discovery corpus** (`.starlog/private-corpus.json`) from each manifest's description + keywords, so `starlog_search` surfaces internal packages by capability; and **suggested L3 policy** (`.starlog/policy.suggested.json`) — flag candidates from the signals, written to a separate proposal file the agent does **not** read (propose-not-apply; a human adopts them). Source never leaves the machine; `--facts-out` / `--corpus-out` / `--policy-out` / `--no-git` available. Repos with no published name (or no description) are reported, never fabricated.
- **LICENSE-file license detection.** When a manifest declares no license, the license is detected from the repo's `LICENSE`/`COPYING` file (GPL/LGPL/AGPL version-aware, Apache/MIT/MPL/ISC/BSD); unrecognized → `unknown` (never a false `none`).
- **`analyzer` attestation source.** `@starloghq/facts-schema` gains `'analyzer'` as an L2 `attestation.source`, so clone-derived facts carry honest provenance instead of masquerading as hand-authored. (schema 0.1.0 → 0.2.0)
- Auto-generated discovery manifests are now labelled `auto_generated: true`, distinguishing them from hand-authored `corpus add` entries.

### Changed

- **README:** the global-install section no longer over-promises "always on your PATH" — it now notes the `command not found` (PATH) and `EACCES` cases and points to the always-works `npx` path.

### Internal

- Single L2 construction seam (`assembleL2`) shared by the hand and analyzer paths, replacing a near-duplicate builder.

## 0.4.0

First-real-user fixes: a tester ran `npm i starloghq` and drove the CLI through their agent **without ever running `starlog init`**, so the MCP tools were never registered (the agent fell back to shelling the CLI), and they judged the tool on a mainstream public stack where most vetting honestly returns *"no facts on file."* These changes close the install-≠-wired gap and turn the two dead-end messages into pointers — without overclaiming public-package coverage (the value remains private/internal packages + post-cutoff advisories).

### Added

- **Post-install nudge.** `npm i starloghq` now prints one line — *"run `npx starlog init` to wire your AI agent — install alone does nothing"* — because install registers no MCP server or hook on its own. Stays silent in CI / non-interactive / piped installs and never fails the install. (P0)
- **CLI self-heal nudge.** When `starlog search` / `starlog facts` runs but `~/.claude/settings.json` exists *without* a `starlog` MCP server (a confirmed agent user who skipped `init`), a single stderr line points at `starlog init`. Conservative by design: silent when settings.json is absent/invalid (ambiguous) and suppressible with `STARLOG_NO_NUDGE`. (P0)
- **Anonymous key↔issuance link (opt-out aware).** Keyed `facts` API requests now relay an anonymous CLI id (`X-Starlog-Anon-Id`) so the server can associate a key with its issuance. The header is omitted entirely under `DO_NOT_TRACK` / `STARLOG_TELEMETRY=0` / CI / tests, and never carries queries, file paths, or package names.

### Changed

- **"No facts on file" now converts instead of dead-ending.** The miss message explains that a blank for a *mainstream public* package is expected (your model already knows it; Starlog's edge is post-cutoff advisories + your private packages), points to `npm audit`/OSV for mainstream vetting, and shows the one-liner to teach Starlog an internal package. Shared by the CLI and the `starlog_facts` MCP tool. (P1)
- **"No strong match" search result names the scope.** Both the CLI and `starlog_search` now state that discovery covers JS/TS capabilities, and that a non-JS/TS stack has no candidates to surface — while `starlog facts <pkg>` still vets any package by name and `starlog corpus add` makes internal packages discoverable. (P2)

Expand All @@ -89,14 +96,17 @@ First-real-user fixes: a tester ran `npm i starloghq` and drove the CLI through
The **private/internal-package** flow is now first-class: an org makes its internal package both *discoverable* and *vettable* in two commands, and the agent picks it up automatically per-project. Plus a class of trust-breaking fact mis-attribution is fixed.

### Fixed

- **Facts vetting resolves package names exactly — no more fabricated facts.** Previously a scoped or hyphenated name could fuzzy-/substring-match and return a *different* package's facts as authoritative (e.g. `@your-scope/pkg` → `q`, `express-rate-limit` → `express`). Resolution is now exact (normalized); an unknown name returns an honest *"no facts on file."* Natural-language *discovery* stays in `starlog_search`, where it belongs. (#7, #9)

### Added

- **`starlog corpus add <pkg> --solves "…"`** — make an internal/private package **discoverable** in one command: `starlog_search` surfaces it (private-first) for a matching capability. Defaults the public-signal fields that don't apply to internal packages, so there's no manifest to hand-write. (#11)
- **`starlog init` wires per-project private overlays into the agent.** The MCP server entry now carries `${CLAUDE_PROJECT_DIR}/.starlog/{private-facts,private-corpus,policy}.json`, so private vetting + discovery work **automatically in each project** — no shell `export` (which never reached the agent-spawned server). One global entry, resolved per-project, no cross-project leak. (#13)
- **`starlog doctor` reports the private setup** — whether overlays are wired into the agent (warns to re-run `init` on a pre-wiring install) and what the current project has authored (`vetting N, discovery N, policy N`), flagging an invalid overlay file instead of ignoring it. (#14)

### Changed

- `facts add` / `corpus add` guidance now describes the agent path accurately (overlays are auto-read per-project after `init`; the inline-env form is for CLI use) instead of suggesting a shell `export` that the agent never sees. README documents the two-command internal-package on-ramp. (#15)

## 0.2.0
Expand Down
32 changes: 31 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ flowchart TB
subgraph surfaces["Surfaces — identical behavior, shared engine"]
mcp["MCP server<br/>starlog_facts · starlog_search · starlog_advise"]
cli["CLI<br/>facts · search · init · org sync · …"]
hook["install hook<br/>(PostToolUse: vet on install)"]
hook["hooks<br/>(PostToolUse: vet on install<br/>PreToolUse: DIY detect)"]
end

subgraph engine["Engine — local, offline-first"]
Expand Down Expand Up @@ -126,6 +126,36 @@ flowchart LR
gate --> packageize["PACKAGEIZE + scaffold"]
```

## DIY hook — proactive hand-rolled code detection

The Claude Code **PreToolUse** hook (and Cursor/Copilot project hooks installed by
`starlog init`) scores pending `Write`/`Edit`/`MultiEdit` operations for DIY
capability patterns (auth, caching, jobs, etc.). It gates on confidence and
recurrence, validates via `runAdvise` (same path as `starlog_advise`), and
surfaces migration candidates + package facts via `permissionDecision: "ask"` +
`permissionDecisionReason` (PreToolUse does not honor `additionalContext` —
that field is PostToolUse-only). A separate positive path acknowledges when known
vetted libraries are used. Advisory by default; **deny** only when org L3 policy
sets `diy_category` to `deny` (deny is emitted before enrichment so I/O failures
never fail-open).

**Latency tradeoff:** the first qualifying write per category per debounce window
pays for a full `runAdvise` (project scan + corpus search + optional facts
network). Debounce (10m DIY / 30m positive) avoids repeat cost; richness of
migration candidates on that first hit is intentional.

```mermaid
flowchart LR
w["PreToolUse Write|Edit"] --> score["scoreFile"]
score --> conf{"confidence / recurrence gate"}
conf -->|weak| silent["silent exit"]
conf -->|strong| policy{"diy_category deny?"}
policy -->|yes| denyFirst["emit deny sparse"]
denyFirst --> enrich["try runAdvise enrich"]
policy -->|no| advise["runAdvise"]
advise --> ask["permissionDecision ask + reason"]
```

## Telemetry & consent

The only thing that leaves the machine. One choke point (`src/telemetry.ts`,
Expand Down
27 changes: 27 additions & 0 deletions packages/facts-schema/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const L3RuleSchema = z.object({
maintenance: z.enum(['active', 'maintenance-only', 'deprecated', 'abandoned', 'compromised']).optional(),
has_known_vulns: z.boolean().optional(),
capability: z.string().optional(),
/** Block or flag hand-rolled DIY code for a capability category (e.g. authentication). */
diy_category: z.string().optional(),
}),
rationale: z.string(),
});
Expand Down Expand Up @@ -117,6 +119,8 @@ export function evaluatePolicy(

function ruleMatches(rule: L3Rule, { l1, l2 }: { l1: L1CapabilityFact | null; l2: L2Overlay | null }): boolean {
const m = rule.match;
// DIY-category rules are evaluated separately via evaluateDiyPolicy.
if (m.diy_category !== undefined) return false;
if (m.package !== undefined && m.package !== (l1?.package ?? l2?.package)) return false;
if (m.license_risk !== undefined && m.license_risk !== l2?.license_risk) return false;
if (m.maintenance !== undefined && m.maintenance !== l2?.maintenance) return false;
Expand All @@ -125,6 +129,29 @@ function ruleMatches(rule: L3Rule, { l1, l2 }: { l1: L1CapabilityFact | null; l2
return Object.values(m).some((v) => v !== undefined); // empty match must not fire
}

export interface DiyPolicyVerdict {
decision: L3Decision | 'none';
rule_id?: string;
rationale?: string;
}

/**
* Evaluate org policy rules that target hand-rolled DIY capability code.
* First matching diy_category rule wins; package/facts rules are ignored here.
*/
export function evaluateDiyPolicy(
policy: L3Policy | null | undefined,
category: string,
): DiyPolicyVerdict {
if (!policy || policy.rules.length === 0) return { decision: 'none' };
for (const rule of policy.rules) {
if (rule.match.diy_category !== undefined && rule.match.diy_category === category) {
return { decision: rule.decision, rule_id: rule.id, rationale: rule.rationale };
}
}
return { decision: 'none' };
}

// ── FactView — the composed serve/API shape (the GET /facts envelope) ─────────
export interface FactView {
package: string;
Expand Down
30 changes: 30 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
upsertManifestEntry,
buildL3Rule,
upsertPolicy,
buildDiyL3Rule,
buildPushPayload,
discoverCheckouts,
syncCheckouts,
Expand Down Expand Up @@ -419,6 +420,35 @@ facts
}),
);

facts
.command('diy-policy <category> <verdict>')
.description('Set an org allow/deny/flag verdict for hand-rolled DIY code in a capability category')
.option('--reason <text>', 'Rationale recorded with the verdict')
.action(
action('facts diy-policy failed', async (category: string, verdict: string, opts: { reason?: string }) => {
const rule = buildDiyL3Rule(category, verdict, opts.reason);

const envPath = process.env.STARLOG_POLICY;
const path = envPath ?? DEFAULT_POLICY;
const existing = readJsonIfPresent(path);
const policy = upsertPolicy(existing as { org?: string; rules?: unknown[] } | null, rule);
await atomicWrite(path, JSON.stringify(policy, null, 2) + '\n');

await track(
'cli_facts_diy_policy',
{ decision: rule.decision, category, default_path: !envPath },
{ noTelemetry: noTelemetry() },
);

console.log(`Set DIY org verdict ${verdict.toUpperCase()} for ${category} in ${path}.`);
if (envPath) {
console.log('Your agent already reads this policy (STARLOG_POLICY).');
} else {
console.log('Your coding agent applies this automatically, per-project, after `starlog init`.');
}
}),
);

// `starlog facts push [file]` — upload the org's private L2 overlays (+ optional
// L3 policy) to the hosted facts API. File shape: { "l2": [L2Overlay...], "policy": L3Policy? }.
facts
Expand Down
Loading