@damiangilgonzalez/harness 1.3.12 → 1.3.14
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/README.md +36 -11
- package/manifest.json +10 -2
- package/package.json +3 -2
- package/scripts/doctor.js +122 -6
- package/scripts/install-apply.js +195 -14
- package/scripts/lib/backups.js +13 -0
- package/scripts/lib/capabilities.js +19 -2
- package/scripts/lib/divergence.js +8 -1
- package/scripts/lib/onboarding-apply.js +91 -13
- package/scripts/lib/onboarding.js +35 -2
- package/scripts/lib/repair.js +8 -4
- package/scripts/lib/review-findings.js +175 -0
- package/scripts/lib/versions.js +31 -0
- package/scripts/rsc.js +117 -19
- package/skills/animar/SKILL.md +1 -0
- package/skills/claude-project-setup/SKILL.md +1 -0
- package/skills/code-review/SKILL.md +18 -1
- package/skills/diseno-apple/SKILL.md +1 -0
- package/skills/diseno-landing/SKILL.md +1 -0
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/flujo-interfaz/SKILL.md +1 -0
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/handoff/SKILL.md +1 -0
- package/skills/harness/SKILL.md +6 -0
- package/skills/harness/references/wiki-protocol.md +5 -0
- package/skills/implement/SKILL.md +1 -0
- package/skills/ingeniero-diseno-web/SKILL.md +1 -0
- package/skills/init/references/accompaniment-and-profile.md +1 -1
- package/skills/leyes-de-percepcion/SKILL.md +1 -0
- package/skills/leyes-de-retencion/SKILL.md +1 -0
- package/skills/mejor-accesibilidad/SKILL.md +1 -0
- package/skills/mejor-colores/SKILL.md +1 -0
- package/skills/mejor-layout/SKILL.md +1 -0
- package/skills/mejor-redaccion/SKILL.md +1 -0
- package/skills/mejor-tipografia/SKILL.md +1 -0
- package/skills/mejor-ui/SKILL.md +1 -0
- package/skills/muscle-memory/SKILL.md +1 -0
- package/skills/orient/SKILL.md +9 -1
- package/skills/orient/references/orientation-contract.md +1 -1
- package/skills/prototype/SKILL.md +1 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/revision-de-cambios/SKILL.md +1 -0
- package/skills/revision-interfaz/SKILL.md +1 -0
- package/skills/sitios-calidad-premio/SKILL.md +1 -0
- package/skills/suggest/SKILL.md +10 -9
- package/skills/tastemaker/SKILL.md +1 -0
- package/skills/teach/SKILL.md +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -0
- package/skills/to-spec/SKILL.md +1 -0
- package/skills/to-tickets/SKILL.md +1 -0
- package/skills/video-a-superprompt/SKILL.md +1 -0
- package/skills/vocabulario-animacion/SKILL.md +1 -0
- package/skills/wait-what/SKILL.md +1 -0
- package/skills/wayfinder/SKILL.md +1 -0
- package/skills/write-adr/SKILL.md +1 -0
- package/skills/writing-for-agents/SKILL.md +1 -0
- package/targets/_md-block.js +8 -1
- package/targets/agents-md-shadow.js +153 -0
- package/targets/agents.js +46 -3
- package/targets/claude.js +76 -26
- package/targets/clone-bootstrap.mjs +454 -0
- package/targets/gitmoji-guard.mjs +30 -3
- package/targets/hook-once.mjs +1 -0
- package/targets/memory.js +1 -1
- package/targets/opt-outs.js +40 -0
- package/targets/sello.mjs +2 -2
- package/targets/session-memory-adapter.mjs +70 -14
- package/targets/session-memory-core.mjs +40 -8
- package/targets/session-start.mjs +65 -12
- package/targets/ship-guard.mjs +1 -1
- package/targets/worktree-reaper.mjs +175 -11
|
@@ -4,6 +4,7 @@ import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
4
4
|
import { TARGET_IDS } from '../../targets/index.js';
|
|
5
5
|
import { resolveAgentNames } from '../../targets/agents.js';
|
|
6
6
|
import { loadManifest, skillsForProfile } from './manifest.js';
|
|
7
|
+
import { readManifest } from './manifest-file.js';
|
|
7
8
|
import { managedPathsForInstall } from '../install-apply.js';
|
|
8
9
|
|
|
9
10
|
export const ONBOARDING_SCHEMA_VERSION = 1;
|
|
@@ -68,7 +69,17 @@ export function scanProject(root = process.cwd()) {
|
|
|
68
69
|
let markdownCount = 0;
|
|
69
70
|
let sourceFileCount = 0;
|
|
70
71
|
const visit = (dir) => {
|
|
71
|
-
|
|
72
|
+
// A directory we are not allowed to open is a fact about that directory, not a reason to abandon
|
|
73
|
+
// the install. Reported from a real Windows box: the global harness died walking `Temp\WinSAT`,
|
|
74
|
+
// which Windows protects, on 1.3.2 and again on 1.3.6 — so it had never worked there at all.
|
|
75
|
+
// The safe form already existed in `detect-repo.js` and was simply not reused here.
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
72
83
|
if (ignored.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
73
84
|
const path = join(dir, entry.name);
|
|
74
85
|
const rel = relative(absolute, path).split(sep).join('/');
|
|
@@ -169,11 +180,32 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
169
180
|
const catalog = loadManifest();
|
|
170
181
|
const catalogIds = new Set(catalog.skills.map((skill) => skill.id));
|
|
171
182
|
const detectedSkills = (evidence.stacks || []).filter((stack) => catalogIds.has(stack));
|
|
172
|
-
|
|
183
|
+
// Skills the user asked for BY NAME are kept across an update; skills a profile handed out are not.
|
|
184
|
+
//
|
|
185
|
+
// Without this, an update recomputed the set from the profile and the detected stacks alone, and
|
|
186
|
+
// the install step then deleted from disk whatever was not in that fresh set — every `rsc add`,
|
|
187
|
+
// on every update. But choosing a narrower scope on purpose still has to be able to REMOVE what the
|
|
188
|
+
// wider one installed, so the difference comes from where the skill came from: a profile skill
|
|
189
|
+
// declares its profiles, and a skill you can only get by naming it declares none. What a profile
|
|
190
|
+
// gave you, a profile may take away; what you asked for by name, only you may. Filtered against the
|
|
191
|
+
// catalog on the way in, because preserving a declaration is not the same as trusting it.
|
|
192
|
+
//
|
|
193
|
+
// `readManifest` returns null on a first install, which is the most common path of all.
|
|
194
|
+
const inAnyProfile = new Set(catalog.skills.filter((s) => (s.profiles || []).length).map((s) => s.id));
|
|
195
|
+
const declared = (readManifest(evidence.root ?? process.cwd())?.skills ?? [])
|
|
196
|
+
.filter((id) => catalogIds.has(id) && !inAnyProfile.has(id));
|
|
197
|
+
const skills = [...new Set([...skillsForProfile(catalog, profile), ...detectedSkills, ...declared])].sort();
|
|
173
198
|
const baseAgents = needsWorkflow;
|
|
174
199
|
const hooks = needsWorkflow;
|
|
175
200
|
const agents = baseAgents ? resolveAgentNames(skills, []).sort() : [];
|
|
176
201
|
const gitmojiGuard = hooks && normalized.targets.includes('claude');
|
|
202
|
+
// The danger guard is NOT code machinery, and tying it to `hooks` was the mistake behind #273. The
|
|
203
|
+
// three guards above need a code project with git — a branch to protect, a commit to lint. This one
|
|
204
|
+
// stops recursive deletes, history-rewriting pushes and friends, which are exactly as irreversible
|
|
205
|
+
// in an invoicing workspace, and `init` promises it by technical level: non-technical and mixed get
|
|
206
|
+
// it, whatever the project is. The guard also decides at runtime from the profile, so the plan only
|
|
207
|
+
// chooses whether it is present, never whether it bites.
|
|
208
|
+
const dangerGuard = normalized.technicalLevel !== 'technical';
|
|
177
209
|
const decisions = skills.map((id) => detectedSkills.includes(id)
|
|
178
210
|
? selected(id, 'skill', `Detected ${id} evidence inside the selected project root.`, 'workspace-evidence')
|
|
179
211
|
: selected(id, 'skill', needsWorkflow
|
|
@@ -223,6 +255,7 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
223
255
|
alwaysOn: true,
|
|
224
256
|
codeHooks: needsWorkflow,
|
|
225
257
|
gitmojiGuard,
|
|
258
|
+
dangerGuard,
|
|
226
259
|
memory: true,
|
|
227
260
|
context7: false,
|
|
228
261
|
plugins: needsWorkflow ? ['superpowers@claude-plugins-official'] : [],
|
package/scripts/lib/repair.js
CHANGED
|
@@ -131,22 +131,26 @@ export async function repair({ cwd = process.cwd(), target, home = homedir(), dr
|
|
|
131
131
|
|
|
132
132
|
const paths = targetPaths(target, home, cwd);
|
|
133
133
|
const ids = [...new Set([...Object.keys(readState(paths.stateFile).skills || {}), ...(readManifest(cwd)?.skills || [])])];
|
|
134
|
+
// The accepted plan governs a repair exactly as it governs a sync. Without it, `wireHook` saw `{}`,
|
|
135
|
+
// read `codeHooks !== false` as true and wired every guard the plan had declined — and the next
|
|
136
|
+
// `sync`, which does pass the policy, took them away again (#274). Restoring is not deciding.
|
|
137
|
+
const policy = readManifest(cwd)?.onboarding?.plan?.policy;
|
|
134
138
|
const backup = createBackup({
|
|
135
139
|
cwd, operation: 'repair', target,
|
|
136
|
-
paths: managedPathsForInstall({ skillIds: ids, target, home, cwd }),
|
|
140
|
+
paths: managedPathsForInstall({ skillIds: ids, target, home, cwd, policy }),
|
|
137
141
|
});
|
|
138
142
|
|
|
139
143
|
for (const f of todo) {
|
|
140
144
|
if (f.id === 'nested-layout') rmSync(join(paths.root, 'rsc'), { recursive: true, force: true });
|
|
141
|
-
if (f.id === 'duplicate-hooks') { unwireHook(target, paths); await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair' }); }
|
|
142
|
-
if (f.id === 'dangling-links') await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair' });
|
|
145
|
+
if (f.id === 'duplicate-hooks') { unwireHook(target, paths); await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair', policy }); }
|
|
146
|
+
if (f.id === 'dangling-links') await applyInstall({ skillIds: ids, target, home, cwd, operation: 'repair', policy });
|
|
143
147
|
if (f.id === 'no-manifest') recordInManifest({ cwd, target, skillIds: ids });
|
|
144
148
|
if (f.id === 'wrong-target') {
|
|
145
149
|
// Install where the project actually points, THEN take the old wiring down — in that
|
|
146
150
|
// order, so a failure halfway leaves a working harness rather than none. Taking the
|
|
147
151
|
// block out of the old file gives it back byte-identical: in #249 that file was the
|
|
148
152
|
// project's hand-written constitution, and it had already been written into once.
|
|
149
|
-
await applyInstall({ skillIds: ids, target: f.to, home, cwd, operation: 'repair' });
|
|
153
|
+
await applyInstall({ skillIds: ids, target: f.to, home, cwd, operation: 'repair', policy });
|
|
150
154
|
unwireHook(target, paths);
|
|
151
155
|
removeManagedSkills(paths);
|
|
152
156
|
recordInManifest({ cwd, target: f.to, skillIds: ids, dropTarget: target });
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Consolidation of the refuter panel's findings — the deterministic half of `review`.
|
|
2
|
+
//
|
|
3
|
+
// Three fresh-context lenses attack the same diff, so they often report the SAME defect in three
|
|
4
|
+
// different sentences, and the author spends triage time discovering that eleven findings are four.
|
|
5
|
+
// Grouping them is an algorithm, not a judgement (P1), so it lives here and not in the skill's prose.
|
|
6
|
+
// What survives grouping and is serious enough to block goes to `finding-verifier` — that half IS
|
|
7
|
+
// judgement, and stays in the agent.
|
|
8
|
+
//
|
|
9
|
+
// Input: what the lenses emit — fenced ```json findings blocks inside their reports, or plain JSON.
|
|
10
|
+
// Output: unique findings, highest severity first, each with the lenses that reported it and a
|
|
11
|
+
// `verify` flag on the ones the verifier must try to refute before they may block.
|
|
12
|
+
|
|
13
|
+
export const SEVERITIES = ['blocker', 'should-fix', 'nit', 'question'];
|
|
14
|
+
const ALIASES = {
|
|
15
|
+
critical: 'blocker', high: 'blocker', blocking: 'blocker',
|
|
16
|
+
medium: 'should-fix', shouldfix: 'should-fix', 'should_fix': 'should-fix', major: 'should-fix',
|
|
17
|
+
low: 'nit', info: 'nit', minor: 'nit',
|
|
18
|
+
};
|
|
19
|
+
const rank = (s) => SEVERITIES.indexOf(s);
|
|
20
|
+
export const DEFAULT_WINDOW = 3;
|
|
21
|
+
|
|
22
|
+
export function normalizeSeverity(raw) {
|
|
23
|
+
const s = String(raw || '').trim().toLowerCase();
|
|
24
|
+
if (SEVERITIES.includes(s)) return s;
|
|
25
|
+
return ALIASES[s] || ALIASES[s.replace(/[-\s]/g, '')] || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const text = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
29
|
+
const normFile = (f) => text(f).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
30
|
+
const normEvidence = (e) => text(e).replace(/\s+/g, ' ').toLowerCase();
|
|
31
|
+
|
|
32
|
+
// Pull every finding out of one source. A source is a lens report (markdown with fenced blocks) or a
|
|
33
|
+
// JSON file. Anything unparseable is reported, never silently dropped: a lost finding is the one
|
|
34
|
+
// failure a consolidation step must not add.
|
|
35
|
+
export function parseFindings(raw, source = 'stdin') {
|
|
36
|
+
const findings = [];
|
|
37
|
+
const errors = [];
|
|
38
|
+
const take = (value, where) => {
|
|
39
|
+
const list = Array.isArray(value) ? value : (Array.isArray(value?.findings) ? value.findings : [value]);
|
|
40
|
+
for (const item of list) {
|
|
41
|
+
if (!item || typeof item !== 'object') { errors.push(`${where}: a finding must be an object`); continue; }
|
|
42
|
+
findings.push({ ...item, source });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const blocks = [...String(raw).matchAll(/```json[ \t]+findings[^\n]*\n([\s\S]*?)```/g)];
|
|
46
|
+
if (blocks.length) {
|
|
47
|
+
blocks.forEach((m, i) => {
|
|
48
|
+
try { take(JSON.parse(m[1]), `${source} block ${i + 1}`); }
|
|
49
|
+
catch (e) { errors.push(`${source} block ${i + 1}: not valid JSON (${e.message})`); }
|
|
50
|
+
});
|
|
51
|
+
return { findings, errors };
|
|
52
|
+
}
|
|
53
|
+
const trimmed = String(raw).trim();
|
|
54
|
+
if (!trimmed) return { findings, errors };
|
|
55
|
+
try { take(JSON.parse(trimmed), source); }
|
|
56
|
+
catch { errors.push(`${source}: no \`\`\`json findings block and not plain JSON`); }
|
|
57
|
+
return { findings, errors };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The pre-report gate, checked instead of trusted: a blocker or should-fix with no location or no
|
|
61
|
+
// concrete failure scenario is a suspicion, and a suspicion is a question. The downgrade is visible.
|
|
62
|
+
export function normalizeFinding(f) {
|
|
63
|
+
const out = {
|
|
64
|
+
lens: text(f.lens) || text(f.source) || 'unknown',
|
|
65
|
+
severity: normalizeSeverity(f.severity),
|
|
66
|
+
file: normFile(f.file),
|
|
67
|
+
line: Number.isInteger(Number(f.line)) && Number(f.line) > 0 ? Number(f.line) : null,
|
|
68
|
+
claim: text(f.claim) || text(f.title) || text(f.summary),
|
|
69
|
+
failure_scenario: text(f.failure_scenario) || text(f.repro),
|
|
70
|
+
evidence: text(f.evidence),
|
|
71
|
+
notes: [],
|
|
72
|
+
};
|
|
73
|
+
if (!out.severity) {
|
|
74
|
+
out.notes.push(`severity "${f.severity ?? ''}" unknown → question`);
|
|
75
|
+
out.severity = 'question';
|
|
76
|
+
}
|
|
77
|
+
if (rank(out.severity) <= rank('should-fix')) {
|
|
78
|
+
const missing = [];
|
|
79
|
+
if (!out.file || !out.line) missing.push('file:line');
|
|
80
|
+
if (!out.failure_scenario) missing.push('failure_scenario');
|
|
81
|
+
if (missing.length) {
|
|
82
|
+
out.notes.push(`${out.severity} without ${missing.join(' and ')} → question`);
|
|
83
|
+
out.severity = 'question';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Two findings are the same defect when they quote the same evidence, or when they point at the same
|
|
90
|
+
// place (same file, lines within the window) AND say the same thing. Place alone is not enough: the
|
|
91
|
+
// first real panel run put two different defects on adjacent lines (a discount applied to one unit,
|
|
92
|
+
// and a missing 0..100 check), and merging them buried the second one where no verifier looked.
|
|
93
|
+
// A missed merge costs one extra verification; a wrong merge hides a defect — so the bar is
|
|
94
|
+
// conservative. "Say the same thing" is word overlap (Jaccard) of claim, or claim + failure scenario.
|
|
95
|
+
const STOP = new Set('the and for with not that this its are was from any only one but has have into than then when which will can its'.split(' '));
|
|
96
|
+
const words = (s) => new Set(String(s || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP.has(w)));
|
|
97
|
+
function overlap(a, b) {
|
|
98
|
+
const A = words(a); const B = words(b);
|
|
99
|
+
let shared = 0;
|
|
100
|
+
for (const w of A) if (B.has(w)) shared += 1;
|
|
101
|
+
const union = A.size + B.size - shared;
|
|
102
|
+
return union ? shared / union : 0;
|
|
103
|
+
}
|
|
104
|
+
export const SAME_CLAIM = 0.2;
|
|
105
|
+
export function similarity(a, b) {
|
|
106
|
+
return Math.max(overlap(a.claim, b.claim),
|
|
107
|
+
overlap(`${a.claim} ${a.failure_scenario}`, `${b.claim} ${b.failure_scenario}`));
|
|
108
|
+
}
|
|
109
|
+
function sameDefect(a, b, window) {
|
|
110
|
+
const ea = normEvidence(a.evidence);
|
|
111
|
+
if (ea.length >= 12 && ea === normEvidence(b.evidence)) return true;
|
|
112
|
+
const near = a.file && a.file === b.file && a.line && b.line && Math.abs(a.line - b.line) <= window;
|
|
113
|
+
return Boolean(near) && similarity(a, b) >= SAME_CLAIM;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function consolidate(rawFindings, { window = DEFAULT_WINDOW } = {}) {
|
|
117
|
+
const items = rawFindings.map(normalizeFinding);
|
|
118
|
+
const parent = items.map((_, i) => i);
|
|
119
|
+
const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i])));
|
|
120
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
121
|
+
for (let j = i + 1; j < items.length; j += 1) {
|
|
122
|
+
if (sameDefect(items[i], items[j], window)) parent[find(j)] = find(i);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const groups = new Map();
|
|
126
|
+
items.forEach((it, i) => {
|
|
127
|
+
const root = find(i);
|
|
128
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
129
|
+
groups.get(root).push(it);
|
|
130
|
+
});
|
|
131
|
+
const uniq = (xs) => [...new Set(xs.filter(Boolean))];
|
|
132
|
+
const merged = [...groups.values()].map((members) => {
|
|
133
|
+
const severity = SEVERITIES[Math.min(...members.map((m) => rank(m.severity)))];
|
|
134
|
+
const lines = members.map((m) => m.line).filter(Boolean);
|
|
135
|
+
return {
|
|
136
|
+
severity,
|
|
137
|
+
file: uniq(members.map((m) => m.file))[0] || '',
|
|
138
|
+
lines: lines.length ? [Math.min(...lines), Math.max(...lines)] : [],
|
|
139
|
+
lenses: uniq(members.map((m) => m.lens)).sort(),
|
|
140
|
+
claims: uniq(members.map((m) => m.claim)),
|
|
141
|
+
failure_scenarios: uniq(members.map((m) => m.failure_scenario)),
|
|
142
|
+
evidence: uniq(members.map((m) => m.evidence)),
|
|
143
|
+
notes: uniq(members.flatMap((m) => m.notes)),
|
|
144
|
+
reported: members.length,
|
|
145
|
+
verify: rank(severity) <= rank('should-fix'),
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
merged.sort((a, b) => rank(a.severity) - rank(b.severity)
|
|
149
|
+
|| a.file.localeCompare(b.file) || (a.lines[0] || 0) - (b.lines[0] || 0));
|
|
150
|
+
merged.forEach((m, i) => { m.id = `F${i + 1}`; });
|
|
151
|
+
return {
|
|
152
|
+
reported: items.length,
|
|
153
|
+
unique: merged.length,
|
|
154
|
+
toVerify: merged.filter((m) => m.verify).length,
|
|
155
|
+
findings: merged,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const where = (f) => (f.file ? `${f.file}${f.lines.length ? `:${f.lines[0]}${f.lines[1] !== f.lines[0] ? `-${f.lines[1]}` : ''}` : ''}` : '(no location)');
|
|
160
|
+
|
|
161
|
+
export function renderConsolidation(result, errors = []) {
|
|
162
|
+
const out = [
|
|
163
|
+
`${result.reported} finding(s) reported → ${result.unique} unique · ${result.toVerify} go to finding-verifier`,
|
|
164
|
+
];
|
|
165
|
+
for (const f of result.findings) {
|
|
166
|
+
out.push('', `${f.id} [${f.severity}] ${where(f)} — ${f.claims[0] || '(no claim)'}`);
|
|
167
|
+
out.push(` lenses: ${f.lenses.join(', ')}${f.reported > 1 ? ` (${f.reported} reports merged)` : ''}`);
|
|
168
|
+
for (const c of f.claims.slice(1)) out.push(` also: ${c}`);
|
|
169
|
+
for (const s of f.failure_scenarios) out.push(` failure: ${s}`);
|
|
170
|
+
for (const n of f.notes) out.push(` note: ${n}`);
|
|
171
|
+
if (f.verify) out.push(' → verify: dispatch finding-verifier; it blocks only if CONFIRMED');
|
|
172
|
+
}
|
|
173
|
+
for (const e of errors) out.push('', `⚠️ ${e}`);
|
|
174
|
+
return out.join('\n');
|
|
175
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// Which rsc is running, and which rsc this project was last built with. Two different numbers, and
|
|
6
|
+
// the gap between them is where a published fix fails to arrive: hooks are materialized as copies
|
|
7
|
+
// under `.rsc/`, so a release changes nothing in a project until someone syncs it. Nobody could see
|
|
8
|
+
// that gap before — there was no `--version`, and bug reports arrived without one.
|
|
9
|
+
|
|
10
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
|
+
export const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
12
|
+
|
|
13
|
+
/** `.rsc/.version` is written at install and at every sync — the version this project's hooks came from. */
|
|
14
|
+
export function installedVersion(root) {
|
|
15
|
+
try { return readFileSync(join(root, '.rsc', '.version'), 'utf8').trim() || null; } catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isNewer(a, b) {
|
|
19
|
+
const pa = String(a).split('.').map(Number);
|
|
20
|
+
const pb = String(b).split('.').map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
23
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function versionReport(root) {
|
|
29
|
+
const installed = installedVersion(root);
|
|
30
|
+
return { cli: CLI_VERSION, installed, behind: Boolean(installed && isNewer(CLI_VERSION, installed)) };
|
|
31
|
+
}
|
package/scripts/rsc.js
CHANGED
|
@@ -4,18 +4,19 @@ import { detectTarget, installedTargets, resolveTargets, TARGETS } from '../targ
|
|
|
4
4
|
import { detectRepo } from './detect-repo.js';
|
|
5
5
|
import { rank } from './consult.js';
|
|
6
6
|
import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
|
|
7
|
-
import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions } from './install-apply.js';
|
|
7
|
+
import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purge, collisions, ownSkillPaths } from './install-apply.js';
|
|
8
8
|
import { stackAgentNames } from '../targets/agents.js';
|
|
9
9
|
import { doctor } from './doctor.js';
|
|
10
10
|
import { ask, say, select, pickFrom, banner, confirm, isInteractive } from './lib/ui.js';
|
|
11
11
|
import { refreshRegistry, registryStatus } from './lib/registry.js';
|
|
12
12
|
import { audit, writeAuditReport } from './audit.js';
|
|
13
13
|
import { DOMAINS } from './lib/domains.js';
|
|
14
|
-
import { listBackups, restoreBackup } from './lib/backups.js';
|
|
14
|
+
import { listBackups, restoreBackup, whereKept } from './lib/backups.js';
|
|
15
15
|
import { runUpgrade } from './lib/upgrade.js';
|
|
16
16
|
import { diagnose, repair } from './lib/repair.js';
|
|
17
17
|
import { DEFAULT_SKILL_FLOOR, withDefaultSkillFloor } from './lib/default-skill-floor.js';
|
|
18
18
|
import { readManifest, writeManifest } from './lib/manifest-file.js';
|
|
19
|
+
import { versionReport } from './lib/versions.js';
|
|
19
20
|
import {
|
|
20
21
|
normalizeOnboarding, missingOnboardingFields, scanProject,
|
|
21
22
|
buildOnboardingPlan, decodeGoal, encodeGoal, identifyPlan, recommendDeferredComponents,
|
|
@@ -23,10 +24,24 @@ import {
|
|
|
23
24
|
import { applyAcceptedOnboarding, verifyOnboarding, ensureHarnessSkeleton, harnessReadiness } from './lib/onboarding-apply.js';
|
|
24
25
|
|
|
25
26
|
const rawArgv = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
// `rsc --version` — answered before anything else runs, offline, and in a shape a bug report can
|
|
29
|
+
// paste. Two numbers because there are two things: the CLI you just ran, and the harness this
|
|
30
|
+
// project was built with. When they differ the project is running old hooks, and a fix that was
|
|
31
|
+
// published may simply not be here yet.
|
|
32
|
+
if (['--version', '-v', 'version'].includes(rawArgv[0])) {
|
|
33
|
+
const v = versionReport(process.cwd());
|
|
34
|
+
const lines = [`rsc ${v.cli}`];
|
|
35
|
+
if (v.installed) lines.push(`installed in this project: ${v.installed}${v.behind ? ' (behind)' : ''}`);
|
|
36
|
+
else lines.push('installed in this project: none');
|
|
37
|
+
if (v.behind) lines.push('This project runs older hooks than this CLI. To catch up, run here:', ' npx @damiangilgonzalez/harness@latest');
|
|
38
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
26
41
|
const GLOBAL_VALUE_FLAGS = new Set([
|
|
27
42
|
'--target', '--technical-level', '--accompaniment', '--project-kind', '--goal', '--goal-base64', '--software-scope', '--accept-plan',
|
|
28
43
|
]);
|
|
29
|
-
const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'sello', 'repair', 'uninstall', 'purge']);
|
|
44
|
+
const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'review', 'sello', 'repair', 'uninstall', 'purge']);
|
|
30
45
|
function positionalTokens(input) {
|
|
31
46
|
const out = [];
|
|
32
47
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -127,6 +142,12 @@ function renderPlan(plan, planId) {
|
|
|
127
142
|
for (const decision of plan.decisions.filter((d) => d.state === 'excluded')) say(` × ${decision.kind}/${decision.id} — ${decision.reason}`);
|
|
128
143
|
say('Managed paths:');
|
|
129
144
|
for (const path of plan.governedPaths) say(` ${path}`);
|
|
145
|
+
if (plan.replacesUserSkills?.length) {
|
|
146
|
+
say('Your own skills this plan replaces:');
|
|
147
|
+
for (const path of plan.replacesUserSkills) say(` ! ${path} — already here and not put by rsc; the catalog skill of the same name takes its place, and yours is kept in .rsc/backups/`);
|
|
148
|
+
say(' To keep yours, rename its folder (for example review → review-own) and generate the plan again.');
|
|
149
|
+
say(' AGENT: name each of these to the user before accepting, and ask whether to replace or rename.');
|
|
150
|
+
}
|
|
130
151
|
if (plan.evidence.parentHarness) say(`Parent harness detected at ${plan.evidence.parentHarness}; it is not inherited by this plan.`);
|
|
131
152
|
const pieces = [
|
|
132
153
|
'npx @damiangilgonzalez/harness@latest onboard',
|
|
@@ -186,6 +207,11 @@ async function runOnboarding(targets) {
|
|
|
186
207
|
return;
|
|
187
208
|
}
|
|
188
209
|
const plan = buildOnboardingPlan(record, scanProject(process.cwd()));
|
|
210
|
+
// #275 — a folder where a catalog skill goes that rsc did not put is the user's own work. It goes
|
|
211
|
+
// INTO the plan, so the id they accept names it: an id made before that folder existed no longer
|
|
212
|
+
// applies, and accepting is never consent to something the plan did not show.
|
|
213
|
+
const replacesUserSkills = ownSkillPaths({ cwd: process.cwd(), targets: plan.policy.targets, skillIds: plan.policy.skills });
|
|
214
|
+
if (replacesUserSkills.length) plan.replacesUserSkills = replacesUserSkills;
|
|
189
215
|
const planId = identifyPlan(plan);
|
|
190
216
|
const accepted = flag('accept-plan');
|
|
191
217
|
if (!accepted) {
|
|
@@ -214,6 +240,10 @@ async function runOnboarding(targets) {
|
|
|
214
240
|
} catch (error) {
|
|
215
241
|
say(`RSC_SKELETON_FAILED ${error.message}`);
|
|
216
242
|
}
|
|
243
|
+
for (const path of plan.replacesUserSkills || []) {
|
|
244
|
+
const kept = whereKept({ cwd: process.cwd(), relPath: path });
|
|
245
|
+
if (kept) say(`Your previous ${path} is kept in ${kept}`);
|
|
246
|
+
}
|
|
217
247
|
emitHarnessReadiness(plan, planId);
|
|
218
248
|
}
|
|
219
249
|
|
|
@@ -486,7 +516,18 @@ function printContextBudget(b) {
|
|
|
486
516
|
say('════════════════════════════════════════════════\n');
|
|
487
517
|
}
|
|
488
518
|
|
|
489
|
-
async function
|
|
519
|
+
async function syncDeclared(targets, dry = false) {
|
|
520
|
+
for (const t of targets) {
|
|
521
|
+
const result = await syncInstalled({ target: t, dryRun: dry });
|
|
522
|
+
const verb = dry ? 'Would sync' : 'Synced';
|
|
523
|
+
say(`${verb} ${t}: ${result.synced.length ? result.synced.join(', ') : '(nothing to sync)'}`);
|
|
524
|
+
if (dry && result.paths?.length) {
|
|
525
|
+
for (const p of result.paths) say(` ${p}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function wizard(flagTargets, declaredTargets = []) {
|
|
490
531
|
const m = loadManifest();
|
|
491
532
|
await banner(m.counts.skills);
|
|
492
533
|
say(' the skill catalog for your assistant (Claude Code · Codex · Cursor · Gemini · Antigravity)\n');
|
|
@@ -496,12 +537,16 @@ async function wizard(flagTargets) {
|
|
|
496
537
|
const baseIds = skillsForProfile(m, 'minimal');
|
|
497
538
|
// Navigable loop: esc / "← Back" / "no" all return here instead of quitting.
|
|
498
539
|
for (;;) {
|
|
540
|
+
const declared = readManifest();
|
|
499
541
|
const choice = await select('What do you want to do?', [
|
|
542
|
+
// First, because it is what someone who already has a harness came here for nine times in ten.
|
|
543
|
+
...(declared ? [{ key: 'update', label: `Update this project — keeps its ${declared.skills.length} skills, refreshes hooks and content` }] : []),
|
|
500
544
|
{ key: 'base', label: `Base install — the essentials (${baseIds.length} skills)` },
|
|
501
545
|
{ key: 'workflow', label: 'Base + tu flujo de trabajo — grill-with-docs → to-spec → plan o tickets' },
|
|
502
546
|
{ key: 'manual', label: 'Pick skills by hand, by area' },
|
|
503
547
|
]);
|
|
504
548
|
if (choice === null) { say('\nOK — nothing installed. Anytime: npx @damiangilgonzalez/harness'); return; }
|
|
549
|
+
if (choice === 'update') return syncDeclared(declaredTargets);
|
|
505
550
|
|
|
506
551
|
let ids;
|
|
507
552
|
if (choice === 'base') ids = baseIds;
|
|
@@ -560,6 +605,27 @@ async function guardCollisions(targets, ids) {
|
|
|
560
605
|
return confirm('Replace them anyway?');
|
|
561
606
|
}
|
|
562
607
|
|
|
608
|
+
// The governed list that `sync` reads lives INSIDE the onboarding receipt, not in `manifest.skills`.
|
|
609
|
+
// Updating one without the other is what deleted users' skills: `add` installed a skill and recorded
|
|
610
|
+
// it in `manifest.skills`, `sync` then read a governed list that predated the install, and pruned
|
|
611
|
+
// everything absent from it — recursively. Reported by a user, reproduced 2026-09-17: nine declared
|
|
612
|
+
// skills against eight governed, visible in the manifest before sync was even run.
|
|
613
|
+
//
|
|
614
|
+
// So the two travel together, always. Any command that changes what is installed carries both, and
|
|
615
|
+
// `applyInstall` persists the receipt rather than conserving the stale one.
|
|
616
|
+
function governedBy(receipt, skills) {
|
|
617
|
+
// The receipt is hash-checked against the plan the user accepted, so its policy is NOT a place to
|
|
618
|
+
// record what happened afterwards: rewriting `plan.policy.skills` made every install report "the
|
|
619
|
+
// receipt does not match its accepted plan id", which is the integrity check doing its job.
|
|
620
|
+
//
|
|
621
|
+
// An earlier fix for the reported data loss did exactly that, and it was wrong — the right place
|
|
622
|
+
// was never `add`, it was `sync`, which treated the accepted plan as the only declaration and
|
|
623
|
+
// pruned everything absent from it. The policy still travels for hooks and agents; what must not
|
|
624
|
+
// travel is a rewritten receipt.
|
|
625
|
+
if (!receipt) return { policy: undefined, onboarding: undefined };
|
|
626
|
+
return { policy: { ...receipt.plan.policy, skills }, onboarding: undefined };
|
|
627
|
+
}
|
|
628
|
+
|
|
563
629
|
async function main() {
|
|
564
630
|
// One resolution for every command, doctor included: `doctor` used to resolve on its
|
|
565
631
|
// own and could report a different assistant than the one just installed into.
|
|
@@ -589,7 +655,16 @@ async function main() {
|
|
|
589
655
|
const target = targets[0];
|
|
590
656
|
switch (cmd) {
|
|
591
657
|
case undefined:
|
|
592
|
-
|
|
658
|
+
if (!hasDeclaredHarness()) return runOnboarding(f ? targets : []);
|
|
659
|
+
// A project that already has a harness: the bare command means "bring it up to date". It is
|
|
660
|
+
// what every update notice, the README and our own replies to users tell people to run, with
|
|
661
|
+
// the promise "that reinstalls and refreshes". It used to open the install wizard instead, and
|
|
662
|
+
// with no terminal — which is how an agent runs it when it obeys the notice — it did nothing.
|
|
663
|
+
// Restoring what the project already declared is not a decision, so there is nothing to ask.
|
|
664
|
+
// RSC_FORCE_WIZARD opens the wizard anyway, answering from piped stdin: that is how its
|
|
665
|
+
// choices are tested end to end, since a test has no terminal to offer.
|
|
666
|
+
if (!isInteractive() && !process.env.RSC_FORCE_WIZARD) return syncDeclared(targets);
|
|
667
|
+
return wizard(f ? targets : null, targets);
|
|
593
668
|
case 'onboard':
|
|
594
669
|
return runOnboarding(f ? targets : []);
|
|
595
670
|
case 'reassess':
|
|
@@ -604,8 +679,8 @@ async function main() {
|
|
|
604
679
|
const currentManifest = readManifest();
|
|
605
680
|
const receipt = currentManifest?.onboarding;
|
|
606
681
|
const maintainedIds = receipt ? [...new Set([...(currentManifest.skills || []), ...(receipt.plan.policy.skills || []), ...ids])].sort() : ids;
|
|
607
|
-
const policy
|
|
608
|
-
for (const t of targets) await applyInstall({ skillIds: maintainedIds, agentIds: selected.agents, target: t, policy });
|
|
682
|
+
const { policy, onboarding } = governedBy(receipt, maintainedIds);
|
|
683
|
+
for (const t of targets) await applyInstall({ skillIds: maintainedIds, agentIds: selected.agents, target: t, policy, onboarding });
|
|
609
684
|
markMaintenanceDrift(`add ${requested.join(',')}`);
|
|
610
685
|
say(`✅ Installed for ${targets.join(', ')}: ${requested.join(', ')}`);
|
|
611
686
|
return void say(' ↻ Reload/restart your assistant so the new skill activates.');
|
|
@@ -675,6 +750,8 @@ async function main() {
|
|
|
675
750
|
}
|
|
676
751
|
case 'doctor': {
|
|
677
752
|
const report = doctor({ target });
|
|
753
|
+
// #277 — an automation on top of rsc reads the exit code, not the prose.
|
|
754
|
+
if (!report.healthy) process.exitCode = 1;
|
|
678
755
|
if (argv.includes('--json')) return void say(JSON.stringify(report, null, 2));
|
|
679
756
|
printContextBudget(report.contextBudget);
|
|
680
757
|
return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));
|
|
@@ -727,18 +804,8 @@ async function main() {
|
|
|
727
804
|
process.exitCode = 1;
|
|
728
805
|
return;
|
|
729
806
|
}
|
|
730
|
-
case 'sync':
|
|
731
|
-
|
|
732
|
-
for (const t of targets) {
|
|
733
|
-
const result = await syncInstalled({ target: t, dryRun: dry });
|
|
734
|
-
const verb = dry ? 'Would sync' : 'Synced';
|
|
735
|
-
say(`${verb} ${t}: ${result.synced.length ? result.synced.join(', ') : '(nothing to sync)'}`);
|
|
736
|
-
if (dry && result.paths?.length) {
|
|
737
|
-
for (const p of result.paths) say(` ${p}`);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
807
|
+
case 'sync':
|
|
808
|
+
return syncDeclared(targets, argv.includes('--dry-run'));
|
|
742
809
|
case 'backups': {
|
|
743
810
|
const backups = listBackups();
|
|
744
811
|
if (!backups.length) return void say('(no backups)');
|
|
@@ -778,6 +845,37 @@ async function main() {
|
|
|
778
845
|
say('Use: npx @damiangilgonzalez/harness registry refresh | registry status');
|
|
779
846
|
return;
|
|
780
847
|
}
|
|
848
|
+
case 'review': {
|
|
849
|
+
// `review consolidate` — the deterministic half of the refuter panel (P1): the three lenses
|
|
850
|
+
// often report one defect three ways, so grouping happens here, by location and evidence, and
|
|
851
|
+
// only the unique serious findings go on to `finding-verifier`, which is judgement.
|
|
852
|
+
const USE = 'Use: npx @damiangilgonzalez/harness review consolidate [report.md|findings.json ...] [--json] [--window <lines>]\n Without files it reads stdin. Each lens report carries its findings in ```json findings blocks.';
|
|
853
|
+
if (argv[1] !== 'consolidate') { say(USE); if (argv[1]) process.exitCode = 1; return; }
|
|
854
|
+
const { readFileSync } = await import('node:fs');
|
|
855
|
+
const R = await import('./lib/review-findings.js');
|
|
856
|
+
const w = flag('window');
|
|
857
|
+
const window = w === undefined ? R.DEFAULT_WINDOW : Number(w);
|
|
858
|
+
if (!Number.isInteger(window) || window < 0) { say(`rsc: --window takes a whole number of lines, got '${w}'.\n${USE}`); process.exitCode = 1; return; }
|
|
859
|
+
const rest = argv.slice(2);
|
|
860
|
+
const files = rest.filter((a, i) => !a.startsWith('--') && rest[i - 1] !== '--window');
|
|
861
|
+
const sources = [];
|
|
862
|
+
try {
|
|
863
|
+
if (files.length) for (const f of files) sources.push([readFileSync(f, 'utf8'), f]);
|
|
864
|
+
else if (!process.stdin.isTTY) sources.push([readFileSync(0, 'utf8'), 'stdin']);
|
|
865
|
+
} catch (e) { say(`rsc: cannot read ${e.path || 'stdin'} (${e.code || e.message}).`); process.exitCode = 1; return; }
|
|
866
|
+
if (!sources.length) { say(USE); process.exitCode = 1; return; }
|
|
867
|
+
const all = []; const errors = [];
|
|
868
|
+
for (const [raw, src] of sources) {
|
|
869
|
+
const parsed = R.parseFindings(raw, src);
|
|
870
|
+
all.push(...parsed.findings); errors.push(...parsed.errors);
|
|
871
|
+
}
|
|
872
|
+
const result = R.consolidate(all, { window });
|
|
873
|
+
if (argv.includes('--json')) say(JSON.stringify({ ...result, errors }, null, 2));
|
|
874
|
+
else say(R.renderConsolidation(result, errors));
|
|
875
|
+
// An unreadable block is a finding that may have been lost: say it with the exit code too.
|
|
876
|
+
if (errors.length) process.exitCode = 1;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
781
879
|
case 'worktrees': {
|
|
782
880
|
// The deterministic half of "the cleanup is the default". `ship` and the `worktrees` skill call
|
|
783
881
|
// this instead of judging by eye, which is what turns a rule that was only ever written down
|
package/skills/animar/SKILL.md
CHANGED
|
@@ -3,6 +3,7 @@ name: animar
|
|
|
3
3
|
description: "Use when asked to animate something, add motion, make a component feel alive, or build a transition. Builds the animation from scratch, making the decisions in the order that determines whether it feels right — whether it should animate at all, for what purpose, with which tool, which properties, which curve and duration, how it is interrupted, and how it exits. Writes the implementation. For critiquing existing motion use revisar-animaciones."
|
|
4
4
|
tags: ["animation", "motion", "transitions"]
|
|
5
5
|
profiles: [ui, full]
|
|
6
|
+
origin: damiangil
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Construir animaciones
|
|
@@ -3,6 +3,7 @@ name: claude-project-setup
|
|
|
3
3
|
description: "Use when starting a new project, initializing a repository for Claude Code, or when the user asks to configure CLAUDE.md, rules, commands, skills, or agents for a project."
|
|
4
4
|
tags: ["setup", "claude-code", "project-init"]
|
|
5
5
|
profiles: [core, ui, full]
|
|
6
|
+
origin: damiangil
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Claude Project Setup
|
|
@@ -3,6 +3,7 @@ name: code-review
|
|
|
3
3
|
description: "Reviews changes from a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow the repo's documented coding standards?) and Spec (does the code match what the source issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work in progress, or asks to \\"review from X\\"."
|
|
4
4
|
tags: ["code-review", "standards", "spec-compliance"]
|
|
5
5
|
profiles: [core, ui, full]
|
|
6
|
+
origin: damiangil
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
Revisión en dos ejes del diff entre `HEAD` y un punto fijo que proporciona el usuario:
|
|
@@ -75,7 +76,23 @@ Envía un único mensaje con dos llamadas a la herramienta `Agent`. Usa el subag
|
|
|
75
76
|
|
|
76
77
|
Si falta el spec, sáltate el sub-agente de Spec y anótalo en el reporte final.
|
|
77
78
|
|
|
78
|
-
### 5.
|
|
79
|
+
### 5. Verificar los graves antes de presentarlos
|
|
80
|
+
|
|
81
|
+
Un sub-agente que busca problemas puede exagerar uno. Antes de agregar, **dentro de cada eje
|
|
82
|
+
por separado** (nunca mezclando Estándares con Spec):
|
|
83
|
+
|
|
84
|
+
1. **Agrupar.** Pide a cada sub-agente que termine con un bloque ` ```json findings ` (`lens`, `severity`
|
|
85
|
+
blocker|should-fix|nit|question, `file`, `line`, `claim`, `failure_scenario`, `evidence`). Guarda
|
|
86
|
+
cada reporte y ejecuta `npx @damiangilgonzalez/harness review consolidate <reporte>`. Junta los
|
|
87
|
+
duplicados y baja a pregunta un blocker o should-fix sin `file:line` o sin escenario de fallo.
|
|
88
|
+
2. **Verificar.** Cada hallazgo que marque `verify` va al agente **`finding-verifier`** (si el
|
|
89
|
+
proyecto no lo tiene, a un `general-purpose` con esa misma postura): contexto limpio, parte de que
|
|
90
|
+
el hallazgo es falso y devuelve **CONFIRMED** (con la prueba), **REFUTED** (cita lo que lo hace
|
|
91
|
+
imposible) o **UNPROVEN** (pasa a pregunta). Solo CONFIRMED se presenta como grave.
|
|
92
|
+
|
|
93
|
+
Di la cuenta con honestidad ("7 reportados → 5 únicos → 2 confirmados") y lista lo refutado y por qué.
|
|
94
|
+
|
|
95
|
+
### 6. Agregar
|
|
79
96
|
|
|
80
97
|
Presenta los dos reportes bajo los encabezados `## Estándares` y `## Spec`, verbatim o ligeramente limpiados. **No** fusiones ni reordenes los hallazgos — los dos ejes son deliberadamente separados (ver _Por qué dos ejes_).
|
|
81
98
|
|
|
@@ -3,6 +3,7 @@ name: diseno-apple
|
|
|
3
3
|
description: "Apple's approach to interface design and fluid, physical motion, translated to the web. Use when building or reviewing gesture-driven UI, spring animations, drag/swipe/sheet interactions, inertia and interruptible transitions, translucent materials and depth, typography (optical size, tracking, line height), reduced motion, or the design fundamentals (feedback, spatial consistency, containment) behind Apple-style interfaces."
|
|
4
4
|
tags: ["design", "apple", "motion", "interface"]
|
|
5
5
|
profiles: [ui, full]
|
|
6
|
+
origin: damiangil
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Diseno Apple
|
|
@@ -3,6 +3,7 @@ name: diseno-landing
|
|
|
3
3
|
description: "Complete system for building high-conversion landing pages: intake questions, page structure, layout choice, conversion copy, SEO, plus strict visual rules for typography, spacing, radii, backgrounds, hero, icons, and motion. Use whenever building, editing, styling, reviewing, or writing copy for ANY landing page, marketing site, web UI, page section, component, or prototype, even if the user does not mention design, conversion, tokens, or a design system. Always consult it before choosing sections, headlines, CTAs, fonts, text sizes, spacing values, radii, background colors, icons, or transitions."
|
|
4
4
|
tags: ["landing-page", "conversion", "design-system", "copywriting"]
|
|
5
5
|
profiles: [ui, full]
|
|
6
|
+
origin: damiangil
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Diseno de landing page
|
|
@@ -3,6 +3,7 @@ name: domain-modeling
|
|
|
3
3
|
description: "Build and refine the project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to keep the domain model up to date."
|
|
4
4
|
tags: ["domain-modeling", "glossary", "architecture"]
|
|
5
5
|
profiles: [core, ui, full]
|
|
6
|
+
origin: damiangil
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Modelado de Dominio
|
|
@@ -4,6 +4,7 @@ description: "Use when work touches a screen — building or reshaping a landing
|
|
|
4
4
|
tags: ["ui-design", "routing", "workflow"]
|
|
5
5
|
profiles: [ui, full]
|
|
6
6
|
recommends: [ingeniero-diseno-web, diseno-landing, revision-interfaz]
|
|
7
|
+
origin: damiangil
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# Flujo de interfaz — quien manda en cada paso
|