@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guilherme Cunha Rodrigues
|
|
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
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# wrxn
|
|
2
|
+
|
|
3
|
+
The WRXN Kernel — an installable AI operating system. One kernel, two install profiles
|
|
4
|
+
(`project` | `workspace`), pull-based updates, and a managed/seeded/state file-class engine
|
|
5
|
+
so an update can never overwrite your config or touch your data.
|
|
6
|
+
|
|
7
|
+
> **Status: walking skeleton** (wrxn-kernel-05). This is the first tracer — the file-class
|
|
8
|
+
> install engine plus a minimal payload. The full pipeline, intelligence layer, worktree
|
|
9
|
+
> lifecycle, and `wrxn update`/`connect` land in later slices. PRD provenance: the WRXN Kernel
|
|
10
|
+
> extraction grill (12 locked decisions, 2026-06-12).
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
wrxn --version # print the kernel version
|
|
16
|
+
wrxn init [--project] [--root <dir>] # lay the kernel payload into <dir> (default: cwd)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## File classes
|
|
20
|
+
|
|
21
|
+
Every shipped file is classified in `manifest.json`:
|
|
22
|
+
|
|
23
|
+
| Class | On install | On update | Example |
|
|
24
|
+
|-------|-----------|-----------|---------|
|
|
25
|
+
| **managed** | laid | overwritten (kernel-owned) | `.claude/constitution.md`, hooks, skills |
|
|
26
|
+
| **seeded** | created once | never overwritten | `.claude/constitution.local.md` |
|
|
27
|
+
| **state** | created empty | never touched | `.wrxn/wiki/` |
|
|
28
|
+
|
|
29
|
+
The installer refuses any payload file the manifest cannot classify.
|
|
30
|
+
|
|
31
|
+
## Develop
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm test # node:test — engine + idempotency + packed-tarball e2e
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The kernel self-hosts: it is built with its own installed pipeline, and `npm test` green is the
|
|
38
|
+
push gate (Constitution Art. III).
|
package/bin/wrxn.cjs
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { init } = require('../lib/install.cjs');
|
|
8
|
+
const { update } = require('../lib/update.cjs');
|
|
9
|
+
const worktree = require('../lib/worktree.cjs');
|
|
10
|
+
const executor = require('../lib/executor.cjs');
|
|
11
|
+
const onboard = require('../lib/onboard.cjs');
|
|
12
|
+
const connect = require('../lib/connect.cjs');
|
|
13
|
+
|
|
14
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
function version() {
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
18
|
+
return pkg.version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const args = { _: [], flags: {} };
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === '--version' || a === '-v') {
|
|
26
|
+
args.flags.version = true;
|
|
27
|
+
} else if (a === '--help' || a === '-h') {
|
|
28
|
+
args.flags.help = true;
|
|
29
|
+
} else if (a === '--project') {
|
|
30
|
+
args.flags.profile = 'project';
|
|
31
|
+
} else if (a === '--workspace') {
|
|
32
|
+
args.flags.profile = 'workspace';
|
|
33
|
+
} else if (a === '--root') {
|
|
34
|
+
args.flags.root = argv[++i];
|
|
35
|
+
} else if (a === '--base') {
|
|
36
|
+
args.flags.base = argv[++i];
|
|
37
|
+
} else if (a === '--path') {
|
|
38
|
+
args.flags.path = argv[++i];
|
|
39
|
+
} else if (a === '--executor') {
|
|
40
|
+
args.flags.executor = argv[++i];
|
|
41
|
+
} else if (a === '--transport') {
|
|
42
|
+
args.flags.transport = argv[++i];
|
|
43
|
+
} else if (a === '--command') {
|
|
44
|
+
args.flags.command = argv[++i];
|
|
45
|
+
} else if (a === '--args') {
|
|
46
|
+
args.flags.args = argv[++i];
|
|
47
|
+
} else if (a === '--scopes') {
|
|
48
|
+
args.flags.scopes = argv[++i];
|
|
49
|
+
} else if (a === '--credential') {
|
|
50
|
+
args.flags.credential = argv[++i];
|
|
51
|
+
} else if (a === '--owner') {
|
|
52
|
+
args.flags.owner = argv[++i];
|
|
53
|
+
} else if (a === '--probe') {
|
|
54
|
+
args.flags.probe = argv[++i];
|
|
55
|
+
} else if (a === '--check-report') {
|
|
56
|
+
args.flags['check-report'] = true;
|
|
57
|
+
} else if (a.startsWith('--')) {
|
|
58
|
+
args.flags[a.slice(2)] = true;
|
|
59
|
+
} else {
|
|
60
|
+
args._.push(a);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const USAGE = `wrxn — WRXN Kernel installer
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
wrxn --version print the kernel version
|
|
70
|
+
wrxn init [--project] [--root <dir>]
|
|
71
|
+
lay the kernel payload into <dir> (default: cwd).
|
|
72
|
+
brownfield-safe: an existing file is never overwritten —
|
|
73
|
+
it is preserved and reported as a collision.
|
|
74
|
+
wrxn update [--root <dir>] update an install: replace managed files, keep
|
|
75
|
+
seeded + state; refuses a downgrade
|
|
76
|
+
wrxn worktree <sub> [--root <repo>] [--base <branch>] [--path <dir>]
|
|
77
|
+
worktree lifecycle (two faces, one engine):
|
|
78
|
+
list show the repo's worktrees
|
|
79
|
+
add <name> ephemeral AFK track on track/<name> (temp path, off base)
|
|
80
|
+
new <name> named durable worktree on wt/<name> (persistent path)
|
|
81
|
+
status <name> clean/dirty + ahead/behind for a worktree
|
|
82
|
+
integrate <name> merge <name> back to base, then auto-prune
|
|
83
|
+
prune <name> [--force] remove a worktree + branch (refuses unmerged unless --force)
|
|
84
|
+
check <tracks.json> refuse an overlapping disjoint-file split
|
|
85
|
+
wrxn dispatch <issue-file> [--executor <type>]
|
|
86
|
+
print the dispatch spec for a ready-for-agent issue — the
|
|
87
|
+
structured order a thin subagent of <type> follows (skill or
|
|
88
|
+
instructions, ACs, isolation, boundary gates). <type> is one of:
|
|
89
|
+
builder (default) | reviewer | security | qa-walker | researcher |
|
|
90
|
+
devops. Only devops passes the push gate.
|
|
91
|
+
wrxn dispatch --check-report <report.json> [--executor <type>]
|
|
92
|
+
validate an executor's structured report against the contract +
|
|
93
|
+
boundary gates (rejects a non-devops report that claims a push)
|
|
94
|
+
|
|
95
|
+
wrxn connect <sub> [--root <dir>]
|
|
96
|
+
connections registry — the workspace nervous system. MCP is the
|
|
97
|
+
socket, CLI is the floor, credentials are state.
|
|
98
|
+
add <name> --transport <mcp|cli> --command <cmd> [--args a,b] [--scopes a,b]
|
|
99
|
+
[--credential env:NAME|state:relpath] [--owner who] [--probe <arg>]
|
|
100
|
+
register a tool only AFTER validating its interface by invocation;
|
|
101
|
+
an unreachable interface is rejected. Stores the credential POINTER,
|
|
102
|
+
never the secret (registry is per-install state, never shipped).
|
|
103
|
+
list print all registered connections (agent-readable JSON)
|
|
104
|
+
get <name> print one connection by name
|
|
105
|
+
|
|
106
|
+
wrxn onboard [--root <dir>] scaffold the Day-1 operator file set under context/ from a filled
|
|
107
|
+
aios-intake.md (the deterministic half of the onboard skill;
|
|
108
|
+
workspace installs only). Idempotent.
|
|
109
|
+
|
|
110
|
+
Profiles: --project (default, the dev pipeline + intelligence + enforcement) |
|
|
111
|
+
--workspace (adds the operator layer: onboard/audit/level-up + intake + decisions log +
|
|
112
|
+
connections registry).`;
|
|
113
|
+
|
|
114
|
+
function main(argv) {
|
|
115
|
+
const args = parseArgs(argv);
|
|
116
|
+
|
|
117
|
+
if (args.flags.version) {
|
|
118
|
+
process.stdout.write(version() + '\n');
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cmd = args._[0];
|
|
123
|
+
|
|
124
|
+
if (!cmd || args.flags.help) {
|
|
125
|
+
process.stdout.write(USAGE + '\n');
|
|
126
|
+
return cmd ? 0 : (args.flags.help ? 0 : 2);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// An explicit --root must carry a real path. An empty/missing value (e.g. an unset
|
|
130
|
+
// shell var expanding to "") must NOT silently fall through to cwd — that footgun
|
|
131
|
+
// writes into whatever dir you happen to be standing in. Shared by init + update.
|
|
132
|
+
if ('root' in args.flags) {
|
|
133
|
+
const r = args.flags.root;
|
|
134
|
+
if (typeof r !== 'string' || r.trim() === '') {
|
|
135
|
+
process.stderr.write('wrxn: --root requires a non-empty directory path\n');
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (cmd === 'init') {
|
|
141
|
+
const target = path.resolve(args.flags.root || process.cwd());
|
|
142
|
+
const profile = args.flags.profile || 'project';
|
|
143
|
+
const report = init({ pkgRoot: PKG_ROOT, target, profile });
|
|
144
|
+
process.stdout.write(`wrxn init (${profile}) → ${target}\n`);
|
|
145
|
+
for (const f of report.laid) {
|
|
146
|
+
process.stdout.write(` laid [${f.class}] ${f.path}\n`);
|
|
147
|
+
}
|
|
148
|
+
for (const f of report.skipped) {
|
|
149
|
+
process.stdout.write(` skipped [${f.class}] ${f.path} (${f.collision ? 'collision — existing file preserved' : 'exists'})\n`);
|
|
150
|
+
}
|
|
151
|
+
process.stdout.write(`${report.laid.length} laid, ${report.skipped.length} unchanged.\n`);
|
|
152
|
+
if (report.brownfield) {
|
|
153
|
+
process.stdout.write(`brownfield install — ${report.collisions.length} existing file(s) preserved (never overwritten): ${report.collisions.map((c) => c.path).join(', ')}\n`);
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cmd === 'update') {
|
|
159
|
+
const target = path.resolve(args.flags.root || process.cwd());
|
|
160
|
+
let report;
|
|
161
|
+
try {
|
|
162
|
+
report = update({ pkgRoot: PKG_ROOT, target });
|
|
163
|
+
} catch (err) {
|
|
164
|
+
process.stderr.write(`wrxn: ${err.message}\n`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write(`wrxn update ${report.from} → ${report.to} (${target})\n`);
|
|
168
|
+
for (const f of report.updated) {
|
|
169
|
+
process.stdout.write(` ${f.reason === 'new-in-version' ? 'added ' : 'updated'} [${f.class}] ${f.path}\n`);
|
|
170
|
+
}
|
|
171
|
+
for (const f of report.preserved) {
|
|
172
|
+
process.stdout.write(` kept [${f.class}] ${f.path}\n`);
|
|
173
|
+
}
|
|
174
|
+
process.stdout.write(`${report.updated.length} updated, ${report.preserved.length} kept.\n`);
|
|
175
|
+
if (report.migrationsRan && report.migrationsRan.length) {
|
|
176
|
+
process.stdout.write(`migrations applied: ${report.migrationsRan.join(', ')}\n`);
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (cmd === 'worktree') {
|
|
182
|
+
const sub = args._[1];
|
|
183
|
+
const repo = path.resolve(args.flags.root || process.cwd());
|
|
184
|
+
const base = args.flags.base || 'main';
|
|
185
|
+
const name = args._[2];
|
|
186
|
+
// A name resolves to a named (wt/) worktree if one exists, else the ephemeral (track/) face.
|
|
187
|
+
const detectPrefix = (n) => worktree.listWorktrees(repo).some((w) => w.branch === worktree.NAMED_PREFIX + n)
|
|
188
|
+
? worktree.NAMED_PREFIX : worktree.BRANCH_PREFIX;
|
|
189
|
+
try {
|
|
190
|
+
if (sub === 'list') {
|
|
191
|
+
for (const w of worktree.listWorktrees(repo)) {
|
|
192
|
+
process.stdout.write(` ${w.branch || '(detached)'}\t${w.path}\n`);
|
|
193
|
+
}
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (sub === 'add') {
|
|
197
|
+
if (!name) { process.stderr.write('wrxn: worktree add requires <name>\n'); return 2; }
|
|
198
|
+
const r = worktree.createWorktree(repo, name, { base, path: args.flags.path });
|
|
199
|
+
process.stdout.write(`worktree ${r.branch} → ${r.path}\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
if (sub === 'new') {
|
|
203
|
+
if (!name) { process.stderr.write('wrxn: worktree new requires <name>\n'); return 2; }
|
|
204
|
+
const r = worktree.createNamedWorktree(repo, name, { base, path: args.flags.path });
|
|
205
|
+
process.stdout.write(`named worktree ${r.branch} → ${r.path}\n`);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (sub === 'status') {
|
|
209
|
+
if (!name) { process.stderr.write('wrxn: worktree status requires <name>\n'); return 2; }
|
|
210
|
+
const s = worktree.worktreeStatus(repo, name, { base, prefix: detectPrefix(name) });
|
|
211
|
+
process.stdout.write(`${s.branch}\t${s.clean ? 'clean' : 'dirty'}\tahead ${s.ahead}, behind ${s.behind}\t${s.path || '(no worktree)'}\n`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (sub === 'integrate') {
|
|
215
|
+
if (!name) { process.stderr.write('wrxn: worktree integrate requires <name>\n'); return 2; }
|
|
216
|
+
const r = worktree.integrateWorktree(repo, name, { base, prefix: detectPrefix(name) });
|
|
217
|
+
process.stdout.write(`integrated ${r.branch} → ${r.base}, worktree + branch pruned\n`);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
if (sub === 'prune') {
|
|
221
|
+
if (!name) { process.stderr.write('wrxn: worktree prune requires <name>\n'); return 2; }
|
|
222
|
+
const r = worktree.pruneWorktree(repo, name, { base, force: !!args.flags.force, prefix: detectPrefix(name) });
|
|
223
|
+
process.stdout.write(`pruned ${r.branch}${r.forced ? ' (forced)' : ''}\n`);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (sub === 'check') {
|
|
227
|
+
const spec = args._[2];
|
|
228
|
+
if (!spec) { process.stderr.write('wrxn: worktree check requires <tracks.json>\n'); return 2; }
|
|
229
|
+
const tracks = JSON.parse(fs.readFileSync(path.resolve(spec), 'utf8'));
|
|
230
|
+
worktree.verifyDisjoint(tracks);
|
|
231
|
+
process.stdout.write(`disjoint OK (${tracks.length} tracks)\n`);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
process.stderr.write(`wrxn: unknown worktree subcommand "${sub || ''}"\n\n${USAGE}\n`);
|
|
235
|
+
return 2;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
process.stderr.write(`wrxn: ${err.message}\n`);
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (cmd === 'dispatch') {
|
|
243
|
+
const file = args._[1];
|
|
244
|
+
if (!file) { process.stderr.write('wrxn: dispatch requires <issue-file> (or --check-report <report.json>)\n'); return 2; }
|
|
245
|
+
const type = args.flags.executor || 'builder';
|
|
246
|
+
if (!executor.EXECUTOR_TYPES.includes(type)) {
|
|
247
|
+
process.stderr.write(`wrxn: unknown executor "${type}" (one of ${executor.EXECUTOR_TYPES.join(', ')})\n`);
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
// --check-report <report.json>: validate an executor's structured report against the contract + gates.
|
|
251
|
+
if (args.flags['check-report']) {
|
|
252
|
+
let report;
|
|
253
|
+
try {
|
|
254
|
+
report = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
|
|
255
|
+
} catch (err) {
|
|
256
|
+
process.stderr.write(`wrxn: cannot read report: ${err.message}\n`);
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
const result = executor.validateReport(report, type);
|
|
260
|
+
if (result.ok) {
|
|
261
|
+
process.stdout.write('report OK\n');
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
process.stderr.write(`report INVALID:\n${result.errors.map((e) => ` - ${e}`).join('\n')}\n`);
|
|
265
|
+
return 2;
|
|
266
|
+
}
|
|
267
|
+
// Default: print the dispatch spec for the issue (what the subagent of this type is ordered to do).
|
|
268
|
+
let issueText;
|
|
269
|
+
try {
|
|
270
|
+
issueText = fs.readFileSync(path.resolve(file), 'utf8');
|
|
271
|
+
} catch (err) {
|
|
272
|
+
process.stderr.write(`wrxn: cannot read issue: ${err.message}\n`);
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
process.stdout.write(JSON.stringify(executor.buildDispatchSpec(issueText, type), null, 2) + '\n');
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (cmd === 'onboard') {
|
|
280
|
+
const root = path.resolve(args.flags.root || process.cwd());
|
|
281
|
+
let report;
|
|
282
|
+
try {
|
|
283
|
+
report = onboard.scaffold(root);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
process.stderr.write(`wrxn: ${err.message}\n`);
|
|
286
|
+
return 2;
|
|
287
|
+
}
|
|
288
|
+
process.stdout.write(`wrxn onboard → ${root}\n`);
|
|
289
|
+
for (const f of report.scaffolded) process.stdout.write(` scaffolded ${f}\n`);
|
|
290
|
+
for (const f of report.skipped) process.stdout.write(` skipped ${f} (no filled intake answer)\n`);
|
|
291
|
+
process.stdout.write(`${report.scaffolded.length} scaffolded, ${report.skipped.length} skipped.\n`);
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (cmd === 'connect') {
|
|
296
|
+
const sub = args._[1];
|
|
297
|
+
const root = path.resolve(args.flags.root || process.cwd());
|
|
298
|
+
try {
|
|
299
|
+
if (sub === 'add') {
|
|
300
|
+
const name = args._[2];
|
|
301
|
+
if (!name) { process.stderr.write('wrxn: connect add requires <name>\n'); return 2; }
|
|
302
|
+
const entry = {
|
|
303
|
+
name,
|
|
304
|
+
transport: args.flags.transport,
|
|
305
|
+
command: args.flags.command,
|
|
306
|
+
scopes: args.flags.scopes ? String(args.flags.scopes).split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
307
|
+
credential: args.flags.credential || null,
|
|
308
|
+
owner: args.flags.owner || null,
|
|
309
|
+
};
|
|
310
|
+
if (args.flags.probe) entry.probe = args.flags.probe;
|
|
311
|
+
// An mcp socket launcher usually needs args (e.g. `node <server> serve`). Comma-separated.
|
|
312
|
+
if (args.flags.args) entry.args = String(args.flags.args).split(',').map((s) => s.trim()).filter(Boolean);
|
|
313
|
+
const res = connect.registerConnection(root, entry);
|
|
314
|
+
process.stdout.write(`connected ${res.entry.name} [${res.entry.transport}] — ${res.validated.detail}\n`);
|
|
315
|
+
process.stdout.write(` credential: ${res.entry.credential || '(none)'} → ${res.credential.resolved ? 'resolved' : 'UNRESOLVED'}\n`);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
if (sub === 'list') {
|
|
319
|
+
process.stdout.write(JSON.stringify(connect.listConnections(root), null, 2) + '\n');
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
if (sub === 'get') {
|
|
323
|
+
const name = args._[2];
|
|
324
|
+
if (!name) { process.stderr.write('wrxn: connect get requires <name>\n'); return 2; }
|
|
325
|
+
const found = connect.findConnection(root, name);
|
|
326
|
+
if (!found) { process.stderr.write(`wrxn: no connection named "${name}"\n`); return 2; }
|
|
327
|
+
process.stdout.write(JSON.stringify(found, null, 2) + '\n');
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
process.stderr.write(`wrxn: unknown connect subcommand "${sub || ''}"\n\n${USAGE}\n`);
|
|
331
|
+
return 2;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
process.stderr.write(`wrxn: ${err.message}\n`);
|
|
334
|
+
return 2;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
process.stderr.write(`wrxn: unknown command "${cmd}"\n\n${USAGE}\n`);
|
|
339
|
+
return 2;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
process.exit(main(process.argv.slice(2)));
|
package/lib/connect.cjs
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WRXN connect + connections registry (wrxn-kernel-21) — the workspace nervous system.
|
|
4
|
+
//
|
|
5
|
+
// A schema'd registry of every interface the AIOS can reach, plus a `connect` command that
|
|
6
|
+
// REGISTERS a tool only after VALIDATING its interface by invocation. The governing rule:
|
|
7
|
+
// MCP is the socket, CLI is the floor, credentials are state.
|
|
8
|
+
// - transport 'mcp' → a socket: a stdio launch command that must spawn (the socket opens).
|
|
9
|
+
// - transport 'cli' → a floor: a binary that must run when probed.
|
|
10
|
+
// - credential → a POINTER into state (env:NAME | state:relpath), never the secret value.
|
|
11
|
+
// The secret is NEVER stored in the registry and NEVER shipped.
|
|
12
|
+
//
|
|
13
|
+
// The registry lives in the install's STATE tier (.wrxn/connections.json) — never in the payload,
|
|
14
|
+
// so it is per-install and never published. It is agent-readable structured JSON (lookup, not a
|
|
15
|
+
// briefing): findConnection / listConnections.
|
|
16
|
+
//
|
|
17
|
+
// lib/connect.cjs is package code (invoked via bin/wrxn.cjs), NOT payload — no manifest entry,
|
|
18
|
+
// consistent with lib/executor.cjs and lib/onboard.cjs.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const TRANSPORTS = ['mcp', 'cli'];
|
|
25
|
+
const REGISTRY_REL = path.join('.wrxn', 'connections.json');
|
|
26
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate a registry entry against the schema.
|
|
30
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
function validateEntry(entry) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
const e = entry || {};
|
|
35
|
+
if (typeof e.name !== 'string' || e.name.trim() === '') {
|
|
36
|
+
errors.push('name is required (non-empty string)');
|
|
37
|
+
}
|
|
38
|
+
if (!TRANSPORTS.includes(e.transport)) {
|
|
39
|
+
errors.push(`transport must be one of ${TRANSPORTS.join('|')} (got ${JSON.stringify(e.transport)})`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof e.command !== 'string' || e.command.trim() === '') {
|
|
42
|
+
errors.push('command is required (the mcp socket launcher or the cli binary to invoke)');
|
|
43
|
+
}
|
|
44
|
+
if ('scopes' in e && !Array.isArray(e.scopes)) {
|
|
45
|
+
errors.push('scopes must be an array of strings');
|
|
46
|
+
}
|
|
47
|
+
if ('credential' in e && e.credential != null && typeof e.credential !== 'string') {
|
|
48
|
+
errors.push('credential must be a pointer string (env:NAME | state:relpath)');
|
|
49
|
+
}
|
|
50
|
+
return { ok: errors.length === 0, errors };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a credential POINTER to its location in state — never the secret value itself.
|
|
55
|
+
* - "env:NAME" → { kind:'env', ref:'NAME', resolved: NAME is set in the environment }
|
|
56
|
+
* - "state:relpath" → { kind:'state', ref:'relpath', resolved: the file exists under root }
|
|
57
|
+
* - falsy / absent → { kind:'none', ref:null, resolved:true } (no credential required)
|
|
58
|
+
* The secret VALUE is deliberately never read or returned.
|
|
59
|
+
*/
|
|
60
|
+
function resolveCredential(pointer, root) {
|
|
61
|
+
if (!pointer) return { kind: 'none', ref: null, resolved: true };
|
|
62
|
+
const idx = pointer.indexOf(':');
|
|
63
|
+
const kind = idx === -1 ? pointer : pointer.slice(0, idx);
|
|
64
|
+
const ref = idx === -1 ? '' : pointer.slice(idx + 1);
|
|
65
|
+
if (kind === 'env') {
|
|
66
|
+
return { kind: 'env', ref, resolved: Object.prototype.hasOwnProperty.call(process.env, ref) && process.env[ref] !== '' };
|
|
67
|
+
}
|
|
68
|
+
if (kind === 'state') {
|
|
69
|
+
// f2: constrain resolution to within the install root — a `../` escape never resolves.
|
|
70
|
+
const abs = path.resolve(root, ref);
|
|
71
|
+
const rootAbs = path.resolve(root);
|
|
72
|
+
if (abs !== rootAbs && !abs.startsWith(rootAbs + path.sep)) {
|
|
73
|
+
return { kind: 'state', ref, resolved: false, escaped: true };
|
|
74
|
+
}
|
|
75
|
+
return { kind: 'state', ref, resolved: fs.existsSync(abs) };
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'unknown', ref: pointer, resolved: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default interface invoker — proves the interface by invocation.
|
|
82
|
+
* cli: spawn `command <probe>`; reachable iff it actually ran (not ENOENT).
|
|
83
|
+
* mcp: spawn `command [args]`; reachable iff the socket launcher spawned (not ENOENT). The process
|
|
84
|
+
* is killed immediately — we only confirm the socket opens, we do not drive a session.
|
|
85
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
86
|
+
*/
|
|
87
|
+
function defaultInvoke(entry) {
|
|
88
|
+
if (entry.transport === 'cli') {
|
|
89
|
+
const probe = entry.probe || '--version';
|
|
90
|
+
const r = spawnSync(entry.command, [probe], { timeout: PROBE_TIMEOUT_MS, stdio: 'ignore' });
|
|
91
|
+
if (r.error) {
|
|
92
|
+
return { ok: false, detail: `cli "${entry.command} ${probe}" did not run: ${r.error.code || r.error.message}` };
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, detail: `cli "${entry.command}" responded (exit ${r.status})` };
|
|
95
|
+
}
|
|
96
|
+
// mcp — confirm the socket launcher spawns and stays up, then kill it.
|
|
97
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
98
|
+
const r = spawnSync(entry.command, args, { timeout: PROBE_TIMEOUT_MS, stdio: 'ignore', killSignal: 'SIGKILL' });
|
|
99
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
100
|
+
return { ok: false, detail: `mcp socket "${entry.command}" not found: ENOENT` };
|
|
101
|
+
}
|
|
102
|
+
// A timeout means the launcher is alive and waiting on the stdio socket — that IS reachable
|
|
103
|
+
// (the healthy long-lived server never exits during the probe window).
|
|
104
|
+
if (r.error && r.error.code === 'ETIMEDOUT') {
|
|
105
|
+
return { ok: true, detail: `mcp socket "${entry.command}" launched (alive)` };
|
|
106
|
+
}
|
|
107
|
+
if (r.error) {
|
|
108
|
+
return { ok: false, detail: `mcp socket "${entry.command}" failed to launch: ${r.error.code || r.error.message}` };
|
|
109
|
+
}
|
|
110
|
+
// f1: the launcher exited on its own within the probe window. A real server would have stayed up
|
|
111
|
+
// (→ timeout). A non-zero exit means it crashed on launch — reject it (NOTE: full protocol-level
|
|
112
|
+
// validation, e.g. an MCP `initialize` handshake, stays out of scope — a non-MCP process that
|
|
113
|
+
// merely stays alive still reads as reachable).
|
|
114
|
+
if (typeof r.status === 'number' && r.status !== 0) {
|
|
115
|
+
return { ok: false, detail: `mcp socket "${entry.command}" crashed on launch (exit ${r.status})` };
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, detail: `mcp socket "${entry.command}" launched` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Probe an interface. The invoker is injectable so unit tests are deterministic; the CLI layer
|
|
122
|
+
* wires defaultInvoke (a real spawn) — that is what makes registration "validated by invocation".
|
|
123
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
124
|
+
*/
|
|
125
|
+
function probeInterface(entry, { invoke } = {}) {
|
|
126
|
+
return (invoke || defaultInvoke)(entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function registryPath(root) {
|
|
130
|
+
return path.join(root, REGISTRY_REL);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Read the registry. A MISSING file is an empty registry (not an error). A PRESENT-but-corrupt
|
|
135
|
+
* file throws (f3) — silently treating it as empty would clobber it on the next register.
|
|
136
|
+
*/
|
|
137
|
+
function readRegistry(root) {
|
|
138
|
+
let raw;
|
|
139
|
+
try {
|
|
140
|
+
raw = fs.readFileSync(registryPath(root), 'utf8');
|
|
141
|
+
} catch {
|
|
142
|
+
return { connections: [] }; // absent → legitimately empty
|
|
143
|
+
}
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(raw);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new Error(`connections registry at ${REGISTRY_REL} is corrupt (${err.message}) — fix or remove it`);
|
|
149
|
+
}
|
|
150
|
+
return Array.isArray(parsed.connections) ? parsed : { connections: [] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function writeRegistry(root, registry) {
|
|
154
|
+
const dir = path.dirname(registryPath(root));
|
|
155
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
156
|
+
fs.writeFileSync(registryPath(root), JSON.stringify(registry, null, 2) + '\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** List all registered connections (agent lookup). */
|
|
160
|
+
function listConnections(root) {
|
|
161
|
+
return readRegistry(root).connections;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Find one connection by name, or null (agent lookup, not a briefing). */
|
|
165
|
+
function findConnection(root, name) {
|
|
166
|
+
return readRegistry(root).connections.find((c) => c.name === name) || null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register (or re-register) a connection. Schema is validated first, then the interface is probed
|
|
171
|
+
* by invocation — an unreachable interface is REJECTED with a useful error. Only the credential
|
|
172
|
+
* POINTER is stored; the secret value is never read or persisted.
|
|
173
|
+
* @returns {{ entry: object, validated: {ok, detail}, credential: {kind, ref, resolved} }}
|
|
174
|
+
* @throws on schema error or unreachable interface.
|
|
175
|
+
*/
|
|
176
|
+
function registerConnection(root, entry, { invoke } = {}) {
|
|
177
|
+
const schema = validateEntry(entry);
|
|
178
|
+
if (!schema.ok) {
|
|
179
|
+
throw new Error(`invalid connection: ${schema.errors.join('; ')}`);
|
|
180
|
+
}
|
|
181
|
+
const validated = probeInterface(entry, { invoke });
|
|
182
|
+
if (!validated.ok) {
|
|
183
|
+
throw new Error(`interface unreachable — ${validated.detail} (not registered)`);
|
|
184
|
+
}
|
|
185
|
+
const stored = {
|
|
186
|
+
name: entry.name,
|
|
187
|
+
transport: entry.transport,
|
|
188
|
+
command: entry.command,
|
|
189
|
+
scopes: Array.isArray(entry.scopes) ? entry.scopes : [],
|
|
190
|
+
credential: entry.credential || null, // POINTER only — never the secret value
|
|
191
|
+
owner: entry.owner || null,
|
|
192
|
+
};
|
|
193
|
+
if (Array.isArray(entry.args) && entry.args.length) stored.args = entry.args;
|
|
194
|
+
|
|
195
|
+
const registry = readRegistry(root);
|
|
196
|
+
const i = registry.connections.findIndex((c) => c.name === stored.name);
|
|
197
|
+
if (i === -1) registry.connections.push(stored);
|
|
198
|
+
else registry.connections[i] = stored; // upsert by name
|
|
199
|
+
writeRegistry(root, registry);
|
|
200
|
+
|
|
201
|
+
return { entry: stored, validated, credential: resolveCredential(stored.credential, root) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
TRANSPORTS,
|
|
206
|
+
REGISTRY_REL,
|
|
207
|
+
validateEntry,
|
|
208
|
+
resolveCredential,
|
|
209
|
+
probeInterface,
|
|
210
|
+
defaultInvoke,
|
|
211
|
+
readRegistry,
|
|
212
|
+
listConnections,
|
|
213
|
+
findConnection,
|
|
214
|
+
registerConnection,
|
|
215
|
+
registryPath,
|
|
216
|
+
};
|