forgetrail 0.4.13 → 0.4.15
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/INITIAL_PROMPT.md +1 -1
- package/README.md +1 -1
- package/TRY_FORGETRAIL.md +44 -5
- package/WORKFLOW.md +15 -2
- package/content/AGENT_INTEGRATION_claude.md +3 -2
- package/content/AGENT_INTEGRATION_cursor.md +2 -1
- package/content/COMPANION_TOOLS.md +1 -0
- package/content/FORGETRAIL_LITE.md +41 -75
- package/content/GREENFIELD_INTAKE.md +9 -2
- package/content/LITE_WORKFLOW_TRACKING.json +2 -1
- package/content/NEW_PROJECT_BOOTSTRAP.md +2 -3
- package/content/companion-tools.json +18 -0
- package/content/cursor-rules/npm-name-hold.mdc +1 -1
- package/content/cursor-rules/spec-completion.mdc +4 -1
- package/content/cursor-rules/specs-and-todo.mdc +5 -1
- package/content/cursor-rules/url-host-matching.mdc +12 -13
- package/content/cursor-rules/user-facing-content.mdc +2 -2
- package/content/cursor-rules/writing-voice.mdc +6 -0
- package/content/forgetrail-workspace-README.md +2 -4
- package/content/hooks/README.md +36 -0
- package/content/hooks/claude-settings-hooks.json +20 -0
- package/content/hooks/cursor-hooks.json +30 -0
- package/content/hooks/guard-edit.mjs +85 -0
- package/content/hooks/guard-shell.mjs +168 -0
- package/content/hooks/session-start.mjs +93 -0
- package/content/hooks/session-stop.mjs +56 -0
- package/content/hooks/validate-tracking-core.mjs +229 -0
- package/content/hooks/validate-tracking.mjs +65 -0
- package/docs/SPEC_FEATURE_TEMPLATE.md +12 -4
- package/docs/TECHNICAL_REFERENCE.md +1 -1
- package/package.json +2 -1
- package/prompts/microcopy-centralization.md +1 -1
- package/prompts/pre-launch-audit.md +8 -0
- package/prompts/propagate-to-forgetrail.md +1 -1
- package/prompts/rules-audit.md +74 -0
- package/prompts/second-family-critique.md +43 -0
- package/prompts/spec-review.md +65 -0
- package/scripts/forgetrail-cli.mjs +6 -0
- package/scripts/install.mjs +43 -1
- package/scripts/workspace-index.mjs +128 -0
- package/workflow_tracking.json +1 -0
- package/content/cursor-rules/forgetrail-no-trailer.mdc +0 -49
- package/content/cursor-rules/us-english.mdc +0 -9
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Prefer
|
|
2
|
+
description: Prefer exact domain/suffix matching over substring includes for URL checks and SSRF hygiene
|
|
3
3
|
globs:
|
|
4
4
|
- src/**/*.{ts,js}
|
|
5
|
-
- outpost/**/*.{js,mjs}
|
|
6
5
|
- scripts/**/*.{js,mjs,ts}
|
|
7
6
|
alwaysApply: false
|
|
8
7
|
---
|
|
9
8
|
|
|
10
|
-
# URL host matching
|
|
9
|
+
# URL host matching and SSRF hygiene
|
|
11
10
|
|
|
12
|
-
##
|
|
11
|
+
## Domain and host checks
|
|
13
12
|
|
|
14
|
-
- For
|
|
15
|
-
- **Do not** write `hostname.includes('
|
|
13
|
+
- For external integration and host allow/deny decisions, use exact domain or suffix matching (`endsWith('.example.com')` or a shared helper like `hostMatchesDomain`).
|
|
14
|
+
- **Do not** write `hostname.includes('example.com')` or `url.includes('example.com')` for host allow/deny. Substring checks match attacker lookalikes (`evil-example.com`, `example.com.attacker.net`) and trip CodeQL `js/incomplete-url-substring-sanitization`.
|
|
16
15
|
|
|
17
16
|
## Parse first
|
|
18
17
|
|
|
19
|
-
- Prefer `new URL(...)` then match on **`hostname`**. Keep path checks on **`pathname`** (e.g. `/
|
|
18
|
+
- Prefer `new URL(...)` then match on **`hostname`**. Keep path checks on **`pathname`** (e.g. `/api/v1`), not on the full href string.
|
|
20
19
|
|
|
21
|
-
## Outgoing fetch
|
|
20
|
+
## Outgoing fetch (SSRF hygiene)
|
|
22
21
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- Validate external URLs before slot acquisition or server-side fetch.
|
|
23
|
+
- Disallow access to local and private network ranges: `localhost`, `127.0.0.1`, `::1`, RFC 1918 private IPs (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), `.local`, `.internal`, and credentials in URLs (`user:pass@host`).
|
|
24
|
+
- Follow redirects with re-validation of each redirect target.
|
|
25
25
|
|
|
26
26
|
## Schemes and sanitization
|
|
27
27
|
|
|
28
28
|
- Scheme checks: **allowlist** `http:` / `https:`; do not only deny `javascript:`.
|
|
29
|
-
- Tag stripping / entity decode: use shared
|
|
30
|
-
-
|
|
31
|
-
- Never return `err.stack` or raw exception text to HTTP clients; log server-side and return stable error codes.
|
|
29
|
+
- Tag stripping / entity decode: use shared parser utilities. If a regex is extraction-only, annotate it in a comment.
|
|
30
|
+
- Never return `err.stack` or raw exception text to external HTTP clients; log server-side and return stable error codes.
|
|
@@ -19,8 +19,8 @@ alwaysApply: true
|
|
|
19
19
|
|
|
20
20
|
When describing what **the product does**, use direct verbs — not hedging **can**:
|
|
21
21
|
|
|
22
|
-
- **Do:** “
|
|
23
|
-
- **Don't:** “…**can
|
|
22
|
+
- **Do:** “The pipeline **validates** each record.” / “The analyzer **identifies** bottlenecks.” / “The engine **generates** clean exports.”
|
|
23
|
+
- **Don't:** “…**can validate**…” / “…**can identify**…” when stating a shipped capability.
|
|
24
24
|
|
|
25
25
|
**Keep *can* when it means:**
|
|
26
26
|
|
|
@@ -28,6 +28,12 @@ Fill the bracketed placeholders once brand voice crystallizes (usually after sev
|
|
|
28
28
|
- **Ban vague nouns** in marketing and narrative copy: "something," "stuff," "things" (fine in error strings). Name the thing.
|
|
29
29
|
- **Ban squish intensifiers:** "strongly," "deeply," "easily," "incredibly," "very." If the sentence needs the intensifier, rewrite the sentence.
|
|
30
30
|
|
|
31
|
+
## Language and locale
|
|
32
|
+
|
|
33
|
+
- Default to **US English** conventions unless context indicates otherwise.
|
|
34
|
+
- **Adapt to the audience:** Adapt spelling and phrasing to what is discovered about the user, team, or target audience (e.g. Commonwealth/UK English when operating in those markets).
|
|
35
|
+
- **No em dashes (—) in user-facing copy:** Use a colon, comma, period, or parentheses instead.
|
|
36
|
+
|
|
31
37
|
## Named-person constraints (narratives)
|
|
32
38
|
|
|
33
39
|
If real people appear in origin/brand narratives, record their hard constraints here (employer safety, public roles, verb restrictions like "no founder verbs for X") and audit after every pass. Keep narrative locks in a constraints file next to the long-form (LOCKED FRAMING / ARC / VOICE pattern).
|
|
@@ -12,9 +12,9 @@ Used for **both** ForgeTrail Lite file bootstrap and **MCP greenfield** kickoff.
|
|
|
12
12
|
| `FORGETRAIL_LITE.md` | Full Lite kickoff protocol (Lite file bootstrap only) |
|
|
13
13
|
| `FORGETRAIL_LITE_UPDATES.md` | Optional local feedback log (§1.6) — merge accepted items upstream |
|
|
14
14
|
| `AGENTS.md` | Agent instructions — cite or symlink for your IDE |
|
|
15
|
-
| `CLAUDE.md` | Claude Code
|
|
15
|
+
| `CLAUDE.md` | Claude Code instructions |
|
|
16
16
|
| `IDEAS.md` | Backlog parking lot |
|
|
17
|
-
| `cursor/rules/*.mdc` | Cursor rule snippets (
|
|
17
|
+
| `cursor/rules/*.mdc` | Cursor rule snippets (updates-log, …) |
|
|
18
18
|
|
|
19
19
|
## Wire up Cursor (one-time)
|
|
20
20
|
|
|
@@ -24,7 +24,6 @@ From the **repo root**:
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
mkdir -p .cursor/rules
|
|
27
|
-
ln -sf ../../.forgetrail/cursor/rules/forgetrail-no-trailer.mdc .cursor/rules/
|
|
28
27
|
ln -sf ../../.forgetrail/cursor/rules/forgetrail-updates-log.mdc .cursor/rules/
|
|
29
28
|
```
|
|
30
29
|
|
|
@@ -32,7 +31,6 @@ ln -sf ../../.forgetrail/cursor/rules/forgetrail-updates-log.mdc .cursor/rules/
|
|
|
32
31
|
|
|
33
32
|
```bat
|
|
34
33
|
mkdir .cursor\rules 2>nul
|
|
35
|
-
mklink .cursor\rules\forgetrail-no-trailer.mdc ..\.forgetrail\cursor\rules\forgetrail-no-trailer.mdc
|
|
36
34
|
mklink .cursor\rules\forgetrail-updates-log.mdc ..\.forgetrail\cursor\rules\forgetrail-updates-log.mdc
|
|
37
35
|
```
|
|
38
36
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# ForgeTrail Hooks
|
|
2
|
+
|
|
3
|
+
Enforces ForgeTrail rules and guards at the host tool level, rather than relying only on prompt requests.
|
|
4
|
+
|
|
5
|
+
## What is in this directory
|
|
6
|
+
|
|
7
|
+
- `guard-shell.mjs`: runs pre-commit verification (`pnpm run verify`), prevents npm/yarn when `pnpm-lock.yaml` is present, gates `git push` to upstream remotes (pushing to IngotVault or local backup mirrors is allowed automatically), and prompts on destructive git or filesystem operations.
|
|
8
|
+
- `guard-edit.mjs`: guards `.env*` secrets files and `specs/completed/**` or `specs/canonical/**` records against accidental mutation.
|
|
9
|
+
- `session-start.mjs`: on session start, reads `.forgetrail/workflow_tracking.json` and injects live phase status, open exit criteria, and last session notes into agent context.
|
|
10
|
+
- `validate-tracking.mjs`: after editing `workflow_tracking.json`, runs structural validation and returns any issues as context.
|
|
11
|
+
- `validate-tracking-core.mjs`: standalone zero-dependency tracking validator.
|
|
12
|
+
- `session-stop.mjs`: on session stop, checks that session notes were added to tracking.
|
|
13
|
+
- `cursor-hooks.json`: standard Cursor hooks configuration.
|
|
14
|
+
- `claude-settings-hooks.json`: standard Claude Code configuration fragment.
|
|
15
|
+
|
|
16
|
+
## Host setup
|
|
17
|
+
|
|
18
|
+
### Cursor
|
|
19
|
+
|
|
20
|
+
Copy or link `cursor-hooks.json` to `.cursor/hooks.json` in your repository root. The hook scripts reside in `.forgetrail/hooks/`.
|
|
21
|
+
|
|
22
|
+
### Claude Code
|
|
23
|
+
|
|
24
|
+
Add the contents of `claude-settings-hooks.json` to `.claude/settings.json` in your project root or user configuration.
|
|
25
|
+
|
|
26
|
+
## Verification
|
|
27
|
+
|
|
28
|
+
Run any script with test input via stdin:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
echo '{"command": "npm install lodash"}' | node .forgetrail/hooks/guard-shell.mjs
|
|
32
|
+
# Output: {"permission":"deny", ...} (when pnpm-lock.yaml is present)
|
|
33
|
+
|
|
34
|
+
echo '{"command": "git push"}' | node .forgetrail/hooks/guard-shell.mjs
|
|
35
|
+
# Output: {"permission":"ask", ...}
|
|
36
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash|shell",
|
|
6
|
+
"command": "node .forgetrail/hooks/guard-shell.mjs"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"matcher": "Write|Edit|StrReplace",
|
|
10
|
+
"command": "node .forgetrail/hooks/guard-edit.mjs"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PostToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Write|Edit|StrReplace",
|
|
16
|
+
"command": "node .forgetrail/hooks/validate-tracking.mjs"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"beforeShellExecution": [
|
|
5
|
+
{
|
|
6
|
+
"command": "node .forgetrail/hooks/guard-shell.mjs"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"preToolUse": [
|
|
10
|
+
{
|
|
11
|
+
"command": "node .forgetrail/hooks/guard-edit.mjs"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"sessionStart": [
|
|
15
|
+
{
|
|
16
|
+
"command": "node .forgetrail/hooks/session-start.mjs"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"afterFileEdit": [
|
|
20
|
+
{
|
|
21
|
+
"command": "node .forgetrail/hooks/validate-tracking.mjs"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"stop": [
|
|
25
|
+
{
|
|
26
|
+
"command": "node .forgetrail/hooks/session-stop.mjs"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ForgeTrail Edit Guard
|
|
5
|
+
* Hook event: preToolUse (Cursor) / PreToolUse (Claude Code)
|
|
6
|
+
* Guards against accidental mutation of secrets (.env*) and completed/canonical specs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
function readStdin() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = readFileSync(0, "utf-8");
|
|
14
|
+
if (raw.trim()) return JSON.parse(raw);
|
|
15
|
+
} catch {}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractTargetPaths(tool, input) {
|
|
20
|
+
if (!input) return [];
|
|
21
|
+
const paths = [];
|
|
22
|
+
if (typeof input.path === "string") paths.push(input.path);
|
|
23
|
+
if (typeof input.file_path === "string") paths.push(input.file_path);
|
|
24
|
+
if (typeof input.target_file === "string") paths.push(input.target_file);
|
|
25
|
+
return paths;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
const payload = readStdin();
|
|
30
|
+
const tool = payload.tool || payload.tool_name || "";
|
|
31
|
+
const input = payload.input || payload.tool_input || {};
|
|
32
|
+
|
|
33
|
+
// Only check write/edit tools
|
|
34
|
+
const isEditTool =
|
|
35
|
+
/^(Write|Edit|StrReplace|WriteFile|file_editor|edit_file)$/i.test(tool);
|
|
36
|
+
|
|
37
|
+
if (!isEditTool) {
|
|
38
|
+
console.log(JSON.stringify({ permission: "allow" }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const paths = extractTargetPaths(tool, input);
|
|
43
|
+
|
|
44
|
+
for (const path of paths) {
|
|
45
|
+
const normalized = path.replace(/\\/g, "/");
|
|
46
|
+
|
|
47
|
+
// 1. Guard environment secrets (.env, .env.local, etc. but allow .env.example)
|
|
48
|
+
if (/(^|\/)\.env(\.[a-zA-Z0-9_-]+)?$/.test(normalized)) {
|
|
49
|
+
if (!normalized.endsWith(".env.example")) {
|
|
50
|
+
console.log(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
permission: "ask",
|
|
53
|
+
user_message:
|
|
54
|
+
"ForgeTrail Safety: Modifying environment secrets file (" +
|
|
55
|
+
path +
|
|
56
|
+
"). Please confirm before proceeding.",
|
|
57
|
+
agent_message:
|
|
58
|
+
"Editing environment secrets file requires user confirmation.",
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Guard completed and canonical delivery specs
|
|
66
|
+
if (/(^|\/)specs\/(completed|canonical)\//.test(normalized)) {
|
|
67
|
+
console.log(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
permission: "ask",
|
|
70
|
+
user_message:
|
|
71
|
+
"ForgeTrail Safety: Completed delivery specs (" +
|
|
72
|
+
path +
|
|
73
|
+
") are historical records. Modify only if intentionally updating links or summaries.",
|
|
74
|
+
agent_message:
|
|
75
|
+
"Editing completed/canonical spec requires user confirmation.",
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(JSON.stringify({ permission: "allow" }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ForgeTrail Shell Guard
|
|
5
|
+
* Hook event: beforeShellExecution (Cursor) / PreToolUse (Claude Code)
|
|
6
|
+
* Enforces Lite §0, git commit attribution rules, pnpm consistency, and git push guards.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { resolve, join } from "node:path";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
function readStdin() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(0, "utf-8");
|
|
16
|
+
if (raw.trim()) return JSON.parse(raw);
|
|
17
|
+
} catch {}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasPnpmLock(cwd) {
|
|
22
|
+
let curr = resolve(cwd || process.cwd());
|
|
23
|
+
for (let i = 0; i < 6; i++) {
|
|
24
|
+
if (existsSync(join(curr, "pnpm-lock.yaml"))) return true;
|
|
25
|
+
const parent = resolve(curr, "..");
|
|
26
|
+
if (parent === curr) break;
|
|
27
|
+
curr = parent;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasVerifyScript(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const pkgPath = join(cwd || process.cwd(), "package.json");
|
|
35
|
+
if (existsSync(pkgPath)) {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
return Boolean(pkg.scripts && pkg.scripts.verify);
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function main() {
|
|
44
|
+
const input = readStdin();
|
|
45
|
+
const command = (input.command || input.tool_input?.command || "").trim();
|
|
46
|
+
const cwd = input.cwd || process.cwd();
|
|
47
|
+
|
|
48
|
+
if (!command) {
|
|
49
|
+
console.log(JSON.stringify({ permission: "allow" }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 1. Verify-before-commit: run whenever package.json has a verify script (unless skipped via FORGETRAIL_SKIP_VERIFY=1 or --no-verify)
|
|
54
|
+
if (/\bgit\s+commit\b/.test(command)) {
|
|
55
|
+
if (
|
|
56
|
+
process.env.FORGETRAIL_SKIP_VERIFY !== "1" &&
|
|
57
|
+
!/\s+--no-verify\b/.test(command) &&
|
|
58
|
+
hasVerifyScript(cwd)
|
|
59
|
+
) {
|
|
60
|
+
try {
|
|
61
|
+
execSync("pnpm run verify", { cwd, stdio: "pipe" });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.log(
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
permission: "deny",
|
|
66
|
+
user_message:
|
|
67
|
+
"ForgeTrail Verification: 'pnpm run verify' failed. Code checks must pass before committing.",
|
|
68
|
+
agent_message:
|
|
69
|
+
"Pre-commit verification failed. Run pnpm verify and fix errors before committing.",
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Check for git push (Do not push to upstream remote unless explicitly requested by user)
|
|
78
|
+
if (/\bgit\s+push\b/.test(command)) {
|
|
79
|
+
if (/\s+(-f|--force|--force-with-lease)\b/.test(command)) {
|
|
80
|
+
console.log(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
permission: "ask",
|
|
83
|
+
user_message:
|
|
84
|
+
"ForgeTrail Safety: Force-pushing to remote can overwrite git history. Please confirm if you wish to proceed.",
|
|
85
|
+
agent_message:
|
|
86
|
+
"Force-push requires explicit user confirmation.",
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pushing to IngotVault or local backup mirrors is automatic to preserve work
|
|
93
|
+
const isIngotVaultOrBackup =
|
|
94
|
+
/\bgit\s+push\b.*?\b(backup|ingotvault)\b/i.test(command) ||
|
|
95
|
+
/\bgit\s+push\b.*?(?:[a-zA-Z]:[\\/]|file:\/\/|\.\/|\.\.\/)/i.test(command);
|
|
96
|
+
|
|
97
|
+
if (isIngotVaultOrBackup) {
|
|
98
|
+
console.log(JSON.stringify({ permission: "allow" }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(
|
|
103
|
+
JSON.stringify({
|
|
104
|
+
permission: "ask",
|
|
105
|
+
user_message:
|
|
106
|
+
"ForgeTrail Safety: git push to upstream remote requires explicit user approval per project rules. Would you like to push?",
|
|
107
|
+
agent_message:
|
|
108
|
+
"git push to upstream remote requires explicit user approval per project rules.",
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Check for npm/yarn when pnpm-lock exists (Lite §0 Rule 3)
|
|
115
|
+
if (/\b(npm\s+(i|install|add)|yarn(\s+add|\s+install)?)\b/.test(command)) {
|
|
116
|
+
if (hasPnpmLock(cwd)) {
|
|
117
|
+
console.log(
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
permission: "deny",
|
|
120
|
+
user_message:
|
|
121
|
+
"ForgeTrail Lite §0: This repository uses pnpm (pnpm-lock.yaml detected). Do not run npm or yarn install.",
|
|
122
|
+
agent_message:
|
|
123
|
+
"Package manager is locked to pnpm. Run pnpm instead.",
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Check for destructive git operations
|
|
131
|
+
if (/\bgit\s+(reset\s+--hard|clean\s+-[a-zA-Z]*f)/.test(command)) {
|
|
132
|
+
console.log(
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
permission: "ask",
|
|
135
|
+
user_message:
|
|
136
|
+
"ForgeTrail Safety: Destructive git operation (" +
|
|
137
|
+
command +
|
|
138
|
+
") will permanently discard uncommitted changes. Confirm?",
|
|
139
|
+
agent_message: "Destructive git operation requires user approval.",
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 5. Check for dangerous rm -rf outside build/temp
|
|
146
|
+
if (/\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+/.test(command)) {
|
|
147
|
+
const isSafe =
|
|
148
|
+
/\b(temp|build|\.svelte-kit|\.turbo|dist|\.wrangler|node_modules)[/\\]?/.test(
|
|
149
|
+
command
|
|
150
|
+
);
|
|
151
|
+
if (!isSafe) {
|
|
152
|
+
console.log(
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
permission: "ask",
|
|
155
|
+
user_message:
|
|
156
|
+
"ForgeTrail Safety: Recursive file deletion outside temporary/build directories. Confirm?",
|
|
157
|
+
agent_message:
|
|
158
|
+
"Dangerous recursive deletion requires user approval.",
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(JSON.stringify({ permission: "allow" }));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ForgeTrail Session Start Context Injector
|
|
5
|
+
* Hook event: sessionStart (Cursor)
|
|
6
|
+
* Injects live ForgeTrail phase status and open exit criteria into session context.
|
|
7
|
+
* Retires static phase-status rules on hosts with hook support.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
11
|
+
import { resolve, join } from "node:path";
|
|
12
|
+
|
|
13
|
+
function findTrackingFile(startDir) {
|
|
14
|
+
let curr = resolve(startDir || process.cwd());
|
|
15
|
+
for (let i = 0; i < 6; i++) {
|
|
16
|
+
const candidate1 = join(curr, ".forgetrail", "workflow_tracking.json");
|
|
17
|
+
if (existsSync(candidate1)) return candidate1;
|
|
18
|
+
const candidate2 = join(curr, "workflow_tracking.json");
|
|
19
|
+
if (existsSync(candidate2)) return candidate2;
|
|
20
|
+
const parent = resolve(curr, "..");
|
|
21
|
+
if (parent === curr) break;
|
|
22
|
+
curr = parent;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
const trackingPath = findTrackingFile(process.cwd());
|
|
29
|
+
|
|
30
|
+
if (!trackingPath) {
|
|
31
|
+
console.log(JSON.stringify({}));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const tracking = JSON.parse(readFileSync(trackingPath, "utf-8"));
|
|
37
|
+
const projectName = tracking.project?.name || "App";
|
|
38
|
+
const phase = tracking.currentPhase || "1";
|
|
39
|
+
const isLite = tracking.schemaVersion === "lite-1";
|
|
40
|
+
|
|
41
|
+
let remaining = [];
|
|
42
|
+
if (isLite) {
|
|
43
|
+
const phaseObj = tracking.phases?.[String(phase)];
|
|
44
|
+
if (phaseObj?.exitCriteria) {
|
|
45
|
+
remaining = Object.entries(phaseObj.exitCriteria)
|
|
46
|
+
.filter(([_, met]) => !met)
|
|
47
|
+
.map(([flag]) => flag);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
const phaseObj = tracking.phases?.[phase];
|
|
51
|
+
if (Array.isArray(phaseObj?.exitCriteriaRemaining)) {
|
|
52
|
+
remaining = phaseObj.exitCriteriaRemaining;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let lastLeftOff = "";
|
|
57
|
+
if (Array.isArray(tracking.sessions) && tracking.sessions.length > 0) {
|
|
58
|
+
const lastSession = tracking.sessions[tracking.sessions.length - 1];
|
|
59
|
+
lastLeftOff = lastSession.leftOff || lastSession.notes || "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lines = [
|
|
63
|
+
`=== ForgeTrail Context (${projectName}) ===`,
|
|
64
|
+
`Current Phase: ${phase} (${isLite ? "Lite schema" : "MCP schema"})`,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
if (lastLeftOff) {
|
|
68
|
+
lines.push(`Last Session Left Off: ${lastLeftOff}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (remaining.length > 0) {
|
|
72
|
+
lines.push("Open Exit Criteria for Current Phase:");
|
|
73
|
+
for (const item of remaining.slice(0, 5)) {
|
|
74
|
+
lines.push(` - ${item}`);
|
|
75
|
+
}
|
|
76
|
+
if (remaining.length > 5) {
|
|
77
|
+
lines.push(` ...and ${remaining.length - 5} more`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push("========================================");
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
additional_context: lines.join("\n"),
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
console.log(JSON.stringify({}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ForgeTrail Session Stop Check
|
|
5
|
+
* Hook event: stop (Cursor)
|
|
6
|
+
* Prompts a reminder if a session concluded without updating sessions[] in tracking.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { resolve, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
function findTrackingFile(startDir) {
|
|
13
|
+
let curr = resolve(startDir || process.cwd());
|
|
14
|
+
for (let i = 0; i < 6; i++) {
|
|
15
|
+
const candidate1 = join(curr, ".forgetrail", "workflow_tracking.json");
|
|
16
|
+
if (existsSync(candidate1)) return candidate1;
|
|
17
|
+
const candidate2 = join(curr, "workflow_tracking.json");
|
|
18
|
+
if (existsSync(candidate2)) return candidate2;
|
|
19
|
+
const parent = resolve(curr, "..");
|
|
20
|
+
if (parent === curr) break;
|
|
21
|
+
curr = parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function main() {
|
|
27
|
+
const trackingPath = findTrackingFile(process.cwd());
|
|
28
|
+
if (!trackingPath) {
|
|
29
|
+
console.log(JSON.stringify({}));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const tracking = JSON.parse(readFileSync(trackingPath, "utf-8"));
|
|
35
|
+
const sessions = tracking.sessions;
|
|
36
|
+
|
|
37
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
38
|
+
const hasTodaySession =
|
|
39
|
+
Array.isArray(sessions) &&
|
|
40
|
+
sessions.some((s) => s.date === today || (s.timestamp && s.timestamp.startsWith(today)));
|
|
41
|
+
|
|
42
|
+
if (!hasTodaySession) {
|
|
43
|
+
console.log(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
followup_message:
|
|
46
|
+
"ForgeTrail Reminder: Update .forgetrail/workflow_tracking.json (sessions[], decisions[], gotchas[]) with what was accomplished before ending.",
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
console.log(JSON.stringify({}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main();
|