wendkeep 0.32.0 → 0.34.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/CHANGELOG.md +51 -0
- package/README.md +14 -11
- package/README.pt-BR.md +3 -3
- package/bin/wendkeep.mjs +5 -4
- package/hooks/brain-inject.mjs +3 -3
- package/hooks/change-context.mjs +6 -15
- package/hooks/change-core.mjs +105 -22
- package/hooks/harness-doctor.mjs +16 -32
- package/hooks/spec-core.mjs +163 -3
- package/package.json +1 -1
- package/src/change.mjs +62 -9
- package/src/doctor.mjs +6 -0
- package/src/init.mjs +10 -5
- package/src/skills-seed.mjs +35 -24
- package/src/spec.mjs +68 -2
- package/src/sync-defs.mjs +84 -14
- package/src/verify.mjs +27 -3
package/src/spec.mjs
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
|
|
2
2
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
adoptSpecsState,
|
|
6
|
+
buildEffectiveRequirementPackage,
|
|
7
|
+
captureSpecBaseline,
|
|
8
|
+
discoverSpecDeltas,
|
|
9
|
+
parseRequirements,
|
|
10
|
+
specConflicts,
|
|
11
|
+
} from '../hooks/spec-core.mjs';
|
|
12
|
+
import { activeChange, parseTasks } from '../hooks/change-core.mjs';
|
|
5
13
|
import { getLocale } from '../hooks/locale.mjs';
|
|
6
14
|
|
|
7
15
|
function resolveVault(argv) {
|
|
@@ -24,6 +32,64 @@ export function runSpec(argv) {
|
|
|
24
32
|
const vaultBase = resolveVault(rest);
|
|
25
33
|
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
26
34
|
|
|
35
|
+
const option = (name) => {
|
|
36
|
+
const index = rest.indexOf(name);
|
|
37
|
+
if (index >= 0) return rest[index + 1];
|
|
38
|
+
const entry = rest.find((a) => a.startsWith(`${name}=`));
|
|
39
|
+
return entry ? entry.slice(name.length + 1) : undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (sub === 'effective') {
|
|
43
|
+
const slug = option('--change') || activeChange(vaultBase);
|
|
44
|
+
if (!slug) { process.stderr.write('wendkeep spec effective: no change (--change or current)\n'); process.exit(2); }
|
|
45
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
46
|
+
let tasks = [];
|
|
47
|
+
try {
|
|
48
|
+
readFileSync(join(changeDir, 'proposta.md'), 'utf8');
|
|
49
|
+
tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
|
|
52
|
+
const reqIds = [...new Set(tasks.map((task) => task.req).filter(Boolean))];
|
|
53
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
54
|
+
if (effective.errors.length) {
|
|
55
|
+
process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (rest.includes('--json')) {
|
|
59
|
+
process.stdout.write(`${JSON.stringify({ slug, effectiveSpecHash: effective.hash, specs: effective.specs }, null, 2)}\n`);
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write(`change: ${slug}\neffective-spec-hash: ${effective.hash}\n`);
|
|
62
|
+
for (const spec of effective.specs) {
|
|
63
|
+
process.stdout.write(`spec: ${spec.capability}\n`);
|
|
64
|
+
for (const req of spec.requirements) process.stdout.write(` ${req.operation === 'BASE' ? '=' : req.operation === 'ADDED' ? '+' : '~'} ${req.id || req.name} [${req.source}]\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sub === 'migrate') {
|
|
71
|
+
const state = adoptSpecsState(vaultBase);
|
|
72
|
+
process.stdout.write(`spec state adopted: ${Object.keys(state.specs).length} living spec(s); 07-Specs is generated/read-only\n`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sub === 'rebase') {
|
|
77
|
+
const slug = option('--change') || activeChange(vaultBase);
|
|
78
|
+
if (!slug) { process.stderr.write('wendkeep spec rebase: no change (--change or current)\n'); process.exit(2); }
|
|
79
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
80
|
+
try { readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
|
|
81
|
+
catch { process.stderr.write(`wendkeep spec rebase: change not found: ${slug}\n`); process.exit(2); }
|
|
82
|
+
const capabilities = discoverSpecDeltas(changeDir);
|
|
83
|
+
const conflicts = specConflicts(vaultBase, changeDir, capabilities);
|
|
84
|
+
if (conflicts.length && !rest.includes('--accept-current')) {
|
|
85
|
+
process.stderr.write(`wendkeep spec rebase: conflicts: ${conflicts.join('; ')} — reconcile delta, then rerun with --accept-current\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
captureSpecBaseline(vaultBase, changeDir, { refresh: true });
|
|
89
|
+
process.stdout.write(`spec baseline rebased: ${slug}${conflicts.length ? ` (${conflicts.length} conflict(s) accepted)` : ''}\n`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
27
93
|
if (sub === 'list') {
|
|
28
94
|
let files = [];
|
|
29
95
|
try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
|
|
@@ -49,6 +115,6 @@ export function runSpec(argv) {
|
|
|
49
115
|
process.exit(0);
|
|
50
116
|
}
|
|
51
117
|
|
|
52
|
-
process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
|
|
118
|
+
process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show, effective, migrate, rebase.\n`);
|
|
53
119
|
process.exit(2);
|
|
54
120
|
}
|
package/src/sync-defs.mjs
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// they travel with the project in git. They have no automatic consumer — agents
|
|
3
3
|
// read them from their own dirs — so `wendkeep sync-defs` copies them there:
|
|
4
4
|
// .brain/agents/*.toml -> <project>/.codex/agents/ (Codex agent format)
|
|
5
|
-
// .brain/skills/<name>/ -> <project>/.claude/skills/
|
|
5
|
+
// .brain/skills/<name>/ -> <project>/.claude/skills/ + .agents/skills/ (skill format)
|
|
6
6
|
// .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
|
|
7
7
|
// cross-platform robustness.
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
10
12
|
import { seedWkSkills } from './skills-seed.mjs';
|
|
11
13
|
import { getLocale } from '../hooks/locale.mjs';
|
|
12
14
|
|
|
@@ -15,6 +17,27 @@ import { getLocale } from '../hooks/locale.mjs';
|
|
|
15
17
|
// the markers is ours; user content around it is never touched.
|
|
16
18
|
const AG_START = '<!-- wendkeep:skills:start -->';
|
|
17
19
|
const AG_END = '<!-- wendkeep:skills:end -->';
|
|
20
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const WENDKEEP_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
22
|
+
const META_FILE = '.wendkeep-meta.json';
|
|
23
|
+
|
|
24
|
+
function directoryHash(root) {
|
|
25
|
+
const hash = createHash('sha256');
|
|
26
|
+
const visit = (dir) => {
|
|
27
|
+
let names = [];
|
|
28
|
+
try { names = readdirSync(dir).sort(); } catch { return; }
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
if (name === META_FILE) continue;
|
|
31
|
+
const path = join(dir, name);
|
|
32
|
+
const rel = relative(root, path).replaceAll('\\', '/');
|
|
33
|
+
const stat = statSync(path);
|
|
34
|
+
if (stat.isDirectory()) visit(path);
|
|
35
|
+
else { hash.update(rel); hash.update('\0'); hash.update(readFileSync(path)); hash.update('\0'); }
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
visit(root);
|
|
39
|
+
return hash.digest('hex');
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
function skillInventory(skillsSrc) {
|
|
20
43
|
const out = [];
|
|
@@ -31,9 +54,10 @@ function skillInventory(skillsSrc) {
|
|
|
31
54
|
return out;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
function renderAgentsSection(skills) {
|
|
57
|
+
function renderAgentsSection(skills, sourceHash = '') {
|
|
35
58
|
const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
|
|
36
59
|
return `${AG_START}
|
|
60
|
+
<!-- wendkeep-version: ${WENDKEEP_VERSION}; skills-sha256: ${sourceHash} -->
|
|
37
61
|
## wendkeep — process skills & loop
|
|
38
62
|
|
|
39
63
|
This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
|
|
@@ -41,9 +65,10 @@ through its change loop: \`wendkeep change new <slug>\` → implement tasks test
|
|
|
41
65
|
(tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
|
|
42
66
|
\`wendkeep verify --deep\` + an independent read-only verification pass writing
|
|
43
67
|
\`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
|
|
44
|
-
status\` / \`spec
|
|
68
|
+
status\` / \`spec effective --change <slug>\` / \`sensors list\`. Author specs only in
|
|
69
|
+
\`08-Mudanças/<slug>/specs/\`; \`07-Specs\` is generated and must not be edited directly.
|
|
45
70
|
|
|
46
|
-
Process skills (full text in \`.claude/skills
|
|
71
|
+
Process skills (full text in \`.claude/skills/\`, \`.agents/skills/\`, and the vault's \`.brain/skills/\`):
|
|
47
72
|
${list}
|
|
48
73
|
${AG_END}`;
|
|
49
74
|
}
|
|
@@ -52,7 +77,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
|
|
|
52
77
|
const skills = skillInventory(skillsSrc);
|
|
53
78
|
if (!skills.length) return false;
|
|
54
79
|
const path = join(projectPath, 'AGENTS.md');
|
|
55
|
-
const section = renderAgentsSection(skills);
|
|
80
|
+
const section = renderAgentsSection(skills, directoryHash(skillsSrc));
|
|
56
81
|
let content = '';
|
|
57
82
|
try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
|
|
58
83
|
if (content.includes(AG_START) && content.includes(AG_END)) {
|
|
@@ -67,7 +92,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
|
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
export function syncDefs(vaultBase, projectPath) {
|
|
70
|
-
const out = { agents: [], skills: [], agentsMd: false };
|
|
95
|
+
const out = { agents: [], skills: [], codexSkills: [], agentsMd: false };
|
|
71
96
|
|
|
72
97
|
const agentsSrc = join(vaultBase, '.brain', 'agents');
|
|
73
98
|
if (existsSync(agentsSrc)) {
|
|
@@ -85,10 +110,19 @@ export function syncDefs(vaultBase, projectPath) {
|
|
|
85
110
|
for (const name of readdirSync(skillsSrc)) {
|
|
86
111
|
const dir = join(skillsSrc, name);
|
|
87
112
|
if (!statSync(dir).isDirectory()) continue; // skip skills/README.md
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
const destinations = [
|
|
114
|
+
join(projectPath, '.claude', 'skills', name),
|
|
115
|
+
join(projectPath, '.agents', 'skills', name),
|
|
116
|
+
];
|
|
117
|
+
const sourceHash = directoryHash(dir);
|
|
118
|
+
for (const dest of destinations) {
|
|
119
|
+
rmSync(dest, { recursive: true, force: true });
|
|
120
|
+
mkdirSync(dest, { recursive: true });
|
|
121
|
+
cpSync(dir, dest, { recursive: true });
|
|
122
|
+
writeFileSync(join(dest, META_FILE), `${JSON.stringify({ wendkeepVersion: WENDKEEP_VERSION, sourceHash }, null, 2)}\n`, 'utf8');
|
|
123
|
+
}
|
|
91
124
|
out.skills.push(name);
|
|
125
|
+
out.codexSkills.push(name);
|
|
92
126
|
}
|
|
93
127
|
}
|
|
94
128
|
|
|
@@ -98,6 +132,33 @@ export function syncDefs(vaultBase, projectPath) {
|
|
|
98
132
|
return out;
|
|
99
133
|
}
|
|
100
134
|
|
|
135
|
+
export function checkSyncDefs(vaultBase, projectPath) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
const skillsSrc = join(vaultBase, '.brain', 'skills');
|
|
138
|
+
if (!existsSync(skillsSrc)) return { ok: false, issues: ['fonte .brain/skills ausente'] };
|
|
139
|
+
const names = readdirSync(skillsSrc).filter((name) => {
|
|
140
|
+
try { return statSync(join(skillsSrc, name)).isDirectory(); } catch { return false; }
|
|
141
|
+
});
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
const expected = directoryHash(join(skillsSrc, name));
|
|
144
|
+
for (const relDest of [join('.claude', 'skills', name), join('.agents', 'skills', name)]) {
|
|
145
|
+
const dest = join(projectPath, relDest);
|
|
146
|
+
if (!existsSync(join(dest, 'SKILL.md'))) { issues.push(`${relDest}: ausente`); continue; }
|
|
147
|
+
if (directoryHash(dest) !== expected) issues.push(`${relDest}: conteúdo divergiu da fonte`);
|
|
148
|
+
let meta = null;
|
|
149
|
+
try { meta = JSON.parse(readFileSync(join(dest, META_FILE), 'utf8')); } catch { /* missing */ }
|
|
150
|
+
if (meta?.wendkeepVersion !== WENDKEEP_VERSION || meta?.sourceHash !== expected) {
|
|
151
|
+
issues.push(`${relDest}: metadata stale/ausente (esperado WendKeep ${WENDKEEP_VERSION})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const expectedSection = renderAgentsSection(skillInventory(skillsSrc), directoryHash(skillsSrc));
|
|
156
|
+
let agentsMd = '';
|
|
157
|
+
try { agentsMd = readFileSync(join(projectPath, 'AGENTS.md'), 'utf8'); } catch { /* missing */ }
|
|
158
|
+
if (!agentsMd.includes(expectedSection)) issues.push('AGENTS.md: bloco gerenciado stale/ausente');
|
|
159
|
+
return { ok: issues.length === 0, issues, version: WENDKEEP_VERSION, skills: names.length };
|
|
160
|
+
}
|
|
161
|
+
|
|
101
162
|
// CLI entry for `wendkeep sync-defs`.
|
|
102
163
|
export function runSyncDefs(argv) {
|
|
103
164
|
let vault;
|
|
@@ -116,6 +177,15 @@ export function runSyncDefs(argv) {
|
|
|
116
177
|
}
|
|
117
178
|
const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
|
|
118
179
|
const projectPath = resolve(project || process.cwd());
|
|
180
|
+
if (argv.includes('--check')) {
|
|
181
|
+
const r = checkSyncDefs(vaultBase, projectPath);
|
|
182
|
+
if (r.ok) process.stdout.write(`wendkeep sync-defs --check: ok (${r.skills} skill(s), ${r.version})\n`);
|
|
183
|
+
else {
|
|
184
|
+
process.stderr.write(`wendkeep sync-defs --check: drift detectado\n - ${r.issues.join('\n - ')}\n`);
|
|
185
|
+
process.stderr.write('rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex\n');
|
|
186
|
+
}
|
|
187
|
+
process.exit(r.ok ? 0 : 1);
|
|
188
|
+
}
|
|
119
189
|
// --reseed (0.31.0): sobrescreve as wk-* de .brain/skills com os seeds da versão instalada
|
|
120
190
|
// ANTES de copiar — é como um vault existente recebe descriptions/HARD-GATE novos.
|
|
121
191
|
if (argv.includes('--reseed')) {
|
|
@@ -124,7 +194,7 @@ export function runSyncDefs(argv) {
|
|
|
124
194
|
}
|
|
125
195
|
const r = syncDefs(vaultBase, projectPath);
|
|
126
196
|
process.stdout.write(
|
|
127
|
-
`wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
|
|
197
|
+
`wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills + .agents/skills\n`,
|
|
128
198
|
);
|
|
129
199
|
if (r.agents.length) process.stdout.write(` agents: ${r.agents.join(', ')}\n`);
|
|
130
200
|
if (r.skills.length) process.stdout.write(` skills: ${r.skills.join(', ')}\n`);
|
|
@@ -151,8 +221,8 @@ model = "gpt-5.5"
|
|
|
151
221
|
const SKILLS_README = `# .brain/skills — versioned custom skill definitions
|
|
152
222
|
|
|
153
223
|
Canonical, versioned source for your project's custom skills (\`<name>/SKILL.md\`).
|
|
154
|
-
\`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\`
|
|
155
|
-
|
|
224
|
+
\`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` and
|
|
225
|
+
\`<project>/.agents/skills/\`. Edit here (source of truth); re-run sync after changes.
|
|
156
226
|
`;
|
|
157
227
|
|
|
158
228
|
const EXAMPLE_SKILL = `---
|
package/src/verify.mjs
CHANGED
|
@@ -5,7 +5,11 @@ import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
|
|
7
7
|
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildEffectiveRequirementPackage,
|
|
10
|
+
captureSpecBaseline,
|
|
11
|
+
tasksHashOf,
|
|
12
|
+
} from '../hooks/spec-core.mjs';
|
|
9
13
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
10
14
|
import { getLocale } from '../hooks/locale.mjs';
|
|
11
15
|
|
|
@@ -88,16 +92,36 @@ export function runVerify(argv) {
|
|
|
88
92
|
const tasks = parseTasks(tarefas);
|
|
89
93
|
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
90
94
|
const tasksHash = tasksHashOf(tarefas);
|
|
95
|
+
captureSpecBaseline(vaultBase, changeDir);
|
|
96
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
97
|
+
if (effective.errors.length) {
|
|
98
|
+
process.stderr.write(`verify --deep: spec efetiva inválida: ${effective.errors.join('; ')}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (effective.missing.length) {
|
|
102
|
+
process.stderr.write(`verify --deep: requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}\n`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
91
105
|
const pkg = {
|
|
92
106
|
slug,
|
|
93
107
|
tasksHash,
|
|
94
|
-
|
|
108
|
+
effectiveSpecHash: effective.hash,
|
|
109
|
+
requirements: effective.requirements.map((req) => {
|
|
110
|
+
return {
|
|
111
|
+
id: req.id,
|
|
112
|
+
name: req.name,
|
|
113
|
+
capability: req.capability,
|
|
114
|
+
operation: req.operation,
|
|
115
|
+
source: req.source,
|
|
116
|
+
body: req.body,
|
|
117
|
+
};
|
|
118
|
+
}),
|
|
95
119
|
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
96
120
|
sensors: evidence,
|
|
97
121
|
};
|
|
98
122
|
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
99
123
|
if (reqIds.length === 0) {
|
|
100
|
-
writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
|
|
124
|
+
writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
|
|
101
125
|
process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
|
|
102
126
|
} else {
|
|
103
127
|
process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');
|