@vortex-os/base 0.10.0 → 0.12.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/LICENSE +21 -21
- package/README.md +48 -1
- package/bin/vortex.mjs +17 -17
- package/dist/{catch-up-KIHTAUPX.js → catch-up-GDDKPZHJ.js} +2 -2
- package/dist/chunk-2FVNWW77.js +166 -0
- package/dist/chunk-2FVNWW77.js.map +1 -0
- package/dist/{chunk-7SNLVGBO.js → chunk-3L5DLEGP.js} +1 -1
- package/dist/chunk-3L5DLEGP.js.map +1 -0
- package/dist/chunk-EAKDR5B2.js +501 -0
- package/dist/chunk-EAKDR5B2.js.map +1 -0
- package/dist/chunk-T53UWSTR.js +301 -0
- package/dist/chunk-T53UWSTR.js.map +1 -0
- package/dist/chunk-UV76ZEDC.js +292 -0
- package/dist/chunk-UV76ZEDC.js.map +1 -0
- package/dist/failures-PMURLMVB.js +25 -0
- package/dist/failures-PMURLMVB.js.map +1 -0
- package/dist/guard-IMJR6ET7.js +23 -0
- package/dist/guard-IMJR6ET7.js.map +1 -0
- package/dist/index.d.ts +425 -3
- package/dist/index.js +472 -709
- package/dist/index.js.map +1 -1
- package/dist/statusline-6KSHISXO.js +36 -0
- package/dist/statusline-6KSHISXO.js.map +1 -0
- package/dist/{vectorize-RBDBTSTW.js → vectorize-PN4Y7XMO.js} +1 -1
- package/dist/vectorize-PN4Y7XMO.js.map +1 -0
- package/package.json +1 -1
- package/templates/commands/agenda.md +15 -15
- package/templates/commands/handoff.md +26 -26
- package/templates/commands/resume.md +52 -52
- package/templates/config/vortex.json +13 -13
- package/templates/manifest.json +2 -2
- package/templates/routers/.cursorrules +14 -14
- package/templates/routers/AGENTS.md +27 -27
- package/templates/routers/AI-RULES.md +3 -1
- package/templates/routers/GEMINI.md +16 -16
- package/dist/chunk-7SNLVGBO.js.map +0 -1
- package/dist/vectorize-RBDBTSTW.js.map +0 -1
- /package/dist/{catch-up-KIHTAUPX.js.map → catch-up-GDDKPZHJ.js.map} +0 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 vortex-os-project
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vortex-os-project
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ npx vortex init # scaffold the instance
|
|
|
40
40
|
|
|
41
41
|
- the per-agent files — `AGENTS.md` (the thin Codex-CLI entry, auto-loaded by Codex and other `AGENTS.md`-aware tools) plus thin routers `CLAUDE.md`, `GEMINI.md`, `.cursorrules`, all pointing at `AI-RULES.md` (the single source of truth for shared rules) — so any agent host finds VortEX's behavior contract (these are generic templates you personalize over time);
|
|
42
42
|
- the `data/` skeleton (`_memory/`, `worklog/`, `decision-log/`, `runbooks/`, `hubs/`, `inbox/`), your user-profile memory, and today's first worklog;
|
|
43
|
-
- `.claude/settings.json` with the session hooks wired as `npx --no-install vortex session-start` / `… session-end` (the `--no-install` flag makes the auto-firing hook fail closed rather than install on a cache miss; resolving the bare `vortex` bin — local `node_modules/.bin` first, then PATH — lets the `global-setup` hook fire from any folder, not only where a local install exists), plus the agent-mediated slash-commands in `.claude/commands/`;
|
|
43
|
+
- `.claude/settings.json` with the session hooks wired as `npx --no-install vortex session-start` / `… session-end` (the `--no-install` flag makes the auto-firing hook fail closed rather than install on a cache miss; resolving the bare `vortex` bin — local `node_modules/.bin` first, then PATH — lets the `global-setup` hook fire from any folder, not only where a local install exists), the write-guard hook (`vortex guard write` — denies literal control bytes in file writes; remove its `PreToolUse` group to opt out), plus the agent-mediated slash-commands in `.claude/commands/`;
|
|
44
44
|
- `.agent/vortex.json` (auto-record config) and a minimal `package.json` with `"type":"module"` if none exists.
|
|
45
45
|
|
|
46
46
|
`vortex import --from <folder>` brings an existing notes folder in — markdown is auto-classified (worklog / decision-log / …) and **attachments (PDFs, images, …) are copied byte-for-byte** into the same layout; credential files (`*.key`, `.env`, `secrets/` …) and oversized files are skipped and reported. As the framework improves, `vortex update` refreshes the installed templates **without ever overwriting a file you edited** (your edit is parked at `<file>.new`); `vortex doctor` checks instance health.
|
|
@@ -61,6 +61,53 @@ npx vortex session-start # start-of-session boot report (git pull + counts +
|
|
|
61
61
|
npx vortex session-end # no-op (kept for hook compatibility; gap handling is at session-start)
|
|
62
62
|
npx vortex doctor # health diagnosis
|
|
63
63
|
npx vortex update # refresh framework templates (never clobbers your edits)
|
|
64
|
+
npx vortex statusline # Claude Code status-bar renderer (see below)
|
|
65
|
+
npx vortex failure # failure ledger: record / list (the self-improvement loop)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Self-improvement loop (failure ledger)
|
|
69
|
+
|
|
70
|
+
Rules saved in `data/_memory/` are loaded at session start — but at action time
|
|
71
|
+
the model's attention is on the task, and a registered rule can still slip.
|
|
72
|
+
VortEX therefore counts: when the agent's own failure becomes visible (you
|
|
73
|
+
correct it, a test disproves it, a cross-check catches it), the agent records
|
|
74
|
+
it with `vortex failure record --key <root-cause-key> --what "<one line>"`
|
|
75
|
+
into `data/_failures/` (append-only, one file per occurrence). Recurrence is
|
|
76
|
+
machine-counted per key and escalates on a ladder:
|
|
77
|
+
|
|
78
|
+
1. **1st occurrence** — recorded.
|
|
79
|
+
2. **2nd, while a rule already covers it** — that rule's write-time gate
|
|
80
|
+
becomes mandatory.
|
|
81
|
+
3. **3rd+** — the agent must **propose** a deterministic defense (a hook, a
|
|
82
|
+
pre-commit check, a wrapper) — proposals only, nothing self-installs.
|
|
83
|
+
|
|
84
|
+
The session-start report surfaces recurring keys automatically, and promoting
|
|
85
|
+
a lesson into `_memory/` requires evidence links, a replay check ("would this
|
|
86
|
+
rule have prevented the recorded cases?") and your approval. Disable with
|
|
87
|
+
`autoRecord.failures: false` in `.agent/vortex.json`.
|
|
88
|
+
|
|
89
|
+
The first guard ships in the box: `vortex init` wires a Claude Code hook
|
|
90
|
+
(`vortex guard write`) that **denies file writes containing literal control
|
|
91
|
+
bytes** (escape notation always works instead) — a denial also records itself
|
|
92
|
+
into the ledger. Remove the `PreToolUse` group in `.claude/settings.json` to
|
|
93
|
+
opt out.
|
|
94
|
+
|
|
95
|
+
## Claude Code status bar (optional)
|
|
96
|
+
|
|
97
|
+
`vortex statusline` renders a colored status bar for Claude Code's `statusLine` setting — model + a reasoning-effort meter (one bar whose height/color encode `low`→`ultracode`), a context-window gauge with used/total tokens, a clock, session cost, the 5-hour/7-day rate-limit gauges with time-to-reset, cache hit-rate, project + git branch/commit, session duration, lines changed, the number of open Claude Code sessions (best-effort process count) — and, inside a VortEX instance, the framework version and the latest worklog file:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
🧠 Fable 5 │ █ max │ █░░░░░░░░░ 12% · 120K/1M │ 🕐 22:25 │ 💰 $9.22
|
|
101
|
+
5h ██████░░ 75%(1h9m) │ 7d ███████░ 96%(7h30m) │ 📦 cache 99%
|
|
102
|
+
📁 my-project │ ⎇ main 2b0949d │ ⏱ 1h26m │ +90 -49 │ ⧉ 1
|
|
103
|
+
🌀 VortEX v0.11.0 │ last: 2026-06-10_0452-notes.md
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Wire it up once — this writes the `statusLine` entry into the **current folder's** `.claude/settings.json` and **keeps any bar you already configured** (replace it explicitly with `--force`):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx vortex statusline install # full bar (4 lines inside an instance, 3 outside)
|
|
110
|
+
npx vortex statusline install --lite # compact 1-line bar
|
|
64
111
|
```
|
|
65
112
|
|
|
66
113
|
## Library usage
|
package/bin/vortex.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// `vortex` — the CLI shipped by @vortex-os/base.
|
|
3
|
-
//
|
|
4
|
-
// `npm i @vortex-os/base` puts this on the instance's path (node_modules/.bin),
|
|
5
|
-
// so `npx vortex init` / `npx vortex session-start` / `npx vortex --list` work
|
|
6
|
-
// without any monorepo checkout. It is a thin wrapper over the canonical
|
|
7
|
-
// dispatch (`runVortexCli`), which is bundled into base from
|
|
8
|
-
// `@vortex-os/session-rituals` — exactly one source of truth for the CLI logic.
|
|
9
|
-
//
|
|
10
|
-
// The dispatch lazily probes the optional `@vortex-os/memory-extended` add-on:
|
|
11
|
-
// when it is installed alongside base, `/recall` lights up; on a lean base-only
|
|
12
|
-
// install the probe is caught and the CLI runs with every other command.
|
|
13
|
-
|
|
14
|
-
import { sessionRituals } from "../dist/index.js";
|
|
15
|
-
|
|
16
|
-
const code = await sessionRituals.runVortexCli(process.argv.slice(2));
|
|
17
|
-
process.exitCode = code;
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `vortex` — the CLI shipped by @vortex-os/base.
|
|
3
|
+
//
|
|
4
|
+
// `npm i @vortex-os/base` puts this on the instance's path (node_modules/.bin),
|
|
5
|
+
// so `npx vortex init` / `npx vortex session-start` / `npx vortex --list` work
|
|
6
|
+
// without any monorepo checkout. It is a thin wrapper over the canonical
|
|
7
|
+
// dispatch (`runVortexCli`), which is bundled into base from
|
|
8
|
+
// `@vortex-os/session-rituals` — exactly one source of truth for the CLI logic.
|
|
9
|
+
//
|
|
10
|
+
// The dispatch lazily probes the optional `@vortex-os/memory-extended` add-on:
|
|
11
|
+
// when it is installed alongside base, `/recall` lights up; on a lean base-only
|
|
12
|
+
// install the probe is caught and the CLI runs with every other command.
|
|
13
|
+
|
|
14
|
+
import { sessionRituals } from "../dist/index.js";
|
|
15
|
+
|
|
16
|
+
const code = await sessionRituals.runVortexCli(process.argv.slice(2));
|
|
17
|
+
process.exitCode = code;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadVortexConfig,
|
|
3
|
+
makeContext
|
|
4
|
+
} from "./chunk-T53UWSTR.js";
|
|
5
|
+
|
|
6
|
+
// ../plugins/session-rituals/dist/guard.js
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { dirname, join, resolve } from "path";
|
|
9
|
+
var GUARD_WRITE_COMMAND = "npx --no-install vortex guard write || exit 0";
|
|
10
|
+
var GUARD_WRITE_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
11
|
+
function isForbidden(codePoint) {
|
|
12
|
+
if (codePoint === 9 || codePoint === 10 || codePoint === 13)
|
|
13
|
+
return false;
|
|
14
|
+
return codePoint < 32 || codePoint === 127;
|
|
15
|
+
}
|
|
16
|
+
function findControlChar(text) {
|
|
17
|
+
for (let i = 0; i < text.length; i++) {
|
|
18
|
+
const cp = text.charCodeAt(i);
|
|
19
|
+
if (isForbidden(cp))
|
|
20
|
+
return { index: i, codePoint: cp };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
var CONTROL_NAMES = {
|
|
25
|
+
0: "NUL",
|
|
26
|
+
7: "BEL",
|
|
27
|
+
8: "BS",
|
|
28
|
+
11: "VT",
|
|
29
|
+
12: "FF",
|
|
30
|
+
27: "ESC",
|
|
31
|
+
127: "DEL"
|
|
32
|
+
};
|
|
33
|
+
function describeCodePoint(cp) {
|
|
34
|
+
const hex = cp.toString(16).toUpperCase().padStart(4, "0");
|
|
35
|
+
const name = CONTROL_NAMES[cp];
|
|
36
|
+
return `U+${hex}${name ? ` (${name})` : ""}`;
|
|
37
|
+
}
|
|
38
|
+
function scanToolInput(toolInput) {
|
|
39
|
+
const check = (field, value) => {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
const hit = findControlChar(value);
|
|
43
|
+
return hit ? { field, index: hit.index, codePoint: hit.codePoint } : null;
|
|
44
|
+
};
|
|
45
|
+
const direct = check("content", toolInput.content) ?? check("new_string", toolInput.new_string) ?? check("new_source", toolInput.new_source);
|
|
46
|
+
if (direct)
|
|
47
|
+
return direct;
|
|
48
|
+
const edits = toolInput.edits;
|
|
49
|
+
if (Array.isArray(edits)) {
|
|
50
|
+
for (let i = 0; i < edits.length; i++) {
|
|
51
|
+
const e = edits[i];
|
|
52
|
+
if (e && typeof e === "object") {
|
|
53
|
+
const hit = check(`edits[${i}].new_string`, e.new_string);
|
|
54
|
+
if (hit)
|
|
55
|
+
return hit;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function buildDenyDecision(finding) {
|
|
62
|
+
const reason = `VortEX guard: literal control character ${describeCodePoint(finding.codePoint)} at offset ${finding.index} of ${finding.field} \u2014 control bytes are never written literally into text files. Re-issue the write using escape notation for that character (e.g. \\x1b / \\u001b in code or regexes, the two-character sequence backslash+x, not the raw byte). If you are CLEANING an already-polluted file: old_string is not scanned \u2014 keep the raw byte only on the old_string side and put escape notation in the new text.`;
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
hookSpecificOutput: {
|
|
65
|
+
hookEventName: "PreToolUse",
|
|
66
|
+
permissionDecision: "deny",
|
|
67
|
+
permissionDecisionReason: reason
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
var GUARDED_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
72
|
+
function toolOutOfScope(payload) {
|
|
73
|
+
const name = payload.tool_name;
|
|
74
|
+
return typeof name === "string" && name.length > 0 && !GUARDED_TOOLS.has(name);
|
|
75
|
+
}
|
|
76
|
+
function guardWriteDecision(payloadText) {
|
|
77
|
+
try {
|
|
78
|
+
const payload = JSON.parse(payloadText);
|
|
79
|
+
if (toolOutOfScope(payload))
|
|
80
|
+
return null;
|
|
81
|
+
const toolInput = payload.tool_input;
|
|
82
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput))
|
|
83
|
+
return null;
|
|
84
|
+
const finding = scanToolInput(toolInput);
|
|
85
|
+
return finding ? buildDenyDecision(finding) : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function runGuardCli(argv, out, err) {
|
|
91
|
+
if (argv[0] !== "write") {
|
|
92
|
+
err("[vortex] guard subcommands: write \u2014 PreToolUse hook body, reads the payload JSON from stdin.\n");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (process.stdin.isTTY) {
|
|
96
|
+
err("[vortex] guard write reads a Claude Code PreToolUse payload from stdin \u2014 it is wired by `vortex init` as a hook, not run by hand.\n");
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
let raw = "";
|
|
100
|
+
try {
|
|
101
|
+
raw = readFileSync(0, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
let payload;
|
|
106
|
+
try {
|
|
107
|
+
payload = JSON.parse(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (toolOutOfScope(payload))
|
|
112
|
+
return 0;
|
|
113
|
+
const toolInput = payload.tool_input;
|
|
114
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput))
|
|
115
|
+
return 0;
|
|
116
|
+
const finding = scanToolInput(toolInput);
|
|
117
|
+
if (!finding)
|
|
118
|
+
return 0;
|
|
119
|
+
out(buildDenyDecision(finding) + "\n");
|
|
120
|
+
try {
|
|
121
|
+
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
122
|
+
const root = resolveInstanceRoot(cwd);
|
|
123
|
+
if (root) {
|
|
124
|
+
const ctx = makeContext(root);
|
|
125
|
+
if (loadVortexConfig(ctx).autoRecord.failures) {
|
|
126
|
+
const filePath = toolInput.file_path;
|
|
127
|
+
const fileNote = typeof filePath === "string" && filePath ? ` (target: ${filePath})` : "";
|
|
128
|
+
const { recordGuardDenial } = await import("./failures-PMURLMVB.js");
|
|
129
|
+
await recordGuardDenial(ctx.dataDir, `write guard denied a literal control character in ${finding.field}${fileNote}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
function resolveInstanceRoot(startDir) {
|
|
137
|
+
const isInstance = (d) => existsSync(join(d, ".agent", "vortex.json"));
|
|
138
|
+
const envRoot = process.env.VORTEX_REPO_ROOT?.trim();
|
|
139
|
+
if (envRoot && isInstance(envRoot))
|
|
140
|
+
return envRoot;
|
|
141
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR?.trim();
|
|
142
|
+
if (projectDir && isInstance(projectDir))
|
|
143
|
+
return projectDir;
|
|
144
|
+
let dir = resolve(startDir);
|
|
145
|
+
for (let i = 0; i < 16; i++) {
|
|
146
|
+
if (isInstance(dir))
|
|
147
|
+
return dir;
|
|
148
|
+
const parent = dirname(dir);
|
|
149
|
+
if (parent === dir)
|
|
150
|
+
return null;
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
GUARD_WRITE_COMMAND,
|
|
158
|
+
GUARD_WRITE_MATCHER,
|
|
159
|
+
findControlChar,
|
|
160
|
+
scanToolInput,
|
|
161
|
+
buildDenyDecision,
|
|
162
|
+
guardWriteDecision,
|
|
163
|
+
runGuardCli,
|
|
164
|
+
resolveInstanceRoot
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=chunk-2FVNWW77.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../plugins/session-rituals/src/guard.ts"],"sourcesContent":["/**\n * `vortex guard` — deterministic write-time guards, wired as Claude Code hooks.\n *\n * `guard write` is a PreToolUse hook for the file-writing tools\n * (Write / Edit / MultiEdit / NotebookEdit). It reads the hook payload JSON\n * from stdin and DENIES the call when the NEW text contains a literal control\n * character (anything below U+0020 except tab/LF/CR, or U+007F) — the\n * documented repeat-failure class where escape notation was intended but the\n * raw byte got written into source files.\n *\n * Why a hook and not (another) rule: the failure happens precisely when the\n * model's active context is saturated with the offending bytes, i.e. when\n * memory-based discipline is weakest. A deterministic check at the write\n * boundary does not depend on recall. This is the \"3rd occurrence → promote to\n * a deterministic guard\" rung of the failure-ledger ladder, shipped as a\n * framework default because the class has zero legitimate uses in text writes\n * (escape notation is always available).\n *\n * Contract (mirrors the statusline renderer's \"never break the host\"):\n * - only the NEW text is scanned (`content`, `new_string`, `edits[].new_string`,\n * `new_source`) — `old_string` is deliberately NOT scanned, so cleaning an\n * already-polluted file stays possible;\n * - a clean payload produces NO output (exit 0) — the normal permission flow\n * proceeds untouched;\n * - a malformed payload, an unexpected tool, or any internal error is\n * FAIL-OPEN: exit 0, no output, never a blocked write from a guard bug.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { loadVortexConfig, makeContext } from \"@vortex-os/core\";\n\n/** The PreToolUse hook command `vortex init` wires (self-silencing like the session hooks). */\nexport const GUARD_WRITE_COMMAND = \"npx --no-install vortex guard write || exit 0\";\n\n/** Tool-name matcher for the guard's hook group. */\nexport const GUARD_WRITE_MATCHER = \"Write|Edit|MultiEdit|NotebookEdit\";\n\n/** A literal control character found in to-be-written text. */\nexport interface GuardFinding {\n /** Which payload field carried it (e.g. `content`, `edits[2].new_string`). */\n readonly field: string;\n /** Character offset within that field. */\n readonly index: number;\n readonly codePoint: number;\n}\n\n/** Control characters that are NEVER legitimate in tool-written text (tab/LF/CR excluded). */\nfunction isForbidden(codePoint: number): boolean {\n if (codePoint === 9 || codePoint === 10 || codePoint === 13) return false;\n return codePoint < 32 || codePoint === 127;\n}\n\n/** First forbidden control character in `text`, or null. */\nexport function findControlChar(text: string): { index: number; codePoint: number } | null {\n for (let i = 0; i < text.length; i++) {\n const cp = text.charCodeAt(i);\n if (isForbidden(cp)) return { index: i, codePoint: cp };\n }\n return null;\n}\n\nconst CONTROL_NAMES: Record<number, string> = {\n 0: \"NUL\",\n 7: \"BEL\",\n 8: \"BS\",\n 11: \"VT\",\n 12: \"FF\",\n 27: \"ESC\",\n 127: \"DEL\",\n};\n\nfunction describeCodePoint(cp: number): string {\n const hex = cp.toString(16).toUpperCase().padStart(4, \"0\");\n const name = CONTROL_NAMES[cp];\n return `U+${hex}${name ? ` (${name})` : \"\"}`;\n}\n\n/**\n * Scan a PreToolUse `tool_input` for forbidden control characters in every\n * NEW-text field. Returns the first finding, or null when clean.\n */\nexport function scanToolInput(toolInput: Record<string, unknown>): GuardFinding | null {\n const check = (field: string, value: unknown): GuardFinding | null => {\n if (typeof value !== \"string\") return null;\n const hit = findControlChar(value);\n return hit ? { field, index: hit.index, codePoint: hit.codePoint } : null;\n };\n\n const direct =\n check(\"content\", toolInput.content) ??\n check(\"new_string\", toolInput.new_string) ??\n check(\"new_source\", toolInput.new_source);\n if (direct) return direct;\n\n const edits = toolInput.edits;\n if (Array.isArray(edits)) {\n for (let i = 0; i < edits.length; i++) {\n const e = edits[i];\n if (e && typeof e === \"object\") {\n const hit = check(`edits[${i}].new_string`, (e as Record<string, unknown>).new_string);\n if (hit) return hit;\n }\n }\n }\n return null;\n}\n\n/** Build the PreToolUse deny JSON for a finding. */\nexport function buildDenyDecision(finding: GuardFinding): string {\n const reason =\n `VortEX guard: literal control character ${describeCodePoint(finding.codePoint)} at offset ${finding.index} of ${finding.field} — ` +\n `control bytes are never written literally into text files. Re-issue the write using escape notation for that character ` +\n `(e.g. \\\\x1b / \\\\u001b in code or regexes, the two-character sequence backslash+x, not the raw byte). ` +\n `If you are CLEANING an already-polluted file: old_string is not scanned — keep the raw byte only on the old_string side and put escape notation in the new text.`;\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n });\n}\n\n/** The tools this guard understands. Anything else is out of scope → silent. */\nconst GUARDED_TOOLS = new Set([\"Write\", \"Edit\", \"MultiEdit\", \"NotebookEdit\"]);\n\n/** Out-of-scope tool? (A named tool that is not one of ours — e.g. a custom matcher routed `Task` here.) */\nfunction toolOutOfScope(payload: Record<string, unknown>): boolean {\n const name = payload.tool_name;\n return typeof name === \"string\" && name.length > 0 && !GUARDED_TOOLS.has(name);\n}\n\n/**\n * Decide on a raw PreToolUse payload. Returns the hook's stdout JSON (a deny\n * decision) when a forbidden character is found, or null to stay silent\n * (allow / not applicable / out-of-scope tool / unparseable — fail-open by design).\n */\nexport function guardWriteDecision(payloadText: string): string | null {\n try {\n const payload = JSON.parse(payloadText) as Record<string, unknown>;\n if (toolOutOfScope(payload)) return null;\n const toolInput = payload.tool_input;\n if (!toolInput || typeof toolInput !== \"object\" || Array.isArray(toolInput)) return null;\n const finding = scanToolInput(toolInput as Record<string, unknown>);\n return finding ? buildDenyDecision(finding) : null;\n } catch {\n return null; // fail-open: a guard bug must never block a write\n }\n}\n\n/**\n * CLI entry — `vortex guard write` (stdin: PreToolUse payload JSON).\n * Always exits 0: a deny is expressed via the JSON decision on stdout, and\n * everything else stays silent so the hook's `|| exit 0` wrapper never fires.\n */\nexport async function runGuardCli(\n argv: readonly string[],\n out: (s: string) => void,\n err: (s: string) => void,\n): Promise<number> {\n if (argv[0] !== \"write\") {\n err(\"[vortex] guard subcommands: write — PreToolUse hook body, reads the payload JSON from stdin.\\n\");\n return 0;\n }\n if (process.stdin.isTTY) {\n err(\"[vortex] guard write reads a Claude Code PreToolUse payload from stdin — it is wired by `vortex init` as a hook, not run by hand.\\n\");\n return 0;\n }\n let raw = \"\";\n try {\n raw = readFileSync(0, \"utf8\");\n } catch {\n return 0;\n }\n\n // Parse once: the decision needs tool_input, the self-recording needs cwd +\n // file_path for an honest ledger line. Any parse problem → fail-open silent.\n let payload: Record<string, unknown>;\n try {\n payload = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return 0;\n }\n if (toolOutOfScope(payload)) return 0;\n const toolInput = payload.tool_input;\n if (!toolInput || typeof toolInput !== \"object\" || Array.isArray(toolInput)) return 0;\n const finding = scanToolInput(toolInput as Record<string, unknown>);\n if (!finding) return 0;\n out(buildDenyDecision(finding) + \"\\n\");\n\n // Self-record the denial into the failure ledger (VortEX instances only) so\n // recurrence counting never depends on the agent noticing its own miss.\n // Best-effort: a ledger problem must never affect the deny above. The\n // instance root is resolved upward from the hook's cwd (Claude may have\n // `cd`-ed into a subfolder), and `autoRecord.failures: false` turns\n // self-recording off along with the rest of the loop.\n try {\n const cwd = typeof payload.cwd === \"string\" && payload.cwd ? payload.cwd : process.cwd();\n const root = resolveInstanceRoot(cwd);\n if (root) {\n const ctx = makeContext(root);\n if (loadVortexConfig(ctx).autoRecord.failures) {\n const filePath = (toolInput as Record<string, unknown>).file_path;\n const fileNote = typeof filePath === \"string\" && filePath ? ` (target: ${filePath})` : \"\";\n const { recordGuardDenial } = await import(\"./failures.js\");\n await recordGuardDenial(\n ctx.dataDir,\n `write guard denied a literal control character in ${finding.field}${fileNote}`,\n );\n }\n }\n } catch {\n // best-effort only\n }\n return 0;\n}\n\n/**\n * Nearest VortEX instance root at or above `startDir` (marker:\n * `.agent/vortex.json`), preferring an explicit `VORTEX_REPO_ROOT`, then the\n * host-provided `CLAUDE_PROJECT_DIR` — each honored only when it actually IS\n * an instance. Bounded walk; null when nothing matches (not an instance —\n * the guard still denies, it just has no ledger to count into).\n */\nexport function resolveInstanceRoot(startDir: string): string | null {\n const isInstance = (d: string): boolean => existsSync(join(d, \".agent\", \"vortex.json\"));\n const envRoot = process.env.VORTEX_REPO_ROOT?.trim();\n if (envRoot && isInstance(envRoot)) return envRoot;\n const projectDir = process.env.CLAUDE_PROJECT_DIR?.trim();\n if (projectDir && isInstance(projectDir)) return projectDir;\n let dir = resolve(startDir);\n for (let i = 0; i < 16; i++) {\n if (isInstance(dir)) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../plugins/session-rituals/src/catch-up.ts"],"sourcesContent":["import type { ModuleContext } from \"@vortex-os/core\";\n// Type-only — erased at compile time, so importing it does NOT pull the\n// `@vortex-os/memory-extended` add-on (or its native sqlite/level deps) into\n// the module graph of consumers that only need the types. The runtime engine\n// is loaded lazily inside the function body via `await import(...)`.\nimport type { sessionArchive } from \"@vortex-os/memory-extended\";\n\n/**\n * Start-of-session \"catch-up\": fold conversation transcripts into the local\n * search archive without the user ever having to wrap up a session.\n *\n * Two sources, one pass:\n * - **local (a)** — this machine's own transcripts that are not archived yet,\n * read from every detected agent host's transcript store (Claude Code,\n * Codex, Gemini) and scoped to the current project. Because all hosts are\n * swept on every start, a single Claude Code session-start also folds in the\n * Codex/Gemini sessions you ran in the same project, into one archive.\n * - **pulled (b)** — transcripts created on another machine that arrived as\n * normalized text via git sync. Their text is present but this machine's\n * DB (local, derived, gitignored) has never indexed them.\n *\n * Text only — vectorization is deferred to recall/rebuild so session start\n * stays fast. The whole step is gated by `autoRecord.archive` at the call site\n * and is best-effort: callers should treat a thrown archive backend (e.g. the\n * native sqlite module not built) as \"skip\", never as a fatal start error.\n */\nexport interface CatchUpResult {\n /** Local transcripts newly archived this run (source a). */\n readonly ingestedLocal: number;\n /** Normalized transcripts from another machine newly indexed (source b). */\n readonly indexedPulled: number;\n /** Per-session ingest errors (source a). */\n readonly errors: number;\n}\n\nexport interface CatchUpOptions {\n /** Restrict local ingest to one project's transcripts. Default: `ctx.repoRoot`. */\n readonly cwd?: string;\n /**\n * Transcript adapters for local ingest. Default: all CLI hosts — Claude Code,\n * Codex, and Gemini. Each adapter's `detect()` returns false when its host is\n * absent, so registering all three is ~free on a machine that only uses one.\n * (Claude Desktop is opt-in — it needs the `classic-level` dependency — so it\n * is not in the default set; a caller can add it.) Tests inject fakes (or a\n * sandbox `env.home`) so the scan never touches the real home directory.\n */\n readonly adapters?: sessionArchive.IngestParams[\"adapters\"];\n /** Adapter environment override (e.g. a sandbox HOME). Tests use this. */\n readonly env?: sessionArchive.IngestParams[\"env\"];\n}\n\nexport async function catchUpSessions(\n ctx: ModuleContext,\n opts?: CatchUpOptions,\n): Promise<CatchUpResult> {\n // Lazy-load the optional add-on. Base ships without `memory-extended`; this\n // import resolves only when the add-on is installed alongside it. The call\n // site already gates this step on `autoRecord.archive` and treats a thrown\n // backend as \"skip\", so a missing add-on surfaces as a normal load error\n // the caller can catch.\n const { sessionArchive } = await import(\"@vortex-os/memory-extended\");\n\n const dataDir = ctx.dataDir;\n const cwd = opts?.cwd ?? ctx.repoRoot;\n const adapters = opts?.adapters ?? [\n sessionArchive.claudeCodeAdapter,\n sessionArchive.codexAdapter,\n sessionArchive.geminiAdapter,\n ];\n\n // (a) Ingest this machine's not-yet-archived transcripts for the current\n // project. Writes the normalized copy into the archive (which git syncs) and\n // a local DB row. Text only — no vectorization here.\n const ingestResult = await sessionArchive.ingest({ adapters, dataDir, cwd, env: opts?.env });\n\n // (b) Index normalized transcripts that arrived from another machine via git\n // pull — their text is on disk but this machine's DB has no row yet.\n const store = new sessionArchive.SessionArchiveStore(dataDir);\n let indexedPulled = 0;\n try {\n indexedPulled = store.reindexFromNormalized().indexed;\n } finally {\n store.close();\n }\n\n return {\n ingestedLocal: ingestResult.sessionsIngested,\n indexedPulled,\n errors: ingestResult.errors.length,\n };\n}\n"],"mappings":";AAmDA,eAAsB,gBACpB,KACA,MAAqB;AAOrB,QAAM,EAAE,eAAc,IAAK,MAAM,OAAO,4BAA4B;AAEpE,QAAM,UAAU,IAAI;AACpB,QAAM,MAAM,MAAM,OAAO,IAAI;AAC7B,QAAM,WAAW,MAAM,YAAY;IACjC,eAAe;IACf,eAAe;IACf,eAAe;;AAMjB,QAAM,eAAe,MAAM,eAAe,OAAO,EAAE,UAAU,SAAS,KAAK,KAAK,MAAM,IAAG,CAAE;AAI3F,QAAM,QAAQ,IAAI,eAAe,oBAAoB,OAAO;AAC5D,MAAI,gBAAgB;AACpB,MAAI;AACF,oBAAgB,MAAM,sBAAqB,EAAG;EAChD;AACE,UAAM,MAAK;EACb;AAEA,SAAO;IACL,eAAe,aAAa;IAC5B;IACA,QAAQ,aAAa,OAAO;;AAEhC;","names":[]}
|