@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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/bin/wrxn.cjs +342 -0
  4. package/lib/connect.cjs +216 -0
  5. package/lib/executor.cjs +238 -0
  6. package/lib/install.cjs +105 -0
  7. package/lib/manifest.cjs +67 -0
  8. package/lib/migrate.cjs +93 -0
  9. package/lib/onboard.cjs +84 -0
  10. package/lib/semver.cjs +14 -0
  11. package/lib/update.cjs +91 -0
  12. package/lib/worktree.cjs +217 -0
  13. package/manifest.json +451 -0
  14. package/migrations/README.md +21 -0
  15. package/package.json +23 -0
  16. package/payload/.claude/constitution.local.md +13 -0
  17. package/payload/.claude/constitution.md +28 -0
  18. package/payload/.claude/hooks/code-intel-push.cjs +108 -0
  19. package/payload/.claude/hooks/enforce-managed-guard.cjs +68 -0
  20. package/payload/.claude/hooks/enforce-managed-precommit.cjs +74 -0
  21. package/payload/.claude/hooks/enforce-push-authority.cjs +51 -0
  22. package/payload/.claude/hooks/enforce-review-marker.cjs +62 -0
  23. package/payload/.claude/hooks/enforce-tests-on-push.cjs +40 -0
  24. package/payload/.claude/hooks/recall-surface.cjs +127 -0
  25. package/payload/.claude/hooks/reference-detect.cjs +83 -0
  26. package/payload/.claude/hooks/session-end.cjs +132 -0
  27. package/payload/.claude/hooks/session-history.cjs +76 -0
  28. package/payload/.claude/hooks/session-start.cjs +117 -0
  29. package/payload/.claude/hooks/synapse-engine.cjs +351 -0
  30. package/payload/.claude/hooks/wiki-lint.cjs +104 -0
  31. package/payload/.claude/settings.json +60 -0
  32. package/payload/.claude/skills/audit/SKILL.md +23 -0
  33. package/payload/.claude/skills/diagnose/SKILL.md +117 -0
  34. package/payload/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  35. package/payload/.claude/skills/grill-me/SKILL.md +10 -0
  36. package/payload/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  37. package/payload/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  38. package/payload/.claude/skills/grill-with-docs/SKILL.md +88 -0
  39. package/payload/.claude/skills/handoff/SKILL.md +19 -0
  40. package/payload/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  41. package/payload/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  42. package/payload/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  43. package/payload/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  44. package/payload/.claude/skills/improve-codebase-architecture/SKILL.md +81 -0
  45. package/payload/.claude/skills/level-up/SKILL.md +28 -0
  46. package/payload/.claude/skills/memory/SKILL.md +79 -0
  47. package/payload/.claude/skills/onboard/SKILL.md +43 -0
  48. package/payload/.claude/skills/prototype/LOGIC.md +79 -0
  49. package/payload/.claude/skills/prototype/SKILL.md +30 -0
  50. package/payload/.claude/skills/prototype/UI.md +112 -0
  51. package/payload/.claude/skills/qa-walk/SKILL.md +227 -0
  52. package/payload/.claude/skills/qa-walk/references/cli-mode.md +28 -0
  53. package/payload/.claude/skills/qa-walk/references/finding-issue-template.md +48 -0
  54. package/payload/.claude/skills/qa-walk/references/walk-report-template.md +56 -0
  55. package/payload/.claude/skills/qa-walk/references/web-mode.md +112 -0
  56. package/payload/.claude/skills/setup-matt-pocock-skills/SKILL.md +121 -0
  57. package/payload/.claude/skills/setup-matt-pocock-skills/domain.md +51 -0
  58. package/payload/.claude/skills/setup-matt-pocock-skills/issue-tracker-github.md +22 -0
  59. package/payload/.claude/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +23 -0
  60. package/payload/.claude/skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
  61. package/payload/.claude/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
  62. package/payload/.claude/skills/skill-creator/LICENSE.txt +202 -0
  63. package/payload/.claude/skills/skill-creator/SKILL.md +209 -0
  64. package/payload/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  65. package/payload/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  66. package/payload/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
  67. package/payload/.claude/skills/synapse/SKILL.md +132 -0
  68. package/payload/.claude/skills/synapse/assets/README.md +50 -0
  69. package/payload/.claude/skills/synapse/references/brackets.md +100 -0
  70. package/payload/.claude/skills/synapse/references/commands.md +118 -0
  71. package/payload/.claude/skills/synapse/references/domains.md +126 -0
  72. package/payload/.claude/skills/synapse/references/layers.md +186 -0
  73. package/payload/.claude/skills/synapse/references/manifest.md +142 -0
  74. package/payload/.claude/skills/tdd/SKILL.md +22 -0
  75. package/payload/.claude/skills/tech-search/SKILL.md +431 -0
  76. package/payload/.claude/skills/tech-search/prompts/page-extract.md +133 -0
  77. package/payload/.claude/skills/to-issues/SKILL.md +83 -0
  78. package/payload/.claude/skills/to-prd/SKILL.md +74 -0
  79. package/payload/.claude/skills/triage/AGENT-BRIEF.md +168 -0
  80. package/payload/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
  81. package/payload/.claude/skills/triage/SKILL.md +103 -0
  82. package/payload/.claude/skills/write-a-skill/SKILL.md +117 -0
  83. package/payload/.recon.json +3 -0
  84. package/payload/.synapse/global +6 -0
  85. package/payload/.synapse/manifest +38 -0
  86. package/payload/.synapse/pipeline +6 -0
  87. package/payload/.synapse/routing +8 -0
  88. package/payload/.wrxn/continuity/.gitkeep +0 -0
  89. package/payload/.wrxn/history/.gitkeep +0 -0
  90. package/payload/.wrxn/wiki/.gitkeep +0 -0
  91. package/payload/.wrxn/wiki/concepts/.gitkeep +0 -0
  92. package/payload/.wrxn/wiki/decisions/.gitkeep +0 -0
  93. package/payload/.wrxn/wiki/gotchas/.gitkeep +0 -0
  94. package/payload/.wrxn/wiki/sessions/.gitkeep +0 -0
  95. package/payload/.wrxn/wiki.cjs +164 -0
  96. package/payload/aios-intake.md +32 -0
  97. package/payload/connections.md +15 -0
  98. package/payload/decisions/log.md +18 -0
  99. package/payload/docs/agents/domain.md +38 -0
  100. package/payload/docs/agents/issue-tracker.md +25 -0
  101. package/payload/docs/agents/triage-labels.md +15 -0
  102. package/payload/docs/workspace/operator-layer.md +14 -0
package/lib/update.cjs ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { loadManifest, inProfile } = require('./manifest.cjs');
7
+ const { RECEIPT, packageVersion } = require('./install.cjs');
8
+ const { compareVersions } = require('./semver.cjs');
9
+ const { runMigrations } = require('./migrate.cjs');
10
+
11
+ /**
12
+ * Pull-based update honoring the three file classes:
13
+ * - managed → overwritten with the new package version (kernel-owned).
14
+ * - seeded → never touched if present (operator-owned); laid only if new in this version.
15
+ * - state → never touched if present (project data); created only if new in this version.
16
+ *
17
+ * Refuses a downgrade (installed version newer than the package). Pins the new version in the receipt.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string} opts.pkgRoot absolute path to the new kernel package
21
+ * @param {string} opts.target absolute path to an existing wrxn install
22
+ * @returns {{ from: string, to: string, updated: Array, preserved: Array }}
23
+ */
24
+ function update(opts) {
25
+ const { pkgRoot, target } = opts;
26
+
27
+ const receipt = readReceipt(target);
28
+ const from = receipt.kernelVersion;
29
+ const to = packageVersion(pkgRoot);
30
+ const profile = receipt.profile || 'project';
31
+
32
+ if (compareVersions(to, from) < 0) {
33
+ throw new Error(`refusing downgrade: install is ${from}, package is the older ${to}`);
34
+ }
35
+
36
+ const manifest = loadManifest(path.join(pkgRoot, 'manifest.json'));
37
+ const payloadDir = path.join(pkgRoot, 'payload');
38
+ const updated = [];
39
+ const preserved = [];
40
+
41
+ // Update only the files of the install's RECORDED profile — a project install never gains
42
+ // workspace files on update (and vice versa is moot: workspace is the superset).
43
+ const files = manifest.files.filter((entry) => inProfile(entry.profile, profile));
44
+ for (const entry of files) {
45
+ const src = path.join(payloadDir, entry.path);
46
+ const dest = path.join(target, entry.path);
47
+ if (!fs.existsSync(src)) {
48
+ throw new Error(`package payload is missing "${entry.path}"`);
49
+ }
50
+
51
+ if (entry.class === 'managed') {
52
+ lay(src, dest);
53
+ updated.push({ path: entry.path, class: entry.class });
54
+ } else if (!fs.existsSync(dest)) {
55
+ // seeded/state that did not exist in the prior version → lay it once now
56
+ lay(src, dest);
57
+ updated.push({ path: entry.path, class: entry.class, reason: 'new-in-version' });
58
+ } else {
59
+ preserved.push({ path: entry.path, class: entry.class });
60
+ }
61
+ }
62
+
63
+ receipt.kernelVersion = to;
64
+ receipt.profile = profile;
65
+ receipt.files = files.map((f) => ({ path: f.path, class: f.class }));
66
+ receipt.installs = receipt.installs || [];
67
+ receipt.installs.push({ update: { from, to, updatedCount: updated.length, preservedCount: preserved.length } });
68
+ fs.writeFileSync(path.join(target, RECEIPT), JSON.stringify(receipt, null, 2) + '\n');
69
+
70
+ // Run pending migrations AFTER the file-class update (they fix up state for the new code). A failing
71
+ // migration throws out of here — the receipt above is already written (version + applied-so-far), so
72
+ // the next `wrxn update` resumes from the failed migration. See lib/migrate.cjs.
73
+ const migrationsRan = runMigrations(pkgRoot, target, { fromVersion: from, toVersion: to });
74
+
75
+ return { from, to, updated, preserved, migrationsRan };
76
+ }
77
+
78
+ function readReceipt(target) {
79
+ const p = path.join(target, RECEIPT);
80
+ if (!fs.existsSync(p)) {
81
+ throw new Error(`not a wrxn install (no ${RECEIPT}) — run \`wrxn init\` first`);
82
+ }
83
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
84
+ }
85
+
86
+ function lay(src, dest) {
87
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
88
+ fs.copyFileSync(src, dest);
89
+ }
90
+
91
+ module.exports = { update, compareVersions };
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ /**
9
+ * Worktree engine — the ephemeral-track face of the worktree lifecycle (PRD US14).
10
+ *
11
+ * One module providing the git plumbing + safety checks an orchestrator needs to run AFK
12
+ * parallel tracks: a disjoint-file gate (refuse overlapping splits), create-track, integrate-back,
13
+ * and a safe prune that NEVER deletes unmerged work. Named durable worktrees (US15) are a later
14
+ * issue (17) that reuses this same plumbing.
15
+ *
16
+ * All git access shells out via execFileSync; a non-zero git exit becomes a thrown Error so the
17
+ * caller can surface it (and, for prune, so unmerged work is preserved rather than force-dropped).
18
+ */
19
+
20
+ // Two faces of the one lifecycle module share this engine, distinguished by branch prefix:
21
+ // track/ — ephemeral AFK tracks (default; temp path, auto-pruned at integrate).
22
+ // wt/ — named durable HITL worktrees (createNamedWorktree; persistent path, operator-pruned).
23
+ const BRANCH_PREFIX = 'track/';
24
+ const NAMED_PREFIX = 'wt/';
25
+
26
+ function git(repo, args, opts = {}) {
27
+ return execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8', ...opts }).trim();
28
+ }
29
+
30
+ function branchName(name, prefix = BRANCH_PREFIX) {
31
+ return prefix + name;
32
+ }
33
+
34
+ /**
35
+ * Refuse an overlapping track split (the File List Intersection Matrix — AC-2).
36
+ * @param {Array<{name:string, files:string[]}>} tracks
37
+ * @returns {{ ok: true, pairs: Array<[string,string]> }}
38
+ * @throws if any two tracks share a file (the shared paths are named in the error).
39
+ */
40
+ function verifyDisjoint(tracks) {
41
+ const list = Array.isArray(tracks) ? tracks : [];
42
+ const pairs = [];
43
+ for (let i = 0; i < list.length; i++) {
44
+ for (let j = i + 1; j < list.length; j++) {
45
+ const a = new Set(list[i].files || []);
46
+ const overlap = (list[j].files || []).filter((f) => a.has(f));
47
+ if (overlap.length) {
48
+ throw new Error(
49
+ `tracks "${list[i].name}" and "${list[j].name}" both touch: ${overlap.join(', ')} — ` +
50
+ `split is not disjoint (assign each shared file to ONE track or serialize)`
51
+ );
52
+ }
53
+ pairs.push([list[i].name, list[j].name]);
54
+ }
55
+ }
56
+ return { ok: true, pairs };
57
+ }
58
+
59
+ /** Default EPHEMERAL worktree location — OS temp dir (auto-pruned at integrate), keyed by repo + name. */
60
+ function defaultPath(repo, name) {
61
+ return path.join(os.tmpdir(), 'wrxn-worktrees', path.basename(path.resolve(repo)), name);
62
+ }
63
+
64
+ /** Default NAMED-DURABLE location — a persistent sibling dir of the repo, so it survives session close. */
65
+ function durablePath(repo, name) {
66
+ const resolved = path.resolve(repo);
67
+ return path.join(path.dirname(resolved), path.basename(resolved) + '.worktrees', name);
68
+ }
69
+
70
+ /**
71
+ * Create a worktree on a fresh `<prefix><name>` branch off `base` (AC-1 creation). The default
72
+ * prefix is the ephemeral `track/`; the named face passes `wt/` + a durable path.
73
+ * @param {string} repo the install/repo root
74
+ * @param {string} name the worktree name (the branch becomes <prefix><name>)
75
+ * @param {{base?:string, path?:string, prefix?:string}} [opts]
76
+ * @returns {{ name:string, branch:string, path:string, base:string }}
77
+ */
78
+ function createWorktree(repo, name, opts = {}) {
79
+ const base = opts.base || 'main';
80
+ const prefix = opts.prefix || BRANCH_PREFIX;
81
+ const wtPath = path.resolve(opts.path || defaultPath(repo, name));
82
+ const branch = branchName(name, prefix);
83
+ fs.mkdirSync(path.dirname(wtPath), { recursive: true });
84
+ git(repo, ['worktree', 'add', wtPath, '-b', branch, base]);
85
+ return { name, branch, path: wtPath, base };
86
+ }
87
+
88
+ /**
89
+ * Create a NAMED DURABLE worktree (the HITL face — PRD US15). Same engine as createWorktree, but on
90
+ * a `wt/<name>` branch at a persistent path so the operator can live in it across sessions and
91
+ * integrate/prune it on command (prune still refuses unmerged work via the shared pruneWorktree).
92
+ * @returns {{ name:string, branch:string, path:string, base:string }}
93
+ */
94
+ function createNamedWorktree(repo, name, opts = {}) {
95
+ const wtPath = path.resolve(opts.path || durablePath(repo, name));
96
+ return createWorktree(repo, name, { base: opts.base, path: wtPath, prefix: NAMED_PREFIX });
97
+ }
98
+
99
+ /** Parse `git worktree list --porcelain` into [{path, head, branch}] (branch is the short ref). */
100
+ function listWorktrees(repo) {
101
+ const out = git(repo, ['worktree', 'list', '--porcelain']);
102
+ const entries = [];
103
+ let cur = null;
104
+ for (const line of out.split('\n')) {
105
+ if (line.startsWith('worktree ')) {
106
+ cur = { path: line.slice('worktree '.length), head: null, branch: null };
107
+ entries.push(cur);
108
+ } else if (line.startsWith('HEAD ') && cur) {
109
+ cur.head = line.slice('HEAD '.length);
110
+ } else if (line.startsWith('branch ') && cur) {
111
+ cur.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
112
+ }
113
+ }
114
+ return entries;
115
+ }
116
+
117
+ /** Resolve the on-disk path of the worktree holding <prefix><name>, or null. */
118
+ function resolveWorktreePath(repo, name, prefix = BRANCH_PREFIX) {
119
+ const branch = branchName(name, prefix);
120
+ const hit = listWorktrees(repo).find((w) => w.branch === branch);
121
+ return hit ? hit.path : null;
122
+ }
123
+
124
+ /**
125
+ * Status of a worktree (the named-face listing surface — AC-1 "listed with status"):
126
+ * clean/dirty (uncommitted changes) + ahead/behind commit counts vs `base`.
127
+ * @returns {{ name, branch, path, clean, ahead, behind }}
128
+ */
129
+ function worktreeStatus(repo, name, opts = {}) {
130
+ const prefix = opts.prefix || BRANCH_PREFIX;
131
+ const base = opts.base || 'main';
132
+ const branch = branchName(name, prefix);
133
+ const wtPath = resolveWorktreePath(repo, name, prefix);
134
+ const dirty = wtPath ? git(wtPath, ['status', '--porcelain']) : '';
135
+ const ahead = Number(git(repo, ['rev-list', '--count', `${base}..${branch}`]) || 0);
136
+ const behind = Number(git(repo, ['rev-list', '--count', `${branch}..${base}`]) || 0);
137
+ return { name, branch, path: wtPath, clean: dirty === '', ahead, behind };
138
+ }
139
+
140
+ /**
141
+ * Integrate a track back into base, then auto-prune (AC-1 integrate + AC-3 prune).
142
+ * Assumes the PRIMARY worktree is on a clean `base` (the AFK orchestrator flow): a dirty/conflicting
143
+ * tree makes the checkout/merge throw (fail-closed), preserving the track.
144
+ * Merges track/<name> into base in the primary worktree, removes the track worktree, and deletes
145
+ * the branch with the SAFE `-d` (which only succeeds because the merge just made it merged). A merge
146
+ * conflict throws and leaves everything in place (nothing is pruned — the work is preserved).
147
+ * @returns {{ name, branch, base, merged:true }}
148
+ */
149
+ function integrateWorktree(repo, name, opts = {}) {
150
+ const base = opts.base || 'main';
151
+ const prefix = opts.prefix || BRANCH_PREFIX;
152
+ const branch = branchName(name, prefix);
153
+ const wtPath = resolveWorktreePath(repo, name, prefix);
154
+
155
+ // Bring base into the primary worktree and merge the track in. A conflict / non-zero exit throws.
156
+ git(repo, ['checkout', base]);
157
+ try {
158
+ git(repo, ['merge', '--no-ff', '-m', `integrate ${branch}`, branch]);
159
+ } catch (err) {
160
+ // Abort a half-done merge so the repo is left clean; the track branch + worktree survive.
161
+ try { git(repo, ['merge', '--abort']); } catch { /* nothing to abort */ }
162
+ throw new Error(`integrate ${branch} failed (merge conflict?) — track preserved: ${err.message}`);
163
+ }
164
+
165
+ if (wtPath) git(repo, ['worktree', 'remove', '--force', wtPath]);
166
+ git(repo, ['worktree', 'prune']);
167
+ git(repo, ['branch', '-d', branch]); // safe delete: branch is now merged into base
168
+ return { name, branch, base, merged: true };
169
+ }
170
+
171
+ /**
172
+ * Prune an ephemeral worktree + its branch. SAFETY (AC-4): a branch with unmerged commits is
173
+ * NEVER deleted unless `force` is set — `git branch -d` refuses the unmerged delete, and we surface
174
+ * that as a thrown Error with the worktree left intact. `force` (operator override) uses -D + --force.
175
+ */
176
+ function pruneWorktree(repo, name, opts = {}) {
177
+ const base = opts.base || 'main';
178
+ const prefix = opts.prefix || BRANCH_PREFIX;
179
+ const branch = branchName(name, prefix);
180
+ const wtPath = resolveWorktreePath(repo, name, prefix);
181
+ const force = !!opts.force;
182
+
183
+ // SAFETY PROBE FIRST (AC-4): commits on the branch but not in base = unmerged work. Decide BEFORE
184
+ // touching anything so a refusal leaves the worktree AND the branch fully intact. (`git branch -d`
185
+ // alone is the wrong gate: a still-checked-out branch fails it for being checked-out, not unmerged.)
186
+ if (!force) {
187
+ const unmerged = git(repo, ['rev-list', `${base}..${branch}`]);
188
+ if (unmerged) {
189
+ throw new Error(`refusing to prune "${branch}": it has unmerged commits (integrate it, or use force to discard)`);
190
+ }
191
+ }
192
+
193
+ // Safe (or forced): remove the worktree FIRST (frees the branch checkout), then delete the branch.
194
+ if (wtPath) {
195
+ try { git(repo, ['worktree', 'remove', '--force', wtPath]); } catch { /* already gone */ }
196
+ }
197
+ git(repo, ['worktree', 'prune']);
198
+ try {
199
+ git(repo, ['branch', force ? '-D' : '-d', branch]);
200
+ } catch (err) {
201
+ if (!force) throw new Error(`could not delete "${branch}": ${err.message}`);
202
+ }
203
+ return { name, branch, pruned: true, forced: force };
204
+ }
205
+
206
+ module.exports = {
207
+ verifyDisjoint,
208
+ createWorktree,
209
+ createNamedWorktree,
210
+ worktreeStatus,
211
+ listWorktrees,
212
+ resolveWorktreePath,
213
+ integrateWorktree,
214
+ pruneWorktree,
215
+ BRANCH_PREFIX,
216
+ NAMED_PREFIX,
217
+ };