@gcunharodrigues/wrxn 0.8.0 → 0.10.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/bin/wrxn.cjs +145 -0
- package/lib/agent-conformance.cjs +92 -0
- package/lib/compass-coverage.cjs +53 -0
- package/lib/flow-status.cjs +87 -0
- package/manifest.json +55 -0
- package/package.json +1 -1
- package/payload/.claude/agents/builder.md +58 -0
- package/payload/.claude/agents/devops.md +60 -0
- package/payload/.claude/agents/qa-walker.md +57 -0
- package/payload/.claude/agents/researcher.md +55 -0
- package/payload/.claude/agents/reviewer.md +59 -0
- package/payload/.claude/agents/security.md +58 -0
- package/payload/.claude/skills/compass/SKILL.md +96 -0
- package/payload/.claude/skills/diagnose/SKILL.md +20 -3
- package/payload/.claude/skills/grill-me/SKILL.md +1 -1
- package/payload/.claude/skills/handoff/SKILL.md +1 -0
- package/payload/.claude/skills/improve-codebase-architecture/LANGUAGE.md +60 -0
- package/payload/.claude/skills/qa-walk/SKILL.md +33 -0
- package/payload/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/payload/.claude/skills/tdd/SKILL.md +2 -1
- package/payload/.claude/skills/tdd/mocking.md +59 -0
- package/payload/.claude/skills/tdd/tests.md +61 -0
- package/payload/.claude/skills/to-issues/SKILL.md +2 -0
- package/payload/.claude/skills/to-prd/SKILL.md +1 -1
- package/payload/.claude/skills/triage/OUT-OF-SCOPE.md +5 -1
- package/payload/.claude/skills/triage/SKILL.md +2 -1
- package/payload/.claude/skills/write-a-skill/GLOSSARY.md +181 -0
- package/payload/.claude/skills/write-a-skill/SKILL.md +91 -85
- package/payload/.synapse/manifest +3 -1
- package/payload/.synapse/pipeline +6 -6
package/bin/wrxn.cjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
7
8
|
|
|
8
9
|
const { init } = require('../lib/install.cjs');
|
|
9
10
|
const { update } = require('../lib/update.cjs');
|
|
@@ -15,6 +16,7 @@ const brain = require('../lib/brain.cjs');
|
|
|
15
16
|
const statusline = require('../lib/statusline.cjs');
|
|
16
17
|
const { convert } = require('../lib/convert.cjs');
|
|
17
18
|
const { ingest } = require('../lib/ingest.cjs');
|
|
19
|
+
const { flowStatus } = require('../lib/flow-status.cjs');
|
|
18
20
|
|
|
19
21
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
20
22
|
|
|
@@ -23,6 +25,12 @@ function version() {
|
|
|
23
25
|
return pkg.version;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
// Escape a dynamic fragment before embedding it in a RegExp. Slice ids derive from the prd slug and
|
|
29
|
+
// issue filenames, so an unescaped metachar would otherwise build an invalid pattern (crash / ReDoS).
|
|
30
|
+
function escapeRegExp(s) {
|
|
31
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function parseArgs(argv) {
|
|
27
35
|
const args = { _: [], flags: {} };
|
|
28
36
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -150,6 +158,14 @@ Usage:
|
|
|
150
158
|
aios-intake.md (the deterministic half of the onboard skill;
|
|
151
159
|
workspace installs only). Idempotent.
|
|
152
160
|
|
|
161
|
+
wrxn flow status <prd> [--root <dir>]
|
|
162
|
+
print the flow-status board for a PRD's issue set. Reads issues
|
|
163
|
+
from .scratch/<prd>/issues/, detects gate artifacts (green commit
|
|
164
|
+
via git log, review-<id>.md, security-<id>.md, walk-findings-<id>.md),
|
|
165
|
+
and prints per-slice gate progress (build/review/sec/qa) plus state
|
|
166
|
+
(done | in-progress | stalled | queued). No separate state store —
|
|
167
|
+
derives from the durable artifacts only.
|
|
168
|
+
|
|
153
169
|
Profiles: --project (default, the dev pipeline + intelligence + enforcement) |
|
|
154
170
|
--workspace (adds the operator layer: onboard/audit/level-up + intake + decisions log +
|
|
155
171
|
connections registry).`;
|
|
@@ -490,6 +506,135 @@ async function main(argv) {
|
|
|
490
506
|
return 0;
|
|
491
507
|
}
|
|
492
508
|
|
|
509
|
+
if (cmd === 'flow') {
|
|
510
|
+
const sub = args._[1];
|
|
511
|
+
if (sub === 'status') {
|
|
512
|
+
const prd = args._[2];
|
|
513
|
+
if (!prd) {
|
|
514
|
+
process.stderr.write('wrxn: flow status requires <prd>\n');
|
|
515
|
+
return 2;
|
|
516
|
+
}
|
|
517
|
+
const root = path.resolve(args.flags.root || process.cwd());
|
|
518
|
+
// S2: prd flows into a filesystem path — keep it a plain dir name contained within
|
|
519
|
+
// <root>/.scratch/. Reject "..", absolute paths, and anything that resolves outside .scratch/.
|
|
520
|
+
const scratchRoot = path.join(root, '.scratch');
|
|
521
|
+
const issuesDir = path.join(scratchRoot, prd, 'issues');
|
|
522
|
+
if (prd.includes('..') || path.isAbsolute(prd)
|
|
523
|
+
|| !path.resolve(issuesDir).startsWith(path.resolve(scratchRoot) + path.sep)) {
|
|
524
|
+
process.stderr.write(`wrxn: invalid prd "${prd}" — must be a directory name under .scratch/\n`);
|
|
525
|
+
return 2;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Read issue files sorted by filename (numeric ordering preserved by filename prefix)
|
|
529
|
+
let issueFiles;
|
|
530
|
+
try {
|
|
531
|
+
issueFiles = fs.readdirSync(issuesDir).filter((f) => f.endsWith('.md')).sort();
|
|
532
|
+
} catch (err) {
|
|
533
|
+
process.stderr.write(`wrxn: cannot read issues from ${issuesDir}: ${err.message}\n`);
|
|
534
|
+
return 2;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Derive the id prefix from the PRD slug (e.g. "flow-redesign" → "flow", "myprd" → "myprd")
|
|
538
|
+
const prdPrefix = prd.split('-')[0];
|
|
539
|
+
|
|
540
|
+
// Parse each issue file: id from filename, title + blockedBy from content
|
|
541
|
+
const issues = issueFiles.map((f) => {
|
|
542
|
+
const stem = f.replace(/\.md$/, '');
|
|
543
|
+
const numMatch = stem.match(/^(\d+)/);
|
|
544
|
+
const num = numMatch ? numMatch[1] : stem;
|
|
545
|
+
const id = `${prdPrefix}-${num}`;
|
|
546
|
+
let title = stem;
|
|
547
|
+
const blockedBy = [];
|
|
548
|
+
try {
|
|
549
|
+
const text = fs.readFileSync(path.join(issuesDir, f), 'utf8');
|
|
550
|
+
// First heading: # N — Title (em-dash or hyphen)
|
|
551
|
+
const titleMatch = text.match(/^#\s+\d+\s*[—\-]\s*(.+)/m);
|
|
552
|
+
if (titleMatch) title = titleMatch[1].trim();
|
|
553
|
+
// ## Blocked by section — bullet items
|
|
554
|
+
let inBlocked = false;
|
|
555
|
+
for (const line of text.split('\n')) {
|
|
556
|
+
if (/^##\s+/.test(line)) { inBlocked = /^##\s+blocked by/i.test(line); continue; }
|
|
557
|
+
if (inBlocked) {
|
|
558
|
+
const m = line.match(/^\s*-\s+(.+)$/);
|
|
559
|
+
if (m) {
|
|
560
|
+
const item = m[1].trim();
|
|
561
|
+
// B1: the "None — can start immediately." sentinel is not a real dependency.
|
|
562
|
+
if (/^none\b/i.test(item)) continue;
|
|
563
|
+
// B2: normalize a "02 (desc)." bullet to the canonical id "<prefix>-02" so it can be
|
|
564
|
+
// resolved against the done set. A bullet that is already an id is kept as-is.
|
|
565
|
+
const numM = item.match(/^(\d+)/);
|
|
566
|
+
blockedBy.push(numM ? `${prdPrefix}-${numM[1]}` : item);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} catch { /* id still usable without content */ }
|
|
571
|
+
return { id, title, blockedBy };
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// git log for greenCommit detection — fail-open (not a git repo is fine)
|
|
575
|
+
let gitLog = '';
|
|
576
|
+
try {
|
|
577
|
+
gitLog = execFileSync('git', ['-C', root, 'log', '--all', '--oneline'], {
|
|
578
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
579
|
+
});
|
|
580
|
+
} catch { /* not a repo, or git unavailable — all build gates stay pending */ }
|
|
581
|
+
|
|
582
|
+
// Look for an artifact file in the common locations an executor would write it
|
|
583
|
+
function findArtifactFile(name) {
|
|
584
|
+
const candidates = [
|
|
585
|
+
path.join(root, name),
|
|
586
|
+
path.join(root, '.scratch', prd, name),
|
|
587
|
+
path.join(root, '.claude', 'ai', 'output', name),
|
|
588
|
+
];
|
|
589
|
+
for (const c of candidates) {
|
|
590
|
+
try { if (fs.existsSync(c)) return c; } catch { /* skip */ }
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Build artifact map for flowStatus
|
|
596
|
+
const artifacts = {};
|
|
597
|
+
for (const issue of issues) {
|
|
598
|
+
const { id } = issue;
|
|
599
|
+
const entry = {};
|
|
600
|
+
// greenCommit: commit message containing [id] or the bare id word.
|
|
601
|
+
// S1: escape the id (+ guard construction) so a metachar id can never build an invalid RegExp.
|
|
602
|
+
const eid = escapeRegExp(id);
|
|
603
|
+
let re;
|
|
604
|
+
try { re = new RegExp(`\\[${eid}\\]|\\b${eid}\\b`); } catch { re = null; }
|
|
605
|
+
const commitLine = re ? gitLog.split('\n').find((l) => re.test(l)) : null;
|
|
606
|
+
if (commitLine) entry.greenCommit = commitLine.trim().split(/\s+/)[0];
|
|
607
|
+
// review marker
|
|
608
|
+
const reviewFile = findArtifactFile(`review-${id}.md`);
|
|
609
|
+
if (reviewFile) entry.reviewMarker = reviewFile;
|
|
610
|
+
// security report
|
|
611
|
+
const secFile = findArtifactFile(`security-${id}.md`);
|
|
612
|
+
if (secFile) entry.securityReport = secFile;
|
|
613
|
+
// walk findings (two common naming conventions)
|
|
614
|
+
const walkFile = findArtifactFile(`walk-findings-${id}.md`) || findArtifactFile(`walk-${id}.md`);
|
|
615
|
+
if (walkFile) entry.walkFindings = walkFile;
|
|
616
|
+
artifacts[id] = entry;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const board = flowStatus(issues, artifacts);
|
|
620
|
+
|
|
621
|
+
// Print readable board: id build✓ review· sec· qa· state
|
|
622
|
+
for (const slice of board) {
|
|
623
|
+
const g = slice.gates;
|
|
624
|
+
const cols = [
|
|
625
|
+
`build${g.build === 'done' ? '✓' : '·'}`,
|
|
626
|
+
`review${g.review === 'done' ? '✓' : '·'}`,
|
|
627
|
+
`sec${g.security === 'done' ? '✓' : '·'}`,
|
|
628
|
+
`qa${g.qa === 'done' ? '✓' : '·'}`,
|
|
629
|
+
];
|
|
630
|
+
process.stdout.write(`${slice.id.padEnd(16)} ${cols.join(' ')} ${slice.state}\n`);
|
|
631
|
+
}
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
process.stderr.write(`wrxn: unknown flow subcommand "${sub || ''}"\n\n${USAGE}\n`);
|
|
635
|
+
return 2;
|
|
636
|
+
}
|
|
637
|
+
|
|
493
638
|
process.stderr.write(`wrxn: unknown command "${cmd}"\n\n${USAGE}\n`);
|
|
494
639
|
return 2;
|
|
495
640
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Agent-contract conformance (wrxn-kernel flow-redesign flow-02).
|
|
4
|
+
// A pure transform mirroring lib/executor.cjs: it confirms a native executor subagent definition
|
|
5
|
+
// (.claude/agents/<type>.md) is a faithful THIN WRAPPER of the dispatch contract for its type — it
|
|
6
|
+
// declares least-privilege tools, a model, and an output contract that EQUALS that type's
|
|
7
|
+
// reportSchema (EXECUTORS[type].required). The agent file carries no logic the harness doesn't
|
|
8
|
+
// already define, so validateReport's guarantees still hold by construction.
|
|
9
|
+
//
|
|
10
|
+
// An agent .md declares its output contract in a fenced ```output-contract block in the body — one
|
|
11
|
+
// required report field per line. validateAgentFile accepts EITHER the raw markdown (it parses the
|
|
12
|
+
// frontmatter + that block) OR a pre-parsed { tools, model, outputContract } object, so it is
|
|
13
|
+
// unit-testable without a live LLM.
|
|
14
|
+
|
|
15
|
+
const { EXECUTORS } = require('./executor.cjs');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse an agent .md into { name, tools, model, outputContract }.
|
|
19
|
+
* - frontmatter `tools:` → comma-separated least-privilege allowlist
|
|
20
|
+
* - frontmatter `model:` → scalar
|
|
21
|
+
* - the fenced ```output-contract block → one report field per line
|
|
22
|
+
* Tolerant: a missing piece yields an empty value rather than throwing.
|
|
23
|
+
*/
|
|
24
|
+
function parseAgentFile(markdown) {
|
|
25
|
+
const text = String(markdown || '');
|
|
26
|
+
const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
|
|
27
|
+
const fm = fmEnd !== -1 ? text.slice(3, fmEnd) : '';
|
|
28
|
+
|
|
29
|
+
const scalar = (key) => {
|
|
30
|
+
const m = fm.match(new RegExp(`^${key}\\s*:\\s*(.+)$`, 'm'));
|
|
31
|
+
return m ? m[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const toolsRaw = scalar('tools');
|
|
35
|
+
const tools = toolsRaw
|
|
36
|
+
? toolsRaw.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
// The fenced ```output-contract block (info-string `output-contract`), one field per line; a
|
|
40
|
+
// leading bullet marker is tolerated so the block can read as a list.
|
|
41
|
+
const block = text.match(/```output-contract[^\n]*\n([\s\S]*?)```/);
|
|
42
|
+
const outputContract = block
|
|
43
|
+
? block[1].split('\n').map((l) => l.replace(/^\s*[-*]\s*/, '').trim()).filter(Boolean)
|
|
44
|
+
: [];
|
|
45
|
+
|
|
46
|
+
return { name: scalar('name'), tools, model: scalar('model'), outputContract };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Order-independent set-equality over two string arrays (duplicate-safe). */
|
|
50
|
+
function sameSet(a, b) {
|
|
51
|
+
const sa = new Set(a);
|
|
52
|
+
const sb = new Set(b);
|
|
53
|
+
if (sa.size !== sb.size) return false;
|
|
54
|
+
for (const x of sa) if (!sb.has(x)) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate that an executor agent definition conforms to EXECUTORS[type]:
|
|
60
|
+
* - declares a non-empty `tools` allowlist (least-privilege; presence, not a frozen list —
|
|
61
|
+
* new MCP tools are valid, per the write-an-agent doctrine),
|
|
62
|
+
* - declares a `model`,
|
|
63
|
+
* - its declared output contract EQUALS that type's reportSchema (EXECUTORS[type].required),
|
|
64
|
+
* - the type is known.
|
|
65
|
+
* `agentDef` may be the raw agent markdown (string) or a pre-parsed
|
|
66
|
+
* { tools, model, outputContract } object. Returns { ok, errors } (mirrors validateReport).
|
|
67
|
+
*/
|
|
68
|
+
function validateAgentFile(agentDef, type) {
|
|
69
|
+
const def = EXECUTORS[type];
|
|
70
|
+
if (!def) return { ok: false, errors: [`unknown executor type: ${type}`] };
|
|
71
|
+
|
|
72
|
+
const parsed = typeof agentDef === 'string' ? parseAgentFile(agentDef) : (agentDef || {});
|
|
73
|
+
const errors = [];
|
|
74
|
+
|
|
75
|
+
const tools = Array.isArray(parsed.tools) ? parsed.tools : [];
|
|
76
|
+
if (tools.length === 0) errors.push('agent declares no tools (a least-privilege allowlist is required)');
|
|
77
|
+
|
|
78
|
+
if (!parsed.model || typeof parsed.model !== 'string' || !parsed.model.trim()) {
|
|
79
|
+
errors.push('agent declares no model');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const declared = Array.isArray(parsed.outputContract) ? parsed.outputContract : [];
|
|
83
|
+
if (!sameSet(declared, def.required)) {
|
|
84
|
+
errors.push(
|
|
85
|
+
`output contract ${JSON.stringify(declared)} does not equal the ${type} reportSchema ${JSON.stringify(def.required)}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ok: errors.length === 0, errors };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { validateAgentFile, parseAgentFile };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// compass coverage guard (wrxn-kernel flow-04).
|
|
4
|
+
// compass/SKILL.md carries a static ```buckets``` block routing every installed skill to a flow bucket
|
|
5
|
+
// (dev-pipeline / knowledge / setup-health / meta / cross-session). The runtime live-read in the skill
|
|
6
|
+
// body is the resilience layer; THIS is the drift-guard on the static map — an installed skill missing
|
|
7
|
+
// from every bucket is an orphan, i.e. the map fell behind a newly-added skill.
|
|
8
|
+
//
|
|
9
|
+
// Pure data transforms (no I/O), mirroring lib/executor.cjs: parseBuckets is the tolerant parser, the
|
|
10
|
+
// test reads the real SKILL.md + skills dir around them.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse the fenced ```buckets``` block out of compass/SKILL.md into { bucket: [skill, …] }.
|
|
14
|
+
* Each line is `bucket: a, b, c`. Tolerant: a missing block, blank lines, comment (#) lines, or
|
|
15
|
+
* lines without a colon are skipped rather than thrown on.
|
|
16
|
+
*/
|
|
17
|
+
function parseBuckets(skillMd) {
|
|
18
|
+
const text = String(skillMd || '');
|
|
19
|
+
const m = text.match(/```buckets\s*\n([\s\S]*?)```/);
|
|
20
|
+
if (!m) return {};
|
|
21
|
+
|
|
22
|
+
const buckets = {};
|
|
23
|
+
for (const line of m[1].split('\n')) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
26
|
+
const idx = trimmed.indexOf(':');
|
|
27
|
+
if (idx === -1) continue;
|
|
28
|
+
const bucket = trimmed.slice(0, idx).trim();
|
|
29
|
+
const skills = trimmed
|
|
30
|
+
.slice(idx + 1)
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((s) => s.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
if (bucket) buckets[bucket] = skills;
|
|
35
|
+
}
|
|
36
|
+
return buckets;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The coverage check: every installed skill must appear in some bucket. Returns { ok, orphans },
|
|
41
|
+
* where orphans is the list of installed skills absent from every bucket (the static map drifted
|
|
42
|
+
* behind a newly-added skill). Pure: the caller supplies the installed skill list and parsed buckets.
|
|
43
|
+
*/
|
|
44
|
+
function compassCoverage(installedSkills, buckets) {
|
|
45
|
+
const routed = new Set();
|
|
46
|
+
for (const skills of Object.values(buckets || {})) {
|
|
47
|
+
for (const s of skills || []) routed.add(s);
|
|
48
|
+
}
|
|
49
|
+
const orphans = (installedSkills || []).filter((s) => !routed.has(s));
|
|
50
|
+
return { ok: orphans.length === 0, orphans };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { parseBuckets, compassCoverage };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pure flow-status aggregator (wrxn-kernel flow-05).
|
|
4
|
+
// flowStatus(issues, artifacts) reconstructs each slice's gate progress from durable artifacts —
|
|
5
|
+
// no separate mutable state, no I/O, no time-of-day logic. The CLI (bin/wrxn.cjs flow status)
|
|
6
|
+
// wraps this with the actual file reads and git log detection.
|
|
7
|
+
|
|
8
|
+
// The four pipeline gates in order: build (green commit) → review (marker) → security → qa (walk).
|
|
9
|
+
const GATES = ['build', 'review', 'security', 'qa'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map one issue's artifact entry to gate booleans.
|
|
13
|
+
* A gate is done only when its artifact field is a non-empty truthy string — never a false pass.
|
|
14
|
+
*/
|
|
15
|
+
function gatesFor(artifact) {
|
|
16
|
+
const a = artifact || {};
|
|
17
|
+
return {
|
|
18
|
+
build: !!(a.greenCommit && typeof a.greenCommit === 'string' && a.greenCommit.trim()),
|
|
19
|
+
review: !!(a.reviewMarker && typeof a.reviewMarker === 'string' && a.reviewMarker.trim()),
|
|
20
|
+
security: !!(a.securityReport && typeof a.securityReport === 'string' && a.securityReport.trim()),
|
|
21
|
+
qa: !!(a.walkFindings && typeof a.walkFindings === 'string' && a.walkFindings.trim()),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Derive one slice's overall state from its gate booleans and whether it still has an
|
|
27
|
+
* UNRESOLVED blocker (a listed dependency whose own slice is not yet fully done).
|
|
28
|
+
*
|
|
29
|
+
* done — all four gates done (wins over any listed blocker: completion is never masked)
|
|
30
|
+
* queued — an unresolved blocker, or no gates done at all
|
|
31
|
+
* stalled — build done but review not done (stuck at the first critical handoff, never reviewed)
|
|
32
|
+
* in-progress — any other partial completion (review done, or build+review but security/qa pending)
|
|
33
|
+
*/
|
|
34
|
+
function sliceState(gates, hasUnresolvedBlocker) {
|
|
35
|
+
const { build, review, security, qa } = gates;
|
|
36
|
+
if (build && review && security && qa) return 'done';
|
|
37
|
+
if (hasUnresolvedBlocker) return 'queued';
|
|
38
|
+
if (!build && !review && !security && !qa) return 'queued';
|
|
39
|
+
if (build && !review) return 'stalled';
|
|
40
|
+
return 'in-progress';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* flowStatus(issues, artifacts) → per-issue board array.
|
|
45
|
+
*
|
|
46
|
+
* issues — Array<{ id: string, title?: string, blockedBy?: string[] }>
|
|
47
|
+
* artifacts — { [id: string]: { greenCommit?, reviewMarker?, securityReport?, walkFindings? } }
|
|
48
|
+
*
|
|
49
|
+
* Returns Array<{ id, title, gates: { build, review, security, qa }, state }> where each gate
|
|
50
|
+
* value is 'done'|'pending' and state is 'done'|'in-progress'|'queued'|'stalled'.
|
|
51
|
+
*/
|
|
52
|
+
function flowStatus(issues, artifacts) {
|
|
53
|
+
const arts = artifacts || {};
|
|
54
|
+
const list = issues || [];
|
|
55
|
+
|
|
56
|
+
// First pass: each slice's gate booleans + the set of fully-done slice ids (all four gates).
|
|
57
|
+
const gatesById = new Map();
|
|
58
|
+
const doneIds = new Set();
|
|
59
|
+
for (const issue of list) {
|
|
60
|
+
const g = gatesFor(arts[issue.id]);
|
|
61
|
+
gatesById.set(issue.id, g);
|
|
62
|
+
if (g.build && g.review && g.security && g.qa) doneIds.add(issue.id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Second pass: resolve each slice's state. A blocker is RESOLVED once its own slice is fully done;
|
|
66
|
+
// a slice is queued only when it still lists an UNRESOLVED blocker (or has no gate progress).
|
|
67
|
+
return list.map((issue) => {
|
|
68
|
+
const id = issue.id;
|
|
69
|
+
const g = gatesById.get(id);
|
|
70
|
+
const blockedBy = Array.isArray(issue.blockedBy) ? issue.blockedBy : [];
|
|
71
|
+
const hasUnresolvedBlocker = blockedBy.some((b) => !doneIds.has(b));
|
|
72
|
+
const state = sliceState(g, hasUnresolvedBlocker);
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
title: issue.title || '',
|
|
76
|
+
gates: {
|
|
77
|
+
build: g.build ? 'done' : 'pending',
|
|
78
|
+
review: g.review ? 'done' : 'pending',
|
|
79
|
+
security: g.security ? 'done' : 'pending',
|
|
80
|
+
qa: g.qa ? 'done' : 'pending',
|
|
81
|
+
},
|
|
82
|
+
state,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { flowStatus, GATES };
|
package/manifest.json
CHANGED
|
@@ -8,6 +8,36 @@
|
|
|
8
8
|
"state"
|
|
9
9
|
],
|
|
10
10
|
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": ".claude/agents/builder.md",
|
|
13
|
+
"class": "managed",
|
|
14
|
+
"profile": "project"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": ".claude/agents/devops.md",
|
|
18
|
+
"class": "managed",
|
|
19
|
+
"profile": "project"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": ".claude/agents/qa-walker.md",
|
|
23
|
+
"class": "managed",
|
|
24
|
+
"profile": "project"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": ".claude/agents/researcher.md",
|
|
28
|
+
"class": "managed",
|
|
29
|
+
"profile": "project"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": ".claude/agents/reviewer.md",
|
|
33
|
+
"class": "managed",
|
|
34
|
+
"profile": "project"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"path": ".claude/agents/security.md",
|
|
38
|
+
"class": "managed",
|
|
39
|
+
"profile": "project"
|
|
40
|
+
},
|
|
11
41
|
{
|
|
12
42
|
"path": ".claude/constitution.local.md",
|
|
13
43
|
"class": "seeded",
|
|
@@ -83,6 +113,11 @@
|
|
|
83
113
|
"class": "managed",
|
|
84
114
|
"profile": "project"
|
|
85
115
|
},
|
|
116
|
+
{
|
|
117
|
+
"path": ".claude/skills/compass/SKILL.md",
|
|
118
|
+
"class": "managed",
|
|
119
|
+
"profile": "project"
|
|
120
|
+
},
|
|
86
121
|
{
|
|
87
122
|
"path": ".claude/skills/diagnose/SKILL.md",
|
|
88
123
|
"class": "managed",
|
|
@@ -298,11 +333,26 @@
|
|
|
298
333
|
"class": "managed",
|
|
299
334
|
"profile": "project"
|
|
300
335
|
},
|
|
336
|
+
{
|
|
337
|
+
"path": ".claude/skills/resolving-merge-conflicts/SKILL.md",
|
|
338
|
+
"class": "managed",
|
|
339
|
+
"profile": "project"
|
|
340
|
+
},
|
|
301
341
|
{
|
|
302
342
|
"path": ".claude/skills/tdd/SKILL.md",
|
|
303
343
|
"class": "managed",
|
|
304
344
|
"profile": "project"
|
|
305
345
|
},
|
|
346
|
+
{
|
|
347
|
+
"path": ".claude/skills/tdd/tests.md",
|
|
348
|
+
"class": "managed",
|
|
349
|
+
"profile": "project"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"path": ".claude/skills/tdd/mocking.md",
|
|
353
|
+
"class": "managed",
|
|
354
|
+
"profile": "project"
|
|
355
|
+
},
|
|
306
356
|
{
|
|
307
357
|
"path": ".claude/skills/tech-search/SKILL.md",
|
|
308
358
|
"class": "managed",
|
|
@@ -343,6 +393,11 @@
|
|
|
343
393
|
"class": "managed",
|
|
344
394
|
"profile": "project"
|
|
345
395
|
},
|
|
396
|
+
{
|
|
397
|
+
"path": ".claude/skills/write-a-skill/GLOSSARY.md",
|
|
398
|
+
"class": "managed",
|
|
399
|
+
"profile": "project"
|
|
400
|
+
},
|
|
346
401
|
{
|
|
347
402
|
"path": ".claude/skills/write-an-agent/SKILL.md",
|
|
348
403
|
"class": "managed",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gcunharodrigues/wrxn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "WRXN Kernel — installable AI operating system. Two profiles (project | workspace), pull-based updates, managed/seeded/state file classes.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"wrxn": "bin/wrxn.cjs"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: builder
|
|
3
|
+
description: >
|
|
4
|
+
AFK build executor for ONE ready-for-agent slice. Reads+follows the tdd skill, builds
|
|
5
|
+
test-first (red → green → refactor), honors the boundary gates (never pushes), and returns
|
|
6
|
+
the builder report the dispatch harness validates. Use PROACTIVELY to run the build step of a
|
|
7
|
+
slice unattended — "dispatch the builder", "build this slice tdd-first", the AFK builder phase.
|
|
8
|
+
tools: Read, Edit, Write, Bash, Grep, Glob, mcp__recon-wrxn__recon_impact, mcp__recon-wrxn__recon_find, mcp__recon-wrxn__recon_explain
|
|
9
|
+
model: opus
|
|
10
|
+
---
|
|
11
|
+
You are the **builder** executor. Your one job: build the single slice named in your spec,
|
|
12
|
+
test-first, and return a validated report. You are a thin wrapper over the dispatch contract —
|
|
13
|
+
you add no behavior the harness does not already define.
|
|
14
|
+
|
|
15
|
+
## Process
|
|
16
|
+
1. **Read `.claude/skills/tdd/SKILL.md` FIRST, then follow it** — it IS your build loop. Never
|
|
17
|
+
paraphrase or skip it.
|
|
18
|
+
2. Build the slice test-first: write ONE failing (red) test at the highest seam, watch it fail for
|
|
19
|
+
the right reason, then write the MINIMUM code to pass (green). Refactor with the suite green. One
|
|
20
|
+
test → one impl → repeat; no horizontal slices.
|
|
21
|
+
3. Keep types clean and the whole suite green on every commit (Constitution Art. III). Use
|
|
22
|
+
`recon_impact` before touching an exported symbol to gauge blast radius.
|
|
23
|
+
4. Commit locally with a conventional message referencing the issue id. **Do not push.**
|
|
24
|
+
5. Return the structured report below. If blocked (ambiguous AC, missing dependency), stop and
|
|
25
|
+
return a `blocked` report instead of guessing.
|
|
26
|
+
|
|
27
|
+
## Constraints (hard)
|
|
28
|
+
- **Never `git push`** — only the devops executor may. Integration happens downstream.
|
|
29
|
+
- Do **not** edit managed (kernel-owned) files without the managed-confirm token.
|
|
30
|
+
- A review marker (`review-<id>.md`) is required downstream before this work is pushed — do not
|
|
31
|
+
fabricate it.
|
|
32
|
+
- Stay inside the one slice. No scope creep, no speculative features.
|
|
33
|
+
|
|
34
|
+
## Output contract
|
|
35
|
+
Your final message IS the return value — return the report object, not a conversational reply.
|
|
36
|
+
Lead with it, drop prose. Your declared output contract equals the builder reportSchema (the fields
|
|
37
|
+
`validateReport` requires for type `builder`):
|
|
38
|
+
|
|
39
|
+
```output-contract
|
|
40
|
+
issueId
|
|
41
|
+
status
|
|
42
|
+
redTest
|
|
43
|
+
greenCommit
|
|
44
|
+
typesClean
|
|
45
|
+
pushed
|
|
46
|
+
summary
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`status` ∈ `completed | blocked`; `pushed` is always `false` (you never push). Example:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "issueId": "flow-02", "status": "completed", "redTest": true, "greenCommit": "abc1234",
|
|
53
|
+
"typesClean": true, "pushed": false, "summary": "built the validator tdd-first; suite green" }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Stateless
|
|
57
|
+
You get only your spawn prompt + this file — no main-thread memory, no inherited persona. Everything
|
|
58
|
+
you need is on the page or in the files your spec names. Keep yourself self-sufficient.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: devops
|
|
3
|
+
description: >
|
|
4
|
+
AFK integration executor — the ONLY agent authorized to push. Integrates a reviewed +
|
|
5
|
+
security-passed + qa-walked track to the trunk: verifies the review marker + a green suite, then
|
|
6
|
+
passes the push gate via the WRXN_ACTIVE_AGENT dance and pushes. Runs ATTENDED. Use to run the
|
|
7
|
+
integration/push step — "dispatch devops", "integrate this track", "push the reviewed slice".
|
|
8
|
+
tools: Read, Edit, Write, Bash
|
|
9
|
+
model: sonnet
|
|
10
|
+
---
|
|
11
|
+
You are the **devops** executor — the single integration/push path. Your one job: integrate the
|
|
12
|
+
named, already-reviewed track to the trunk and push it. You are a thin wrapper over the dispatch
|
|
13
|
+
contract — you add no behavior the harness does not already define. You run **attended**.
|
|
14
|
+
|
|
15
|
+
## Process
|
|
16
|
+
You are the ONLY executor authorized to push (`/code-review` and `/security-review` are global
|
|
17
|
+
slash-skills with no local file; the push is yours). Follow these instructions directly (they ARE
|
|
18
|
+
`EXECUTORS.devops.instructions`):
|
|
19
|
+
1. **Verify the gate inputs exist FIRST:** the review marker `review-<id>.md` AND a green suite.
|
|
20
|
+
If either is missing, do not push — return a `blocked` report.
|
|
21
|
+
2. Authorize the push by setting `WRXN_ACTIVE_AGENT` to `devops` under the **`env`** key of
|
|
22
|
+
`.claude/settings.local.json` (an inline, command-scoped `WRXN_ACTIVE_AGENT=devops git push`
|
|
23
|
+
never reaches the gate hook — the flag must be in settings to be read).
|
|
24
|
+
3. `git push`.
|
|
25
|
+
4. **REMOVE `WRXN_ACTIVE_AGENT` from `.claude/settings.local.json`** — a persistent flag defeats
|
|
26
|
+
the anti-accidental-push gate. This cleanup is mandatory, even if the push failed. This
|
|
27
|
+
set → push → unset is the single path through the push gate.
|
|
28
|
+
5. Return the structured report below. A `completed` devops report MUST record `pushed: true`.
|
|
29
|
+
|
|
30
|
+
## Constraints (hard)
|
|
31
|
+
- The set→push→unset dance is the ONLY sanctioned push path — never leave `WRXN_ACTIVE_AGENT`
|
|
32
|
+
persisted, and never push without first verifying the review marker + green suite.
|
|
33
|
+
- Do **not** edit managed (kernel-owned) files without the managed-confirm token (the
|
|
34
|
+
`settings.local.json` env toggle above is the sanctioned exception).
|
|
35
|
+
- Stay inside the one track. Integrate what was reviewed; do not amend the work itself.
|
|
36
|
+
|
|
37
|
+
## Output contract
|
|
38
|
+
Your final message IS the return value — return the report object, not a conversational reply.
|
|
39
|
+
Lead with it, drop prose. Your declared output contract equals the devops reportSchema (the fields
|
|
40
|
+
`validateReport` requires for type `devops`):
|
|
41
|
+
|
|
42
|
+
```output-contract
|
|
43
|
+
issueId
|
|
44
|
+
status
|
|
45
|
+
artifact
|
|
46
|
+
pushed
|
|
47
|
+
summary
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`status` ∈ `completed | blocked`; `artifact` records the authorized push; `pushed` is `true` on a
|
|
51
|
+
completed integration (you are the one executor that may push). Example:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{ "issueId": "flow-03", "status": "completed", "artifact": "authorized-push",
|
|
55
|
+
"pushed": true, "summary": "verified marker + green suite; set/push/unset gate; pushed to trunk" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Stateless
|
|
59
|
+
You get only your spawn prompt + this file — no main-thread memory, no inherited persona. Everything
|
|
60
|
+
you need is on the page or in the files your spec names. Keep yourself self-sufficient.
|