@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,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN managed hook — protect MANAGED kernel files inside an install (the ring-0 reshape).
|
|
5
|
+
// PreToolUse:Edit|Write. Blocks an agent edit/write to a file classified `managed` in the
|
|
6
|
+
// install receipt unless WRXN_MANAGED_CONFIRM is set. Seeded + state files (and anything outside
|
|
7
|
+
// the install) edit freely. Self-contained: hooks ship into installs and cannot import the kernel lib.
|
|
8
|
+
//
|
|
9
|
+
// Contract: PreToolUse event JSON on stdin → decision JSON on stdout (exit 0).
|
|
10
|
+
// allow → {} block → { "decision": "block", "reason": "..." }
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
function emit(decision) {
|
|
16
|
+
process.stdout.write(JSON.stringify(decision));
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Walk up from CLAUDE_PROJECT_DIR (or cwd) to the install root carrying wrxn.install.json.
|
|
21
|
+
function findInstallRoot() {
|
|
22
|
+
let dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
23
|
+
for (let i = 0; i < 8; i++) {
|
|
24
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
25
|
+
const up = path.dirname(dir);
|
|
26
|
+
if (up === dir) break;
|
|
27
|
+
dir = up;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function managedPaths(root) {
|
|
33
|
+
try {
|
|
34
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(root, 'wrxn.install.json'), 'utf8'));
|
|
35
|
+
return (receipt.files || []).filter((f) => f.class === 'managed').map((f) => f.path);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function main() {
|
|
42
|
+
let event;
|
|
43
|
+
try {
|
|
44
|
+
event = JSON.parse(fs.readFileSync(0, 'utf8') || '{}');
|
|
45
|
+
} catch {
|
|
46
|
+
return emit({}); // unparseable → fail open
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filePath = event.tool_input && event.tool_input.file_path;
|
|
50
|
+
if (!filePath) return emit({}); // not a file write
|
|
51
|
+
|
|
52
|
+
const root = findInstallRoot();
|
|
53
|
+
if (!root) return emit({}); // not inside a wrxn install → nothing to guard
|
|
54
|
+
|
|
55
|
+
const rel = path.relative(root, path.resolve(filePath));
|
|
56
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return emit({}); // outside the install
|
|
57
|
+
|
|
58
|
+
if (!managedPaths(root).includes(rel)) return emit({}); // seeded/state/other → free
|
|
59
|
+
|
|
60
|
+
if (process.env.WRXN_MANAGED_CONFIRM) return emit({}); // explicit confirm → allowed
|
|
61
|
+
|
|
62
|
+
return emit({
|
|
63
|
+
decision: 'block',
|
|
64
|
+
reason: `"${rel}" is a MANAGED kernel file — kernel-owned, overwritten on \`wrxn update\`. To change it intentionally, set WRXN_MANAGED_CONFIRM. (Seeded + state files edit freely.)`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN managed hook — the commit-side half of the managed-files guard.
|
|
5
|
+
// PreToolUse:Bash. Blocks a `git commit` that stages any MANAGED kernel file unless
|
|
6
|
+
// WRXN_MANAGED_CONFIRM is set. Seeded + state files commit freely. Self-contained.
|
|
7
|
+
//
|
|
8
|
+
// Contract: PreToolUse event JSON on stdin → decision JSON on stdout (exit 0).
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
function emit(decision) {
|
|
15
|
+
process.stdout.write(JSON.stringify(decision));
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findInstallRoot() {
|
|
20
|
+
let dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
21
|
+
for (let i = 0; i < 8; i++) {
|
|
22
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
23
|
+
const up = path.dirname(dir);
|
|
24
|
+
if (up === dir) break;
|
|
25
|
+
dir = up;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function managedSet(root) {
|
|
31
|
+
try {
|
|
32
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(root, 'wrxn.install.json'), 'utf8'));
|
|
33
|
+
return new Set((receipt.files || []).filter((f) => f.class === 'managed').map((f) => f.path));
|
|
34
|
+
} catch {
|
|
35
|
+
return new Set();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function main() {
|
|
40
|
+
let event;
|
|
41
|
+
try {
|
|
42
|
+
event = JSON.parse(fs.readFileSync(0, 'utf8') || '{}');
|
|
43
|
+
} catch {
|
|
44
|
+
return emit({});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const command = (event.tool_input && event.tool_input.command) || '';
|
|
48
|
+
if (!/\bgit\s+commit\b/.test(command)) return emit({}); // not a commit
|
|
49
|
+
|
|
50
|
+
const root = findInstallRoot();
|
|
51
|
+
if (!root) return emit({});
|
|
52
|
+
|
|
53
|
+
let staged = [];
|
|
54
|
+
try {
|
|
55
|
+
staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: root, encoding: 'utf8' })
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
} catch {
|
|
59
|
+
return emit({}); // not a git repo / git unavailable → fail open
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const managed = managedSet(root);
|
|
63
|
+
const hits = staged.filter((f) => managed.has(f));
|
|
64
|
+
if (hits.length === 0) return emit({});
|
|
65
|
+
|
|
66
|
+
if (process.env.WRXN_MANAGED_CONFIRM) return emit({});
|
|
67
|
+
|
|
68
|
+
return emit({
|
|
69
|
+
decision: 'block',
|
|
70
|
+
reason: `Commit stages MANAGED kernel file(s): ${hits.join(', ')}. Set WRXN_MANAGED_CONFIRM to confirm an intentional kernel change.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN managed hook — Constitution Article I (Agent Authority).
|
|
5
|
+
// PreToolUse:Bash gate: a remote git op (push / PR / tag push) is allowed only when
|
|
6
|
+
// the session declares the devops role via WRXN_ACTIVE_AGENT=devops. A bare push runs
|
|
7
|
+
// as @unknown and is denied. Fails OPEN on any internal error (never over-blocks).
|
|
8
|
+
//
|
|
9
|
+
// Contract: reads a PreToolUse hook event as JSON on stdin, writes a decision to stdout.
|
|
10
|
+
// allow → {} (exit 0)
|
|
11
|
+
// deny → { "decision": "block", "reason": "..." } (exit 0; the harness blocks the call)
|
|
12
|
+
|
|
13
|
+
const REMOTE_OP = /\bgit\s+push\b|\bgh\s+pr\s+(create|merge)\b|\bgit\s+push\s+.*--tags\b/;
|
|
14
|
+
|
|
15
|
+
function main() {
|
|
16
|
+
let input = '';
|
|
17
|
+
try {
|
|
18
|
+
input = require('fs').readFileSync(0, 'utf8');
|
|
19
|
+
} catch {
|
|
20
|
+
return emit({}); // no stdin → nothing to gate
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let event;
|
|
24
|
+
try {
|
|
25
|
+
event = JSON.parse(input || '{}');
|
|
26
|
+
} catch {
|
|
27
|
+
return emit({}); // unparseable → fail open
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const command = (event.tool_input && event.tool_input.command) || '';
|
|
31
|
+
if (!REMOTE_OP.test(command)) {
|
|
32
|
+
return emit({}); // not a remote op
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (process.env.WRXN_ACTIVE_AGENT === 'devops') {
|
|
36
|
+
return emit({}); // authorized
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return emit({
|
|
40
|
+
decision: 'block',
|
|
41
|
+
reason:
|
|
42
|
+
'Remote git op is devops-exclusive (Constitution Art. I). Re-run with WRXN_ACTIVE_AGENT=devops, or delegate to the devops role.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function emit(decision) {
|
|
47
|
+
process.stdout.write(JSON.stringify(decision));
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN managed hook — the review-gate: a push referencing an unreviewed issue BLOCKS.
|
|
5
|
+
// PreToolUse:Bash. On a `git push`, scans the pushed commit messages for bracketed issue ids
|
|
6
|
+
// `[id]` and requires a review marker `review-<id>.md` in the markers dir for each. Missing → block.
|
|
7
|
+
// Self-contained; shell-free git via execFileSync.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execFileSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
function emit(decision) {
|
|
14
|
+
process.stdout.write(JSON.stringify(decision));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pushedMessages(cwd) {
|
|
19
|
+
// Prefer an explicit range, then the upstream range, then the last commit as a floor.
|
|
20
|
+
const ranges = [process.env.WRXN_PUSH_RANGE, '@{u}..HEAD'].filter(Boolean);
|
|
21
|
+
for (const r of ranges) {
|
|
22
|
+
try {
|
|
23
|
+
return execFileSync('git', ['log', r, '--format=%B'], { cwd, encoding: 'utf8' });
|
|
24
|
+
} catch {
|
|
25
|
+
/* try next */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync('git', ['log', '-1', '--format=%B'], { cwd, encoding: 'utf8' });
|
|
30
|
+
} catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function main() {
|
|
36
|
+
let event;
|
|
37
|
+
try {
|
|
38
|
+
event = JSON.parse(fs.readFileSync(0, 'utf8') || '{}');
|
|
39
|
+
} catch {
|
|
40
|
+
return emit({});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const command = (event.tool_input && event.tool_input.command) || '';
|
|
44
|
+
if (!/\bgit\s+push\b/.test(command)) return emit({});
|
|
45
|
+
|
|
46
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
47
|
+
const messages = pushedMessages(cwd);
|
|
48
|
+
const ids = [...new Set((messages.match(/\[([a-z0-9][a-z0-9._-]*)\]/gi) || []).map((s) => s.slice(1, -1)))];
|
|
49
|
+
if (ids.length === 0) return emit({}); // no issue referenced → nothing to gate
|
|
50
|
+
|
|
51
|
+
const markersRel = process.env.WRXN_REVIEW_MARKERS_DIR || '.claude/ai/output';
|
|
52
|
+
const dir = path.join(cwd, markersRel);
|
|
53
|
+
const missing = ids.filter((id) => !fs.existsSync(path.join(dir, `review-${id}.md`)));
|
|
54
|
+
if (missing.length === 0) return emit({});
|
|
55
|
+
|
|
56
|
+
return emit({
|
|
57
|
+
decision: 'block',
|
|
58
|
+
reason: `Push blocked: issue id(s) ${missing.join(', ')} in the pushed commits have no review marker (expected review-<id>.md in ${markersRel}). Run the review and write the marker first.`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN managed hook — Quality-First (Art. III): a red suite blocks a push.
|
|
5
|
+
// PreToolUse:Bash. On a `git push`, runs the configured test command in the install root and
|
|
6
|
+
// blocks on a non-zero exit. WRXN_TEST_CMD (default "npm test") is OPERATOR config, not event
|
|
7
|
+
// data — it is intentionally a shell command line, so a shell is appropriate here.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
function emit(decision) {
|
|
13
|
+
process.stdout.write(JSON.stringify(decision));
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function main() {
|
|
18
|
+
let event;
|
|
19
|
+
try {
|
|
20
|
+
event = JSON.parse(fs.readFileSync(0, 'utf8') || '{}');
|
|
21
|
+
} catch {
|
|
22
|
+
return emit({});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const command = (event.tool_input && event.tool_input.command) || '';
|
|
26
|
+
if (!/\bgit\s+push\b/.test(command)) return emit({});
|
|
27
|
+
|
|
28
|
+
const testCmd = process.env.WRXN_TEST_CMD || 'npm test';
|
|
29
|
+
try {
|
|
30
|
+
execSync(testCmd, { cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), stdio: 'ignore' });
|
|
31
|
+
return emit({});
|
|
32
|
+
} catch {
|
|
33
|
+
return emit({
|
|
34
|
+
decision: 'block',
|
|
35
|
+
reason: `Push blocked: the test suite is red (\`${testCmd}\` exited non-zero). Green it first (Constitution Art. III, Quality-First).`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN recall-surface hook — per-prompt recall nudge (wrxn-kernel-11).
|
|
5
|
+
// UserPromptSubmit. The symmetric RECALL half of reference-detect's CAPTURE: on each prompt it
|
|
6
|
+
// matches the prompt's SALIENT terms against the wiki knowledge tiers (concepts/decisions/gotchas)
|
|
7
|
+
// and, ONLY when a page matches, injects a <recall-surface> nudge — "you already have a page on X,
|
|
8
|
+
// recall it before re-deriving / re-asking the operator". Gated → silent on non-matching prompts.
|
|
9
|
+
//
|
|
10
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only). The kernel
|
|
11
|
+
// wiki engine is substring (not BM25) — recall here is a deliberately simple distinct-salient-term
|
|
12
|
+
// count, ranked, top-N. Fail-open: any fault emits {} — the hook NEVER blocks a prompt.
|
|
13
|
+
//
|
|
14
|
+
// Contract: UserPromptSubmit event JSON on stdin → envelope JSON on stdout (exit 0).
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const TIERS = ['concepts', 'decisions', 'gotchas']; // knowledge tiers; sessions (episodic) excluded
|
|
20
|
+
const TOP_N = 2;
|
|
21
|
+
const MIN_PROMPT_LEN = 8; // skip trivial prompts ("ok", "yes")
|
|
22
|
+
// A page must share >=2 DISTINCT salient terms with the prompt to surface — the substring engine has
|
|
23
|
+
// no BM25 score, so a single shared common word is too weak a signal (anti-noise). Tradeoff: a genuine
|
|
24
|
+
// single-strong-term recall is silenced (fail-silent — a missed nudge is safer than a false one).
|
|
25
|
+
const MIN_DISTINCT = 2;
|
|
26
|
+
const MAX_BLOCK_CHARS = 600;
|
|
27
|
+
|
|
28
|
+
// Drop stopwords + short tokens so common words don't match common page words (anti-noise).
|
|
29
|
+
const STOPWORDS = new Set(['about', 'after', 'again', 'against', 'because', 'before', 'being', 'between',
|
|
30
|
+
'could', 'does', 'doing', 'down', 'during', 'each', 'from', 'further', 'have', 'having', 'here', 'how',
|
|
31
|
+
'into', 'just', 'like', 'more', 'most', 'only', 'other', 'over', 'same', 'should', 'some', 'such',
|
|
32
|
+
'than', 'that', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those', 'through', 'under',
|
|
33
|
+
'until', 'very', 'want', 'what', 'when', 'where', 'which', 'while', 'with', 'would', 'your', 'today',
|
|
34
|
+
'tell', 'show', 'give', 'make', 'need', 'know', 'help', 'please', 'thing', 'stuff', 'really', 'going']);
|
|
35
|
+
|
|
36
|
+
function emit(envelope) {
|
|
37
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findInstallRoot(startDir) {
|
|
42
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
43
|
+
for (let i = 0; i < 12; i++) {
|
|
44
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
45
|
+
const up = path.dirname(dir);
|
|
46
|
+
if (up === dir) break;
|
|
47
|
+
dir = up;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// De-duped salient content tokens (lowercased, >=4 chars, non-stopword).
|
|
53
|
+
function salientTerms(prompt) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const tok of (String(prompt || '').toLowerCase().match(/[a-z][a-z0-9]{3,}/g) || [])) {
|
|
56
|
+
if (!STOPWORDS.has(tok)) seen.add(tok);
|
|
57
|
+
}
|
|
58
|
+
return [...seen];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Score each wiki page by the count of DISTINCT salient terms it contains; keep pages with >=1.
|
|
62
|
+
function recall(root, terms) {
|
|
63
|
+
const hits = [];
|
|
64
|
+
for (const tier of TIERS) {
|
|
65
|
+
const dir = path.join(root, '.wrxn', 'wiki', tier);
|
|
66
|
+
let names;
|
|
67
|
+
try {
|
|
68
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.md'));
|
|
69
|
+
} catch {
|
|
70
|
+
continue; // tier absent → skip
|
|
71
|
+
}
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8').toLowerCase();
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const matched = terms.filter((t) => text.includes(t)).length;
|
|
80
|
+
if (matched >= MIN_DISTINCT) hits.push({ slug: name.replace(/\.md$/, ''), tier, score: matched });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Highest distinct-term count first; ties broken by slug for determinism.
|
|
84
|
+
hits.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
|
|
85
|
+
return hits.slice(0, TOP_N);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function block(hits) {
|
|
89
|
+
const lines = [
|
|
90
|
+
'<recall-surface>',
|
|
91
|
+
'You already have captured page(s) on this topic — READ before answering (do not re-derive, do',
|
|
92
|
+
'not ask the operator to re-explain). Recall with: node .wrxn/wiki.cjs recall "<slug>"',
|
|
93
|
+
...hits.map((h) => `- ${h.slug} (${h.tier})`),
|
|
94
|
+
'</recall-surface>',
|
|
95
|
+
].join('\n');
|
|
96
|
+
return lines.length <= MAX_BLOCK_CHARS ? lines : `${lines.slice(0, MAX_BLOCK_CHARS - 18)}\n</recall-surface>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function main() {
|
|
100
|
+
let event = {};
|
|
101
|
+
try {
|
|
102
|
+
const stdin = fs.readFileSync(0, 'utf8');
|
|
103
|
+
if (stdin.trim()) event = JSON.parse(stdin);
|
|
104
|
+
} catch {
|
|
105
|
+
emit({});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const root = findInstallRoot();
|
|
109
|
+
if (!root) emit({});
|
|
110
|
+
|
|
111
|
+
const prompt = typeof event.prompt === 'string' ? event.prompt : '';
|
|
112
|
+
if (prompt.trim().length < MIN_PROMPT_LEN) emit({});
|
|
113
|
+
|
|
114
|
+
const terms = salientTerms(prompt);
|
|
115
|
+
if (!terms.length) emit({});
|
|
116
|
+
|
|
117
|
+
const hits = recall(root, terms);
|
|
118
|
+
if (!hits.length) emit({}); // no captured page matched → silent (the gate)
|
|
119
|
+
|
|
120
|
+
emit({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: block(hits) } });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
main();
|
|
125
|
+
} catch {
|
|
126
|
+
emit({});
|
|
127
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN reference-detect hook — the capture-nudge (wrxn-kernel-11).
|
|
5
|
+
// UserPromptSubmit. Scans the prompt for reference SIGNALS (URLs + explicit source markers) and,
|
|
6
|
+
// ONLY when one is present, injects a <reference-candidate> nudge telling the agent to OFFER to
|
|
7
|
+
// capture the reference into the wiki WITH provenance. PROPOSE-then-CONFIRM — it never ingests
|
|
8
|
+
// anything itself (the operator's chosen mode). Gated → silent on the vast majority of prompts.
|
|
9
|
+
//
|
|
10
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
11
|
+
// Fail-open: any fault emits {} — the hook NEVER blocks a prompt.
|
|
12
|
+
//
|
|
13
|
+
// Contract: UserPromptSubmit event JSON on stdin → envelope JSON on stdout (exit 0).
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const URL_RE = /\bhttps?:\/\/[^\s<>()[\]]+/gi;
|
|
19
|
+
// Single-word markers are colon-anchored so "open source" / "for reference" do NOT trigger.
|
|
20
|
+
const MARKER_RE = /(?:\b(?:source|reference|cite)\s*:|\b(?:per the|according to|this is from|based on)\b)/i;
|
|
21
|
+
const MAX_URLS = 5;
|
|
22
|
+
const MAX_BLOCK_CHARS = 500;
|
|
23
|
+
|
|
24
|
+
function emit(envelope) {
|
|
25
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findInstallRoot(startDir) {
|
|
30
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
31
|
+
for (let i = 0; i < 12; i++) {
|
|
32
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
33
|
+
const up = path.dirname(dir);
|
|
34
|
+
if (up === dir) break;
|
|
35
|
+
dir = up;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detect(prompt) {
|
|
41
|
+
const text = String(prompt || '');
|
|
42
|
+
const urls = (text.match(URL_RE) || []).map((u) => u.replace(/[.,;:!?"']+$/, ''));
|
|
43
|
+
return { urls: [...new Set(urls)].slice(0, MAX_URLS), hasMarker: MARKER_RE.test(text) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nudge(sig) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
if (sig.urls.length) parts.push(`URL(s): ${sig.urls.join(', ')}`);
|
|
49
|
+
if (sig.hasMarker) parts.push('an explicit source/reference marker');
|
|
50
|
+
const block = [
|
|
51
|
+
'<reference-candidate>',
|
|
52
|
+
`Detected ${parts.join(' + ')} in this prompt.`,
|
|
53
|
+
'If the operator means these as references to KEEP, OFFER to capture them into the wiki WITH',
|
|
54
|
+
'provenance (propose-then-confirm — NEVER auto-ingest). On confirmation:',
|
|
55
|
+
' node .wrxn/wiki.cjs write-page concepts <slug> --description "<what>" --body "source: <url-or-origin>"',
|
|
56
|
+
'</reference-candidate>',
|
|
57
|
+
].join('\n');
|
|
58
|
+
return block.length <= MAX_BLOCK_CHARS ? block : `${block.slice(0, MAX_BLOCK_CHARS - 24)}\n…\n</reference-candidate>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function main() {
|
|
62
|
+
let event = {};
|
|
63
|
+
try {
|
|
64
|
+
const stdin = fs.readFileSync(0, 'utf8');
|
|
65
|
+
if (stdin.trim()) event = JSON.parse(stdin);
|
|
66
|
+
} catch {
|
|
67
|
+
emit({});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!findInstallRoot()) emit({}); // install-scoped — silent outside an install
|
|
71
|
+
|
|
72
|
+
const prompt = typeof event.prompt === 'string' ? event.prompt : '';
|
|
73
|
+
const sig = detect(prompt);
|
|
74
|
+
if (!sig.urls.length && !sig.hasMarker) emit({}); // no signal → silent (the gate)
|
|
75
|
+
|
|
76
|
+
emit({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: nudge(sig) } });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
main();
|
|
81
|
+
} catch {
|
|
82
|
+
emit({});
|
|
83
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WRXN session-end hook — the episodic writer (wrxn-kernel-10).
|
|
5
|
+
// SessionEnd. Writes a dated session page into the install's own wiki sessions tier from the
|
|
6
|
+
// captured turn trail, then clears the trail. CONTINUITY DOCTRINE: this writer touches ONLY
|
|
7
|
+
// dated session pages — it NEVER writes the continuity baton (.wrxn/continuity/latest.md).
|
|
8
|
+
// That slot has a single writer (the handoff skill); keeping the paths disjoint is the
|
|
9
|
+
// structural fix for the clobber observed live 2026-06-12.
|
|
10
|
+
//
|
|
11
|
+
// Self-contained: ships into installs, MUST NOT import the kernel lib (node stdlib only).
|
|
12
|
+
// Fail-open + side-effect-only: emits nothing useful, never blocks; any fault exits 0 silently.
|
|
13
|
+
//
|
|
14
|
+
// Contract: SessionEnd event JSON on stdin → exit 0. Side effect: a sessions/<date>-<sid>.md page.
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
function done() {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findInstallRoot(startDir) {
|
|
24
|
+
let dir = startDir || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
25
|
+
for (let i = 0; i < 12; i++) {
|
|
26
|
+
if (fs.existsSync(path.join(dir, 'wrxn.install.json'))) return dir;
|
|
27
|
+
const up = path.dirname(dir);
|
|
28
|
+
if (up === dir) break;
|
|
29
|
+
dir = up;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function nowISO() {
|
|
35
|
+
return process.env.WRXN_NOW || new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Sanitize a session id into a kebab slug fragment (the page + trail FILE name).
|
|
39
|
+
function safeId(sid) {
|
|
40
|
+
return String(sid || 'session')
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '')
|
|
44
|
+
.slice(0, 48) || 'session';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Collapse any whitespace run (incl. newlines/tabs) to a single space — keeps a value safe to
|
|
48
|
+
// embed on a single frontmatter line (the wiki adapter + synapse engine PARSE these pages, so
|
|
49
|
+
// a stray newline would corrupt the frontmatter and break wiki query).
|
|
50
|
+
function oneLine(s) {
|
|
51
|
+
return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read + consume the captured turn trail for this session (one `<iso>\t<line>` per turn).
|
|
55
|
+
function readTrail(root, sid) {
|
|
56
|
+
const trail = path.join(root, '.wrxn', 'history', `${safeId(sid)}.trail`);
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = fs.readFileSync(trail, 'utf8');
|
|
60
|
+
} catch {
|
|
61
|
+
return { turns: [], trail };
|
|
62
|
+
}
|
|
63
|
+
const turns = raw.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
64
|
+
return { turns, trail };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function main() {
|
|
68
|
+
let event = {};
|
|
69
|
+
try {
|
|
70
|
+
const stdin = fs.readFileSync(0, 'utf8');
|
|
71
|
+
if (stdin.trim()) event = JSON.parse(stdin);
|
|
72
|
+
} catch {
|
|
73
|
+
/* no/garbled stdin → write a minimal page anyway */
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const root = findInstallRoot();
|
|
77
|
+
if (!root) done();
|
|
78
|
+
|
|
79
|
+
const sid = oneLine(event.session_id || 'session');
|
|
80
|
+
const reason = oneLine(event.reason || 'unknown');
|
|
81
|
+
const date = nowISO().slice(0, 10); // YYYY-MM-DD
|
|
82
|
+
const slug = `${date}-${safeId(event.session_id)}`;
|
|
83
|
+
|
|
84
|
+
const { turns, trail } = readTrail(root, event.session_id);
|
|
85
|
+
const trailLines = turns.length
|
|
86
|
+
? turns.map((t, i) => {
|
|
87
|
+
const tab = t.indexOf('\t');
|
|
88
|
+
const line = tab > -1 ? t.slice(tab + 1) : t;
|
|
89
|
+
return `${i + 1}. ${line}`;
|
|
90
|
+
})
|
|
91
|
+
: ['_(no turns captured)_'];
|
|
92
|
+
|
|
93
|
+
const page = [
|
|
94
|
+
'---',
|
|
95
|
+
`name: ${slug}`,
|
|
96
|
+
`description: Session ${sid} — ${turns.length} turn(s), ended ${reason}`,
|
|
97
|
+
'tier: sessions',
|
|
98
|
+
'source: session-end-hook',
|
|
99
|
+
'---',
|
|
100
|
+
'',
|
|
101
|
+
`# Session ${date} (${sid})`,
|
|
102
|
+
'',
|
|
103
|
+
`- Ended: ${reason}`,
|
|
104
|
+
`- Turns: ${turns.length}`,
|
|
105
|
+
'',
|
|
106
|
+
'## Turn trail',
|
|
107
|
+
...trailLines,
|
|
108
|
+
'',
|
|
109
|
+
].join('\n');
|
|
110
|
+
|
|
111
|
+
const dir = path.join(root, '.wrxn', 'wiki', 'sessions');
|
|
112
|
+
try {
|
|
113
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
114
|
+
fs.writeFileSync(path.join(dir, `${slug}.md`), page);
|
|
115
|
+
// Consume the trail so the next session starts clean.
|
|
116
|
+
try {
|
|
117
|
+
fs.rmSync(trail, { force: true });
|
|
118
|
+
} catch {
|
|
119
|
+
/* trail cleanup is best-effort */
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
/* page write failed → fail-open, never block session close */
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
done();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
main();
|
|
130
|
+
} catch {
|
|
131
|
+
done();
|
|
132
|
+
}
|