@maestroagora/agora 1.2.1 → 1.3.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/.agents/plugins/marketplace.json +21 -21
- package/.claude-plugin/marketplace.json +13 -13
- package/.claude-plugin/plugin.json +21 -21
- package/.codex-plugin/plugin.json +33 -33
- package/LICENSE +21 -21
- package/README.md +58 -5
- package/assets/agora-orbit.svg +158 -158
- package/package.json +58 -55
- package/scripts/install.mjs +400 -400
- package/scripts/voice/check.mjs +175 -0
- package/scripts/voice/features.mjs +359 -0
- package/scripts/voice/gates.mjs +244 -0
- package/scripts/voice/ingest.mjs +226 -0
- package/scripts/voice/lexicon.mjs +162 -0
- package/scripts/voice/pipeline.mjs +186 -0
- package/scripts/voice/profile.mjs +528 -0
- package/scripts/voice-measure.mjs +369 -0
- package/skills/agora/SKILL.md +161 -15
- package/skills/agora/agents/openai.yaml +4 -4
- package/skills/agora/references/agora-craft.md +391 -0
- package/skills/agora/references/agora-marketing.md +653 -23
- package/skills/agora/references/agora-voice.md +262 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// agora-voice: the measurement engine behind VOICE.
|
|
4
|
+
//
|
|
5
|
+
// `voice build` is a measurement task, not a description task. A model asked to
|
|
6
|
+
// describe an author's voice writes flattery, so every number this tool reports
|
|
7
|
+
// is computed from the corpus by a frozen pipeline and anything the corpus
|
|
8
|
+
// cannot support is written as insufficient data rather than guessed.
|
|
9
|
+
//
|
|
10
|
+
// Profiles are written to ~/.agora/voices/, never inside the skill directory.
|
|
11
|
+
// The documented update path replaces the installed skill directory, so a
|
|
12
|
+
// profile stored there is destroyed silently on the next update.
|
|
13
|
+
|
|
14
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { basename, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
import { buildOverlapIndex, compare, phraseOverlap, renderReport } from "./voice/check.mjs";
|
|
20
|
+
import { measure } from "./voice/features.mjs";
|
|
21
|
+
import { runGates } from "./voice/gates.mjs";
|
|
22
|
+
import { collectCorpus } from "./voice/ingest.mjs";
|
|
23
|
+
import { PIPELINE } from "./voice/pipeline.mjs";
|
|
24
|
+
import { renderProfile } from "./voice/profile.mjs";
|
|
25
|
+
|
|
26
|
+
const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
27
|
+
|
|
28
|
+
function usage() {
|
|
29
|
+
return `agora-voice: measure a corpus, write a voice profile, check a draft against one.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
agora-voice build --name <slug> --from <path|url> [--from <path|url>]...
|
|
33
|
+
agora-voice list
|
|
34
|
+
agora-voice check --voice <slug> <draft path>
|
|
35
|
+
agora-voice default --voice <slug>
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--name <slug> Profile slug. Lowercase letters, digits, and hyphens.
|
|
39
|
+
--from <path|url> Corpus source. A file, a directory, or an http(s) URL.
|
|
40
|
+
Repeatable. Directories expand to readable text files.
|
|
41
|
+
--register <name> Label applied to every --from that follows it, until the
|
|
42
|
+
next --register. A register earns its own numbers only at
|
|
43
|
+
2,500 clean words across 3 independent documents.
|
|
44
|
+
--voice <slug> Profile to check against or to make the default.
|
|
45
|
+
--store <path> Profile directory. Defaults to ~/.agora/voices.
|
|
46
|
+
--keep-headings Count headings as author prose. Off by default, because
|
|
47
|
+
headlines are often house-written.
|
|
48
|
+
--now <iso date> Freeze the created and updated dates. For reproducible runs.
|
|
49
|
+
--json Machine-readable output.
|
|
50
|
+
-h, --help Show this help.
|
|
51
|
+
|
|
52
|
+
Markdown, plain text, and HTML are read. Binary document formats are refused by
|
|
53
|
+
name rather than partially extracted, because a partial extraction would move
|
|
54
|
+
every admission threshold without saying so.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
agora-voice build --name house --register blog --from ./posts --register email --from ./letters
|
|
58
|
+
agora-voice check --voice house ./draft.md`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseArgs(argv) {
|
|
62
|
+
const options = {
|
|
63
|
+
command: null,
|
|
64
|
+
name: null,
|
|
65
|
+
voice: null,
|
|
66
|
+
store: null,
|
|
67
|
+
sources: [],
|
|
68
|
+
positional: [],
|
|
69
|
+
keepHeadings: false,
|
|
70
|
+
now: null,
|
|
71
|
+
json: false,
|
|
72
|
+
help: false,
|
|
73
|
+
};
|
|
74
|
+
let register = null;
|
|
75
|
+
|
|
76
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
77
|
+
const arg = argv[index];
|
|
78
|
+
const takeValue = () => {
|
|
79
|
+
const next = argv[index + 1];
|
|
80
|
+
if (!next || next.startsWith("--")) throw new Error(`${arg} requires a value`);
|
|
81
|
+
index += 1;
|
|
82
|
+
return next;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (arg === "-h" || arg === "--help") options.help = true;
|
|
86
|
+
else if (arg === "--json") options.json = true;
|
|
87
|
+
else if (arg === "--keep-headings") options.keepHeadings = true;
|
|
88
|
+
else if (arg === "--name") options.name = takeValue();
|
|
89
|
+
else if (arg === "--voice") options.voice = takeValue();
|
|
90
|
+
else if (arg === "--store") options.store = takeValue();
|
|
91
|
+
else if (arg === "--now") options.now = takeValue();
|
|
92
|
+
else if (arg === "--register") register = takeValue();
|
|
93
|
+
else if (arg === "--from") options.sources.push({ source: takeValue(), register });
|
|
94
|
+
else if (arg.startsWith("--")) throw new Error(`unknown option: ${arg}`);
|
|
95
|
+
else if (options.command === null) options.command = arg;
|
|
96
|
+
else options.positional.push(arg);
|
|
97
|
+
}
|
|
98
|
+
options.register = register;
|
|
99
|
+
return options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function storeDirectory(options) {
|
|
103
|
+
return options.store ? resolve(options.store) : join(homedir(), ".agora", "voices");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function readIndex(directory) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(await readFile(join(directory, "index.json"), "utf8"));
|
|
109
|
+
} catch {
|
|
110
|
+
return { default: null, profiles: {} };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function writeIndex(directory, index) {
|
|
115
|
+
await writeFile(join(directory, "index.json"), `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertSlug(slug, option) {
|
|
119
|
+
if (!slug) throw new Error(`${option} is required`);
|
|
120
|
+
if (!SLUG_PATTERN.test(slug)) {
|
|
121
|
+
throw new Error(`${option} must be lowercase letters, digits, and hyphens: got '${slug}'`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reportGates(gates, corpus, refusedFirst = true) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
if (refusedFirst && corpus.refused.length > 0) {
|
|
128
|
+
lines.push("Refused sources:", ...corpus.refused.map((message) => ` ${message}`), "");
|
|
129
|
+
}
|
|
130
|
+
lines.push(
|
|
131
|
+
`Documents: ${corpus.documents.length}`,
|
|
132
|
+
`Clean words: ${gates.clean_words} (raw ${corpus.documents.reduce((total, document) => total + document.raw_words, 0)})`,
|
|
133
|
+
`Tier: ${gates.tier.disposition}. ${gates.tier.reason}`,
|
|
134
|
+
`Independence: ${gates.independence.passed ? "passed" : "failed"}`,
|
|
135
|
+
);
|
|
136
|
+
for (const failure of gates.independence.failures) lines.push(` ${failure}`);
|
|
137
|
+
lines.push("Registers:");
|
|
138
|
+
for (const entry of gates.registers) {
|
|
139
|
+
lines.push(
|
|
140
|
+
` ${entry.register}: ${entry.clean_words} clean words across ${entry.documents} documents, ${entry.numeric ? "own numbers issued" : `qualitative only (${entry.note})`}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (gates.stability.length > 0) {
|
|
144
|
+
const unstable = gates.stability.filter((entry) => !entry.stable);
|
|
145
|
+
lines.push(
|
|
146
|
+
`Feature stability: ${gates.stability.length - unstable.length} of ${gates.stability.length} core features stable`,
|
|
147
|
+
);
|
|
148
|
+
for (const entry of unstable) lines.push(` dropped ${entry.feature}: ${entry.reason}`);
|
|
149
|
+
}
|
|
150
|
+
if (gates.heterogeneity.stopped) {
|
|
151
|
+
lines.push("Heterogeneity stop: this corpus is not profileable as one voice.");
|
|
152
|
+
for (const reason of gates.heterogeneity.reasons) lines.push(` ${reason}`);
|
|
153
|
+
if (gates.heterogeneity.remedy) lines.push(` ${gates.heterogeneity.remedy}`);
|
|
154
|
+
}
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function build(options) {
|
|
159
|
+
assertSlug(options.name, "--name");
|
|
160
|
+
if (options.sources.length === 0) throw new Error("build needs at least one --from source");
|
|
161
|
+
|
|
162
|
+
// Collect one source group at a time so each document carries the register
|
|
163
|
+
// label that was in force when its --from was named.
|
|
164
|
+
const corpus = { documents: [], refused: [] };
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
for (const entry of options.sources) {
|
|
167
|
+
const group = await collectCorpus([entry.source], { keepHeadings: options.keepHeadings });
|
|
168
|
+
corpus.refused.push(...group.refused);
|
|
169
|
+
for (const document of group.documents) {
|
|
170
|
+
if (seen.has(document.source)) continue;
|
|
171
|
+
seen.add(document.source);
|
|
172
|
+
corpus.documents.push({ ...document, register: entry.register });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
corpus.documents.sort((left, right) => left.source.localeCompare(right.source));
|
|
176
|
+
|
|
177
|
+
if (corpus.documents.length === 0) {
|
|
178
|
+
const detail = corpus.refused.length > 0 ? `\n\nRefused sources:\n ${corpus.refused.join("\n ")}` : "";
|
|
179
|
+
throw new Error(`no readable documents were found in the supplied sources${detail}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pooledText = corpus.documents.map((document) => document.text).join("\n\n");
|
|
183
|
+
const measured = measure(pooledText);
|
|
184
|
+
const gates = runGates(corpus.documents, measured);
|
|
185
|
+
const now = options.now || new Date().toISOString().slice(0, 10);
|
|
186
|
+
|
|
187
|
+
if (!gates.certified) {
|
|
188
|
+
const report = [
|
|
189
|
+
"No profile was certified.",
|
|
190
|
+
"",
|
|
191
|
+
reportGates(gates, corpus),
|
|
192
|
+
"",
|
|
193
|
+
"Nothing was written. Report what the corpus needs, supply it, and rerun.",
|
|
194
|
+
].join("\n");
|
|
195
|
+
if (options.json) {
|
|
196
|
+
process.stdout.write(`${JSON.stringify({ certified: false, gates, refused: corpus.refused }, null, 2)}\n`);
|
|
197
|
+
} else {
|
|
198
|
+
process.stdout.write(`${report}\n`);
|
|
199
|
+
}
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const directory = storeDirectory(options);
|
|
205
|
+
await mkdir(directory, { recursive: true });
|
|
206
|
+
const profilePath = join(directory, `${options.name}.md`);
|
|
207
|
+
const measurementsPath = join(directory, `${options.name}.measurements.json`);
|
|
208
|
+
|
|
209
|
+
await writeFile(profilePath, renderProfile({ name: options.name, measured, gates, corpus, pipeline: PIPELINE, now }), "utf8");
|
|
210
|
+
await writeFile(
|
|
211
|
+
measurementsPath,
|
|
212
|
+
`${JSON.stringify(
|
|
213
|
+
{
|
|
214
|
+
name: options.name,
|
|
215
|
+
updated: now,
|
|
216
|
+
pipeline: PIPELINE,
|
|
217
|
+
confidence: gates.tier.confidence,
|
|
218
|
+
measured,
|
|
219
|
+
registers: gates.registers.map((entry) => ({
|
|
220
|
+
register: entry.register,
|
|
221
|
+
numeric: entry.numeric,
|
|
222
|
+
measured: entry.measured,
|
|
223
|
+
})),
|
|
224
|
+
// Hashed token runs only. The store never holds the corpus prose.
|
|
225
|
+
overlap_index: buildOverlapIndex(corpus.documents),
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
2,
|
|
229
|
+
)}\n`,
|
|
230
|
+
"utf8",
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const index = await readIndex(directory);
|
|
234
|
+
index.profiles[options.name] = {
|
|
235
|
+
documents: corpus.documents.length,
|
|
236
|
+
clean_words: gates.clean_words,
|
|
237
|
+
registers: gates.registers.map((entry) => entry.register),
|
|
238
|
+
confidence: gates.tier.confidence,
|
|
239
|
+
pipeline: PIPELINE,
|
|
240
|
+
updated: now,
|
|
241
|
+
};
|
|
242
|
+
if (!index.default) index.default = options.name;
|
|
243
|
+
await writeIndex(directory, index);
|
|
244
|
+
|
|
245
|
+
if (options.json) {
|
|
246
|
+
process.stdout.write(`${JSON.stringify({ certified: true, profile: profilePath, gates }, null, 2)}\n`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
process.stdout.write(
|
|
250
|
+
[
|
|
251
|
+
`Wrote ${profilePath}`,
|
|
252
|
+
`Wrote ${measurementsPath}`,
|
|
253
|
+
"",
|
|
254
|
+
reportGates(gates, corpus),
|
|
255
|
+
"",
|
|
256
|
+
"Read the profile's `## Not captured` section before writing with it. It lists what this corpus could not tell us.",
|
|
257
|
+
"",
|
|
258
|
+
].join("\n"),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function list(options) {
|
|
263
|
+
const directory = storeDirectory(options);
|
|
264
|
+
const index = await readIndex(directory);
|
|
265
|
+
const names = Object.keys(index.profiles).sort();
|
|
266
|
+
if (options.json) {
|
|
267
|
+
process.stdout.write(`${JSON.stringify({ store: directory, ...index }, null, 2)}\n`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (names.length === 0) {
|
|
271
|
+
process.stdout.write(`No profiles in ${directory}.\n`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write(`Profiles in ${directory}:\n`);
|
|
275
|
+
for (const name of names) {
|
|
276
|
+
const entry = index.profiles[name];
|
|
277
|
+
process.stdout.write(
|
|
278
|
+
` ${name}${index.default === name ? " (default)" : ""}: ${entry.clean_words} clean words, ${entry.documents} documents, registers [${entry.registers.join(", ")}], confidence ${entry.confidence}, updated ${entry.updated}\n`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function check(options) {
|
|
284
|
+
const directory = storeDirectory(options);
|
|
285
|
+
const index = await readIndex(directory);
|
|
286
|
+
const slug = options.voice || index.default;
|
|
287
|
+
assertSlug(slug, "--voice");
|
|
288
|
+
const draftPath = options.positional[0];
|
|
289
|
+
if (!draftPath) throw new Error("check needs a draft path");
|
|
290
|
+
|
|
291
|
+
let stored;
|
|
292
|
+
try {
|
|
293
|
+
stored = JSON.parse(await readFile(join(directory, `${slug}.measurements.json`), "utf8"));
|
|
294
|
+
} catch {
|
|
295
|
+
throw new Error(`no measurements found for '${slug}' in ${directory}. Run build first.`);
|
|
296
|
+
}
|
|
297
|
+
for (const [stage, version] of Object.entries(PIPELINE)) {
|
|
298
|
+
if (stored.pipeline[stage] !== version) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`pipeline mismatch on ${stage}: the profile was built with '${stored.pipeline[stage]}' and this tool runs '${version}'. A comparison across two pipelines is not a comparison. Rebuild the profile.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const draft = await readFile(resolve(draftPath), "utf8");
|
|
306
|
+
const requested = options.register ?? null;
|
|
307
|
+
const subprofile = requested
|
|
308
|
+
? stored.registers.find((entry) => entry.register === requested && entry.numeric)
|
|
309
|
+
: null;
|
|
310
|
+
const registerMatched = requested === null || Boolean(subprofile);
|
|
311
|
+
const reference = subprofile?.measured ?? stored.measured;
|
|
312
|
+
|
|
313
|
+
const result = compare(draft, reference, { registerMatched });
|
|
314
|
+
const overlaps = phraseOverlap(draft, stored.overlap_index);
|
|
315
|
+
|
|
316
|
+
if (options.json) {
|
|
317
|
+
process.stdout.write(`${JSON.stringify({ voice: slug, draft: basename(draftPath), result, overlaps }, null, 2)}\n`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write(
|
|
321
|
+
`Checking ${basename(draftPath)} against '${slug}'${requested ? ` (register ${requested})` : ""}\n\n${renderReport(result, overlaps)}\n`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function setDefault(options) {
|
|
326
|
+
assertSlug(options.voice, "--voice");
|
|
327
|
+
const directory = storeDirectory(options);
|
|
328
|
+
const index = await readIndex(directory);
|
|
329
|
+
if (!index.profiles[options.voice]) throw new Error(`no profile named '${options.voice}' in ${directory}`);
|
|
330
|
+
index.default = options.voice;
|
|
331
|
+
await writeIndex(directory, index);
|
|
332
|
+
process.stdout.write(`Default voice is now '${options.voice}'.\n`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function main() {
|
|
336
|
+
let options;
|
|
337
|
+
try {
|
|
338
|
+
options = parseArgs(process.argv.slice(2));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
process.stderr.write(`Error: ${error.message}\n\n${usage()}\n`);
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (options.help || options.command === null) {
|
|
345
|
+
process.stdout.write(`${usage()}\n`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const commands = { build, list, check, default: setDefault };
|
|
350
|
+
const handler = commands[options.command];
|
|
351
|
+
if (!handler) {
|
|
352
|
+
process.stderr.write(`Error: unknown command '${options.command}'\n\n${usage()}\n`);
|
|
353
|
+
process.exitCode = 1;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
await handler(options);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Run only when invoked as a command, so the tests can import the parser.
|
|
365
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
366
|
+
await main();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export { main, parseArgs, storeDirectory };
|
package/skills/agora/SKILL.md
CHANGED
|
@@ -9,18 +9,38 @@ description: Write, rewrite, shorten, critique, or plan truthful argument-first
|
|
|
9
9
|
|
|
10
10
|
Treat `/agora` as explicit activation. Use all text after the command as the task. If no task follows, ask for the asset or source material.
|
|
11
11
|
|
|
12
|
+
## Enforce the hard em-dash ban
|
|
13
|
+
|
|
14
|
+
Never emit the Unicode em dash character U+2014 anywhere in a response while this skill is active. Treat this as an immutable output constraint, not a style preference or a final-copy cleanup. Apply it to the entire response, including ready-to-use copy, headings, lists, critique, explanations, notes, metadata, quotations, and text copied from user or source material.
|
|
15
|
+
|
|
16
|
+
Do not repeat U+2014 from an input. Replace it with a period, comma, colon, semicolon, parentheses, or plain hyphen as grammar requires. If an exact quotation contains U+2014, paraphrase it or state that it cannot be reproduced verbatim under the active constraint. Never alter a quotation and still present it as exact.
|
|
17
|
+
|
|
18
|
+
Immediately before returning, scan the complete response character by character for U+2014. Replace every occurrence, then scan again. Return only after the count is zero.
|
|
19
|
+
|
|
12
20
|
## Load the authority progressively
|
|
13
21
|
|
|
14
22
|
Use [references/agora-marketing.md](references/agora-marketing.md) as the canonical authority. Read only the sections the task needs:
|
|
15
23
|
|
|
16
|
-
1. Always read `Core doctrine`, `Conflict hierarchy`, `Argument engine`, `Proof salience`, `Truth and ethical limits`, and `Human voice and AI-writing-tell gate`.
|
|
24
|
+
1. Always read `Core doctrine`, `Conflict hierarchy`, `Argument engine`, `Proof salience`, `Plain language and first-read comprehension`, `Truth and ethical limits`, and `Human voice and AI-writing-tell gate`.
|
|
17
25
|
2. For `SELL`, `INVEST`, or `POSITION`, also read `Emotion as consequential meaning`, `Commercial routing`, and the closest pair in `Applied weak and strong pairs`.
|
|
26
|
+
2b. For any asset containing a call to action, button, or closing invitation, also read `CTA standard`.
|
|
18
27
|
3. For a written asset, read `Written GEO/AEO and citability`. For indexable public work, also read `Technical publication boundaries`.
|
|
19
28
|
4. For spoken work, read `Spoken delivery`. Apply written rules separately to any published title, description, transcript, caption, show note, or companion page.
|
|
20
29
|
5. Read `Evidence register` only when making or reviewing a research-backed general claim.
|
|
21
30
|
|
|
22
31
|
Locate the named headings and read those sections only. Do not load the entire reference unless the task genuinely spans most of it.
|
|
23
32
|
|
|
33
|
+
[references/agora-craft.md](references/agora-craft.md) is a second, narrower authority covering four domains the first one does not. Load it only for the job it covers:
|
|
34
|
+
|
|
35
|
+
- `Headlines and titles` for any headline, subheading, page title, search title, social title, subject line, video title, or a set of headings written for one deliverable.
|
|
36
|
+
- `Awareness and sophistication staging` when the brief states or implies what the reader already knows, when deciding whether to name a mechanism, or when routing one fact set across several reader states.
|
|
37
|
+
- `Emotion under a truth constraint` when choosing the emotional job, and whenever the supplied facts contain no outcome data, testimonials, or market claims.
|
|
38
|
+
- `Prosody and rhythm` when rewriting for cadence, when a draft reads as machine-uniform, or when applying an authorized voice profile.
|
|
39
|
+
|
|
40
|
+
Do not load it for routine drafting, claim review, compliance questions, or work that the first reference already covers.
|
|
41
|
+
|
|
42
|
+
[references/agora-voice.md](references/agora-voice.md) governs `VOICE`. Load it only when the task builds a voice profile, writes with `--voice`, inspects a profile, or checks a draft against one. Do not load it for ordinary human-voice cleanup, which the tell gate already covers.
|
|
43
|
+
|
|
24
44
|
Treat source material supplied in the current task as the available product truth. Do not import facts, claim rules, or release controls from another task, repository, company, or example. Examples teach structure, never facts.
|
|
25
45
|
|
|
26
46
|
## Resolve conflicts
|
|
@@ -29,13 +49,21 @@ Apply this order:
|
|
|
29
49
|
|
|
30
50
|
1. Truth, safety, law, and immutable requirements.
|
|
31
51
|
2. Supplied facts, approved sources, and material qualifiers.
|
|
32
|
-
3.
|
|
33
|
-
4.
|
|
34
|
-
5.
|
|
35
|
-
6.
|
|
36
|
-
7.
|
|
52
|
+
3. Immediate comprehension by the intended audience.
|
|
53
|
+
4. The requested decision and surface.
|
|
54
|
+
5. Decision relevance, proof salience, and differentiation.
|
|
55
|
+
6. Emotional relevance and channel fit.
|
|
56
|
+
7. Compression, rhythm, style, and publication optimization.
|
|
37
57
|
|
|
38
|
-
|
|
58
|
+
Compression, cleverness, citability, technical precision, and rhetorical force never make the writing harder to understand than the facts require. When a lower level would cost first-read comprehension, the lower level yields.
|
|
59
|
+
|
|
60
|
+
Two specific conflicts resolve as follows, because both have produced accurate but unreadable copy:
|
|
61
|
+
|
|
62
|
+
- **Qualification against comprehension.** Level 2 requires preserving scope, date, condition, and uncertainty. It does not require carrying every qualifier inside every sentence. Qualify at the passage or section level. A sentence that carries its full qualification set inline reads as a compliance memo and fails level 3.
|
|
63
|
+
- **Citability against comprehension.** Written GEO/AEO asks for passages that stay accurate when quoted alone. That rule governs the passage, not the sentence. Do not compress a paragraph of context into one self-sufficient sentence. Self-containment is achieved by keeping a short passage together, not by loading one clause.
|
|
64
|
+
- **Voice against everything above it.** An active voice profile enters at level 6. It never licenses a claim the facts do not support, never overrides required or legal phrasing, and never overrides the U+2014 ban. Where an author's habitual certainty exceeds the evidence, the evidence wins and the profile yields for that sentence.
|
|
65
|
+
|
|
66
|
+
When soft rules conflict, preserve the strongest truthful argument that the reader can follow on the first pass. Ask only when missing information would materially change the audience, offer, claim, or action. Otherwise narrow or omit the unsupported point.
|
|
39
67
|
|
|
40
68
|
## Choose the job
|
|
41
69
|
|
|
@@ -49,6 +77,21 @@ Select one primary mode. An explicit mode wins unless it would require factual d
|
|
|
49
77
|
| `INFORM` | Editorial or educational work |
|
|
50
78
|
| `TRANSACT` | Buttons, confirmations, alerts, forms, or utility microcopy |
|
|
51
79
|
|
|
80
|
+
`VOICE` is the exception to selecting one mode. It is a modifier, not a job: `--voice <name>` loads a measured author profile on top of whichever mode was already chosen, and `voice build`, `voice list`, and `voice check` are its own operations. Profiles are stored at `~/.agora/voices/`, never inside the skill directory, because the documented update path replaces that directory and would destroy them.
|
|
81
|
+
|
|
82
|
+
Apply the default profile to every mode. When `~/.agora/voices/index.json` names a default and the request carries no voice instruction, load that profile for `POSITION`, `SELL`, `INVEST`, `INFORM`, and `TRANSACT` alike:
|
|
83
|
+
|
|
84
|
+
| Instruction | Effect |
|
|
85
|
+
|---|---|
|
|
86
|
+
| none, and a default profile exists | Load the default profile |
|
|
87
|
+
| `--voice <name>` | Load that profile instead of the default |
|
|
88
|
+
| `--no-voice` or `neutral` | Load no profile at all |
|
|
89
|
+
| none, and no profile exists | Nothing to load; write as normal |
|
|
90
|
+
|
|
91
|
+
Default-on changes nothing above level 6. Required phrasing, legal and regulatory wording, disclosures, accurate quotation, and the U+2014 ban continue to outrank the profile. That matters most on `TRANSACT` microcopy and `INVEST` material, where a required form is common and the profile yields to it for that string.
|
|
92
|
+
|
|
93
|
+
Measurement is computed, never estimated from reading. Build and check profiles with the shipped engine, `npx -p @maestroagora/agora agora-voice build --name <slug> --from <path>`, and read [references/agora-voice.md](references/agora-voice.md) before running it. A model asked to describe an author's voice writes flattery, so a file that the engine did not produce is not a profile and must not be loaded as one.
|
|
94
|
+
|
|
52
95
|
Directory placement or an investor-adjacent audience does not activate `INVEST` by itself. Keep investor relevance implicit in descriptive profiles. Do not write phrases such as `for investors`, `investors should consider`, or `merits evaluation` unless the user explicitly requires that wording.
|
|
53
96
|
|
|
54
97
|
## Route the surface separately
|
|
@@ -97,6 +140,94 @@ Rank candidate facts by decision relevance, differentiation, verifiability, spec
|
|
|
97
140
|
|
|
98
141
|
Every included fact must prove a premise, resolve an objection, distinguish the mechanism, or enable action.
|
|
99
142
|
|
|
143
|
+
## Pass the first-read comprehension gate
|
|
144
|
+
|
|
145
|
+
Factual accuracy is not sufficient. Copy also fails when the wording makes the reader decode internal terminology, reconstruct a missing relationship, or translate an abstraction into a concrete action.
|
|
146
|
+
|
|
147
|
+
Plain language is not simple language. It is precise language with low decoding effort. Expert audiences keep their technical precision and still lose the unnecessary abstraction, compressed syntax, and in-house shorthand.
|
|
148
|
+
|
|
149
|
+
### Model the reader
|
|
150
|
+
|
|
151
|
+
Write for someone who is intelligent, understands their own job, has not read the documentation, does not know the organization's internal vocabulary, will not stop to decode a sentence, and is deciding whether the next line deserves attention. Familiarity with an industry is not familiarity with one organization's terms.
|
|
152
|
+
|
|
153
|
+
### Test every customer-facing sentence
|
|
154
|
+
|
|
155
|
+
Rewrite any sentence an intended reader could not restate after reading it once. A sentence fails when it:
|
|
156
|
+
|
|
157
|
+
- depends on undefined internal terminology;
|
|
158
|
+
- introduces more than one unfamiliar concept at a time;
|
|
159
|
+
- hides the actor, the action, the object, or the result;
|
|
160
|
+
- uses an abstract noun where a concrete verb would be clearer;
|
|
161
|
+
- describes an internal method instead of what the reader needs to know;
|
|
162
|
+
- compresses several reasoning steps into insider shorthand;
|
|
163
|
+
- sounds like a specification, a compliance memo, or an academic method section when the surface does not call for that register;
|
|
164
|
+
- is technically correct and practically unclear;
|
|
165
|
+
- needs a different paragraph to become understandable;
|
|
166
|
+
- sounds impressive before it communicates anything concrete.
|
|
167
|
+
|
|
168
|
+
### Gate specialized terms
|
|
169
|
+
|
|
170
|
+
Accuracy alone does not license an internal product, operational, analytical, or methodological term. Before using one, confirm all four:
|
|
171
|
+
|
|
172
|
+
1. The intended audience already knows it.
|
|
173
|
+
2. It is necessary for accuracy.
|
|
174
|
+
3. Its meaning is clear from the sentence it appears in.
|
|
175
|
+
4. An everyday expression would lose material meaning.
|
|
176
|
+
|
|
177
|
+
If any answer is no, replace the term or define it in place. Every term the reader does not already own is either decision-required and taught where it appears, or removed. Treat a sentence carrying two or more reader-unowned terms as a review trigger and not as an automatic error, and treat that count as a governance default rather than a measured limit.
|
|
178
|
+
|
|
179
|
+
Treat a noun the organization coined as a term the reader has no reason to know. Naming an internal method, stage, score, record type, or framework in customer-facing copy requires the reader to gain something from learning it. Otherwise state what happens and drop the name.
|
|
180
|
+
|
|
181
|
+
### Prefer actor, action, object, result
|
|
182
|
+
|
|
183
|
+
Answer these before drafting a sentence: who or what acts, what it does, what it acts on, and what changes for the reader.
|
|
184
|
+
|
|
185
|
+
Choose the highest-frequency verb the reader already owns that preserves the factual relation and tells them what the action does. Verbs such as check, compare, find, show, verify, measure, review, choose, send, create, remove, correct, approve, reject, schedule, and calculate are a useful house lexicon, not a required set. A specialized verb wins whenever it names the exact action and the reader owns that word.
|
|
186
|
+
|
|
187
|
+
Rewrite noun-heavy constructions into direct actions. Abstract nouns are not banned and must not be counted. Rewrite an abstraction when it conceals an actor, action, causal relation, or consequence the reader needs, and leave it when it carries cohesion or names an established concept. The check on each abstract noun is whether a specific actor, a finite action, and the object or result can still be recovered, and whether the reader needs them.
|
|
188
|
+
|
|
189
|
+
Weak:
|
|
190
|
+
|
|
191
|
+
> Evaluate whether configured operational surfaces produced compliant outcomes for the declared workflow scope.
|
|
192
|
+
|
|
193
|
+
Strong:
|
|
194
|
+
|
|
195
|
+
> Check whether each system finished the task you assigned it.
|
|
196
|
+
|
|
197
|
+
The strong version is longer in words and shorter in effort. That trade is correct.
|
|
198
|
+
|
|
199
|
+
### Reject slogans that survive only on tone
|
|
200
|
+
|
|
201
|
+
For every headline, subheading, closing line, and call to action, ask what it means literally, what action or condition it names, whether it stays useful once the dramatic tone is removed, whether twenty unrelated companies could publish it unchanged, and whether the reader learns anything or only receives a mood. If the literal meaning is thin, rewrite it.
|
|
202
|
+
|
|
203
|
+
Run the last question as a procedure rather than a judgment where the line is short enough to search: paste it into a search engine and read how many unrelated companies already publish it unchanged. Ask also what would make the sentence false. A sentence nothing could contradict is not a claim, whatever its tone. Neither check applies to navigation labels, category nouns, or utility microcopy, which are not claims and are not supposed to be falsifiable.
|
|
204
|
+
|
|
205
|
+
### Manage shape across a corpus
|
|
206
|
+
|
|
207
|
+
Repeating one sanctioned shape across a page or site can produce a corpus that reads as generated even when every line passes on its own. Split this by function before acting on it.
|
|
208
|
+
|
|
209
|
+
Where headings belong to the same task or information class, parallel syntax is correct and should be kept. Forcing variety into a procedure list damages it.
|
|
210
|
+
|
|
211
|
+
Where headings and hooks compete for attention, manage concentration instead of demanding uniqueness. Across roughly twelve such headings, keep at least four distinct syntactic families and avoid more than two consecutive instances of one family. Treat that as a working default, not a measured threshold.
|
|
212
|
+
|
|
213
|
+
The team's own fatigue with a line is not a reader-side signal. The people who write and approve the copy see it every working day and the buyer sees it once, so "we have been saying this forever" is not evidence that anything is failing. Change a line because a reader-side test failed it, because the facts changed, or because a measured result says so. This removes one bad reason to change; it is not a defence of an unvaried corpus, so still run the variance check on the artifact.
|
|
214
|
+
|
|
215
|
+
## Write the CTA as an action label
|
|
216
|
+
|
|
217
|
+
A call to action names an action, not a mood. It tells the reader what happens after they choose it.
|
|
218
|
+
|
|
219
|
+
Use `clear verb + concrete object, destination, or result`. Match the commitment to the destination and to the evidence the copy has actually delivered. Do not put a dramatic or high-commitment label on an informational destination.
|
|
220
|
+
|
|
221
|
+
Workable shapes include `View the report`, `Compare plans`, `Check eligibility`, `See the recommended fixes`, `Review the evidence`, `Book a product demo`, `Start the assessment`, `Download the guide`, `Contact the sales team`, and `Retry the payment`.
|
|
222
|
+
|
|
223
|
+
Reject slogan-shaped labels such as `Take control`, `Move with confidence`, `Fix what matters`, `See the difference`, `Unlock your potential`, `Transform your results`, `Start your journey`, `Make it count`, and `Get clarity`. Reject them for operational ambiguity, because the reader cannot tell what the control does. Do not claim they convert worse; no controlled evidence supports that. Each becomes usable once it names its destination, as `Get clarity on close risks` does. A slogan may sit beside the control as persuasion copy.
|
|
224
|
+
|
|
225
|
+
Avoid the generic labels for the same reason: `Learn more`, `Get started`, `Submit`, `Explore`, `Discover`, `Click here`, and bare product names. On a consequential or irreversible dialog, name the operation rather than using `OK` or `Yes`.
|
|
226
|
+
|
|
227
|
+
Never make the reader infer what opens, what they receive, what they must supply, whether the action is immediate, whether it begins a purchase, form, demo, download, or review, or what commitment it creates.
|
|
228
|
+
|
|
229
|
+
Keep one canonical label for one materially identical action. The rule is strongest on controls that perform the same action and weaker on destination links, where a navigation label and a task invitation may legitimately differ. Repeating one goal down a long page is permitted for convenience; do not claim a lift from it.
|
|
230
|
+
|
|
100
231
|
## Enforce truth and ethical limits
|
|
101
232
|
|
|
102
233
|
Keep fact, inference, interpretation, aspiration, and promise distinct. Preserve source, date, scope, conditions, and uncertainty where material.
|
|
@@ -107,19 +238,28 @@ Do not convert an operational fact into financial, legal, compliance, reputation
|
|
|
107
238
|
|
|
108
239
|
Preserve agency. Do not manufacture fear, shame, guilt, identity pressure, exclusivity, or scarcity. Threat requires a real material risk and a credible response. Ambition requires a mechanism and supportable path.
|
|
109
240
|
|
|
241
|
+
Refuse to build or apply a voice profile of a named third party where the purpose is publication under that person's name. Learning from a writer for work published under your own name is ordinary craft; producing text designed to pass as a specific real person's authored work is not, and it is declined rather than negotiated. The same refusal covers fabricated endorsements, invented positions attributed to a named person, and consequential advice written under someone else's identity without their authorization.
|
|
242
|
+
|
|
110
243
|
## Reject flat or synthetic drafts
|
|
111
244
|
|
|
112
245
|
Rebuild when the draft:
|
|
113
246
|
|
|
114
|
-
-
|
|
247
|
+
- describes the subject only in category terms, or reads as a source-ledger paraphrase, feature inventory, or operational taxonomy;
|
|
115
248
|
- has no felt stake, consequential shift, meaningful mechanism, or defensible destination belief;
|
|
116
|
-
-
|
|
249
|
+
- repeats a line that unrelated companies already publish unchanged, which the search-paste check makes testable;
|
|
117
250
|
- uses generic brand verbs such as `helps`, `shows`, `supports`, or `built for` when a stronger supported causal verb exists;
|
|
118
251
|
- lets minor features bury a decisive fact;
|
|
119
252
|
- opens a very short `POSITION` asset with the subject followed by an operational verb list when the facts contain a verified trigger, threshold, conflict, or exception that can lead instead;
|
|
120
253
|
- announces buyer or investor relevance instead of earning it;
|
|
121
254
|
- exposes compliance, reasoning, routing, or publication process;
|
|
122
|
-
- uses emotion that the facts do not support
|
|
255
|
+
- uses emotion that the facts do not support;
|
|
256
|
+
- names an internal method, stage, record type, or score where the reader only needs to know what happens;
|
|
257
|
+
- stacks abstract nouns instead of naming an actor, an action, and a result;
|
|
258
|
+
- reads as quotable while its literal meaning stays thin;
|
|
259
|
+
- closes on a call to action that hides what the reader gets;
|
|
260
|
+
- runs one attention-oriented heading template through a whole deliverable.
|
|
261
|
+
|
|
262
|
+
Naming the category is orientation, not taxonomy, and the two are opposite defects. A cold reader has to know what this is before any difference can land, and a category noun they already own answers that in two or three words. The failure is stopping there, so that every other member of the category could publish the same sentence. Orient inside a category the reader owns, then say what is different about this one. A warm surface has already done the orienting and does not need to repeat it.
|
|
123
263
|
|
|
124
264
|
When facts describe an input conflict, blocked action, threshold, exception, or before-and-after state, use that verified condition as the opening situation. Do not add a downstream cost or risk that the facts do not establish.
|
|
125
265
|
|
|
@@ -146,14 +286,20 @@ Channel rules change depth and tone. They do not erase the argument.
|
|
|
146
286
|
After the argument is drafted:
|
|
147
287
|
|
|
148
288
|
1. Verify each claim and causal link.
|
|
149
|
-
2.
|
|
150
|
-
3. Apply
|
|
151
|
-
4. Apply
|
|
152
|
-
5.
|
|
289
|
+
2. Run the first-read comprehension gate, the specialized-term gate, and the CTA gate. These run before any style, compression, or publication pass, and their result outranks all three.
|
|
290
|
+
3. Apply written GEO/AEO only to written deliverables, at passage level rather than sentence level.
|
|
291
|
+
4. Apply technical publication checks only to indexable public work.
|
|
292
|
+
5. Apply the human-voice and AI-writing-tell gate without deleting facts or diagnostic lists.
|
|
293
|
+
6. Compress repetition and decoration last, and only where compression does not raise decoding effort.
|
|
294
|
+
7. Run the final U+2014 scan across the complete response and confirm zero occurrences.
|
|
295
|
+
|
|
296
|
+
When a sentence fails the comprehension gate, keep the underlying fact, name the concrete actor, action, object, and result, remove internal process language the reader does not need, split the overloaded sentence, restore any context the compression removed, rewrite the call to action to name its real destination, then test again. Do not repair unclear writing by adding explanatory parentheses, longer noun phrases, or a vague supporting sentence.
|
|
153
297
|
|
|
154
298
|
Keep these passes invisible. Mention a blocker only when silence would make the result misleading, legally unusable, or operationally unshippable.
|
|
155
299
|
|
|
156
|
-
|
|
300
|
+
An active voice profile carries one narrow exception to the AI-vocabulary ban. The words on that profile's owned-vocabulary list, and only those words, are exempt, because they were measured as this author's own across the corpus. Without the exception the tell gate strips the voice it was loaded to keep. The exception covers vocabulary alone: it never suppresses the stock-template bans, the significance-tail bans, the structural-tell rules, the curly-quote ban, or the U+2014 ban, and it never makes an unsupported claim writable.
|
|
301
|
+
|
|
302
|
+
Do not generate curly or smart quotes in final copy. Remove prompt leakage, canned framing, generic significance tails, inflated abstractions, fake human texture, and repeated stock templates. Avoid decorative three-part rhetoric, but preserve necessary factual series. Never promise detector evasion. Never change facts or legal meaning to sound human.
|
|
157
303
|
|
|
158
304
|
## Return the result
|
|
159
305
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Maestro: Agora"
|
|
3
|
-
short_description: "Argument-first copy that earns belief"
|
|
4
|
-
default_prompt: "Use $agora to turn verified facts into the strongest channel-native argument the evidence can support."
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Maestro: Agora"
|
|
3
|
+
short_description: "Argument-first copy that earns belief"
|
|
4
|
+
default_prompt: "Use $agora to turn verified facts into the strongest channel-native argument the evidence can support."
|