@gcunharodrigues/wrxn 0.1.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 -0
- package/README.md +38 -0
- package/bin/wrxn.cjs +342 -0
- package/lib/connect.cjs +216 -0
- package/lib/executor.cjs +238 -0
- package/lib/install.cjs +105 -0
- package/lib/manifest.cjs +67 -0
- package/lib/migrate.cjs +93 -0
- package/lib/onboard.cjs +84 -0
- package/lib/semver.cjs +14 -0
- package/lib/update.cjs +91 -0
- package/lib/worktree.cjs +217 -0
- package/manifest.json +451 -0
- package/migrations/README.md +21 -0
- package/package.json +23 -0
- package/payload/.claude/constitution.local.md +13 -0
- package/payload/.claude/constitution.md +28 -0
- package/payload/.claude/hooks/code-intel-push.cjs +108 -0
- package/payload/.claude/hooks/enforce-managed-guard.cjs +68 -0
- package/payload/.claude/hooks/enforce-managed-precommit.cjs +74 -0
- package/payload/.claude/hooks/enforce-push-authority.cjs +51 -0
- package/payload/.claude/hooks/enforce-review-marker.cjs +62 -0
- package/payload/.claude/hooks/enforce-tests-on-push.cjs +40 -0
- package/payload/.claude/hooks/recall-surface.cjs +127 -0
- package/payload/.claude/hooks/reference-detect.cjs +83 -0
- package/payload/.claude/hooks/session-end.cjs +132 -0
- package/payload/.claude/hooks/session-history.cjs +76 -0
- package/payload/.claude/hooks/session-start.cjs +117 -0
- package/payload/.claude/hooks/synapse-engine.cjs +351 -0
- package/payload/.claude/hooks/wiki-lint.cjs +104 -0
- package/payload/.claude/settings.json +60 -0
- package/payload/.claude/skills/audit/SKILL.md +23 -0
- package/payload/.claude/skills/diagnose/SKILL.md +117 -0
- package/payload/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/payload/.claude/skills/grill-me/SKILL.md +10 -0
- package/payload/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/payload/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/payload/.claude/skills/grill-with-docs/SKILL.md +88 -0
- package/payload/.claude/skills/handoff/SKILL.md +19 -0
- package/payload/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/payload/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/payload/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/payload/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/payload/.claude/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/payload/.claude/skills/level-up/SKILL.md +28 -0
- package/payload/.claude/skills/memory/SKILL.md +79 -0
- package/payload/.claude/skills/onboard/SKILL.md +43 -0
- package/payload/.claude/skills/prototype/LOGIC.md +79 -0
- package/payload/.claude/skills/prototype/SKILL.md +30 -0
- package/payload/.claude/skills/prototype/UI.md +112 -0
- package/payload/.claude/skills/qa-walk/SKILL.md +227 -0
- package/payload/.claude/skills/qa-walk/references/cli-mode.md +28 -0
- package/payload/.claude/skills/qa-walk/references/finding-issue-template.md +48 -0
- package/payload/.claude/skills/qa-walk/references/walk-report-template.md +56 -0
- package/payload/.claude/skills/qa-walk/references/web-mode.md +112 -0
- package/payload/.claude/skills/setup-matt-pocock-skills/SKILL.md +121 -0
- package/payload/.claude/skills/setup-matt-pocock-skills/domain.md +51 -0
- package/payload/.claude/skills/setup-matt-pocock-skills/issue-tracker-github.md +22 -0
- package/payload/.claude/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +23 -0
- package/payload/.claude/skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
- package/payload/.claude/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/payload/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/payload/.claude/skills/skill-creator/SKILL.md +209 -0
- package/payload/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/payload/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/payload/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/payload/.claude/skills/synapse/SKILL.md +132 -0
- package/payload/.claude/skills/synapse/assets/README.md +50 -0
- package/payload/.claude/skills/synapse/references/brackets.md +100 -0
- package/payload/.claude/skills/synapse/references/commands.md +118 -0
- package/payload/.claude/skills/synapse/references/domains.md +126 -0
- package/payload/.claude/skills/synapse/references/layers.md +186 -0
- package/payload/.claude/skills/synapse/references/manifest.md +142 -0
- package/payload/.claude/skills/tdd/SKILL.md +22 -0
- package/payload/.claude/skills/tech-search/SKILL.md +431 -0
- package/payload/.claude/skills/tech-search/prompts/page-extract.md +133 -0
- package/payload/.claude/skills/to-issues/SKILL.md +83 -0
- package/payload/.claude/skills/to-prd/SKILL.md +74 -0
- package/payload/.claude/skills/triage/AGENT-BRIEF.md +168 -0
- package/payload/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/payload/.claude/skills/triage/SKILL.md +103 -0
- package/payload/.claude/skills/write-a-skill/SKILL.md +117 -0
- package/payload/.recon.json +3 -0
- package/payload/.synapse/global +6 -0
- package/payload/.synapse/manifest +38 -0
- package/payload/.synapse/pipeline +6 -0
- package/payload/.synapse/routing +8 -0
- package/payload/.wrxn/continuity/.gitkeep +0 -0
- package/payload/.wrxn/history/.gitkeep +0 -0
- package/payload/.wrxn/wiki/.gitkeep +0 -0
- package/payload/.wrxn/wiki/concepts/.gitkeep +0 -0
- package/payload/.wrxn/wiki/decisions/.gitkeep +0 -0
- package/payload/.wrxn/wiki/gotchas/.gitkeep +0 -0
- package/payload/.wrxn/wiki/sessions/.gitkeep +0 -0
- package/payload/.wrxn/wiki.cjs +164 -0
- package/payload/aios-intake.md +32 -0
- package/payload/connections.md +15 -0
- package/payload/decisions/log.md +18 -0
- package/payload/docs/agents/domain.md +38 -0
- package/payload/docs/agents/issue-tracker.md +25 -0
- package/payload/docs/agents/triage-labels.md +15 -0
- package/payload/docs/workspace/operator-layer.md +14 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN session-history hook — the turn-trail recorder (wrxn-kernel-10).
|
|
5
|
+
// UserPromptSubmit. Appends one `<iso>\t<first-line>` record per turn to the session's trail
|
|
6
|
+
// at .wrxn/history/<sid>.trail. SessionEnd reads this trail to build the durable session page.
|
|
7
|
+
//
|
|
8
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
9
|
+
// Pass-through recorder: it NEVER injects context and NEVER blocks — always emits {} (exit 0).
|
|
10
|
+
// Fail-open: any fault still emits {}.
|
|
11
|
+
//
|
|
12
|
+
// Contract: UserPromptSubmit event JSON on stdin → {} on stdout (exit 0). Side effect: trail append.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
function emitEmpty() {
|
|
18
|
+
process.stdout.write('{}');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findInstallRoot(startDir) {
|
|
23
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
24
|
+
for (let i = 0; i < 12; i++) {
|
|
25
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
26
|
+
const up = path.dirname(dir);
|
|
27
|
+
if (up === dir) break;
|
|
28
|
+
dir = up;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nowISO() {
|
|
34
|
+
return process.env.WRXN_NOW || new Date().toISOString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safeId(sid) {
|
|
38
|
+
return String(sid || 'session')
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
41
|
+
.replace(/^-+|-+$/g, '')
|
|
42
|
+
.slice(0, 48) || 'session';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main() {
|
|
46
|
+
let event = {};
|
|
47
|
+
try {
|
|
48
|
+
const stdin = fs.readFileSync(0, 'utf8');
|
|
49
|
+
if (stdin.trim()) event = JSON.parse(stdin);
|
|
50
|
+
} catch {
|
|
51
|
+
emitEmpty();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const root = findInstallRoot();
|
|
55
|
+
if (!root) emitEmpty();
|
|
56
|
+
|
|
57
|
+
const sid = safeId(event.session_id);
|
|
58
|
+
const prompt = String(event.prompt || '').split('\n')[0].slice(0, 200).replace(/\t/g, ' ').trim();
|
|
59
|
+
if (prompt) {
|
|
60
|
+
try {
|
|
61
|
+
const dir = path.join(root, '.wrxn', 'history');
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
fs.appendFileSync(path.join(dir, `${sid}.trail`), `${nowISO()}\t${prompt}\n`);
|
|
64
|
+
} catch {
|
|
65
|
+
/* trail append is best-effort — never block the prompt */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
emitEmpty();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
main();
|
|
74
|
+
} catch {
|
|
75
|
+
emitEmpty();
|
|
76
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN session-start hook — the orientation surface (wrxn-kernel-10).
|
|
5
|
+
// SessionStart. Injects identity + resume as additionalContext so every new session opens
|
|
6
|
+
// oriented. The resume gives the DELIBERATE handoff baton precedence over the automatic
|
|
7
|
+
// episodic record: a baton at .wrxn/continuity/latest.md (single writer = the handoff skill)
|
|
8
|
+
// wins; otherwise the most-recent dated session page is surfaced as the resume pointer.
|
|
9
|
+
//
|
|
10
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
11
|
+
// Fail-open: any fault emits {} (no orientation) — the hook NEVER blocks a session opening.
|
|
12
|
+
//
|
|
13
|
+
// Contract: SessionStart event JSON on stdin → envelope JSON on stdout (exit 0).
|
|
14
|
+
// inject → { "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "…" } }
|
|
15
|
+
// no-op → {}
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
function emit(envelope) {
|
|
21
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Walk up from CLAUDE_PROJECT_DIR (or cwd) to the install root carrying wrxn.install.json.
|
|
26
|
+
function findInstallRoot(startDir) {
|
|
27
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
28
|
+
for (let i = 0; i < 12; i++) {
|
|
29
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
30
|
+
const up = path.dirname(dir);
|
|
31
|
+
if (up === dir) break;
|
|
32
|
+
dir = up;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readFileOr(p, fallback) {
|
|
38
|
+
try {
|
|
39
|
+
return fs.readFileSync(p, 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function identityLine(root) {
|
|
46
|
+
const name = path.basename(root);
|
|
47
|
+
let version = '';
|
|
48
|
+
let profile = 'project';
|
|
49
|
+
try {
|
|
50
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(root, 'wrxn.install.json'), 'utf8'));
|
|
51
|
+
version = receipt.kernelVersion ? ` v${receipt.kernelVersion}` : '';
|
|
52
|
+
profile = receipt.profile || 'project';
|
|
53
|
+
} catch {
|
|
54
|
+
/* receipt unreadable → name-only identity */
|
|
55
|
+
}
|
|
56
|
+
return `Install: ${name}${version} (${profile} profile)`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The deliberate handoff baton — the intent-carrying continuity slot. Single writer: the
|
|
60
|
+
// handoff skill. Read-only here; its presence takes precedence over the episodic record.
|
|
61
|
+
function readBaton(root) {
|
|
62
|
+
return readFileOr(path.join(root, '.wrxn', 'continuity', 'latest.md'), null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The automatic episodic record: the most-recent dated session page (sessions tier).
|
|
66
|
+
function latestSessionPage(root) {
|
|
67
|
+
const dir = path.join(root, '.wrxn', 'wiki', 'sessions');
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.md'));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (names.length === 0) return null;
|
|
75
|
+
names.sort(); // dated `YYYY-MM-DD-…` slugs sort chronologically
|
|
76
|
+
return names[names.length - 1];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function main() {
|
|
80
|
+
let consumed = '';
|
|
81
|
+
try {
|
|
82
|
+
consumed = fs.readFileSync(0, 'utf8');
|
|
83
|
+
} catch {
|
|
84
|
+
/* no stdin → still try to orient */
|
|
85
|
+
}
|
|
86
|
+
void consumed; // SessionStart carries no field we need beyond the install context
|
|
87
|
+
|
|
88
|
+
const root = findInstallRoot();
|
|
89
|
+
if (!root) emit({});
|
|
90
|
+
|
|
91
|
+
const parts = [identityLine(root)];
|
|
92
|
+
|
|
93
|
+
const baton = readBaton(root);
|
|
94
|
+
if (baton && baton.trim()) {
|
|
95
|
+
parts.push('', 'Resume — deliberate handoff baton (.wrxn/continuity/latest.md):', baton.trim());
|
|
96
|
+
} else {
|
|
97
|
+
const page = latestSessionPage(root);
|
|
98
|
+
if (page) {
|
|
99
|
+
parts.push('', `Resume — last session: .wrxn/wiki/sessions/${page}`);
|
|
100
|
+
} else {
|
|
101
|
+
parts.push('', 'Resume — fresh install, no prior session recorded.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emit({
|
|
106
|
+
hookSpecificOutput: {
|
|
107
|
+
hookEventName: 'SessionStart',
|
|
108
|
+
additionalContext: ['<wrxn-orientation>', ...parts, '</wrxn-orientation>'].join('\n'),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
main();
|
|
115
|
+
} catch {
|
|
116
|
+
emit({}); // fail-open: never block a session opening
|
|
117
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN SYNAPSE engine — the per-prompt context-injection core (the layered port).
|
|
5
|
+
// UserPromptSubmit. Assembles the active domains into a <synapse-rules> block and injects it
|
|
6
|
+
// as additionalContext so every prompt carries the constitution + operational rules.
|
|
7
|
+
//
|
|
8
|
+
// Self-contained: this hook ships into installs and CANNOT import the kernel lib/. It reads the
|
|
9
|
+
// install's own .synapse/ domains + .claude/constitution.md + the manifest. Silent / fail-open:
|
|
10
|
+
// any fault emits {} (no injection) — the engine NEVER blocks a prompt.
|
|
11
|
+
//
|
|
12
|
+
// Contract: UserPromptSubmit event JSON on stdin → envelope JSON on stdout (exit 0).
|
|
13
|
+
// inject → { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "<synapse-rules>…" } }
|
|
14
|
+
// no-op → {}
|
|
15
|
+
//
|
|
16
|
+
// Layers (faithful to the WRXN-OS SYNAPSE model, reimplemented standalone):
|
|
17
|
+
// L0 Constitution — always, sourced from .claude/constitution.md, NEVER trimmed.
|
|
18
|
+
// L1 Global / Pipeline — always-on domains (.synapse/<domain>, KEY=VALUE rules).
|
|
19
|
+
// L6 Keyword-recall — domains that fire only when a trigger word appears in the prompt (06b).
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
function emit(envelope) {
|
|
26
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Walk up from CLAUDE_PROJECT_DIR (or cwd) to the install root carrying wrxn.install.json.
|
|
31
|
+
// Same resolution as the managed-guard hook — the receipt marks an install root.
|
|
32
|
+
function findInstallRoot(startDir) {
|
|
33
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
34
|
+
for (let i = 0; i < 8; i++) {
|
|
35
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
36
|
+
const up = path.dirname(dir);
|
|
37
|
+
if (up === dir) break;
|
|
38
|
+
dir = up;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readFileOr(p, fallback) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(p, 'utf8');
|
|
46
|
+
} catch {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── parsing ────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
// Parse the flat KEY=VALUE .synapse/manifest into a domain map:
|
|
54
|
+
// { GLOBAL: { state, alwaysOn, recall:[...] }, ROUTING: {...}, ... }
|
|
55
|
+
// Per-domain keys: <DOMAIN>_STATE, <DOMAIN>_ALWAYS_ON, <DOMAIN>_RECALL. Non-domain keys
|
|
56
|
+
// (RULES_BUDGET_TOKENS, HANDOFF_PCT) are left for the caller to read raw.
|
|
57
|
+
function parseSynapseManifest(text) {
|
|
58
|
+
const domains = {};
|
|
59
|
+
const ensure = (name) => (domains[name] || (domains[name] = { state: '', alwaysOn: false, recall: [] }));
|
|
60
|
+
for (const line of String(text || '').split('\n')) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
63
|
+
const eq = trimmed.indexOf('=');
|
|
64
|
+
if (eq < 0) continue;
|
|
65
|
+
const key = trimmed.slice(0, eq);
|
|
66
|
+
const val = trimmed.slice(eq + 1);
|
|
67
|
+
let m;
|
|
68
|
+
if ((m = key.match(/^(.+)_STATE$/))) ensure(m[1]).state = val;
|
|
69
|
+
else if ((m = key.match(/^(.+)_ALWAYS_ON$/))) ensure(m[1]).alwaysOn = val === 'true';
|
|
70
|
+
else if ((m = key.match(/^(.+)_RECALL$/))) ensure(m[1]).recall = val.split(',').map((s) => s.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
return domains;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Read a single scalar key from the flat manifest (e.g. RULES_BUDGET_TOKENS), or '' if absent.
|
|
76
|
+
function manifestValue(text, key) {
|
|
77
|
+
for (const line of String(text || '').split('\n')) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
80
|
+
const eq = trimmed.indexOf('=');
|
|
81
|
+
if (eq < 0) continue;
|
|
82
|
+
if (trimmed.slice(0, eq) === key) return trimmed.slice(eq + 1);
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Extract <DOMAIN>_RULE_N values from a domain file, ordered by N ascending.
|
|
88
|
+
function domainRules(domainUpper, text) {
|
|
89
|
+
const re = new RegExp(`^${domainUpper}_RULE_(\\d+)=(.*)$`);
|
|
90
|
+
const found = [];
|
|
91
|
+
for (const line of String(text || '').split('\n')) {
|
|
92
|
+
const m = line.match(re);
|
|
93
|
+
if (m) found.push({ n: Number(m[1]), v: m[2] });
|
|
94
|
+
}
|
|
95
|
+
found.sort((a, b) => a.n - b.n);
|
|
96
|
+
return found.map((x) => x.v);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── rendering ────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
// Render constitution.md into the always-kept L0 section. Keep article headings + their bullets,
|
|
102
|
+
// drop the prose preamble so the injection stays compact. Returns the section body (no header).
|
|
103
|
+
function renderConstitution(md) {
|
|
104
|
+
const lines = String(md || '').split('\n');
|
|
105
|
+
const out = [];
|
|
106
|
+
let inArticle = false;
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (/^##\s+/.test(line)) {
|
|
109
|
+
inArticle = true;
|
|
110
|
+
out.push(line.replace(/^##\s+/, '').trim());
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!inArticle) continue;
|
|
114
|
+
if (/^#\s/.test(line)) { inArticle = false; continue; }
|
|
115
|
+
const t = line.trim();
|
|
116
|
+
if (!t) continue;
|
|
117
|
+
if (t.startsWith('-')) {
|
|
118
|
+
out.push(' ' + t.replace(/^-\s*/, ''));
|
|
119
|
+
} else if (out.length) {
|
|
120
|
+
// A wrapped bullet's continuation line — fold it into the preceding bullet rather than drop it.
|
|
121
|
+
out[out.length - 1] += ' ' + t;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A rules domain section: a `[HEADER]` line followed by numbered rules.
|
|
128
|
+
function renderRulesSection(header, rules) {
|
|
129
|
+
const body = rules.map((r, i) => ` ${i + 1}. ${r}`).join('\n');
|
|
130
|
+
return `[${header}]\n${body}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function estimateTokens(s) {
|
|
134
|
+
return Math.ceil(String(s || '').length / 4);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── budget governor ────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
// Bound the trimmable sections by a token budget. The constitution is OUTSIDE the budget and
|
|
140
|
+
// always kept. Trimmable sections are dropped lowest-priority-LAST first (the array is in priority
|
|
141
|
+
// order; we drop from the end) until the kept set fits. Returns { kept:[...], trimmed:[names] }.
|
|
142
|
+
function applyBudget(trimmable, budget) {
|
|
143
|
+
const kept = trimmable.slice();
|
|
144
|
+
const trimmed = [];
|
|
145
|
+
const total = () => kept.reduce((sum, s) => sum + estimateTokens(s.text), 0);
|
|
146
|
+
while (kept.length && total() > budget) {
|
|
147
|
+
const dropped = kept.pop();
|
|
148
|
+
trimmed.unshift(dropped.name);
|
|
149
|
+
}
|
|
150
|
+
return { kept, trimmed };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── token-base + forced handoff (06c) ────────────────────────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// The handoff math must run on REAL token usage, not an assumed 200k (the original bug fired
|
|
156
|
+
// at ~37% of a 1M window). Both signals are portable into any install:
|
|
157
|
+
// resident → the last assistant line's usage in the transcript (transcript_path is in the payload).
|
|
158
|
+
// window → ~/.claude.json projects[cwd].lastModelUsage KEYS carry the tagged id; [1m] ⇒ 1M else 200k.
|
|
159
|
+
// See memory `synapse-model-window-from-claude-json`.
|
|
160
|
+
|
|
161
|
+
// Resident tokens = the last assistant turn's input + cache_read + cache_creation (output EXCLUDED —
|
|
162
|
+
// it is not resident in the next prompt's context). Returns a number, or null when unreadable.
|
|
163
|
+
function readResidentTokens(transcriptPath) {
|
|
164
|
+
try {
|
|
165
|
+
const lines = fs.readFileSync(transcriptPath, 'utf8').split('\n');
|
|
166
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
167
|
+
const line = lines[i].trim();
|
|
168
|
+
if (!line) continue;
|
|
169
|
+
let obj;
|
|
170
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
171
|
+
const msg = obj && obj.message;
|
|
172
|
+
if (!msg || msg.role !== 'assistant' || !msg.usage) continue;
|
|
173
|
+
const u = msg.usage;
|
|
174
|
+
return (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Model context window, resolved by an explicit precedence (issue 29). On [1m] sessions
|
|
183
|
+
// lastModelUsage is often EMPTY and the transcript model id lacks the [1m] tag — there is no
|
|
184
|
+
// reliable auto-signal — so the window must be settable explicitly rather than guessed:
|
|
185
|
+
// 1. env WRXN_CONTEXT_WINDOW — a positive finite number wins unconditionally.
|
|
186
|
+
// 2. manifest CONTEXT_WINDOW — a positive finite value (when manifestText is supplied).
|
|
187
|
+
// 3. ~/.claude.json lastModelUsage KEYS — a [1m] tag ⇒ 1,000,000 (auto-detect, when present).
|
|
188
|
+
// 4. fallback 200,000.
|
|
189
|
+
// homeDir/manifestText overrides keep it testable.
|
|
190
|
+
function modelWindow(cwd, homeDir, manifestText) {
|
|
191
|
+
// 1. explicit env override.
|
|
192
|
+
const envWin = Number(process.env.WRXN_CONTEXT_WINDOW);
|
|
193
|
+
if (Number.isFinite(envWin) && envWin > 0) return envWin;
|
|
194
|
+
|
|
195
|
+
// 2. manifest CONTEXT_WINDOW (the engine already reads scalar manifest values).
|
|
196
|
+
if (manifestText != null) {
|
|
197
|
+
const manWin = Number(manifestValue(manifestText, 'CONTEXT_WINDOW'));
|
|
198
|
+
if (Number.isFinite(manWin) && manWin > 0) return manWin;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 3. lastModelUsage [1m] auto-detect.
|
|
202
|
+
try {
|
|
203
|
+
const home = homeDir || process.env.HOME || os.homedir();
|
|
204
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'));
|
|
205
|
+
const proj = (cfg.projects && cfg.projects[cwd]) || {};
|
|
206
|
+
const keys = Object.keys(proj.lastModelUsage || {});
|
|
207
|
+
if (keys.some((k) => /\[1m\]/i.test(k))) return 1000000;
|
|
208
|
+
} catch {
|
|
209
|
+
// fall through to the default.
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 4. fallback.
|
|
213
|
+
return 200000;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handoff threshold (fraction of the window): env WRXN_HANDOFF_PCT > manifest HANDOFF_PCT > 0.40.
|
|
217
|
+
function resolveHandoffPct(manifestText) {
|
|
218
|
+
const env = Number(process.env.WRXN_HANDOFF_PCT);
|
|
219
|
+
if (Number.isFinite(env) && env > 0) return env;
|
|
220
|
+
const m = Number(manifestValue(manifestText, 'HANDOFF_PCT'));
|
|
221
|
+
return Number.isFinite(m) && m > 0 ? m : 0.40;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The NON-BLOCKING forced-handoff directive (never refuses work — orders the agent to wrap up cleanly).
|
|
225
|
+
function handoffDirective(consumed, pct) {
|
|
226
|
+
const now = Math.round(consumed * 100);
|
|
227
|
+
const thresh = Math.round(pct * 100);
|
|
228
|
+
return [
|
|
229
|
+
'[HANDOFF REQUIRED]',
|
|
230
|
+
` Context is at ~${now}% of the model window (>= the ${thresh}% handoff threshold). NON-BLOCKING — do NOT stop work:`,
|
|
231
|
+
' 1. Finish the current request.',
|
|
232
|
+
' 2. Run the handoff skill to write the baton (a compact handoff document).',
|
|
233
|
+
' 3. Tell the operator to /clear and open a fresh session, where the baton injects on resume.',
|
|
234
|
+
].join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── assembly ────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
// Build the active section list for a prompt. Returns ordered sections:
|
|
240
|
+
// [{ name, header, text, always }] — `always` marks the never-trimmed constitution.
|
|
241
|
+
function buildSections(root, prompt) {
|
|
242
|
+
const manifestText = readFileOr(path.join(root, '.synapse', 'manifest'), '');
|
|
243
|
+
const domains = parseSynapseManifest(manifestText);
|
|
244
|
+
const promptLower = String(prompt || '').toLowerCase();
|
|
245
|
+
const sections = [];
|
|
246
|
+
|
|
247
|
+
// L0 — Constitution (always; from constitution.md). Skip silently if the file/domain is absent.
|
|
248
|
+
if (domains.CONSTITUTION && domains.CONSTITUTION.state === 'active') {
|
|
249
|
+
const body = renderConstitution(readFileOr(path.join(root, '.claude', 'constitution.md'), ''));
|
|
250
|
+
if (body.trim()) {
|
|
251
|
+
sections.push({ name: 'CONSTITUTION', header: 'CONSTITUTION', text: `[CONSTITUTION] (NON-NEGOTIABLE)\n${body}`, always: true });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// L1/L6 — every other active domain, in manifest order. always-on loads unconditionally;
|
|
256
|
+
// a recall domain loads only when one of its trigger words appears in the prompt (06b).
|
|
257
|
+
for (const [name, d] of Object.entries(domains)) {
|
|
258
|
+
if (name === 'CONSTITUTION' || d.state !== 'active') continue;
|
|
259
|
+
const fires = d.alwaysOn || (d.recall.length > 0 && d.recall.some((w) => promptLower.includes(w.toLowerCase())));
|
|
260
|
+
if (!fires) continue;
|
|
261
|
+
const domainText = readFileOr(path.join(root, '.synapse', name.toLowerCase()), '');
|
|
262
|
+
const rules = domainRules(name, domainText);
|
|
263
|
+
if (!rules.length) continue;
|
|
264
|
+
const header = d.alwaysOn ? name : `RECALL: ${name.toLowerCase()}`;
|
|
265
|
+
sections.push({ name, header, text: renderRulesSection(header, rules), always: false });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { sections, manifestText };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resolveBudget(manifestText) {
|
|
272
|
+
const env = Number(process.env.WRXN_RULES_BUDGET);
|
|
273
|
+
if (Number.isFinite(env) && env > 0) return env;
|
|
274
|
+
const m = Number(manifestValue(manifestText, 'RULES_BUDGET_TOKENS'));
|
|
275
|
+
return Number.isFinite(m) && m > 0 ? m : 600;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Compose the full additionalContext for an UserPromptSubmit event at an install root, or '' to no-op.
|
|
279
|
+
function compose(root, event) {
|
|
280
|
+
const ev = event || {};
|
|
281
|
+
const { sections, manifestText } = buildSections(root, ev.prompt);
|
|
282
|
+
if (!sections.length) return '';
|
|
283
|
+
|
|
284
|
+
const always = sections.filter((s) => s.always);
|
|
285
|
+
const trimmable = sections.filter((s) => !s.always);
|
|
286
|
+
const { kept, trimmed } = applyBudget(trimmable, resolveBudget(manifestText));
|
|
287
|
+
|
|
288
|
+
const out = [...always.map((s) => s.text), ...kept.map((s) => s.text)];
|
|
289
|
+
if (trimmed.length) {
|
|
290
|
+
out.push(`[SYNAPSE-RULES-TRIM] ${trimmed.join(', ')} dropped over the ${resolveBudget(manifestText)}-token rules budget`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 06c — forced handoff at >= threshold of REAL consumed context. Always-kept (outside the budget):
|
|
294
|
+
// a handoff directive must never be trimmed. Silent when the token base is unreadable.
|
|
295
|
+
if (ev.transcript_path) {
|
|
296
|
+
const resident = readResidentTokens(ev.transcript_path);
|
|
297
|
+
if (resident != null) {
|
|
298
|
+
const window = modelWindow(ev.cwd || root, process.env.HOME || os.homedir(), manifestText);
|
|
299
|
+
const consumed = resident / window;
|
|
300
|
+
const pct = resolveHandoffPct(manifestText);
|
|
301
|
+
if (consumed >= pct) out.push(handoffDirective(consumed, pct));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return `<synapse-rules>\n\n${out.join('\n\n')}\n\n</synapse-rules>`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── entrypoint ────────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
function main() {
|
|
311
|
+
let event;
|
|
312
|
+
try {
|
|
313
|
+
event = JSON.parse(fs.readFileSync(0, 'utf8') || '{}');
|
|
314
|
+
} catch {
|
|
315
|
+
return emit({}); // unparseable → fail open (no injection)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const root = findInstallRoot(event.cwd);
|
|
319
|
+
if (!root) return emit({}); // not inside a wrxn install → nothing to inject
|
|
320
|
+
|
|
321
|
+
let additionalContext = '';
|
|
322
|
+
try {
|
|
323
|
+
additionalContext = compose(root, event);
|
|
324
|
+
} catch {
|
|
325
|
+
return emit({}); // any assembly fault → fail open
|
|
326
|
+
}
|
|
327
|
+
if (!additionalContext) return emit({});
|
|
328
|
+
|
|
329
|
+
return emit({
|
|
330
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (require.main === module) main();
|
|
335
|
+
|
|
336
|
+
module.exports = {
|
|
337
|
+
parseSynapseManifest,
|
|
338
|
+
manifestValue,
|
|
339
|
+
domainRules,
|
|
340
|
+
renderConstitution,
|
|
341
|
+
renderRulesSection,
|
|
342
|
+
estimateTokens,
|
|
343
|
+
applyBudget,
|
|
344
|
+
buildSections,
|
|
345
|
+
compose,
|
|
346
|
+
findInstallRoot,
|
|
347
|
+
readResidentTokens,
|
|
348
|
+
modelWindow,
|
|
349
|
+
resolveHandoffPct,
|
|
350
|
+
handoffDirective,
|
|
351
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN wiki-lint hook — session-close wiki-integrity flag (wrxn-kernel-11).
|
|
5
|
+
// Stop. At session close it sweeps the wiki tiers for MALFORMED pages — a page is malformed if it
|
|
6
|
+
// lacks a well-formed frontmatter block or is missing a required key (name / description / tier).
|
|
7
|
+
// When any malformed page exists it injects a <wiki-lint> flag naming them; otherwise silent.
|
|
8
|
+
// REPORT-ONLY: it never edits or deletes a page.
|
|
9
|
+
//
|
|
10
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
11
|
+
// Fail-open: any fault emits {} — session close must NEVER hang or throw.
|
|
12
|
+
//
|
|
13
|
+
// Contract: Stop event JSON on stdin → envelope JSON on stdout (exit 0).
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const TIERS = ['concepts', 'decisions', 'gotchas', 'sessions'];
|
|
19
|
+
const REQUIRED_KEYS = ['name', 'description', 'tier'];
|
|
20
|
+
const MAX_FLAGGED = 20;
|
|
21
|
+
|
|
22
|
+
function emit(envelope) {
|
|
23
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findInstallRoot(startDir) {
|
|
28
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
29
|
+
for (let i = 0; i < 12; i++) {
|
|
30
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
31
|
+
const up = path.dirname(dir);
|
|
32
|
+
if (up === dir) break;
|
|
33
|
+
dir = up;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Return the reason a page is malformed, or null when it is well-formed.
|
|
39
|
+
function lintPage(text) {
|
|
40
|
+
const src = String(text || '');
|
|
41
|
+
if (!src.startsWith('---')) return 'no frontmatter';
|
|
42
|
+
const end = src.indexOf('\n---', 3);
|
|
43
|
+
if (end < 0) return 'unterminated frontmatter';
|
|
44
|
+
const fm = src.slice(3, end);
|
|
45
|
+
const missing = REQUIRED_KEYS.filter((k) => !new RegExp(`^${k}\\s*:`, 'm').test(fm));
|
|
46
|
+
if (missing.length) return `missing ${missing.join('/')}`;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sweep(root) {
|
|
51
|
+
const flagged = [];
|
|
52
|
+
for (const tier of TIERS) {
|
|
53
|
+
const dir = path.join(root, '.wrxn', 'wiki', tier);
|
|
54
|
+
let names;
|
|
55
|
+
try {
|
|
56
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.md'));
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const reason = lintPage(text);
|
|
68
|
+
if (reason) {
|
|
69
|
+
flagged.push(`${tier}/${name} — ${reason}`);
|
|
70
|
+
if (flagged.length >= MAX_FLAGGED) return flagged;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return flagged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
try {
|
|
79
|
+
fs.readFileSync(0, 'utf8'); // drain stdin (Stop payload unused beyond presence)
|
|
80
|
+
} catch {
|
|
81
|
+
/* no stdin → still sweep */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const root = findInstallRoot();
|
|
85
|
+
if (!root) emit({});
|
|
86
|
+
|
|
87
|
+
const flagged = sweep(root);
|
|
88
|
+
if (!flagged.length) emit({}); // clean wiki → silent
|
|
89
|
+
|
|
90
|
+
const ctx = [
|
|
91
|
+
'<wiki-lint>',
|
|
92
|
+
`${flagged.length} malformed wiki page(s) — fix the frontmatter (name / description / tier):`,
|
|
93
|
+
...flagged.map((f) => `- ${f}`),
|
|
94
|
+
'</wiki-lint>',
|
|
95
|
+
].join('\n');
|
|
96
|
+
|
|
97
|
+
emit({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: ctx } });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
main();
|
|
102
|
+
} catch {
|
|
103
|
+
emit({});
|
|
104
|
+
}
|