@1aboveio/skills 0.20.3 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +14 -9
  2. package/package.json +1 -1
  3. package/runtime/skills/distribution/generated/recipes.json +136 -24
  4. package/runtime/skills/distribution/scripts/bundles.mjs +152 -76
  5. package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
  6. package/skills/compliance/doc-authenticity/SKILL.md +2 -1
  7. package/skills/compliance/doc-authenticity/agents/openai.yaml +8 -0
  8. package/skills/content-creator/article-review/LICENSE +3 -0
  9. package/skills/content-creator/article-review/SKILL.md +65 -0
  10. package/skills/content-creator/article-review/references/report-format.md +68 -0
  11. package/skills/content-creator/article-review/references/template-guide.md +79 -0
  12. package/skills/content-creator/article-review/references/template-wechat-payments.md +125 -0
  13. package/skills/content-creator/article-to-image/LICENSE +3 -0
  14. package/skills/content-creator/article-to-image/SKILL.md +90 -0
  15. package/skills/content-creator/article-to-image/references/formats.md +53 -0
  16. package/skills/content-creator/wechat-article-writer/LICENSE +3 -0
  17. package/skills/content-creator/wechat-article-writer/SKILL.md +50 -0
  18. package/skills/content-creator/wechat-article-writer/references/style-comparison.md +64 -0
  19. package/skills/content-creator/wechat-article-writer/references/style-howto.md +64 -0
  20. package/skills/content-creator/wechat-article-writer/references/style-insight.md +63 -0
  21. package/skills/content-creator/wechat-article-writer/references/style-pitfall.md +60 -0
  22. package/skills/content-creator/wechat-article-writer/references/style-selector.md +133 -0
  23. package/skills/content-creator/wechat-article-writer/references/writing-rules.md +150 -0
  24. package/skills/content-creator/wechat-publisher/LICENSE +3 -0
  25. package/skills/content-creator/wechat-publisher/SKILL.md +170 -0
  26. package/skills/content-creator/wechat-publisher/references/api.md +105 -0
  27. package/skills/content-creator/wechat-publisher/references/layout-rules.md +57 -0
  28. package/skills/content-creator/wechat-publisher/references/wechat-safe-template.html +86 -0
  29. package/skills/content-creator/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
  30. package/skills/content-creator/wechat-publisher/scripts/publish.py +239 -0
  31. package/skills/content-creator/wechat-search/LICENSE +3 -0
  32. package/skills/content-creator/wechat-search/SKILL.md +210 -0
  33. package/skills/content-creator/wechat-search/scripts/start-chrome.sh +83 -0
  34. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  35. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
@@ -204,18 +204,22 @@ export function newMemberCandidates(action, groupIds, priorState, transport = {}
204
204
  return candidates;
205
205
  }
206
206
 
207
- // Archived members are offered only to users who previously accepted them in the same group.
208
- // This keeps retirement explicit and prevents a release from treating filesystem presence as
209
- // consent. A successful removal drops the old acceptance; a decline or failure preserves it so a
210
- // later update offers the archive again.
207
+ // Archived members of a selected group are offered when they were previously accepted or are
208
+ // still installed. Presence of a named leftover is not group inference: the group is already
209
+ // selected. A successful removal drops the old acceptance; a decline or failure preserves it so a
210
+ // later run offers the archive again.
211
211
  export function archivedMemberCandidates(action, groupIds, priorState, transport = {}) {
212
- if (action !== 'update' || !priorState) return [];
212
+ if (!ACTIONS.has(action)) return [];
213
213
  const recipes = readRecipes(resolveBundleTransport(transport).recipesPath);
214
214
  const selected = new Set(groupIds);
215
- const acceptedByGroup = new Map(priorState.groups
215
+ const acceptedByGroup = new Map((priorState?.groups ?? [])
216
216
  .map((group) => [group.id, new Set(group.acceptedMembers)]));
217
+ const leftover = (name) => Boolean(
218
+ transport.skillsRoot
219
+ && statSync(join(transport.skillsRoot, name), { throwIfNoEntry: false }),
220
+ );
217
221
  return (recipes.archives ?? []).filter((archive) => selected.has(archive.formerGroupId)
218
- && acceptedByGroup.get(archive.formerGroupId)?.has(archive.installName));
222
+ && (acceptedByGroup.get(archive.formerGroupId)?.has(archive.installName) || leftover(archive.installName)));
219
223
  }
220
224
 
221
225
  // Prior acceptance for the new-member consent delta on explicit selections (picker, --group,
@@ -305,19 +309,14 @@ function localFirstPartyCommand(command, localRoot, groupId) {
305
309
  return command.replace(NATIVE_ADD_SOURCE, (_match, prefix) => `${prefix}${shellArgument(localRoot)}`);
306
310
  }
307
311
 
308
- // The coordinator executes one native add PER SKILL (#1129) rather than coalescing a whole source
309
- // into one invocation. A coalesced add can only attribute a failure to the entire source; per-skill
310
- // adds let the run record exactly which skill failed, continue with the remaining skills, and keep
311
- // only the succeeded skills in selection memory. Recipe step/member order is preserved
312
- // (dependency-first), and a member shared by several selected groups (harness-runtime) is still
313
- // planned exactly once, at its first occurrence. The pinned native CLI has no `--symlink` selector,
314
- // so the coordinator adds `--yes` to choose its default symlink deployment without one installation-
315
- // method prompt per skill. Attribution, ordering and deduplication remain coordinator-owned.
316
- const SKILL_TAIL = /^(.*? --skill )(.+?)( && .*)?$/;
312
+ // The coordinator preserves generated dependency/member order, then coalesces each contiguous run
313
+ // from one source into a native add. A member shared by groups is included once and retains every
314
+ // owning selection for memory. Per-member attribution comes from post-install digest verification,
315
+ // not process count.
316
+ const SKILL_TAIL = /^(.*? --skill )(.+?)( --yes)?( && .*)?$/;
317
317
 
318
- // The generated step binds its runtime suffix (` && npm ci ...`) to the whole coalesced command.
319
- // Per skill it belongs to the runtime member's own add, so the dependency install runs only after
320
- // engineering-runtime itself was actually added — never after a sibling, never when it was skipped.
318
+ // The generated step binds its runtime suffix (` && npm ci ...`) to engineering-runtime. Keep it on
319
+ // the source batch only when that member remains selected after digest and consent filtering.
321
320
  const RUNTIME_MEMBER = 'engineering-runtime';
322
321
 
323
322
  function skillsFromCommand(command) {
@@ -329,14 +328,14 @@ function skillsFromCommand(command) {
329
328
  function withSkills(command, skills) {
330
329
  const match = SKILL_TAIL.exec(command);
331
330
  if (!match) throw new Error(`Native add has no --skill list to rewrite: ${command}`);
332
- return `${match[1]}${skills.join(' ')}${match[3] || ''}`;
331
+ return `${match[1]}${skills.join(' ')}${match[3] || ''}${match[4] || ''}`;
333
332
  }
334
333
 
335
334
  function withYesFlag(command) {
336
335
  // --yes must bind to `skills`, not to a trailing `&& npm ci ...`.
337
336
  const match = SKILL_TAIL.exec(command);
338
337
  if (!match) return `${command} --yes`;
339
- return `${match[1]}${match[2]} --yes${match[3] || ''}`;
338
+ return `${match[1]}${match[2]} --yes${match[4] || ''}`;
340
339
  }
341
340
 
342
341
  export function planBundleCommands(action, groupIds, {
@@ -363,12 +362,10 @@ export function planBundleCommands(action, groupIds, {
363
362
  if (!requested.includes(groupId)) requested.push(groupId);
364
363
  }
365
364
 
366
- // Plan from the selected groups (not recipes.all). A public package may expose only a subset of
367
- // groups while still shipping a full recipes.all from the private catalog; using that all-plan
368
- // would install skills that are not in the selection / not in the package.
365
+ // Plan individual member records first. The execution boundary coalesces adjacent records from
366
+ // the same source without weakening per-member dependency, policy, or selection metadata.
369
367
  const planned = [];
370
368
  const plannedByMember = new Map();
371
-
372
369
  for (const groupId of requested) {
373
370
  const group = groups.get(groupId);
374
371
  for (const step of group[action].steps) {
@@ -379,14 +376,10 @@ export function planBundleCommands(action, groupIds, {
379
376
  const command = step.sourceId === 'first-party'
380
377
  ? localFirstPartyCommand(generated.command, localRoot, groupId)
381
378
  : generated.command;
382
- // An explicit member subset (the digest skip) removes members from the native add but never
383
- // reorders the rest: the remaining skills keep recipe order, support dependency first.
384
- // Declined new members (#1128) are also filtered out, but consent never applies to support
385
- // dependencies (harness-runtime): they are silent install dependencies, not capabilities.
386
379
  const skills = skillsFromCommand(command)
387
380
  .filter((skill) => !skipMembers?.has(skill))
388
381
  .filter((skill) => SUPPORT_GROUPS.has(skill) || !omit.has(skill));
389
- const suffix = SKILL_TAIL.exec(command)?.[3] || '';
382
+ const suffix = SKILL_TAIL.exec(command)?.[4] || '';
390
383
  const bare = suffix ? command.slice(0, command.length - suffix.length) : command;
391
384
  for (const skill of skills) {
392
385
  const existing = plannedByMember.get(skill);
@@ -396,8 +389,6 @@ export function planBundleCommands(action, groupIds, {
396
389
  }
397
390
  let skillCommand = withSkills(bare, [skill]);
398
391
  if (suffix && skill === RUNTIME_MEMBER) skillCommand = `${skillCommand}${suffix}`;
399
- // The pinned CLI has no --symlink flag. --yes is its only supported way to select the
400
- // default symlink deployment without presenting the install-method chooser.
401
392
  skillCommand = withYesFlag(skillCommand);
402
393
  const invocationPolicy = skill === 'tdd' && enableTdd
403
394
  ? undefined
@@ -415,22 +406,51 @@ export function planBundleCommands(action, groupIds, {
415
406
  ...(memberMetadata.get(skill)?.requires?.length > 0
416
407
  ? { requires: memberMetadata.get(skill).requires }
417
408
  : {}),
409
+ metadata: memberMetadata.get(skill) ?? { installName: skill },
418
410
  };
419
411
  plannedByMember.set(skill, entry);
420
412
  planned.push(entry);
421
413
  }
422
414
  }
423
415
  }
424
-
425
416
  return planned;
426
417
  }
427
418
 
428
- // Continue-on-failure (#1129): planned per-skill adds run in dependency order. A failed skill blocks
429
- // only members that require it; unrelated later skills still run. The caller summarizes the buckets,
430
- // decides the exit status, and persists only members whose required closure is ready.
431
- export function runBundleCommands(commands, {
419
+ export function coalesceBundleCommands(commands) {
420
+ const batches = [];
421
+ for (const planned of commands) {
422
+ const previous = batches.at(-1);
423
+ if (!previous || previous.sourceId !== planned.sourceId || previous.cwd !== planned.cwd) {
424
+ batches.push({
425
+ sourceId: planned.sourceId,
426
+ members: [planned],
427
+ command: planned.command,
428
+ cwd: planned.cwd,
429
+ failureMessage: planned.failureMessage,
430
+ });
431
+ continue;
432
+ }
433
+ previous.members.push(planned);
434
+ const suffix = previous.members.map(({ command }) => SKILL_TAIL.exec(command)?.[4] || '').find(Boolean) || '';
435
+ const bare = suffix && previous.command.endsWith(suffix)
436
+ ? previous.command.slice(0, previous.command.length - suffix.length)
437
+ : previous.command;
438
+ previous.command = withSkills(bare, previous.members.map(({ member }) => member));
439
+ if (suffix) previous.command = `${previous.command}${suffix}`;
440
+ }
441
+ for (const batch of batches) {
442
+ batch.groupIds = [...new Set(batch.members.flatMap((member) => member.groupIds))];
443
+ batch.groupName = [...new Set(batch.members.map((member) => member.groupName))].join(', ');
444
+ }
445
+ return batches;
446
+ }
447
+
448
+ // Source batches continue independently. Within each batch, command status is only process evidence;
449
+ // installed-content verification is the per-member authority used for attribution and memory.
450
+ export function runBundleCommands(batches, {
432
451
  execute = (command, cwd) => spawnSync('/bin/bash', ['-lc', command], { cwd, stdio: 'inherit' }),
433
452
  applyPolicy = applyInvocationPolicy,
453
+ verifyMember,
434
454
  skillsRoot,
435
455
  stdout = process.stdout,
436
456
  stderr = process.stderr,
@@ -439,35 +459,79 @@ export function runBundleCommands(commands, {
439
459
  const succeeded = [];
440
460
  const failed = [];
441
461
  const available = new Set(satisfiedMembers);
442
- for (const planned of commands) {
443
- const blockedBy = (planned.requires ?? []).filter((dependency) => !available.has(dependency));
444
- if (blockedBy.length > 0) {
445
- stderr.write(`Dependency failed for ${planned.member}: ${blockedBy.join(', ')} is not available\n`);
446
- failed.push({ planned, status: 1, blockedBy });
447
- continue;
462
+ for (const candidate of batches) {
463
+ const legacySingle = !Array.isArray(candidate.members);
464
+ const batch = legacySingle ? { ...candidate, members: [candidate] } : candidate;
465
+ let runnable = [...batch.members];
466
+ let changed = true;
467
+ while (changed) {
468
+ changed = false;
469
+ const runnableNames = new Set(runnable.map(({ member }) => member));
470
+ for (const planned of [...runnable]) {
471
+ const blockedBy = (planned.requires ?? [])
472
+ .filter((dependency) => !available.has(dependency) && !runnableNames.has(dependency));
473
+ if (blockedBy.length === 0) continue;
474
+ stderr.write(`Dependency failed for ${planned.member}: ${blockedBy.join(', ')} is not available\n`);
475
+ failed.push({ planned, status: 1, blockedBy });
476
+ runnable = runnable.filter((candidate) => candidate !== planned);
477
+ changed = true;
478
+ }
448
479
  }
449
- stdout.write(`\nInstalling ${planned.member} (${planned.sourceId})...\n`);
450
- const result = execute(planned.command, planned.cwd);
451
- if (result.status === 0) {
452
- try {
453
- if (planned.invocationPolicy) {
480
+ if (runnable.length === 0) continue;
481
+ let command = batch.command;
482
+ let batchSuffix = '';
483
+ if (!legacySingle) {
484
+ batchSuffix = SKILL_TAIL.exec(batch.command)?.[4] || '';
485
+ command = withSkills(batch.command, runnable.map(({ member }) => member));
486
+ if (batchSuffix && !runnable.some(({ member }) => member === RUNTIME_MEMBER)) {
487
+ command = command.slice(0, command.length - batchSuffix.length);
488
+ }
489
+ }
490
+ stdout.write(`\nInstalling ${runnable.map(({ member }) => member).join(', ')} (${batch.sourceId})...\n`);
491
+ const result = execute(command, batch.cwd);
492
+ const status = Number.isInteger(result.status) ? result.status : 1;
493
+ const policyFailures = new Set();
494
+ if (status === 0) {
495
+ for (const planned of runnable) {
496
+ if (!planned.invocationPolicy) continue;
497
+ try {
454
498
  applyPolicy({
455
499
  installName: planned.member,
456
500
  policy: planned.invocationPolicy,
457
501
  ...(skillsRoot ? { skillsRoot } : {}),
458
502
  });
503
+ } catch (error) {
504
+ stderr.write(`Invocation policy failed for ${planned.member}: ${error.message}\n`);
505
+ policyFailures.add(planned.member);
459
506
  }
460
- succeeded.push(planned);
461
- available.add(planned.member);
507
+ }
508
+ } else {
509
+ stderr.write(`${batch.failureMessage}\n`);
510
+ }
511
+ for (const planned of runnable) {
512
+ if (policyFailures.has(planned.member)) {
513
+ failed.push({ planned, status: 1 });
462
514
  continue;
515
+ }
516
+ if (status !== 0 && batchSuffix && planned.member === RUNTIME_MEMBER) {
517
+ stderr.write(`Post-install command failed for ${planned.member}\n`);
518
+ failed.push({ planned, status });
519
+ continue;
520
+ }
521
+ try {
522
+ const verified = verifyMember ? verifyMember(planned) : status === 0;
523
+ if (verified) {
524
+ succeeded.push(planned);
525
+ available.add(planned.member);
526
+ } else {
527
+ stderr.write(`Post-install verification failed for ${planned.member}\n`);
528
+ failed.push({ planned, status: status || 1 });
529
+ }
463
530
  } catch (error) {
464
- stderr.write(`Invocation policy failed for ${planned.member}: ${error.message}\n`);
531
+ stderr.write(`Post-install verification failed for ${planned.member}: ${error.message}\n`);
465
532
  failed.push({ planned, status: 1 });
466
- continue;
467
533
  }
468
534
  }
469
- stderr.write(`${planned.failureMessage}\n`);
470
- failed.push({ planned, status: Number.isInteger(result.status) ? result.status : 1 });
471
535
  }
472
536
  return { succeeded, failed };
473
537
  }
@@ -626,8 +690,8 @@ install without --group/--all opens the interactive picker; update without them
626
690
  remembered selection (\${XDG_STATE_HOME:-~/.local/state}/1aboveio-skills/selection.json).
627
691
  When a remembered group's current recipe declares members you have not accepted yet, each new skill
628
692
  is offered individually and defaults to No; --yes accepts them all, --dry-run lists them without
629
- prompting. Retired accepted skills are likewise kept by default; --yes archives them through the
630
- native Skills CLI after their replacement is current. TDD implicit model invocation is enabled by default; --disable-tdd makes TDD explicit-only,
693
+ prompting. Retired leftovers in a selected group are archived by default through the native Skills
694
+ CLI after their replacement is current; decline interactively to keep them. TDD implicit model invocation is enabled by default; --disable-tdd makes TDD explicit-only,
631
695
  and --enable-tdd restores implicit invocation. --manual-tdd remains a compatibility alias for
632
696
  --disable-tdd.
633
697
  `;
@@ -802,29 +866,24 @@ async function consentNewMembers(candidates, { yes, stdin, stdout, stderr }) {
802
866
  return declined;
803
867
  }
804
868
 
805
- async function consentArchivedMembers(candidates, { yes, stdin, stdout, stderr }) {
869
+ async function consentArchivedMembers(candidates, { yes, stdin, stdout }) {
806
870
  const declined = new Set();
807
- if (yes) {
808
- for (const candidate of candidates) stdout.write(`Accepting archive of retired skill "${candidate.installName}" (--yes)\n`);
809
- return declined;
810
- }
811
- if (!stdin.isTTY) {
871
+ if (yes || !stdin.isTTY) {
872
+ const how = yes ? '--yes' : 'default Yes';
812
873
  for (const candidate of candidates) {
813
- stderr.write(`Keeping retired skill "${candidate.installName}": no interactive terminal (default No; re-run with --yes to archive)\n`);
814
- declined.add(candidate.installName);
874
+ stdout.write(`Archiving retired skill "${candidate.installName}" (${how}; replaced by ${candidate.replacement})\n`);
815
875
  }
816
876
  return declined;
817
877
  }
818
878
  const prompter = createLinePrompter({ stdin, stdout });
819
879
  for (const candidate of candidates) {
820
880
  const answer = await prompter.ask(
821
- `Archive retired skill "${candidate.installName}" (replaced by ${candidate.replacement})? [y/N] `,
881
+ `Archive retired skill "${candidate.installName}" (replaced by ${candidate.replacement})? [Y/n] `,
822
882
  );
823
- if (/^y(es)?$/i.test(answer)) stdout.write(`Archiving ${candidate.installName}\n`);
824
- else {
883
+ if (/^n(o)?$/i.test(answer)) {
825
884
  stdout.write(`Keeping ${candidate.installName}\n`);
826
885
  declined.add(candidate.installName);
827
- }
886
+ } else stdout.write(`Archiving ${candidate.installName}\n`);
828
887
  }
829
888
  return declined;
830
889
  }
@@ -873,6 +932,7 @@ function selectionIncludesTdd(groupIds, catalog) {
873
932
  export async function runCli(args, {
874
933
  execute,
875
934
  applyPolicy,
935
+ verifyMember,
876
936
  stdin = process.stdin,
877
937
  stdout = process.stdout,
878
938
  stderr = process.stderr,
@@ -932,18 +992,21 @@ export async function runCli(args, {
932
992
  } else if (candidates.length > 0) {
933
993
  declined = await consentNewMembers(candidates, { yes: parsed.yes, stdin, stdout, stderr });
934
994
  }
935
- const archiveCandidates = archivedMemberCandidates(parsed.action, groupIds, priorState, transport);
995
+ const skillsRoot = join(env.HOME || homedir(), '.agents', 'skills');
996
+ const archiveCandidates = archivedMemberCandidates(parsed.action, groupIds, priorState, {
997
+ ...transport,
998
+ skillsRoot,
999
+ });
936
1000
  let declinedArchives = new Set();
937
1001
  if (archiveCandidates.length > 0 && parsed.dryRun) {
938
1002
  for (const candidate of archiveCandidates) {
939
- stdout.write(`Retired skill "${candidate.installName}" (replaced by ${candidate.replacement}) — would prompt to archive (default: No); --yes accepts archival\n`);
1003
+ stdout.write(`Retired skill "${candidate.installName}" (replaced by ${candidate.replacement}) — would archive by default; decline interactively to keep\n`);
940
1004
  }
941
1005
  } else if (archiveCandidates.length > 0) {
942
1006
  declinedArchives = await consentArchivedMembers(archiveCandidates, {
943
1007
  yes: parsed.yes,
944
1008
  stdin,
945
1009
  stdout,
946
- stderr,
947
1010
  });
948
1011
  }
949
1012
  // Digest skip (#1127): `update` classifies the accepted members against the installed tree and
@@ -952,7 +1015,6 @@ export async function runCli(args, {
952
1015
  // from classification: they never run, so they are not buckets.
953
1016
  let buckets;
954
1017
  let skipMembers;
955
- const skillsRoot = join(env.HOME || homedir(), '.agents', 'skills');
956
1018
  if (parsed.action === 'update') {
957
1019
  buckets = classifySelectedMembers(parsed.action, groupIds, { ...transport, enableTdd, skillsRoot });
958
1020
  skipMembers = new Set(buckets.unchanged.map((entry) => entry.member));
@@ -968,10 +1030,11 @@ export async function runCli(args, {
968
1030
  skipMembers,
969
1031
  omitMembers: declined,
970
1032
  });
1033
+ const batches = coalesceBundleCommands(commands);
971
1034
  if (parsed.dryRun) {
972
1035
  if (buckets) printUpdateBuckets(stdout, buckets);
973
- for (const planned of commands) {
974
- stdout.write(`\n${planned.groupName} / ${planned.sourceId} / ${planned.member}\n${planned.command}\n`);
1036
+ for (const batch of batches) {
1037
+ stdout.write(`\n${batch.groupName} / ${batch.sourceId} / ${batch.members.map(({ member }) => member).join(', ')}\n${batch.command}\n`);
975
1038
  }
976
1039
  if (buckets && commands.length === 0 && archiveCandidates.length === 0) {
977
1040
  stdout.write('Nothing to do: every accepted skill is already up to date.\n');
@@ -984,9 +1047,21 @@ export async function runCli(args, {
984
1047
  const skipping = buckets.unchanged.map((entry) => entry.member).filter((member) => !SUPPORT_GROUPS.has(member));
985
1048
  if (skipping.length > 0) stdout.write(`Up to date, skipping: ${skipping.join(', ')}\n`);
986
1049
  }
987
- const { succeeded, failed } = runBundleCommands(commands, {
1050
+ const verificationTransport = resolveBundleTransport(transport);
1051
+ const verificationSources = new Map(readRecipes(verificationTransport.recipesPath).sources
1052
+ .map((source) => [source.id, source]));
1053
+ const verifyInstalledMember = verifyMember ?? (execute ? undefined : (planned) => {
1054
+ const source = verificationSources.get(planned.sourceId);
1055
+ return classifyInstalledMember(source, planned.metadata, {
1056
+ enableTdd,
1057
+ localRoot: verificationTransport.localRoot,
1058
+ skillsRoot,
1059
+ }).status === 'unchanged';
1060
+ });
1061
+ const { succeeded, failed } = runBundleCommands(batches, {
988
1062
  execute,
989
1063
  applyPolicy,
1064
+ verifyMember: verifyInstalledMember,
990
1065
  skillsRoot,
991
1066
  stdout,
992
1067
  stderr,
@@ -1001,13 +1076,14 @@ export async function runCli(args, {
1001
1076
  [...commands, ...(buckets?.unchanged ?? [])],
1002
1077
  stderr,
1003
1078
  ));
1079
+ const coherentSucceeded = succeeded.filter((planned) => readyMembers.has(planned.member));
1004
1080
  const archives = runArchiveCommands(
1005
1081
  archiveCandidates.filter((candidate) => !declinedArchives.has(candidate.installName)),
1006
1082
  readyMembers,
1007
1083
  { execute, skillsRoot, stdout, stderr },
1008
1084
  );
1009
1085
  printRunSummary(stdout, {
1010
- succeeded,
1086
+ succeeded: coherentSucceeded,
1011
1087
  failed,
1012
1088
  unchanged: buckets?.unchanged.map((entry) => entry.member),
1013
1089
  declined: [...declined],
@@ -86,7 +86,7 @@ export const WORKFLOW_TRUSTED_SOURCES = deepFreeze({
86
86
  id: 'first-party',
87
87
  type: 'first-party',
88
88
  package: '@1aboveio/skills',
89
- version: '0.20.3',
89
+ version: '0.21.0',
90
90
  },
91
91
  matt: {
92
92
  id: 'matt-pocock',
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: doc-authenticity
3
- version: 0.4.0
3
+ version: 0.4.1
4
4
  description: "Screen PDF documents and raster images for fabrication, tampering, Photoshop or other image-editor use, and content validity. Detects file type, runs format-specific authenticity forensics, classifies bank statements, conversation logs, invoices, agreements, and other documents, then applies type-specific validity checks. Produces separate authenticity (NO_EVIDENCE_OF_MODIFICATION / SUSPICIOUS / LIKELY_MODIFIED / INCONCLUSIVE) and validity (VALID / INVALID / INCONCLUSIVE) verdicts. Use whenever a user asks whether a document, screenshot, scan, statement, invoice, contract, chat log, receipt, or payment evidence is genuine, original, edited, Photoshopped, manipulated, fake, or valid."
5
+ disable-model-invocation: true
5
6
  ---
6
7
 
7
8
  # Document and Image Authenticity
@@ -0,0 +1,8 @@
1
+ interface:
2
+ display_name: "Document Authenticity"
3
+ short_description: "Explicit-only document and image authenticity screening"
4
+
5
+ policy:
6
+ # Codex counterpart to SKILL.md disable-model-invocation: true.
7
+ # Keeps explicit invocation while blocking description-based selection.
8
+ allow_implicit_invocation: false
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AboveIO
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: article-review
3
+ description: >
4
+ Evaluate and score articles across multiple dimensions including originality,
5
+ readability, shareability, timeliness, and factual accuracy.
6
+ Use when a user shares an article, pastes article text, sends a URL, or asks to
7
+ review, rate, critique, evaluate, 评价, 点评, 分析, or 看看 any article, post, or 文章.
8
+ Default template is 公众号-支付行业.
9
+ ---
10
+
11
+ # 文章评审引擎
12
+
13
+ 模板化的文章评审系统。评审标准由模板文件定义,引擎负责执行评审流程并输出标准化报告。
14
+
15
+ **开始任何评审前,先读取 `references/report-format.md`**,熟悉输出结构。
16
+
17
+ ## 可用模板
18
+
19
+ - **公众号-支付行业** (`references/template-wechat-payments.md`) — 面向支付/金融科技从业者的公众号内容(默认)
20
+
21
+ 如需其他模板,或用户请求的模板不存在,读取 `references/template-guide.md` 创建新模板,再继续评审。
22
+
23
+ ## 输入处理
24
+
25
+ **粘贴文字:** 直接进入评审流程。
26
+
27
+ **URL:** 用 `web_fetch` 抓取文章正文。若被拦截(如微信需要登录),提示用户粘贴文章内容后继续。
28
+
29
+ ## 评审流程
30
+
31
+ ### 第一步:确认模板
32
+ 在每次评审开始时,读取对应模板文件(默认 `references/template-wechat-payments.md`),获取维度列表和 Level 2 细则。用户未指定模板时使用默认。
33
+
34
+ ### 第二步:通读文章
35
+ 通读全文,理解核心主张、论据、结构、受众定位。
36
+
37
+ ### 第三步:事实核查(Fact-Check Pass)
38
+ 在逐维度评分之前,先做事实核查:
39
+
40
+ 1. 提取文章中所有可验证的主张:
41
+ - 数字、统计、比例、排名
42
+ - 引用他人言论或研究结论
43
+ - 行业事实陈述(市场规模、政策、技术参数等)
44
+
45
+ 2. 对每条主张用 `web_search` 核查,标注结果:
46
+ - ✅ **已核实** — 找到一致的可信来源
47
+ - ❌ **有误** — 找到与之矛盾的可信来源,记录正确值
48
+ - ⚠️ **无法核实** — 无法找到来源或来源相互矛盾,标注存疑
49
+
50
+ 3. 将核查结果用于事实准确性维度的评分依据
51
+
52
+ ### 第四步:逐维度评分
53
+ 按模板定义的每个 Level 1 维度:
54
+ - 给出 1-10 分
55
+ - 引用文章原文 2-3 处作为依据
56
+ - 给出 1-2 条具体可执行的改进建议(如有)
57
+
58
+ 事实准确性维度的评分必须基于第三步的核查结果,而非主观判断。
59
+
60
+ ### 第五步:输出报告
61
+ 按 `references/report-format.md` 中的模板输出完整报告。
62
+
63
+ ## 注意事项
64
+
65
+ 严格按流程执行:评分必须有原文引用,不接受笼统判断;事实准确性必须基于 web_search 核查;改进建议要具体可执行;发布建议根据综合分和事实核查结果综合判断。
@@ -0,0 +1,68 @@
1
+ # 评审报告输出格式
2
+
3
+ 使用以下模板输出完整评审报告。
4
+
5
+ ---
6
+
7
+ ```
8
+ # 文章评审报告
9
+
10
+ **文章标题:** [标题]
11
+ **评审模板:** [模板名称]
12
+ **评审时间:** [日期]
13
+
14
+ ---
15
+
16
+ ## 综合评分:X.X / 10
17
+ (各维度等权平均)
18
+
19
+ ---
20
+
21
+ ## 事实核查结果
22
+
23
+ - ✅ "[原文陈述]" — 来源:[来源名]
24
+ - ❌ "[原文陈述]" — 实际应为:[正确值],来源:[来源名]
25
+ - ⚠️ "[原文陈述]" — 无法核实,建议补充来源
26
+
27
+ ---
28
+
29
+ ## 详细评分
30
+
31
+ ### [维度名] X/10
32
+ **依据:**
33
+ - [引用原文 + 分析]
34
+
35
+ **改进建议:**
36
+ - [具体建议]
37
+
38
+ [其余维度同上格式...]
39
+
40
+ ---
41
+
42
+ ## 传播潜力分析
43
+
44
+ **最强传播钩子:**
45
+ > "[从文章中提取或改写的最具传播力的 1-2 句话,可直接用作转发文案]"
46
+
47
+ **建议传播渠道:**
48
+ - 微信群(行业/专业群):[适合/不适合,理由一句]
49
+ - 朋友圈:[适合/不适合,理由一句]
50
+ - LinkedIn:[适合/不适合,理由一句]
51
+
52
+ **传播半径预估:**
53
+ - [ ] 圈内传播(支付/金融科技从业者为主)
54
+ - [ ] 破圈传播(有潜力触达行业外受众)
55
+
56
+ **提升传播力的快速改动(可选):**
57
+ - [1-2 条具体改动建议]
58
+
59
+ ---
60
+
61
+ ## 总结
62
+ [2-3 句总体评价,突出最大优势和最需改进的地方]
63
+
64
+ ## 发布建议
65
+ [ ] 可直接发布
66
+ [ ] 修改后发布(需解决上述改进点)
67
+ [ ] 建议重写(存在严重问题)
68
+ ```
@@ -0,0 +1,79 @@
1
+ # 新建评审模板指南
2
+
3
+ 新建模板时,复制以下结构,保存为 `references/template-<name>.md`,
4
+ 然后在 `SKILL.md` 的"可用模板"列表中添加一行。
5
+
6
+ ---
7
+
8
+ ## 模板文件结构
9
+
10
+ ```markdown
11
+ # 评审模板:[模板名称]
12
+
13
+ **适用范围:** [描述适用的文章类型]
14
+ **目标受众:** [描述文章面向的读者群体]
15
+ **模板版本:** v1.0
16
+
17
+ ---
18
+
19
+ ## 评审维度(Level 1)及细则(Level 2)
20
+
21
+ ### 1. [维度名] [英文名]
22
+
23
+ [维度说明,1-2句]
24
+
25
+ **Level 2 细则:**
26
+ - **[细则名]**:[说明]
27
+ - **[细则名]**:[说明]
28
+ - (2-5条)
29
+
30
+ **评分参考:**
31
+ - 9-10:[优秀标准]
32
+ - 6-8:[良好标准]
33
+ - 3-5:[及格标准]
34
+ - 1-2:[不合格标准]
35
+
36
+ ---
37
+
38
+ ### 2. [下一个维度...]
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 设计原则
44
+
45
+ **维度数量:** 建议 4-6 个 Level 1 维度。维度太多会分散注意力;太少则覆盖不全。
46
+
47
+ **维度选择:** 每个维度应该:
48
+ - 相互独立(不重叠)
49
+ - 合起来覆盖文章质量的核心方面
50
+ - 与受众和文章类型紧密相关
51
+
52
+ **事实准确性注意:** 若模板包含"事实准确性"维度,必须包含以下说明:
53
+ > ⚠️ 本维度评分必须基于 web_search 实际核查结果,不接受主观判断。
54
+
55
+ **Level 2 细则:** 每个维度建议 3-5 条细则,每条清晰说明考察什么。
56
+
57
+ ---
58
+
59
+ ## 模板示例说明
60
+
61
+ | 场景 | 文件名建议 | 差异化维度 |
62
+ |------|-----------|-----------|
63
+ | 面向 B 端 CFO 的行业报告 | `template-cfo-report.md` | 数据严谨性、决策价值 |
64
+ | 面向年轻用户的品牌故事 | `template-brand-story.md` | 情感温度、品牌一致性 |
65
+ | 合规/政策解读类 | `template-policy.md` | 准确性权重更高、实操指导 |
66
+ | 产品功能介绍 | `template-product.md` | 清晰度、用例覆盖、CTA |
67
+
68
+ ---
69
+
70
+ ## 注册新模板
71
+
72
+ 完成模板文件后,更新 `SKILL.md` 中的"可用模板"列表:
73
+
74
+ ```markdown
75
+ ## 可用模板
76
+
77
+ - **通用** (`references/template-general.md`) — 适用于大多数 [品牌] 公众号内容(默认)
78
+ - **[新模板名]** (`references/template-<name>.md`) — [一句话描述]
79
+ ```