@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/lib/executor.cjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WRXN executor dispatch harness (wrxn-kernel-18 builder + wrxn-kernel-19 the remaining five).
|
|
4
|
+
// The kernel ships the executor CONTRACT, not a live LLM: buildDispatchSpec turns a ready-for-agent
|
|
5
|
+
// issue into the structured order a thin subagent of a given TYPE follows (which skill/instructions,
|
|
6
|
+
// which artifact, isolation, the boundary gates), and validateReport enforces the structured return +
|
|
7
|
+
// the boundary gates on whatever the subagent reports back. The push gate is type-aware: of the six
|
|
8
|
+
// executors only `devops` may pass it. The live LLM execution is out of scope — the harness is the
|
|
9
|
+
// proven contract.
|
|
10
|
+
//
|
|
11
|
+
// Pure data transforms (no I/O); the CLI (bin/wrxn.cjs dispatch) reads/writes files around them.
|
|
12
|
+
|
|
13
|
+
const BUILD_SKILL = '.claude/skills/tdd/SKILL.md';
|
|
14
|
+
|
|
15
|
+
// The builder keeps the rich tdd report contract (wrxn-kernel-18); the other five report a generic
|
|
16
|
+
// type-specific `artifact` + the common boundary fields.
|
|
17
|
+
const BUILDER_REQUIRED = ['issueId', 'status', 'redTest', 'greenCommit', 'typesClean', 'pushed', 'summary'];
|
|
18
|
+
const GENERIC_REQUIRED = ['issueId', 'status', 'artifact', 'pushed', 'summary'];
|
|
19
|
+
const STATUSES = ['completed', 'blocked'];
|
|
20
|
+
|
|
21
|
+
// The executor registry — one entry per type. `skill: null` means the loop is a GLOBAL slash-skill
|
|
22
|
+
// with no local file (code-review / security-review / the devops push), so the spec carries explicit
|
|
23
|
+
// `instructions` instead (the subagent has no Skill tool — it cannot /invoke). `canPush` gates the
|
|
24
|
+
// push: only devops may report pushed=true.
|
|
25
|
+
const EXECUTORS = {
|
|
26
|
+
builder: {
|
|
27
|
+
skill: BUILD_SKILL,
|
|
28
|
+
artifact: 'green-commit',
|
|
29
|
+
canPush: false,
|
|
30
|
+
isolation: 'fresh-context',
|
|
31
|
+
required: BUILDER_REQUIRED,
|
|
32
|
+
procedure: [
|
|
33
|
+
`Read ${BUILD_SKILL} FIRST, then follow it — it IS your build loop (never paraphrase it).`,
|
|
34
|
+
'Build the slice test-first: write a failing (red) test, make it pass (green) with the minimal change, keep types clean.',
|
|
35
|
+
'Commit locally with a conventional message referencing the issue id.',
|
|
36
|
+
'Return the structured report described by reportSchema.',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
reviewer: {
|
|
40
|
+
skill: null, // /code-review is a global slash-skill — no local file to read
|
|
41
|
+
instructions: [
|
|
42
|
+
'You are a fresh-eyes code reviewer. /code-review is a global slash-skill with no local file, and',
|
|
43
|
+
'subagents have no Skill tool — follow these instructions directly: review the diff against the',
|
|
44
|
+
'PRD / issue contracts, verify every claim against ALL sources before flagging, and separate',
|
|
45
|
+
'blocking from non-blocking findings. Write the review marker review-<id>.md.',
|
|
46
|
+
],
|
|
47
|
+
artifact: 'review-marker',
|
|
48
|
+
canPush: false,
|
|
49
|
+
isolation: 'fresh-context',
|
|
50
|
+
required: GENERIC_REQUIRED,
|
|
51
|
+
},
|
|
52
|
+
security: {
|
|
53
|
+
skill: null, // /security-review is a global slash-skill — no local file
|
|
54
|
+
instructions: [
|
|
55
|
+
'You are a defensive security reviewer. /security-review is a global slash-skill with no local',
|
|
56
|
+
'file — follow these instructions directly: scan the diff for injection, path traversal, authz /',
|
|
57
|
+
'secret handling, and fail-open/closed posture; report PASS / PASS-WITH-FINDINGS / FAIL with evidence.',
|
|
58
|
+
],
|
|
59
|
+
artifact: 'security-report',
|
|
60
|
+
canPush: false,
|
|
61
|
+
isolation: 'fresh-context',
|
|
62
|
+
required: GENERIC_REQUIRED,
|
|
63
|
+
},
|
|
64
|
+
'qa-walker': {
|
|
65
|
+
skill: '.claude/skills/qa-walk/SKILL.md',
|
|
66
|
+
artifact: 'walk-findings',
|
|
67
|
+
canPush: false,
|
|
68
|
+
isolation: 'fresh-context',
|
|
69
|
+
required: GENERIC_REQUIRED,
|
|
70
|
+
},
|
|
71
|
+
researcher: {
|
|
72
|
+
skill: '.claude/skills/tech-search/SKILL.md',
|
|
73
|
+
artifact: 'research-summary',
|
|
74
|
+
canPush: false,
|
|
75
|
+
isolation: 'fresh-context',
|
|
76
|
+
required: GENERIC_REQUIRED,
|
|
77
|
+
},
|
|
78
|
+
devops: {
|
|
79
|
+
skill: null, // the integration / push executor — instructions; the ONLY type that may push
|
|
80
|
+
instructions: [
|
|
81
|
+
'You are the devops integration executor — the ONLY executor authorized to push. Integrate the',
|
|
82
|
+
'reviewed + security-passed + qa-walked track to the trunk: verify the review marker (review-<id>.md)',
|
|
83
|
+
'+ a green suite exist, THEN push (AIOX_ACTIVE_AGENT=devops). This is the single path through the push gate.',
|
|
84
|
+
],
|
|
85
|
+
artifact: 'authorized-push',
|
|
86
|
+
canPush: true,
|
|
87
|
+
isolation: 'attended',
|
|
88
|
+
required: GENERIC_REQUIRED,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const EXECUTOR_TYPES = Object.keys(EXECUTORS);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse a ready-for-agent issue markdown into { id, title, labels, whatToBuild, acceptanceCriteria }.
|
|
96
|
+
* Tolerant: missing sections yield empty values rather than throwing.
|
|
97
|
+
*/
|
|
98
|
+
function parseIssue(issueText) {
|
|
99
|
+
const text = String(issueText || '');
|
|
100
|
+
const fm = text.startsWith('---') ? text.slice(3, text.indexOf('\n---', 3)) : '';
|
|
101
|
+
|
|
102
|
+
const scalar = (key) => {
|
|
103
|
+
const m = fm.match(new RegExp(`^${key}\\s*:\\s*(.+)$`, 'm'));
|
|
104
|
+
return m ? m[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const labelsRaw = scalar('labels');
|
|
108
|
+
const labels = labelsRaw
|
|
109
|
+
? labelsRaw.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
110
|
+
: [];
|
|
111
|
+
|
|
112
|
+
// Only the bullets under "## Acceptance criteria", stopping at the next heading so "## Blocked by"
|
|
113
|
+
// bullets never leak in.
|
|
114
|
+
const acceptanceCriteria = [];
|
|
115
|
+
let inAC = false;
|
|
116
|
+
for (const line of text.split('\n')) {
|
|
117
|
+
if (/^##\s+/.test(line)) {
|
|
118
|
+
inAC = /^##\s+acceptance criteria/i.test(line);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (inAC) {
|
|
122
|
+
const m = line.match(/^\s*-\s*\[[ xX]\]\s*(.+)$/);
|
|
123
|
+
if (m) acceptanceCriteria.push(m[1].trim());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const whatMatch = text.match(/##\s+What to build\s*\n+([\s\S]*?)(?:\n##\s|\n*$)/i);
|
|
128
|
+
const whatToBuild = whatMatch ? whatMatch[1].trim() : '';
|
|
129
|
+
|
|
130
|
+
return { id: scalar('id'), title: scalar('title'), labels, whatToBuild, acceptanceCriteria };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The procedure for a non-builder executor: read the skill (or follow instructions), then produce the artifact. */
|
|
134
|
+
function deriveProcedure(def) {
|
|
135
|
+
const head = def.skill
|
|
136
|
+
? [`Read ${def.skill} FIRST, then follow it — it IS your loop (never paraphrase it).`]
|
|
137
|
+
: def.instructions.slice();
|
|
138
|
+
return [...head, `Produce your artifact: ${def.artifact}.`, 'Return the structured report described by reportSchema.'];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Boundary constraints for an executor — type-aware on the push gate. */
|
|
142
|
+
function constraintsFor(def) {
|
|
143
|
+
if (def.canPush) {
|
|
144
|
+
return [
|
|
145
|
+
'You are the ONLY executor authorized to push (the push gate passes for devops alone).',
|
|
146
|
+
'Push only AFTER verifying the review marker (review-<id>.md) + a green suite.',
|
|
147
|
+
'Do NOT edit managed files without the managed-confirm token.',
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
return [
|
|
151
|
+
'Do NOT run git push — only the devops executor may (boundary gate; integration is devops-only).',
|
|
152
|
+
'Do NOT edit managed files without the managed-confirm token.',
|
|
153
|
+
'A review marker (review-<id>.md) is required downstream before this work is pushed.',
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the dispatch spec for an executor of `executorType` (default 'builder') from an issue. The
|
|
159
|
+
* spec is the complete, self-contained order the subagent follows: which skill to read+follow (or
|
|
160
|
+
* the explicit instructions for a global-only skill), the issue ACs, isolation, the boundary
|
|
161
|
+
* constraints, and the structured reportSchema.
|
|
162
|
+
*/
|
|
163
|
+
function buildDispatchSpec(issueText, executorType = 'builder') {
|
|
164
|
+
const def = EXECUTORS[executorType];
|
|
165
|
+
if (!def) throw new Error(`unknown executor type: ${executorType} (one of ${EXECUTOR_TYPES.join(', ')})`);
|
|
166
|
+
const issue = parseIssue(issueText);
|
|
167
|
+
return {
|
|
168
|
+
executor: executorType,
|
|
169
|
+
issue: { id: issue.id, title: issue.title },
|
|
170
|
+
skill: def.skill,
|
|
171
|
+
...(def.skill ? {} : { instructions: def.instructions.slice() }),
|
|
172
|
+
procedure: def.procedure ? def.procedure.slice() : deriveProcedure(def),
|
|
173
|
+
artifact: def.artifact,
|
|
174
|
+
acceptanceCriteria: issue.acceptanceCriteria,
|
|
175
|
+
isolation: def.isolation,
|
|
176
|
+
constraints: constraintsFor(def),
|
|
177
|
+
reportSchema: { required: [...def.required], statuses: [...STATUSES] },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate an executor's structured report against the contract + the boundary gates for its type.
|
|
183
|
+
* Returns { ok, errors }. The push gate is type-aware: a report claiming pushed=true is a boundary
|
|
184
|
+
* violation for every type EXCEPT devops. A `completed` builder report must carry full tdd evidence;
|
|
185
|
+
* a `completed` generic report must carry a non-empty artifact; a `completed` devops report must
|
|
186
|
+
* record the authorized push (pushed=true). A `blocked` report is valid without evidence.
|
|
187
|
+
*/
|
|
188
|
+
function validateReport(report, executorType = 'builder') {
|
|
189
|
+
const def = EXECUTORS[executorType];
|
|
190
|
+
if (!def) return { ok: false, errors: [`unknown executor type: ${executorType}`] };
|
|
191
|
+
|
|
192
|
+
const errors = [];
|
|
193
|
+
if (!report || typeof report !== 'object' || Array.isArray(report)) {
|
|
194
|
+
return { ok: false, errors: ['report is not an object'] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const key of def.required) {
|
|
198
|
+
if (!(key in report)) errors.push(`missing field: ${key}`);
|
|
199
|
+
}
|
|
200
|
+
if ('status' in report && !STATUSES.includes(report.status)) {
|
|
201
|
+
errors.push(`invalid status: ${report.status} (one of ${STATUSES.join(', ')})`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Boundary push gate — only devops may report a push.
|
|
205
|
+
if (report.pushed === true && !def.canPush) {
|
|
206
|
+
errors.push(`boundary violation: the ${executorType} executor must not push (pushed=true)`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Completion contract (per type) — checked only when the executor claims it finished.
|
|
210
|
+
if (report.status === 'completed') {
|
|
211
|
+
if (executorType === 'builder') {
|
|
212
|
+
if (report.redTest !== true) errors.push('completed builder report must record a red test (redTest=true)');
|
|
213
|
+
if (typeof report.greenCommit !== 'string' || !report.greenCommit.trim()) {
|
|
214
|
+
errors.push('completed builder report must record a green commit (greenCommit sha/ref)');
|
|
215
|
+
}
|
|
216
|
+
if (report.typesClean !== true) errors.push('completed builder report must record types clean (typesClean=true)');
|
|
217
|
+
} else {
|
|
218
|
+
if (typeof report.artifact !== 'string' || !report.artifact.trim()) {
|
|
219
|
+
errors.push(`completed ${executorType} report must record a non-empty artifact (${def.artifact})`);
|
|
220
|
+
}
|
|
221
|
+
if (def.canPush && report.pushed !== true) {
|
|
222
|
+
errors.push('completed devops report must record the authorized push (pushed=true) — it is the push path');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { ok: errors.length === 0, errors };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
parseIssue,
|
|
232
|
+
buildDispatchSpec,
|
|
233
|
+
validateReport,
|
|
234
|
+
BUILD_SKILL,
|
|
235
|
+
EXECUTORS,
|
|
236
|
+
EXECUTOR_TYPES,
|
|
237
|
+
STATUSES,
|
|
238
|
+
};
|
package/lib/install.cjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { loadManifest, inProfile } = require('./manifest.cjs');
|
|
7
|
+
|
|
8
|
+
const RECEIPT = 'wrxn.install.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lay the kernel payload into a target root, governed by the file-class manifest.
|
|
12
|
+
*
|
|
13
|
+
* Walking-skeleton install semantics (init only; update is a later issue):
|
|
14
|
+
* - every class is laid create-if-absent — a file already present is left untouched.
|
|
15
|
+
* - that makes init idempotent across all three classes: a second run lays nothing.
|
|
16
|
+
* - the class is recorded per file in the receipt + returned report, so the
|
|
17
|
+
* class-aware engine is observable now even though the divergent UPDATE rules
|
|
18
|
+
* (managed overwrite / seeded preserve / state never-touch) land with `wrxn update`.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {string} opts.pkgRoot absolute path to the installed kernel package (holds manifest.json + payload/)
|
|
22
|
+
* @param {string} opts.target absolute path to the install target (a project root)
|
|
23
|
+
* @param {string} [opts.profile] "project" (default) | "workspace"
|
|
24
|
+
* @returns {{ profile: string, laid: Array, skipped: Array, receipt: string }}
|
|
25
|
+
*/
|
|
26
|
+
function init(opts) {
|
|
27
|
+
const pkgRoot = opts.pkgRoot;
|
|
28
|
+
const target = opts.target;
|
|
29
|
+
const profile = opts.profile || 'project';
|
|
30
|
+
|
|
31
|
+
const manifest = loadManifest(path.join(pkgRoot, 'manifest.json'));
|
|
32
|
+
const payloadDir = path.join(pkgRoot, 'payload');
|
|
33
|
+
|
|
34
|
+
const laid = [];
|
|
35
|
+
const skipped = [];
|
|
36
|
+
|
|
37
|
+
const version = packageVersion(pkgRoot);
|
|
38
|
+
// What a PRIOR wrxn install laid here — used to tell a re-init skip (wrxn's own file) apart from a
|
|
39
|
+
// BROWNFIELD collision (a pre-existing PROJECT file that happens to clash with a payload path).
|
|
40
|
+
const priorPaths = priorReceiptPaths(target);
|
|
41
|
+
|
|
42
|
+
// Lay only the files that belong to the chosen profile: the project subset is the shared floor
|
|
43
|
+
// (laid for both), workspace files lay only for a workspace install. Brownfield-safe by construction:
|
|
44
|
+
// a clashing dest is NEVER overwritten — the existing file is preserved and (if new) reported.
|
|
45
|
+
for (const entry of manifest.files) {
|
|
46
|
+
if (!inProfile(entry.profile, profile)) continue;
|
|
47
|
+
const src = path.join(payloadDir, entry.path);
|
|
48
|
+
const dest = path.join(target, entry.path);
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(src)) {
|
|
51
|
+
throw new Error(`manifest lists "${entry.path}" but payload/${entry.path} does not exist in the package`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(dest)) {
|
|
55
|
+
// A collision = the file existed but was NOT laid by a prior wrxn install (it is the operator's
|
|
56
|
+
// own pre-existing project file). A re-init skip of wrxn's own file is `exists`, not a collision.
|
|
57
|
+
const collision = !priorPaths.has(entry.path);
|
|
58
|
+
skipped.push({ path: entry.path, class: entry.class, reason: collision ? 'collision' : 'exists', collision });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
63
|
+
fs.copyFileSync(src, dest);
|
|
64
|
+
laid.push({ path: entry.path, class: entry.class });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const collisions = skipped.filter((s) => s.collision);
|
|
68
|
+
const brownfield = collisions.length > 0;
|
|
69
|
+
|
|
70
|
+
writeReceipt(target, { version, profile, laid, skipped, brownfield });
|
|
71
|
+
|
|
72
|
+
return { profile, laid, skipped, collisions, brownfield, receipt: path.join(target, RECEIPT) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Paths a prior wrxn install recorded in the receipt (empty when there is no prior install). */
|
|
76
|
+
function priorReceiptPaths(target) {
|
|
77
|
+
try {
|
|
78
|
+
const r = JSON.parse(fs.readFileSync(path.join(target, RECEIPT), 'utf8'));
|
|
79
|
+
return new Set((r.files || []).map((f) => f.path));
|
|
80
|
+
} catch {
|
|
81
|
+
return new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Read the kernel package's release version (the semver `wrxn update` compares). */
|
|
86
|
+
function packageVersion(pkgRoot) {
|
|
87
|
+
return JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')).version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeReceipt(target, data) {
|
|
91
|
+
const receiptPath = path.join(target, RECEIPT);
|
|
92
|
+
// The receipt is generated state, not a payload file — it records what THIS install
|
|
93
|
+
// laid, so a re-run (and a future `wrxn update`) can reason about install history.
|
|
94
|
+
const existing = fs.existsSync(receiptPath)
|
|
95
|
+
? JSON.parse(fs.readFileSync(receiptPath, 'utf8'))
|
|
96
|
+
: { installs: [] };
|
|
97
|
+
existing.kernelVersion = data.version;
|
|
98
|
+
existing.profile = data.profile;
|
|
99
|
+
existing.brownfield = !!data.brownfield;
|
|
100
|
+
existing.files = [...data.laid, ...data.skipped].map((f) => ({ path: f.path, class: f.class }));
|
|
101
|
+
existing.installs.push({ laidCount: data.laid.length, skippedCount: data.skipped.length });
|
|
102
|
+
fs.writeFileSync(receiptPath, JSON.stringify(existing, null, 2) + '\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { init, RECEIPT, packageVersion };
|
package/lib/manifest.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const VALID_CLASSES = ['managed', 'seeded', 'state'];
|
|
7
|
+
const VALID_PROFILES = ['project', 'workspace'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load and validate the file-class manifest.
|
|
11
|
+
*
|
|
12
|
+
* Validation is the load-bearing contract: a manifest that lists a file with no
|
|
13
|
+
* class, an unknown class, or a duplicate path is rejected here — so the installer
|
|
14
|
+
* never lays an unclassifiable file (PRD: "update refuses files it cannot classify").
|
|
15
|
+
*
|
|
16
|
+
* @param {string} manifestPath absolute path to manifest.json
|
|
17
|
+
* @returns {{ version: string, files: Array<{path: string, class: string}> }}
|
|
18
|
+
*/
|
|
19
|
+
function loadManifest(manifestPath) {
|
|
20
|
+
const raw = fs.readFileSync(manifestPath, 'utf8');
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new Error(`manifest is not valid JSON (${manifestPath}): ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!Array.isArray(parsed.files)) {
|
|
29
|
+
throw new Error('manifest.files must be an array');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const entry of parsed.files) {
|
|
34
|
+
if (!entry || typeof entry.path !== 'string' || entry.path.length === 0) {
|
|
35
|
+
throw new Error(`manifest entry missing a path: ${JSON.stringify(entry)}`);
|
|
36
|
+
}
|
|
37
|
+
if (!VALID_CLASSES.includes(entry.class)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`manifest entry "${entry.path}" has unclassifiable class "${entry.class}" — must be one of ${VALID_CLASSES.join(', ')}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!VALID_PROFILES.includes(entry.profile)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`manifest entry "${entry.path}" has unclassifiable profile "${entry.profile}" — must be one of ${VALID_PROFILES.join(', ')}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (path.isAbsolute(entry.path) || entry.path.split(path.sep).includes('..')) {
|
|
48
|
+
throw new Error(`manifest path must be repo-relative, never absolute or escaping: "${entry.path}"`);
|
|
49
|
+
}
|
|
50
|
+
if (seen.has(entry.path)) {
|
|
51
|
+
throw new Error(`manifest lists "${entry.path}" more than once`);
|
|
52
|
+
}
|
|
53
|
+
seen.add(entry.path);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { version: String(parsed.version), profiles: parsed.profiles || VALID_PROFILES, files: parsed.files };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Should a file of `entry.profile` be laid for an install of `installProfile`?
|
|
61
|
+
* The project profile is the shared floor (laid for BOTH); workspace files lay ONLY for workspace.
|
|
62
|
+
*/
|
|
63
|
+
function inProfile(entryProfile, installProfile) {
|
|
64
|
+
return entryProfile === 'project' || entryProfile === installProfile;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { loadManifest, inProfile, VALID_CLASSES, VALID_PROFILES };
|
package/lib/migrate.cjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { RECEIPT } = require('./install.cjs');
|
|
7
|
+
const { compareVersions } = require('./semver.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Migration runner — breaking kernel changes ship as ordered scripts that run once per install
|
|
11
|
+
* (PRD US8). A migration file lives in the package `migrations/` dir and exports:
|
|
12
|
+
* module.exports = { id: '001', version: '0.2.0', up(ctx) { ... } }
|
|
13
|
+
* - id : orderable string (files run in id order; default = filename without .cjs).
|
|
14
|
+
* - version : the release the migration ships with — it runs only once the install reaches it.
|
|
15
|
+
* - up(ctx) : the migration; ctx = { target, fromVersion, toVersion }. Throw to fail (resumable).
|
|
16
|
+
*
|
|
17
|
+
* Semantics: pending = not-yet-applied AND toVersion >= migration.version, run in id order, each
|
|
18
|
+
* recorded in the receipt's migrationsApplied the instant it succeeds (so a later failure keeps the
|
|
19
|
+
* earlier successes). A throwing migration halts the run and is NOT recorded → the next `wrxn update`
|
|
20
|
+
* resumes from it. Re-running with no pending migrations is a no-op.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Load + order the package's migrations. Absent dir → []. */
|
|
24
|
+
function loadMigrations(pkgRoot) {
|
|
25
|
+
const dir = path.join(pkgRoot, 'migrations');
|
|
26
|
+
if (!fs.existsSync(dir)) return [];
|
|
27
|
+
return fs.readdirSync(dir)
|
|
28
|
+
.filter((f) => f.endsWith('.cjs'))
|
|
29
|
+
.map((f) => {
|
|
30
|
+
// eslint-disable-next-line global-require
|
|
31
|
+
const mod = require(path.join(dir, f));
|
|
32
|
+
return {
|
|
33
|
+
id: String(mod.id || f.replace(/\.cjs$/, '')),
|
|
34
|
+
version: String(mod.version || '0.0.0'),
|
|
35
|
+
up: mod.up,
|
|
36
|
+
file: f,
|
|
37
|
+
};
|
|
38
|
+
})
|
|
39
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run the pending migrations against an install. Returns the ids that ran this call.
|
|
44
|
+
* Throws (propagating to the caller) on the first failing migration — the install's receipt then
|
|
45
|
+
* records every migration that succeeded before it, and the failed one stays pending for a resume.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} pkgRoot package root holding migrations/
|
|
48
|
+
* @param {string} target install root holding the receipt
|
|
49
|
+
* @param {{fromVersion?:string, toVersion?:string}} ctx
|
|
50
|
+
* @returns {string[]} ids run this call (in order)
|
|
51
|
+
*/
|
|
52
|
+
function runMigrations(pkgRoot, target, ctx = {}) {
|
|
53
|
+
const receiptPath = path.join(target, RECEIPT);
|
|
54
|
+
const applied = new Set(readReceipt(receiptPath).migrationsApplied || []);
|
|
55
|
+
const toVersion = ctx.toVersion || '0.0.0';
|
|
56
|
+
|
|
57
|
+
const pending = loadMigrations(pkgRoot).filter(
|
|
58
|
+
(m) => !applied.has(m.id) && compareVersions(toVersion, m.version) >= 0,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const ran = [];
|
|
62
|
+
for (const m of pending) {
|
|
63
|
+
if (typeof m.up !== 'function') {
|
|
64
|
+
throw new Error(`migration "${m.id}" (${m.file}) has no up() function`);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
m.up({ target, fromVersion: ctx.fromVersion, toVersion: ctx.toVersion });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`migration "${m.id}" (${m.file}) failed: ${err.message} — resumable: fix it and re-run \`wrxn update\``,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
markApplied(receiptPath, m.id); // persist immediately so a later failure keeps this success
|
|
74
|
+
ran.push(m.id);
|
|
75
|
+
}
|
|
76
|
+
return ran;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readReceipt(p) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function markApplied(p, id) {
|
|
88
|
+
const r = readReceipt(p);
|
|
89
|
+
r.migrationsApplied = [...new Set([...(r.migrationsApplied || []), id])];
|
|
90
|
+
fs.writeFileSync(p, JSON.stringify(r, null, 2) + '\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { loadMigrations, runMigrations };
|
package/lib/onboard.cjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WRXN onboard scaffold (wrxn-kernel-20) — the DETERMINISTIC half of the onboard skill's Step 3.
|
|
4
|
+
// Reads a filled aios-intake.md and scaffolds the Day-1 operator file set under context/. CLI-First:
|
|
5
|
+
// `wrxn onboard --root <ws>` runs this. Idempotent: re-running regenerates the context/ files from the
|
|
6
|
+
// current intake. It NEVER touches the seeded operator files (decisions/log.md, connections.md) — those
|
|
7
|
+
// are the operator's own, hand-edited.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const PLACEHOLDER = '[Your answer here]';
|
|
13
|
+
|
|
14
|
+
// Parse aios-intake.md into { Q1..Q7: <answer text> }.
|
|
15
|
+
function parseIntake(text) {
|
|
16
|
+
const answers = {};
|
|
17
|
+
let curQ = null;
|
|
18
|
+
let buf = [];
|
|
19
|
+
const flush = () => {
|
|
20
|
+
if (curQ) answers[curQ] = buf.join('\n').trim();
|
|
21
|
+
buf = [];
|
|
22
|
+
};
|
|
23
|
+
for (const line of String(text || '').split('\n')) {
|
|
24
|
+
const m = line.match(/^##\s*Q(\d)\b/);
|
|
25
|
+
if (m) {
|
|
26
|
+
flush();
|
|
27
|
+
curQ = `Q${m[1]}`;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (curQ) buf.push(line);
|
|
31
|
+
}
|
|
32
|
+
flush();
|
|
33
|
+
return answers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function filled(answer) {
|
|
37
|
+
const a = (answer || '').trim();
|
|
38
|
+
return a !== '' && a !== PLACEHOLDER;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The Day-1 context files and which intake answers feed each.
|
|
42
|
+
const CONTEXT_FILES = [
|
|
43
|
+
{ name: 'about-me.md', title: 'About me', from: ['Q1'] },
|
|
44
|
+
{ name: 'about-business.md', title: 'About the business', from: ['Q4', 'Q5'] },
|
|
45
|
+
{ name: 'priorities.md', title: 'Priorities (90 days)', from: ['Q3'] },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Scaffold the Day-1 operator file set under <root>/context/ from <root>/aios-intake.md.
|
|
50
|
+
* Returns { scaffolded: string[], skipped: string[] } (paths relative to root). Throws if the
|
|
51
|
+
* intake is absent (run inside a workspace install, or pass an explicit --root).
|
|
52
|
+
*/
|
|
53
|
+
function scaffold(root) {
|
|
54
|
+
const intakePath = path.join(root, 'aios-intake.md');
|
|
55
|
+
let intakeText;
|
|
56
|
+
try {
|
|
57
|
+
intakeText = fs.readFileSync(intakePath, 'utf8');
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error('aios-intake.md not found — run inside a wrxn workspace install (wrxn init --workspace)');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const answers = parseIntake(intakeText);
|
|
63
|
+
const ctxDir = path.join(root, 'context');
|
|
64
|
+
fs.mkdirSync(ctxDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
const scaffolded = [];
|
|
67
|
+
const skipped = [];
|
|
68
|
+
for (const file of CONTEXT_FILES) {
|
|
69
|
+
const sections = file.from
|
|
70
|
+
.filter((q) => filled(answers[q]))
|
|
71
|
+
.map((q) => `## ${q}\n\n${answers[q].trim()}`);
|
|
72
|
+
if (sections.length === 0) {
|
|
73
|
+
skipped.push(path.join('context', file.name)); // no filled answer for this file yet
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const body = [`# ${file.title}`, '', '<!-- generated by `wrxn onboard` from aios-intake.md -->', '', ...sections, ''].join('\n');
|
|
77
|
+
fs.writeFileSync(path.join(ctxDir, file.name), body);
|
|
78
|
+
scaffolded.push(path.join('context', file.name));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { scaffolded, skipped };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { scaffold, parseIntake, filled, CONTEXT_FILES, PLACEHOLDER };
|
package/lib/semver.cjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Compare dotted numeric versions: <0 if a<b, 0 if equal, >0 if a>b. */
|
|
4
|
+
function compareVersions(a, b) {
|
|
5
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
6
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
7
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
8
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
9
|
+
if (d !== 0) return d;
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { compareVersions };
|