forgetrail 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTINUATION_PROMPT.md +42 -0
- package/INITIAL_PROMPT.md +214 -0
- package/LICENSE +201 -0
- package/README.md +198 -0
- package/TRACKING_SCHEMA.md +162 -0
- package/TRY_FORGETRAIL.md +78 -0
- package/WORKFLOW.md +921 -0
- package/content/AGENT_INTEGRATION_claude.md +38 -0
- package/content/AGENT_INTEGRATION_cursor.md +53 -0
- package/content/AGENT_INTEGRATION_generic.md +50 -0
- package/content/AGENT_INTEGRATION_grok.md +82 -0
- package/content/DEV_AUTOMATION_SCRIPTS.md +103 -0
- package/content/FORGETRAIL_LITE.md +1202 -0
- package/content/FORGETRAIL_LITE_UPDATES.md +13 -0
- package/content/FORGETRAIL_PROGRESS.md +26 -0
- package/content/GENESIS_SPEC_PROMPT.md +70 -0
- package/content/GENESIS_STUB.md +18 -0
- package/content/GREENFIELD_INTAKE.md +76 -0
- package/content/KICKOFF_WITHOUT_MCP.md +16 -0
- package/content/LITE_WORKFLOW_TRACKING.json +42 -0
- package/content/NEW_PROJECT_BOOTSTRAP.md +180 -0
- package/content/ONE_CLICK_DEV_SETUP.md +142 -0
- package/content/PLAN_MODE_PATTERNS.md +43 -0
- package/content/POCKETBASE_SCHEMA_SCRIPT.md +52 -0
- package/content/POST_BOOTSTRAP_USER_MESSAGE.md +51 -0
- package/content/SCAFFOLD_INSTALL.json +110 -0
- package/content/SESSION_RESUME_MCP.md +35 -0
- package/content/SYSTEM_HEALTH_CHECKS.md +131 -0
- package/content/USER_REPLY_FORMAT.md +41 -0
- package/content/cursor-rules/forgetrail-lessons-gate.mdc +30 -0
- package/content/cursor-rules/forgetrail-lessons-mcp.mdc +24 -0
- package/content/cursor-rules/forgetrail-no-trailer.mdc +49 -0
- package/content/cursor-rules/forgetrail-phase-status.mdc +61 -0
- package/content/cursor-rules/forgetrail-updates-log.mdc +30 -0
- package/content/cursor-rules/spec-completion.mdc +33 -0
- package/content/cursor-rules/specs-and-todo.mdc +26 -0
- package/content/cursor-rules/url-host-matching.mdc +31 -0
- package/content/cursor-rules/us-english.mdc +9 -0
- package/content/cursor-rules/user-facing-content.mdc +31 -0
- package/content/examples/GENESIS_SAMPLE_mars-habitat-roster.md +154 -0
- package/content/forgetrail-workspace-README.md +43 -0
- package/content/scripts/forgetrail-dev-launcher.mjs +208 -0
- package/content/scripts/forgetrail-env.mjs +51 -0
- package/content/scripts/setup-ollama.mjs +207 -0
- package/content/scripts/setup-pocketbase.mjs +133 -0
- package/content/scripts/test-ollama.mjs +71 -0
- package/content/scripts/test-pocketbase.mjs +110 -0
- package/content/skills/forgetrail/SKILL.md +92 -0
- package/docs/AUTOMATED_TESTING.md +269 -0
- package/docs/BLACK_HAT_REPORT.md +143 -0
- package/docs/BRAND_AND_PRODUCT.md +381 -0
- package/docs/BUGS.md +62 -0
- package/docs/BUSINESS_PLAN.md +256 -0
- package/docs/CODE_QUALITY.md +170 -0
- package/docs/CONTEXT_PROMPT.md +357 -0
- package/docs/DEPLOYMENT.md +181 -0
- package/docs/DESIGN_SYSTEM.md +414 -0
- package/docs/DEV_ESTIMATE.md +127 -0
- package/docs/FORGETRAIL_RENAME.md +193 -0
- package/docs/IDEAS.md +19 -0
- package/docs/MARKETING_GROWTH.md +301 -0
- package/docs/NAMING_EXPLORATION.md +406 -0
- package/docs/NPM.md +98 -0
- package/docs/PHASE_1_BRIEF.md +161 -0
- package/docs/README.md +75 -0
- package/docs/SPEC_FEATURE_TEMPLATE.md +155 -0
- package/docs/SPEC_UI_CHROME_NAV_TEMPLATE.md +48 -0
- package/docs/TECHNICAL_REFERENCE.md +836 -0
- package/docs/TEST_PLAN.md +467 -0
- package/docs/TODO.md +125 -0
- package/package.json +68 -0
- package/prompts/black-hat-audit.md +235 -0
- package/prompts/brand-copy-edit-pass.md +55 -0
- package/prompts/cialdini-marketing-audit.md +141 -0
- package/prompts/competitor-deep-dive.md +201 -0
- package/prompts/docs-alignment-audit.md +108 -0
- package/prompts/engineering-skill-library.md +75 -0
- package/prompts/landing-page-rewrite.md +63 -0
- package/prompts/microcopy-centralization.md +128 -0
- package/prompts/panel-usability-audit.md +197 -0
- package/prompts/personal-beta-outreach.md +195 -0
- package/prompts/pre-launch-audit.md +148 -0
- package/prompts/product-feedback-to-spec.md +76 -0
- package/prompts/propagate-to-forgetrail.md +315 -0
- package/prompts/user-facing-content-sync-audit.md +200 -0
- package/prompts/ux-cohesion-audit.md +165 -0
- package/scripts/ensure-lease.mjs +23 -0
- package/scripts/forgetrail-cli.mjs +85 -0
- package/scripts/install-forgetrail-lite.mjs +11 -0
- package/scripts/install-forgetrail.mjs +11 -0
- package/scripts/install-lib.mjs +115 -0
- package/scripts/install.mjs +163 -0
- package/scripts/link-global.mjs +100 -0
- package/scripts/mcp-lib.mjs +177 -0
- package/scripts/mcp-status.mjs +155 -0
- package/scripts/publish-gate.mjs +110 -0
- package/scripts/rename-to-forgetrail.mjs +273 -0
- package/workflow_tracking.json +124 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install Ollama when possible, detect GPU VRAM, pull a non-thinking local model.
|
|
4
|
+
* Copy to app repo scripts/setup-ollama.mjs
|
|
5
|
+
*
|
|
6
|
+
* Policy: default to instruction models (Granite 4.1, Gemma 3) — NOT reasoning/thinking
|
|
7
|
+
* families unless OLLAMA_USE_THINKING=1 in .env.
|
|
8
|
+
*
|
|
9
|
+
* Env: OLLAMA_MODEL (override), OLLAMA_PREFER_GEMMA=1, OLLAMA_USE_THINKING=1,
|
|
10
|
+
* OLLAMA_BASE_URL, SKIP_OLLAMA_INSTALL=1
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { loadEnv, repoRootFromImportMeta } from "./forgetrail-env.mjs";
|
|
16
|
+
|
|
17
|
+
const repoRoot = repoRootFromImportMeta(import.meta.url);
|
|
18
|
+
const stampPath = join(repoRoot, ".forgetrail", "ollama-model.txt");
|
|
19
|
+
const isWin = process.platform === "win32";
|
|
20
|
+
|
|
21
|
+
const THINKING_MODEL_RE =
|
|
22
|
+
/(^|:|\/)?(deepseek-r1|qwq|qwen3-thinking|o1|o3|reasoning|think\b|-think)/i;
|
|
23
|
+
|
|
24
|
+
/** Approximate pull sizes (MB) for VRAM budgeting — leave ~2GB headroom for context/KV. */
|
|
25
|
+
const MODEL_VRAM_MB = {
|
|
26
|
+
"gemma3:1b": 1200,
|
|
27
|
+
"ibm/granite4.1:3b": 2500,
|
|
28
|
+
"gemma3:4b": 4000,
|
|
29
|
+
"ibm/granite4:3b": 3500,
|
|
30
|
+
"ibm/granite4.1:8b": 6000,
|
|
31
|
+
"gemma3:12b": 9000,
|
|
32
|
+
"gemma3:27b": 18000,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function run(cmd, args, opts = {}) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const p = spawn(cmd, args, { stdio: "inherit", shell: isWin, ...opts });
|
|
38
|
+
p.on("error", reject);
|
|
39
|
+
p.on("exit", (c) => (c === 0 ? resolve() : reject(new Error(`${cmd} exited ${c}`))));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runCapture(cmd, args) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const p = spawn(cmd, args, { shell: isWin });
|
|
46
|
+
let out = "";
|
|
47
|
+
p.stdout?.on("data", (d) => (out += d));
|
|
48
|
+
p.stderr?.on("data", (d) => (out += d));
|
|
49
|
+
p.on("error", () => resolve(""));
|
|
50
|
+
p.on("exit", () => resolve(out.trim()));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function hasOllama() {
|
|
55
|
+
const out = await runCapture("ollama", ["--version"]);
|
|
56
|
+
return /ollama/i.test(out);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function detectVramMb() {
|
|
60
|
+
if (isWin) {
|
|
61
|
+
const nvidia = await runCapture("nvidia-smi", [
|
|
62
|
+
"--query-gpu=memory.total",
|
|
63
|
+
"--format=csv,noheader,nounits",
|
|
64
|
+
]);
|
|
65
|
+
const m = nvidia.match(/(\d+)/);
|
|
66
|
+
if (m) return Number(m[1]);
|
|
67
|
+
} else {
|
|
68
|
+
const nvidia = await runCapture("nvidia-smi", [
|
|
69
|
+
"--query-gpu=memory.total",
|
|
70
|
+
"--format=csv,noheader,nounits",
|
|
71
|
+
]);
|
|
72
|
+
const first = nvidia.split(/\r?\n/)[0]?.match(/(\d+)/);
|
|
73
|
+
if (first) return Number(first[1]);
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assertNotThinking(model, env) {
|
|
79
|
+
if (env.OLLAMA_USE_THINKING === "1") return;
|
|
80
|
+
if (THINKING_MODEL_RE.test(model)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Model "${model}" looks like a reasoning/thinking model. Set OLLAMA_USE_THINKING=1 only if the brief requires it. Default: Granite 4.1 or Gemma 3 instruct variants.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pickModel(vramMb, env) {
|
|
88
|
+
if (env.OLLAMA_MODEL?.trim()) {
|
|
89
|
+
const m = env.OLLAMA_MODEL.trim();
|
|
90
|
+
assertNotThinking(m, env);
|
|
91
|
+
return m;
|
|
92
|
+
}
|
|
93
|
+
const preferGemma = env.OLLAMA_PREFER_GEMMA === "1";
|
|
94
|
+
const tier = (m) => {
|
|
95
|
+
assertNotThinking(m, env);
|
|
96
|
+
return m;
|
|
97
|
+
};
|
|
98
|
+
if (vramMb >= 10000) {
|
|
99
|
+
return tier(preferGemma ? "gemma3:12b" : "ibm/granite4.1:8b");
|
|
100
|
+
}
|
|
101
|
+
if (vramMb >= 6000) {
|
|
102
|
+
return tier(preferGemma ? "gemma3:4b" : "ibm/granite4.1:8b");
|
|
103
|
+
}
|
|
104
|
+
if (vramMb >= 4000) {
|
|
105
|
+
return tier(preferGemma ? "gemma3:4b" : "ibm/granite4.1:3b");
|
|
106
|
+
}
|
|
107
|
+
if (vramMb > 0) {
|
|
108
|
+
return tier("gemma3:4b");
|
|
109
|
+
}
|
|
110
|
+
return tier(preferGemma ? "gemma3:4b" : "ibm/granite4.1:3b");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function installOllama() {
|
|
114
|
+
console.log("Ollama not found — attempting install…");
|
|
115
|
+
if (isWin) {
|
|
116
|
+
try {
|
|
117
|
+
await run("winget", ["install", "-e", "--id", "Ollama.Ollama", "--accept-package-agreements", "--accept-source-agreements"]);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
console.log("winget install failed. Download from https://ollama.com/download/windows and re-run setup-ollama.");
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (process.platform === "darwin") {
|
|
125
|
+
try {
|
|
126
|
+
await run("brew", ["install", "ollama"]);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
console.log("brew install failed. Run: curl -fsSL https://ollama.com/install.sh | sh");
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
await run("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"]);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
console.log("Install script failed. See https://ollama.com/download");
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function ensureOllamaServe() {
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch("http://127.0.0.1:11434/api/version", { signal: AbortSignal.timeout(2000) });
|
|
145
|
+
if (res.ok) return;
|
|
146
|
+
} catch {
|
|
147
|
+
/* start below */
|
|
148
|
+
}
|
|
149
|
+
console.log("Starting Ollama service in background…");
|
|
150
|
+
const child = spawn("ollama", ["serve"], { detached: true, stdio: "ignore", shell: isWin });
|
|
151
|
+
child.unref();
|
|
152
|
+
for (let i = 0; i < 20; i++) {
|
|
153
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch("http://127.0.0.1:11434/api/version", { signal: AbortSignal.timeout(2000) });
|
|
156
|
+
if (res.ok) return;
|
|
157
|
+
} catch {
|
|
158
|
+
/* retry */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new Error("Ollama did not become ready on http://127.0.0.1:11434 — open the Ollama app (Windows/Mac) and retry.");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const env = loadEnv(repoRoot);
|
|
166
|
+
if (env.SKIP_OLLAMA_INSTALL === "1") {
|
|
167
|
+
console.log("SKIP_OLLAMA_INSTALL=1 — skipping Ollama setup.");
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let installed = await hasOllama();
|
|
172
|
+
if (!installed && env.SKIP_OLLAMA_AUTO_INSTALL !== "1") {
|
|
173
|
+
installed = await installOllama();
|
|
174
|
+
}
|
|
175
|
+
if (!installed) {
|
|
176
|
+
console.error("Ollama is not installed. Install from https://ollama.com then run: pnpm run setup:ollama");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await ensureOllamaServe();
|
|
181
|
+
|
|
182
|
+
const vramMb = await detectVramMb();
|
|
183
|
+
const model = pickModel(vramMb, env);
|
|
184
|
+
const needMb = MODEL_VRAM_MB[model] || 5000;
|
|
185
|
+
console.log(
|
|
186
|
+
vramMb
|
|
187
|
+
? `Detected GPU VRAM ≈ ${vramMb} MB — selected ${model} (needs ~${needMb} MB).`
|
|
188
|
+
: `No NVIDIA VRAM detected — using CPU-friendly ${model}. First run may be slow.`
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (existsSync(stampPath) && readFileSync(stampPath, "utf8").trim() === model && !process.argv.includes("--force")) {
|
|
192
|
+
console.log(`Model ${model} already recorded — skipping pull (use --force to re-pull).`);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(`Pulling ${model}…`);
|
|
195
|
+
await run("ollama", ["pull", model]);
|
|
196
|
+
mkdirSync(join(repoRoot, ".forgetrail"), { recursive: true });
|
|
197
|
+
writeFileSync(stampPath, model, "utf8");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log("\nNext: node scripts/test-ollama.mjs (or double-click test-ollama.bat)");
|
|
201
|
+
console.log(`Set in .env: OLLAMA_BASE_URL=http://127.0.0.1:11434 OLLAMA_MODEL=${model}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main().catch((e) => {
|
|
205
|
+
console.error(e.message || e);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reference PocketBase bootstrap — copy to app repo scripts/setup-pocketbase.mjs
|
|
4
|
+
* ForgeTrail: never hardcode a PocketBase version in FORGETRAIL_LITE or SCAFFOLD_INSTALL alone.
|
|
5
|
+
*
|
|
6
|
+
* Version resolution (in order):
|
|
7
|
+
* 1. POCKETBASE_VERSION in .env (semver, e.g. 0.26.2) — use when locked after a successful boot
|
|
8
|
+
* 2. POCKETBASE_VERSION=latest or unset → GitHub releases/latest at install time
|
|
9
|
+
*
|
|
10
|
+
* Idempotent: skips download if binary exists and version file matches (unless --force).
|
|
11
|
+
*/
|
|
12
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { chmod, unlink } from "node:fs/promises";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { pipeline } from "node:stream/promises";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const repoRoot = join(__dirname, "..");
|
|
20
|
+
const pbDir = join(repoRoot, "pocketbase");
|
|
21
|
+
const dataDir = join(pbDir, "pb_data");
|
|
22
|
+
const versionFile = join(pbDir, ".pocketbase-version");
|
|
23
|
+
const isWin = process.platform === "win32";
|
|
24
|
+
const exeName = isWin ? "pocketbase.exe" : "pocketbase";
|
|
25
|
+
const exePath = join(pbDir, exeName);
|
|
26
|
+
|
|
27
|
+
function loadEnv() {
|
|
28
|
+
const envPath = join(repoRoot, ".env");
|
|
29
|
+
if (!existsSync(envPath)) return {};
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
|
|
32
|
+
const t = line.trim();
|
|
33
|
+
if (!t || t.startsWith("#")) continue;
|
|
34
|
+
const i = t.indexOf("=");
|
|
35
|
+
if (i < 1) continue;
|
|
36
|
+
out[t.slice(0, i).trim()] = t.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function resolveVersion(env) {
|
|
42
|
+
const v = (env.POCKETBASE_VERSION || "latest").trim();
|
|
43
|
+
if (v !== "latest") return v.replace(/^v/, "");
|
|
44
|
+
const res = await fetch("https://api.github.com/repos/pocketbase/pocketbase/releases/latest", {
|
|
45
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "forgetrail-setup" },
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) throw new Error(`Could not resolve latest PocketBase release (${res.status})`);
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
const tag = String(data.tag_name || "").replace(/^v/, "");
|
|
50
|
+
if (!tag) throw new Error("GitHub latest release had no tag_name");
|
|
51
|
+
return tag;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function platformAsset(version) {
|
|
55
|
+
const os = isWin ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
|
|
56
|
+
const arch = process.arch === "arm64" ? "arm64" : "amd64";
|
|
57
|
+
const ext = os === "windows" ? "zip" : "zip";
|
|
58
|
+
return {
|
|
59
|
+
url: `https://github.com/pocketbase/pocketbase/releases/download/v${version}/pocketbase_${version}_${os}_${arch}.${ext}`,
|
|
60
|
+
os,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function download(url, dest) {
|
|
65
|
+
const res = await fetch(url);
|
|
66
|
+
if (!res.ok) throw new Error(`Download failed ${res.status}: ${url}`);
|
|
67
|
+
await pipeline(res.body, createWriteStream(dest));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function extractZip(zipPath) {
|
|
71
|
+
if (isWin) {
|
|
72
|
+
const ps = `Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${pbDir.replace(/'/g, "''")}' -Force`;
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
const p = spawn("powershell", ["-NoProfile", "-Command", ps], { stdio: "inherit", cwd: repoRoot });
|
|
75
|
+
p.on("exit", (c) => (c === 0 ? resolve() : reject(new Error(`Expand-Archive exit ${c}`))));
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
const p = spawn("unzip", ["-o", zipPath, "-d", pbDir], { stdio: "inherit" });
|
|
81
|
+
p.on("exit", (c) => (c === 0 ? resolve() : reject(new Error(`unzip exit ${c}`))));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function portInUse(host, port) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`http://${host}:${port}/api/health`, { signal: AbortSignal.timeout(2000) });
|
|
88
|
+
return res.ok;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
const force = process.argv.includes("--force");
|
|
96
|
+
const env = loadEnv();
|
|
97
|
+
const version = await resolveVersion(env);
|
|
98
|
+
const host = env.POCKETBASE_HTTP_HOST || "127.0.0.1";
|
|
99
|
+
const port = env.POCKETBASE_HTTP_PORT || env.PUBLIC_POCKETBASE_PORT || "8096";
|
|
100
|
+
|
|
101
|
+
mkdirSync(pbDir, { recursive: true });
|
|
102
|
+
mkdirSync(dataDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
if (existsSync(exePath) && existsSync(versionFile) && readFileSync(versionFile, "utf8").trim() === version && !force) {
|
|
105
|
+
console.log(`PocketBase ${version} already present — skipping download.`);
|
|
106
|
+
} else {
|
|
107
|
+
console.log(`Installing PocketBase ${version}…`);
|
|
108
|
+
const { url } = platformAsset(version);
|
|
109
|
+
const zipPath = join(pbDir, "pocketbase.zip");
|
|
110
|
+
await download(url, zipPath);
|
|
111
|
+
await extractZip(zipPath);
|
|
112
|
+
try {
|
|
113
|
+
await unlink(zipPath);
|
|
114
|
+
} catch {
|
|
115
|
+
/* ignore */
|
|
116
|
+
}
|
|
117
|
+
if (!isWin) await chmod(exePath, 0o755);
|
|
118
|
+
writeFileSync(versionFile, version, "utf8");
|
|
119
|
+
console.log(`Installed to ${exePath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (await portInUse(host, port)) {
|
|
123
|
+
console.log(`Note: something already responds on http://${host}:${port} — may be this or another PocketBase.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`Start with: pnpm run pocketbase (or double-click run.bat)`);
|
|
127
|
+
console.log(`Admin UI: http://${host}:${port}/_/`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch((e) => {
|
|
131
|
+
console.error(e.message || e);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Isolated Ollama completion smoke test — no app required.
|
|
4
|
+
* Copy to app repo scripts/test-ollama.mjs
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { loadEnv, ollamaBaseUrlFromEnv, repoRootFromImportMeta } from "./forgetrail-env.mjs";
|
|
9
|
+
|
|
10
|
+
const repoRoot = repoRootFromImportMeta(import.meta.url);
|
|
11
|
+
const stampPath = join(repoRoot, ".forgetrail", "ollama-model.txt");
|
|
12
|
+
|
|
13
|
+
function fail(msg) {
|
|
14
|
+
console.error(`Ollama check: FAIL — ${msg}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveModel(env) {
|
|
19
|
+
if (env.OLLAMA_MODEL?.trim()) return env.OLLAMA_MODEL.trim();
|
|
20
|
+
if (existsSync(stampPath)) return readFileSync(stampPath, "utf8").trim();
|
|
21
|
+
return "ibm/granite4.1:3b";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const env = loadEnv(repoRoot);
|
|
26
|
+
const base = ollamaBaseUrlFromEnv(env);
|
|
27
|
+
const model = resolveModel(env);
|
|
28
|
+
|
|
29
|
+
let version;
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`${base.origin}/api/version`, { signal: AbortSignal.timeout(5000) });
|
|
32
|
+
if (!res.ok) fail(`Ollama not reachable at ${base.origin} (HTTP ${res.status}). Run setup-ollama first.`);
|
|
33
|
+
version = await res.json();
|
|
34
|
+
console.log(`Ollama check: server OK — ${JSON.stringify(version)}`);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
fail(`${base.origin} — ${e.message || e}. Install/start Ollama (setup-ollama.bat).`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tagsRes = await fetch(`${base.origin}/api/tags`, { signal: AbortSignal.timeout(5000) });
|
|
40
|
+
const tags = tagsRes.ok ? await tagsRes.json() : { models: [] };
|
|
41
|
+
const names = (tags.models || []).map((m) => m.name || m.model).filter(Boolean);
|
|
42
|
+
const hasModel = names.some((n) => n === model || n.startsWith(`${model}:`) || n.includes(model));
|
|
43
|
+
if (!hasModel) {
|
|
44
|
+
console.warn(`Model "${model}" not in local list (${names.slice(0, 5).join(", ")}…). Run setup-ollama.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const prompt = "Reply with exactly one word: pong";
|
|
48
|
+
console.log(`Completion test (${model})…`);
|
|
49
|
+
const genRes = await fetch(`${base.origin}/api/generate`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
model,
|
|
54
|
+
prompt,
|
|
55
|
+
stream: false,
|
|
56
|
+
options: { num_predict: 16, temperature: 0 },
|
|
57
|
+
}),
|
|
58
|
+
signal: AbortSignal.timeout(120000),
|
|
59
|
+
});
|
|
60
|
+
if (!genRes.ok) {
|
|
61
|
+
const errText = await genRes.text().catch(() => "");
|
|
62
|
+
fail(`generate failed HTTP ${genRes.status}${errText ? `: ${errText.slice(0, 200)}` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
const out = await genRes.json();
|
|
65
|
+
const text = (out.response || "").trim();
|
|
66
|
+
if (!text) fail("empty completion — model may still be loading; retry in a minute.");
|
|
67
|
+
console.log(`Ollama check: OK — model responded: "${text.slice(0, 80)}${text.length > 80 ? "…" : ""}"`);
|
|
68
|
+
console.log(`Use OLLAMA_BASE_URL=${base.origin} OLLAMA_MODEL=${model} in .env for runtime LLM routes.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch((e) => fail(e.message || String(e)));
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Isolated PocketBase health check — no app dev server required.
|
|
4
|
+
* Copy to app repo scripts/test-pocketbase.mjs
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/test-pocketbase.mjs
|
|
7
|
+
* Exit 0 = healthy; non-zero = failure with actionable message.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { loadEnv, pocketBaseUrlFromEnv, repoRootFromImportMeta } from "./forgetrail-env.mjs";
|
|
13
|
+
|
|
14
|
+
const repoRoot = repoRootFromImportMeta(import.meta.url);
|
|
15
|
+
const env = loadEnv(repoRoot);
|
|
16
|
+
const pbUrl = pocketBaseUrlFromEnv(env);
|
|
17
|
+
|
|
18
|
+
function fail(msg, code = 1) {
|
|
19
|
+
console.error(`PocketBase check: FAIL — ${msg}`);
|
|
20
|
+
process.exit(code);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ok(msg) {
|
|
24
|
+
console.log(`PocketBase check: OK — ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchHealth(origin) {
|
|
28
|
+
const res = await fetch(`${origin}/api/health`, { signal: AbortSignal.timeout(5000) });
|
|
29
|
+
if (!res.ok) throw new Error(`health returned ${res.status}`);
|
|
30
|
+
const body = await res.json().catch(() => ({}));
|
|
31
|
+
return body;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function tryStartLocalPb() {
|
|
35
|
+
const pbDir = join(repoRoot, "pocketbase");
|
|
36
|
+
const isWin = process.platform === "win32";
|
|
37
|
+
const exe = join(pbDir, isWin ? "pocketbase.exe" : "pocketbase");
|
|
38
|
+
if (!existsSync(exe)) return false;
|
|
39
|
+
const host = env.POCKETBASE_HTTP_HOST || "127.0.0.1";
|
|
40
|
+
const port = env.POCKETBASE_HTTP_PORT || env.PUBLIC_POCKETBASE_PORT || "8096";
|
|
41
|
+
console.log(`Starting local PocketBase (${host}:${port}) for this test…`);
|
|
42
|
+
const child = spawn(exe, ["serve", `--http=${host}:${port}`], {
|
|
43
|
+
cwd: pbDir,
|
|
44
|
+
detached: true,
|
|
45
|
+
stdio: "ignore",
|
|
46
|
+
});
|
|
47
|
+
child.unref();
|
|
48
|
+
for (let i = 0; i < 15; i++) {
|
|
49
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
50
|
+
try {
|
|
51
|
+
await fetchHealth(`http://${host}:${port}`);
|
|
52
|
+
return `http://${host}:${port}`;
|
|
53
|
+
} catch {
|
|
54
|
+
/* retry */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
let origin = pbUrl?.origin;
|
|
62
|
+
if (!origin) {
|
|
63
|
+
fail(
|
|
64
|
+
"Set PUBLIC_POCKETBASE_URL (or POCKETBASE_HTTP_HOST + POCKETBASE_HTTP_PORT) in .env — see .env.example."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let health;
|
|
69
|
+
try {
|
|
70
|
+
health = await fetchHealth(origin);
|
|
71
|
+
ok(`${origin} — ${JSON.stringify(health)}`);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.log(`No response at ${origin} (${e.message || e}).`);
|
|
74
|
+
const started = await tryStartLocalPb();
|
|
75
|
+
if (!started) {
|
|
76
|
+
fail(
|
|
77
|
+
`Nothing listening at ${origin}. Run setup.bat / pnpm run setup:pocketbase, then pnpm run pocketbase or run.bat.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
origin = started;
|
|
81
|
+
health = await fetchHealth(origin);
|
|
82
|
+
ok(`${origin} (started for test) — ${JSON.stringify(health)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const email = env.POCKETBASE_ADMIN_EMAIL;
|
|
86
|
+
const password = env.POCKETBASE_ADMIN_PASSWORD;
|
|
87
|
+
if (email && password) {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${origin}/api/admins/auth-with-password`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ identity: email, password }),
|
|
93
|
+
signal: AbortSignal.timeout(8000),
|
|
94
|
+
});
|
|
95
|
+
if (res.ok) {
|
|
96
|
+
console.log("Admin auth: OK (POCKETBASE_ADMIN_* credentials accepted).");
|
|
97
|
+
} else {
|
|
98
|
+
console.warn(`Admin auth: HTTP ${res.status} — check POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD.`);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn(`Admin auth: skipped (${e.message || e}).`);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
console.log("Admin auth: skipped (set POCKETBASE_ADMIN_EMAIL + POCKETBASE_ADMIN_PASSWORD in .env to test).");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`Admin UI: ${origin}/_/`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((e) => fail(e.message || String(e)));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: forgetrail
|
|
3
|
+
description: "Enforce the ForgeTrail 7-phase lifecycle (Plan → Build → Stabilize → Iterate → Refine → Align → Harden), maintain .forgetrail/workflow_tracking.json as the system of record, pause at explicit phase transitions for user approval, prefer native plan modes when available, and use subagents for parallel audits/research where the host agent supports it. Activate for any non-trivial full-stack app development. Follow progressive documentation rules and propagate lessons back."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: "kickoff new project | resume session | Phase 4 feature work | run black-hat audit | stabilize current issues"
|
|
6
|
+
allowed-tools: "read_file, search_replace, run_terminal_command, todo_write, spawn_subagent, getPhaseGuidance, runAudit, searchLessons, validateTracking, suggestSubagentDecomposition, getTemplate, getNewProjectKickoff, getResumeSessionInstructions"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# ForgeTrail Skill — Lifecycle + Memory Layer
|
|
10
|
+
|
|
11
|
+
You are operating under the ForgeTrail methodology. Your primary job is to give the user a disciplined, experienced build partner instead of a generic coding assistant.
|
|
12
|
+
|
|
13
|
+
## Core Rules (always active)
|
|
14
|
+
|
|
15
|
+
1. **Follow the 7 phases strictly**:
|
|
16
|
+
- 1. Architecture + Planning (lock decisions before code)
|
|
17
|
+
- 2. Scaffolding + Core Build (full runnable spine + hero flow in one pass)
|
|
18
|
+
- 3. Stabilize (fix env, errors, reliability)
|
|
19
|
+
- 4. Iterate (features with real data)
|
|
20
|
+
- 5. Refine (refactor when files >500 lines or patterns duplicate)
|
|
21
|
+
- 6. Align (map to brand/strategy)
|
|
22
|
+
- 7. Harden (security, perf, docs, production readiness)
|
|
23
|
+
|
|
24
|
+
Phases 4 and 5 commonly alternate. Update `currentPhase` and exit criteria in tracking.
|
|
25
|
+
|
|
26
|
+
2. **At the start of every session or major turn**:
|
|
27
|
+
- Read `.forgetrail/workflow_tracking.json` (create `.forgetrail/` if missing).
|
|
28
|
+
- Read `CONTEXT_PROMPT.md` and `TODO.md` if they exist.
|
|
29
|
+
- Call `getPhaseGuidance` for the current phase if needed.
|
|
30
|
+
- Summarize current phase, exit criteria met/remaining, and recent decisions to the user briefly.
|
|
31
|
+
|
|
32
|
+
3. **Pause at phase transitions**:
|
|
33
|
+
- When you believe exit criteria for the current phase are met, explicitly tell the user: "I think we have completed Phase X because [reasons]. The remaining exit criteria are [list]. Ready to move to Phase Y?"
|
|
34
|
+
- Wait for explicit user approval before advancing `currentPhase` in tracking.
|
|
35
|
+
|
|
36
|
+
4. **Maintain the tracking file** (`.forgetrail/workflow_tracking.json`):
|
|
37
|
+
- Log every major decision with rationale and alternatives_considered.
|
|
38
|
+
- Move items from `exitCriteriaRemaining` to `exitCriteriaMet`.
|
|
39
|
+
- Add to `gotchas[]` immediately when something surprising or painful happens.
|
|
40
|
+
- Append session notes at natural breaks.
|
|
41
|
+
- Use the schema from `getTrackingSchema`.
|
|
42
|
+
|
|
43
|
+
5. **Use native agent capabilities**:
|
|
44
|
+
- If the agent supports a native plan mode (e.g. Grok `/plan`), use it for Phase 1. On approval, export the plan into `docs/PHASE_1_BRIEF.md` (via `getTemplate`) + `decisions[]`.
|
|
45
|
+
- When the host supports `spawn_subagent`, use `suggestSubagentDecomposition` (or reason directly) to run audits, research, and reviews in parallel with appropriate `capability_mode`, `isolation`, and personas. Synthesize results in the parent thread and update tracking/docs.
|
|
46
|
+
- Prefer `read-only` or `execute` modes for subagents doing analysis.
|
|
47
|
+
|
|
48
|
+
6. **Progressive documentation**:
|
|
49
|
+
- Only create docs when the current phase requires them (see `getProgressiveDocSchedule`).
|
|
50
|
+
- Phase 1: `PHASE_1_BRIEF.md` + decisions.
|
|
51
|
+
- Phase 2: Merge brief into `CONTEXT_PROMPT.md`, plus `README.md`, `TODO.md`, `.forgetrail/IDEAS.md`.
|
|
52
|
+
- Later phases add `TECHNICAL_REFERENCE.md`, audits, etc. only when needed.
|
|
53
|
+
|
|
54
|
+
7. **Reply format** (when offering options):
|
|
55
|
+
- Numbered lists for ordered pipelines/steps.
|
|
56
|
+
- Bullets for parallel options.
|
|
57
|
+
- Letters (A/B/C) for pick-one choices.
|
|
58
|
+
- Keep first user-facing replies short after bootstrap (no raw tool dumps).
|
|
59
|
+
|
|
60
|
+
8. **Audits & Lessons**:
|
|
61
|
+
- Before large/complex work, call `runAudit` or `searchLessons` (or `suggestSubagentDecomposition` if subagents are available).
|
|
62
|
+
- Use `getAntiPatterns` for known failure modes.
|
|
63
|
+
|
|
64
|
+
## Phase Transition Protocol
|
|
65
|
+
|
|
66
|
+
When ready to advance:
|
|
67
|
+
- Confirm all exit criteria.
|
|
68
|
+
- Update tracking.
|
|
69
|
+
- Ask user: "Ready to move to Phase X?"
|
|
70
|
+
- Only after confirmation, update `currentPhase` and start the next phase's playbook.
|
|
71
|
+
|
|
72
|
+
## For New Projects (Greenfield)
|
|
73
|
+
|
|
74
|
+
Prefer calling `getNewProjectKickoff` (or `kickoffGreenfield`) at the very beginning. Write the returned `.forgetrail/workflow_tracking.json`, optional Cursor rules, and follow the post-bootstrap instructions exactly. Then start with Phase 1.
|
|
75
|
+
|
|
76
|
+
## For Resuming
|
|
77
|
+
|
|
78
|
+
Call `getResumeSessionInstructions` (or read tracking + CONTEXT_PROMPT) and continue from the current phase.
|
|
79
|
+
|
|
80
|
+
## Key Tools to Use Proactively
|
|
81
|
+
|
|
82
|
+
- `getPhaseGuidance(<phase>)` — before starting work in a phase.
|
|
83
|
+
- `getTemplate({name: "..."})` — for PHASE_1_BRIEF, CONTEXT_PROMPT, audits, etc. Use mode "shell" for clean structure.
|
|
84
|
+
- `runAudit`, `searchLessons`, `getAntiPatterns`
|
|
85
|
+
- `suggestSubagentDecomposition` (when subagents available)
|
|
86
|
+
- `validateTracking` (to keep the state file healthy)
|
|
87
|
+
- `ingestPlanArtifact` (after native plan mode approval — map plan → PHASE_1_BRIEF + decisions[])
|
|
88
|
+
- `todo_write` — mirror open exit criteria and next actions.
|
|
89
|
+
|
|
90
|
+
Always keep the user in the loop on phase progress and major decisions. The tracking file + CONTEXT_PROMPT are the source of truth across sessions, not chat history.
|
|
91
|
+
|
|
92
|
+
This skill makes you a much more reliable long-term build partner. Use it.
|