@geoly-ai/social-hub-cli 0.3.19 → 0.3.20

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/CHANGELOG.md +15 -0
  2. package/dist/cmd-manifest.json +4 -2
  3. package/dist/content-review.test.js +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +29 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/register-calendar-directives.d.ts +64 -0
  8. package/dist/register-calendar-directives.d.ts.map +1 -0
  9. package/dist/register-calendar-directives.js +135 -0
  10. package/dist/register-calendar-directives.js.map +1 -0
  11. package/dist/register-calendar-directives.test.d.ts +2 -0
  12. package/dist/register-calendar-directives.test.d.ts.map +1 -0
  13. package/dist/register-calendar-directives.test.js +127 -0
  14. package/dist/register-calendar-directives.test.js.map +1 -0
  15. package/package.json +2 -2
  16. package/skills/README.md +23 -22
  17. package/skills/manifest.json +6 -1
  18. package/skills/reddit-content-rewrite/SKILL.md +29 -0
  19. package/skills/reddit-content-writing/SKILL.md +53 -4
  20. package/skills/reddit-delivery-check/SKILL.md +38 -6
  21. package/skills/reddit-matrix.lock.json +6 -6
  22. package/skills/reddit-phase-brief/SKILL.md +39 -4
  23. package/skills/reddit-phase-brief/references/phase4-brief-workflow.md +43 -3
  24. package/skills/reddit-strategy-shared/references/content-diversity-contract.md +7 -2
  25. package/skills/reddit-strategy-shared/references/content-review-gate.md +48 -1
  26. package/skills/reddit-strategy-shared/references/handoff-schemas.md +72 -2
  27. package/skills/reddit-strategy-shared/references/native-copy-profile.md +207 -0
  28. package/skills/reddit-strategy-shared/references/persona-card-contract.md +210 -0
  29. package/skills/reddit-strategy-shared/references/version-manifest.md +60 -0
  30. package/skills/reddit-subreddit-compliance/SKILL.md +28 -1
  31. package/skills/social-hub-calendar-directives/SKILL.md +548 -0
  32. package/skills/social-hub-calendar-jobs/SKILL.md +10 -0
  33. package/skills/social-hub-cli/SKILL.md +2 -0
  34. package/skills/social-hub-notifications/SKILL.md +7 -0
  35. package/skills/social-hub-publishing/SKILL.md +5 -0
@@ -0,0 +1,64 @@
1
+ import type { Command } from "commander";
2
+ /**
3
+ * `social-hub calendar preflight` / `social-hub calendar set-delivery-mode`
4
+ * —— 内容发布日历指令(`publishing-calendar-directive.v1`)的 CLI 交付面。
5
+ *
6
+ * 设计与冻结决定见 `docs/publishing-calendar-directive-plan.md` §8(D1–D6),
7
+ * 消费协议见 `skills/social-hub-calendar-directives/SKILL.md`。
8
+ *
9
+ * ## 两条不可协商的形态约束
10
+ *
11
+ * 1. **preflight 的业务拒绝不是错误。** 服务端刻意让「条目已取消 / 审核被撤回 /
12
+ * 版本过期」走 HTTP 200 + `decision:"rejected"`,好让 cron 路径无歧义分流每个
13
+ * `reason`。因此本命令**默认恒 exit 0**,先把完整 JSON 写到 stdout;只有显式
14
+ * `--require-ok` 才在非 ok 时补一个 exit 1。反过来做(默认非零)会让所有
15
+ * `set -e` 的脚本在「条目正常取消了」这种日常路径上直接崩掉。
16
+ *
17
+ * 2. **切 deliveryMode 必须过 `--apply` 门。** 它是防重复发帖的唯一闸门(D1):
18
+ * 服务端会在同一事务里撤销/创建内部 job 并 append 指令。手滑改错的后果是
19
+ * 同一条内容被发两遍,或者谁都不发 —— 这个量级的写操作不该只靠一个 `--mode`。
20
+ * `calendar patch --body '{"deliveryMode":...}'` 同样被这道门挡住(见
21
+ * {@link deliveryModeChangeRequiresApply}),否则新命令的护栏只是装饰。
22
+ */
23
+ /** 契约 `calendarEntryDeliveryModeSchema` 的镜像;真源在 contracts。 */
24
+ declare const DELIVERY_MODES: readonly ["hub_managed", "external_agent"];
25
+ export type DeliveryModeArg = (typeof DELIVERY_MODES)[number];
26
+ export declare function isDeliveryMode(value: unknown): value is DeliveryModeArg;
27
+ export declare class CalendarDirectiveArgError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ /**
31
+ * `--revision` 的本地解析。
32
+ *
33
+ * 两道校验缺一不可:
34
+ * - `^\d+$` 挡住 `""` / `" "` / `1e3` / `-1` / `1.5`。**绝不能用 `Number()` 兜底**:
35
+ * 它把空串和空白都转成 `0`,而 revision 0 是每条条目的合法初始值 —— 于是
36
+ * 「忘了带版本号」会被服务端当成「我手上是第 0 版」而真的放行内容。
37
+ * - `Number.isSafeInteger` 挡住 `99999999999999999999` 这类纯数字但超出 IEEE754
38
+ * 安全区的值:它能过正则,转成 number 后却已经悄悄变了值,比对上的是另一条
39
+ * revision。服务端也有 `MAX_SAFE_INTEGER` 上界,这里提前挡住只是不让明显非法
40
+ * 的值打到网络上。
41
+ */
42
+ export declare function parseRevisionArg(raw: string): number;
43
+ /**
44
+ * `calendar patch` 的 body 是不是在动 `deliveryMode` —— 是就必须过 `--apply` 门。
45
+ *
46
+ * 只看「键在不在」而不看值:`{"deliveryMode": null}` / 写错的枚举值同样是一次
47
+ * 「意图切换执行主体」的调用,服务端会拒,但它该在拒之前先被护栏拦下。
48
+ */
49
+ export declare function deliveryModeChangeRequiresApply(body: Record<string, unknown>): boolean;
50
+ /**
51
+ * `calendar patch` 要不要进 `assertApplyOrDryRun`。
52
+ *
53
+ * 两个判据故意不同:
54
+ * - `--dry-run` **对任何 body 都生效** —— 全局 flag 契约是「预览写入,不调用 API」。
55
+ * 只在 deliveryMode 分支上认它,就会出现「加了 --dry-run 却真的写了」的意外写入。
56
+ * - `--apply` 只在 body 含 `deliveryMode` 时才是必填(那是 D1 闸门);其余字段
57
+ * (status / permalink / …)维持既有的无门行为,不破坏存量自动化。
58
+ */
59
+ export declare function calendarPatchNeedsGate(body: Record<string, unknown>, opts: {
60
+ dryRun?: boolean;
61
+ }): boolean;
62
+ export declare function registerCalendarDirectiveCommands(calendar: Command): void;
63
+ export {};
64
+ //# sourceMappingURL=register-calendar-directives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-calendar-directives.d.ts","sourceRoot":"","sources":["../src/register-calendar-directives.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,8DAA8D;AAC9D,QAAA,MAAM,cAAc,4CAA6C,CAAC;AAClE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAKvE;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAapD;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAET;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB,OAAO,CAET;AAED,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAkFzE"}
@@ -0,0 +1,135 @@
1
+ import { requireClient, resolveTeamId } from "./client.js";
2
+ import { assertApplyOrDryRun } from "./dry-run.js";
3
+ /**
4
+ * `social-hub calendar preflight` / `social-hub calendar set-delivery-mode`
5
+ * —— 内容发布日历指令(`publishing-calendar-directive.v1`)的 CLI 交付面。
6
+ *
7
+ * 设计与冻结决定见 `docs/publishing-calendar-directive-plan.md` §8(D1–D6),
8
+ * 消费协议见 `skills/social-hub-calendar-directives/SKILL.md`。
9
+ *
10
+ * ## 两条不可协商的形态约束
11
+ *
12
+ * 1. **preflight 的业务拒绝不是错误。** 服务端刻意让「条目已取消 / 审核被撤回 /
13
+ * 版本过期」走 HTTP 200 + `decision:"rejected"`,好让 cron 路径无歧义分流每个
14
+ * `reason`。因此本命令**默认恒 exit 0**,先把完整 JSON 写到 stdout;只有显式
15
+ * `--require-ok` 才在非 ok 时补一个 exit 1。反过来做(默认非零)会让所有
16
+ * `set -e` 的脚本在「条目正常取消了」这种日常路径上直接崩掉。
17
+ *
18
+ * 2. **切 deliveryMode 必须过 `--apply` 门。** 它是防重复发帖的唯一闸门(D1):
19
+ * 服务端会在同一事务里撤销/创建内部 job 并 append 指令。手滑改错的后果是
20
+ * 同一条内容被发两遍,或者谁都不发 —— 这个量级的写操作不该只靠一个 `--mode`。
21
+ * `calendar patch --body '{"deliveryMode":...}'` 同样被这道门挡住(见
22
+ * {@link deliveryModeChangeRequiresApply}),否则新命令的护栏只是装饰。
23
+ */
24
+ /** 契约 `calendarEntryDeliveryModeSchema` 的镜像;真源在 contracts。 */
25
+ const DELIVERY_MODES = ["hub_managed", "external_agent"];
26
+ export function isDeliveryMode(value) {
27
+ return (typeof value === "string" &&
28
+ DELIVERY_MODES.includes(value));
29
+ }
30
+ export class CalendarDirectiveArgError extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "CalendarDirectiveArgError";
34
+ }
35
+ }
36
+ /**
37
+ * `--revision` 的本地解析。
38
+ *
39
+ * 两道校验缺一不可:
40
+ * - `^\d+$` 挡住 `""` / `" "` / `1e3` / `-1` / `1.5`。**绝不能用 `Number()` 兜底**:
41
+ * 它把空串和空白都转成 `0`,而 revision 0 是每条条目的合法初始值 —— 于是
42
+ * 「忘了带版本号」会被服务端当成「我手上是第 0 版」而真的放行内容。
43
+ * - `Number.isSafeInteger` 挡住 `99999999999999999999` 这类纯数字但超出 IEEE754
44
+ * 安全区的值:它能过正则,转成 number 后却已经悄悄变了值,比对上的是另一条
45
+ * revision。服务端也有 `MAX_SAFE_INTEGER` 上界,这里提前挡住只是不让明显非法
46
+ * 的值打到网络上。
47
+ */
48
+ export function parseRevisionArg(raw) {
49
+ if (!/^\d+$/.test(raw)) {
50
+ throw new CalendarDirectiveArgError(`--revision must be a non-negative integer (got ${JSON.stringify(raw)})`);
51
+ }
52
+ const value = Number(raw);
53
+ if (!Number.isSafeInteger(value)) {
54
+ throw new CalendarDirectiveArgError(`--revision ${raw} exceeds the safe integer range; it cannot be compared reliably`);
55
+ }
56
+ return value;
57
+ }
58
+ /**
59
+ * `calendar patch` 的 body 是不是在动 `deliveryMode` —— 是就必须过 `--apply` 门。
60
+ *
61
+ * 只看「键在不在」而不看值:`{"deliveryMode": null}` / 写错的枚举值同样是一次
62
+ * 「意图切换执行主体」的调用,服务端会拒,但它该在拒之前先被护栏拦下。
63
+ */
64
+ export function deliveryModeChangeRequiresApply(body) {
65
+ return Object.prototype.hasOwnProperty.call(body, "deliveryMode");
66
+ }
67
+ /**
68
+ * `calendar patch` 要不要进 `assertApplyOrDryRun`。
69
+ *
70
+ * 两个判据故意不同:
71
+ * - `--dry-run` **对任何 body 都生效** —— 全局 flag 契约是「预览写入,不调用 API」。
72
+ * 只在 deliveryMode 分支上认它,就会出现「加了 --dry-run 却真的写了」的意外写入。
73
+ * - `--apply` 只在 body 含 `deliveryMode` 时才是必填(那是 D1 闸门);其余字段
74
+ * (status / permalink / …)维持既有的无门行为,不破坏存量自动化。
75
+ */
76
+ export function calendarPatchNeedsGate(body, opts) {
77
+ return Boolean(opts.dryRun) || deliveryModeChangeRequiresApply(body);
78
+ }
79
+ export function registerCalendarDirectiveCommands(calendar) {
80
+ calendar
81
+ .command("preflight")
82
+ .description("GET /publishing-calendar-directives/:entryId/preflight —— 外部 agent 发帖前的只读校验(拿到 decision=ok 才能发)")
83
+ .option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
84
+ .requiredOption("-e, --entry <entryId>", "Calendar entry UUID")
85
+ .requiredOption("--revision <n>", "你手上那条 directive 的 revision(必填,不能猜:0 是合法初始值)")
86
+ .option("--require-ok", "decision 不是 ok 时以退出码 1 结束(默认恒 0,业务拒绝不是错误)", false)
87
+ .action(async (opts) => {
88
+ let revision;
89
+ try {
90
+ revision = parseRevisionArg(String(opts.revision));
91
+ }
92
+ catch (e) {
93
+ console.error(e instanceof Error ? e.message : "Invalid --revision");
94
+ process.exit(1);
95
+ }
96
+ const res = await requireClient().preflightPublishingCalendarDirective(resolveTeamId(opts.team), opts.entry, revision);
97
+ // 无论放行还是拒绝,都先把完整判定写出去:`--require-ok` 的调用方也需要
98
+ // 那个 `reason` 才知道该删 cron 还是该等新指令。
99
+ console.log(JSON.stringify(res, null, 2));
100
+ if (opts.requireOk && res.decision !== "ok") {
101
+ // `process.exitCode` 而不是 `process.exit()`:后者会在 stdout 还没 flush
102
+ // 完时把进程掐掉,管道下游可能只拿到半截 JSON。
103
+ process.exitCode = 1;
104
+ }
105
+ });
106
+ calendar
107
+ .command("set-delivery-mode")
108
+ .description("PATCH /calendar-entries/:id —— 切换这条条目由谁发布(hub_managed = Hub 内部 job;external_agent = 外部 agent 收指令建 cron)")
109
+ .option("-t, --team <teamId>", "Team UUID(缺省用当前 context)")
110
+ .requiredOption("-e, --entry <entryId>", "Calendar entry UUID")
111
+ .requiredOption("--mode <mode>", `${DELIVERY_MODES.join(" | ")}(防重复发帖的唯一闸门)`)
112
+ .option("--dry-run", "预览", false)
113
+ .option("--apply", "执行写入", false)
114
+ .action(async (opts) => {
115
+ if (!isDeliveryMode(opts.mode)) {
116
+ console.error(`Invalid --mode ${JSON.stringify(String(opts.mode))}; expected one of ${DELIVERY_MODES.join(", ")}`);
117
+ process.exit(1);
118
+ }
119
+ const teamId = resolveTeamId(opts.team);
120
+ if (!assertApplyOrDryRun(opts, {
121
+ command: "calendar set-delivery-mode",
122
+ danger: "切换发布主体:转 external_agent 会撤销内部 publish_post job 并发 upsert 指令;转 hub_managed 会发 cancel 指令并建内部 job。改错 = 双发或谁都不发。",
123
+ summary: {
124
+ teamId,
125
+ calendarEntryId: opts.entry,
126
+ deliveryMode: opts.mode,
127
+ },
128
+ })) {
129
+ return;
130
+ }
131
+ const res = await requireClient().updateCalendarEntry(teamId, opts.entry, { deliveryMode: opts.mode });
132
+ console.log(JSON.stringify(res, null, 2));
133
+ });
134
+ }
135
+ //# sourceMappingURL=register-calendar-directives.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-calendar-directives.js","sourceRoot":"","sources":["../src/register-calendar-directives.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=register-calendar-directives.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-calendar-directives.test.d.ts","sourceRoot":"","sources":["../src/register-calendar-directives.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Command } from "commander";
3
+ import { CalendarDirectiveArgError, calendarPatchNeedsGate, deliveryModeChangeRequiresApply, isDeliveryMode, parseRevisionArg, registerCalendarDirectiveCommands, } from "./register-calendar-directives.js";
4
+ /**
5
+ * revision 是 preflight 的版本钉子:它一旦被「默认值化」,服务端就会拿一条真实
6
+ * 存在的 revision 0 条目放行内容。所以这里锁的不是「解析对不对」,而是
7
+ * **哪些输入必须拒绝**。
8
+ */
9
+ describe("parseRevisionArg", () => {
10
+ it("接受 0 与普通非负整数", () => {
11
+ expect(parseRevisionArg("0")).toBe(0);
12
+ expect(parseRevisionArg("1")).toBe(1);
13
+ expect(parseRevisionArg("42")).toBe(42);
14
+ });
15
+ /**
16
+ * 🔴 这条是整个命令最重要的断言。`Number("")` 和 `Number(" ")` 都是 `0`,
17
+ * 而 0 是每条日历条目的合法初始 revision —— 用 `Number()` 兜底解析等于把
18
+ * 「忘了带版本号」翻译成「我手上是第 0 版」,服务端会照常放行内容。
19
+ */
20
+ it("空串 / 空白 / 纯符号一律拒绝(绝不落到 0)", () => {
21
+ for (const raw of ["", " ", "\t", "+", "-"]) {
22
+ expect(() => parseRevisionArg(raw)).toThrow(CalendarDirectiveArgError);
23
+ }
24
+ });
25
+ it("负数、小数、科学计数法、带空格的数字都拒绝", () => {
26
+ for (const raw of ["-1", "1.5", "1e3", " 1", "1 ", "0x10", "NaN"]) {
27
+ expect(() => parseRevisionArg(raw)).toThrow(CalendarDirectiveArgError);
28
+ }
29
+ });
30
+ /**
31
+ * 纯数字但超出安全整数区:能过正则,转成 number 后值已经悄悄变了,
32
+ * 拿去比对的是另一条 revision。服务端也有 MAX_SAFE_INTEGER 上界。
33
+ */
34
+ it("超出安全整数范围的纯数字串拒绝", () => {
35
+ expect(parseRevisionArg(String(Number.MAX_SAFE_INTEGER))).toBe(Number.MAX_SAFE_INTEGER);
36
+ expect(() => parseRevisionArg("9007199254740993")).toThrow(/safe integer range/);
37
+ expect(() => parseRevisionArg("99999999999999999999")).toThrow(CalendarDirectiveArgError);
38
+ });
39
+ });
40
+ describe("isDeliveryMode", () => {
41
+ it("只认契约里的两个值", () => {
42
+ expect(isDeliveryMode("hub_managed")).toBe(true);
43
+ expect(isDeliveryMode("external_agent")).toBe(true);
44
+ for (const bad of ["", "HUB_MANAGED", "external", null, undefined, 1]) {
45
+ expect(isDeliveryMode(bad)).toBe(false);
46
+ }
47
+ });
48
+ });
49
+ /**
50
+ * `calendar set-delivery-mode` 带 `--apply` 门,但 `calendar patch --body` 能写
51
+ * 任意字段。若 raw patch 不同样挡住 `deliveryMode`,那道门就只是装饰。
52
+ */
53
+ describe("deliveryModeChangeRequiresApply", () => {
54
+ it("body 里出现 deliveryMode 就要门", () => {
55
+ expect(deliveryModeChangeRequiresApply({ deliveryMode: "external_agent" })).toBe(true);
56
+ });
57
+ /** 只看键在不在:写错的值/显式 null 同样是一次「意图切换执行主体」的调用。 */
58
+ it("值非法或为 null 也要门(只看键存在与否)", () => {
59
+ expect(deliveryModeChangeRequiresApply({ deliveryMode: null })).toBe(true);
60
+ expect(deliveryModeChangeRequiresApply({ deliveryMode: "nope" })).toBe(true);
61
+ expect(deliveryModeChangeRequiresApply({ deliveryMode: undefined })).toBe(true);
62
+ });
63
+ /** 既有用法(改状态/回填 permalink)行为必须完全不变,不能被新门牵连。 */
64
+ it("不含 deliveryMode 的既有 body 不要门", () => {
65
+ expect(deliveryModeChangeRequiresApply({
66
+ status: "succeeded",
67
+ permalink: "https://www.reddit.com/r/x/comments/y/",
68
+ })).toBe(false);
69
+ expect(deliveryModeChangeRequiresApply({})).toBe(false);
70
+ });
71
+ });
72
+ describe("calendarPatchNeedsGate", () => {
73
+ /**
74
+ * 🔴 `--dry-run` 的全局契约是「预览写入,不调用 API」。若只在 deliveryMode 分支上
75
+ * 认它,`--dry-run --body '{"status":"succeeded"}'` 会**真的把状态改掉** ——
76
+ * 这是最不该有的一类意外写入。
77
+ */
78
+ it("--dry-run 对任何 body 都要进门", () => {
79
+ expect(calendarPatchNeedsGate({ status: "succeeded" }, { dryRun: true })).toBe(true);
80
+ expect(calendarPatchNeedsGate({}, { dryRun: true })).toBe(true);
81
+ });
82
+ it("deliveryMode 变更即使没 --dry-run 也要进门(--apply 必填)", () => {
83
+ expect(calendarPatchNeedsGate({ deliveryMode: "external_agent" }, {})).toBe(true);
84
+ });
85
+ /** 既有无门用法必须原样保留:不带 --dry-run 又不动 deliveryMode = 直接写。 */
86
+ it("既有 body + 无 --dry-run 时完全不进门", () => {
87
+ expect(calendarPatchNeedsGate({ status: "succeeded", permalink: "https://example.test/" }, { dryRun: false })).toBe(false);
88
+ expect(calendarPatchNeedsGate({ status: "failed" }, {})).toBe(false);
89
+ });
90
+ });
91
+ describe("registerCalendarDirectiveCommands", () => {
92
+ function build() {
93
+ const calendar = new Command("calendar");
94
+ calendar.exitOverride();
95
+ registerCalendarDirectiveCommands(calendar);
96
+ return calendar;
97
+ }
98
+ it("把两个子命令挂到 calendar 组下", () => {
99
+ expect(build()
100
+ .commands.map((c) => c.name())
101
+ .sort()).toEqual(["preflight", "set-delivery-mode"]);
102
+ });
103
+ /**
104
+ * `--revision <n>` 必须带值占位符:写成 `--revision` 会被 commander 当成布尔
105
+ * 开关,`opts.revision` 恒为 `true`,于是 `String(true)` 落进解析器被拒 ——
106
+ * 表现为「传了版本号却说非法」,极难排查。
107
+ */
108
+ it("--revision 是必填且带值,--require-ok 是布尔开关", () => {
109
+ const preflight = build().commands.find((c) => c.name() === "preflight");
110
+ const options = preflight?.options ?? [];
111
+ const revision = options.find((o) => o.long === "--revision");
112
+ expect(revision?.required).toBe(true);
113
+ expect(revision?.mandatory).toBe(true);
114
+ const requireOk = options.find((o) => o.long === "--require-ok");
115
+ expect(requireOk?.required).toBe(false);
116
+ expect(requireOk?.mandatory).toBe(false);
117
+ });
118
+ /** 切发布主体是高风险写:两个门选项必须都在。 */
119
+ it("set-delivery-mode 带 --apply / --dry-run 门", () => {
120
+ const cmd = build().commands.find((c) => c.name() === "set-delivery-mode");
121
+ const longs = (cmd?.options ?? []).map((o) => o.long);
122
+ expect(longs).toContain("--apply");
123
+ expect(longs).toContain("--dry-run");
124
+ expect((cmd?.options ?? []).find((o) => o.long === "--mode")?.mandatory).toBe(true);
125
+ });
126
+ });
127
+ //# sourceMappingURL=register-calendar-directives.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-calendar-directives.test.js","sourceRoot":"","sources":["../src/register-calendar-directives.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geoly-ai/social-hub-cli",
3
- "version": "0.3.19",
3
+ "version": "0.3.20",
4
4
  "type": "module",
5
5
  "description": "social-hub CLI for Social Ops Hub",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "commander": "^12.1.0",
25
25
  "@geoly-ai/social-hub-authz": "0.0.17",
26
- "@geoly-ai/social-hub-sdk": "0.0.56"
26
+ "@geoly-ai/social-hub-sdk": "0.0.57"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.2",
package/skills/README.md CHANGED
@@ -119,28 +119,29 @@ pnpm skills:check
119
119
 
120
120
  ## Skill 列表
121
121
 
122
- | Skill | 用途 |
123
- | ------------------------------------------------------------------ | ---------------------------------- |
124
- | [social-hub-cli](social-hub-cli) | 总路由 + 领域 skill 索引 |
125
- | [social-hub-shared](social-hub-shared) | config、auth、doctor、context |
126
- | [social-hub-ops-runtime](social-hub-ops-runtime) | claim / complete / fail |
127
- | [social-hub-openclaw-context](social-hub-openclaw-context) | DB 聚合上下文 |
128
- | [social-hub-accounts](social-hub-accounts) | 账号 CRUD、browser-envs、personas |
129
- | [social-hub-posts](social-hub-posts) | Reddit 帖快照 CRUD、batch-upsert |
130
- | [social-hub-style-curator](social-hub-style-curator) | style-marks、curator、style-guide |
131
- | [social-hub-style-profiles](social-hub-style-profiles) | 板块画像四眼、帖子标注 |
132
- | [social-hub-calendar-jobs](social-hub-calendar-jobs) | 日历与任务 |
133
- | [social-hub-publishing](social-hub-publishing) | 发布闭环 |
134
- | [social-hub-content-review](social-hub-content-review) | 评论发布前复审/优化(gate 层③) |
135
- | [social-hub-post-review](social-hub-post-review) | 帖子发布前审核/优化(post-review) |
136
- | [social-hub-events-observability](social-hub-events-observability) | events、dashboard、audit、reports |
137
- | [social-hub-graph-compliance](social-hub-graph-compliance) | 图谱与合规风控 |
138
- | [social-hub-admin](social-hub-admin) | 系统/团队管理、通知渠道 |
139
- | [social-hub-intelligence](social-hub-intelligence) | 板块情报与 insights |
140
- | [social-hub-subreddit-pools](social-hub-subreddit-pools) | 运营板块池 catalog/aliases/tier |
141
- | [social-hub-scrape](social-hub-scrape) | 通用网页抓取(Firecrawl) |
142
- | [social-hub-migration](social-hub-migration) | 导入导出与 replay |
143
- | [social-hub-notifications](social-hub-notifications) | 事件订阅通知:notify pull/ack/订阅 |
122
+ | Skill | 用途 |
123
+ | ------------------------------------------------------------------ | -------------------------------------------- |
124
+ | [social-hub-cli](social-hub-cli) | 总路由 + 领域 skill 索引 |
125
+ | [social-hub-shared](social-hub-shared) | config、auth、doctor、context |
126
+ | [social-hub-ops-runtime](social-hub-ops-runtime) | claim / complete / fail |
127
+ | [social-hub-openclaw-context](social-hub-openclaw-context) | DB 聚合上下文 |
128
+ | [social-hub-accounts](social-hub-accounts) | 账号 CRUD、browser-envs、personas |
129
+ | [social-hub-posts](social-hub-posts) | Reddit 帖快照 CRUD、batch-upsert |
130
+ | [social-hub-style-curator](social-hub-style-curator) | style-marks、curator、style-guide |
131
+ | [social-hub-style-profiles](social-hub-style-profiles) | 板块画像四眼、帖子标注 |
132
+ | [social-hub-calendar-jobs](social-hub-calendar-jobs) | 日历与任务 |
133
+ | [social-hub-publishing](social-hub-publishing) | 发布闭环 |
134
+ | [social-hub-content-review](social-hub-content-review) | 评论发布前复审/优化(gate 层③) |
135
+ | [social-hub-post-review](social-hub-post-review) | 帖子发布前审核/优化(post-review) |
136
+ | [social-hub-events-observability](social-hub-events-observability) | events、dashboard、audit、reports |
137
+ | [social-hub-graph-compliance](social-hub-graph-compliance) | 图谱与合规风控 |
138
+ | [social-hub-admin](social-hub-admin) | 系统/团队管理、通知渠道 |
139
+ | [social-hub-intelligence](social-hub-intelligence) | 板块情报与 insights |
140
+ | [social-hub-subreddit-pools](social-hub-subreddit-pools) | 运营板块池 catalog/aliases/tier |
141
+ | [social-hub-scrape](social-hub-scrape) | 通用网页抓取(Firecrawl) |
142
+ | [social-hub-migration](social-hub-migration) | 导入导出与 replay |
143
+ | [social-hub-notifications](social-hub-notifications) | 事件订阅通知:notify pull/ack/订阅 |
144
+ | [social-hub-calendar-directives](social-hub-calendar-directives) | 发布日历指令:外部 agent 建 cron + preflight |
144
145
 
145
146
  ### Reddit 矩阵 Skill(随 CLI 一同发布)
146
147
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
3
  "package": "social-ops-hub",
4
- "cliVersion": "0.3.19",
4
+ "cliVersion": "0.3.20",
5
5
  "repository": "social-ops-hub",
6
6
  "publish": {
7
7
  "registry": "https://skill.sh",
@@ -109,6 +109,11 @@
109
109
  "path": "social-hub-notifications",
110
110
  "description": "事件订阅通知:notify subscriptions/pull/ack/source-configs,本机 agent 拉走后自行执行副作用"
111
111
  },
112
+ {
113
+ "id": "social-hub-calendar-directives",
114
+ "path": "social-hub-calendar-directives",
115
+ "description": "发布日历指令:外部 agent 收 upsert/cancel 建 UTC 一次性 cron,到点先过 calendar preflight"
116
+ },
112
117
  {
113
118
  "id": "reddit-strategy-orchestrator",
114
119
  "path": "reddit-strategy-orchestrator",
@@ -36,6 +36,16 @@ description: >-
36
36
  增加限制说明、增加社区观察句、把分节式结构改成叙事弧光、移动品牌出现位置、
37
37
  更换 `StructureFamily` 或 `ContentFunction`、按固定比例削减产品内容。
38
38
 
39
+ **客户的收紧标准不进默认层。** 品牌出现次数上限、必须以问句结尾、PDP 语言限制、固定
40
+ 输出段式这类要求属于可选 profile(`AcceptanceProfile: NativeCopy`,见
41
+ `../reddit-strategy-shared/references/native-copy-profile.md`)。**这些收紧项分属两个上游
42
+ 阶段,本 skill 两类都不施加**:结构类(品牌次数上限、受限 `EndingMode` 集合)只能由
43
+ `reddit-phase-brief` 在**分配时**施加——`EndingMode` / `BrandEntryMode` 都是锁定指纹字段,
44
+ 改写阶段动它们等于绕过结构预分配并作废碰撞检查;register 类(PDP 语言、fake-Reddit-English、
45
+ 禁用官方全名)由撰写 skill 在**出初稿时**遵守,本 skill 不得新加或加码——那是把「只让你修」
46
+ 的稿子做成 house voice 归一化。把任何收紧项写进默认层,就等于给所有品牌强加一套
47
+ house voice —— 那正是本 skill 被重新定义为约束保持型修复器要防的事。
48
+
39
49
  **没有该 subreddit 的 baseline(`SampleSetID`)时,不得声称已完成 subreddit-native 改写。**
40
50
  此时仍可工作,但只能声称到对应等级(见下方 `SubredditAdaptationStatus` 三档),
41
51
  并把缺口写进 `BlockingGaps`。
@@ -269,6 +279,10 @@ ExplicitStructureSpec:
269
279
  条件输入(可为 NotProvided;仅当本次修复确实依赖它时才 BlockingGap):
270
280
  - TargetSubreddit / SampleSetID / AnchorSlot / ContentKey / MappingRuleID
271
281
  - OpeningMove / EvidenceShape / BrandEntryMode / CaveatMode / EndingMode / LengthBand
282
+ - PersonaCardRef / PersonaCardVersion / PersonaCardHash:Brief 分配的知识边界
283
+ (含 `permittedExperienceClaims` / `knows` / `doesNotKnow` /
284
+ `brandRelationshipDisclosed`;契约见
285
+ `../reddit-strategy-shared/references/persona-card-contract.md`)
272
286
  - KnownProductFacts:可追溯的产品事实(SKU 基准表 / VOC / 甲方语料)
273
287
  - RealLimitations:真实存在的限制
274
288
  - RequiredDisclosure:必须保留的披露
@@ -482,6 +496,16 @@ ReadyForUse: No
482
496
  - 不编造作者身份、国籍、语言背景
483
497
  - **不主动制造 typo、拼写失误、错误冠词、介词误用、缺主语、"移民语法"**——
484
498
  这些不只是同质化,是伪造
499
+ - **不得新增、也不得保留与已分配 Persona Card 相矛盾的主张。** 稿件声称了
500
+ `permittedExperienceClaims` 未授权的第一手经历(ownership / delivery / wear /
501
+ service),或表现出 `doesNotKnow` 列出的知识,属于**无来源主张**,落在
502
+ `FactualExpression` 的既有职责内:按最小改动去掉该主张即可。
503
+ ⚠️ 但**加宽边界不在允许范围内**:本 skill 不得撰写、递增、改写任何 Card。需要更宽的
504
+ 边界就输出 `PersonaCardConflict: Yes` + 相矛盾的原句与对应 Card 字段,退回
505
+ `reddit-phase-brief` 换新版本 Card。`PersonaCardRef` / `PersonaCardVersion` /
506
+ `PersonaCardHash` 一律原样保持。
507
+ ⚠️ 命中「第一手经历」措辞本身**不是违规**——`mine arrived` 在授权 `delivery` 时完全正常;
508
+ 只有与 Card 矛盾才算。也**不得**因此声称做过任何知识边界审核:这只是本地修复。
485
509
  - 所有进入可发布文本(标题 / 正文 / 评论 / 回复)的实质性 claim 必须是 `ClaimEvidenceStatus: Cited`(可映射到已批准证据 `KnownProductFacts` / `SourceEvidence`)或 `ExternallyVerified`(非产品事实类、有外部权威来源 `ExternalSourceRef`);`Inferred`(无来源的模型推断)禁入可发布文本——删除或退回补证据,无法删且必需则 BlockingGap。三态权威定义见 `../reddit-strategy-shared/references/evidence-policy.md`
486
510
 
487
511
  ### 绝对禁用营销词
@@ -825,6 +849,11 @@ subreddit 规则、属于 `SampleSetID` 支持的异常值,否则保持原文
825
849
  - FingerprintSyncStatus: PendingWritingOwner | Confirmed | NotApplicable
826
850
  - DraftHash:
827
851
 
852
+ - PersonaCardRef: # 原样保持;本 skill 不得撰写或改写 Card
853
+ - PersonaCardVersion:
854
+ - PersonaCardHash:
855
+ - PersonaCardConflict: Yes | No | NotApplicable # Yes ⇒ 退回 reddit-phase-brief 换新版本 Card
856
+
828
857
  - TargetSubreddit:
829
858
  - SubredditAdaptationStatus: Verified | RulesOnly | NotClaimed # 阻断统一写 RoutingStatus / BlockingGaps
830
859
  - FactsAdded: None / Listed
@@ -37,7 +37,9 @@ This skill writes planned Reddit posts and comments. It does not redo strategy,
37
37
  - `references/pgc-writing-strategy.md` when writing PGC posts
38
38
  - `references/comp-mention-strategy.md` only when competitor mention permission is explicitly `Yes`
39
39
  - installed `reddit-brand-risk-response` output for pre-publish bot/shill/promo/claim risk gates when the batch is not explicitly low risk
40
- - `../reddit-strategy-shared/references/content-review-gate.md` for the mandatory Social Hub review loop
40
+ - `../reddit-strategy-shared/references/content-review-gate.md` for the mandatory Social Hub review loop, including how to read a dimension the Hub result reports as `not_assessed`
41
+ - `../reddit-strategy-shared/references/persona-card-contract.md` for the Brief-assigned Persona Card this skill writes **inside** and never authors
42
+ - `../reddit-strategy-shared/references/native-copy-profile.md` only when the batch runs `AcceptanceProfile: NativeCopy`
41
43
 
42
44
  ## Gate
43
45
 
@@ -49,6 +51,7 @@ Additionally, stop before writing a single word of body copy if any of these hol
49
51
  - a main-post row has no `StructureFamily` assigned by the Brief
50
52
  - a main-post row has no `AssignedFlair`, or the baseline card's `FlairCoverageStatus` is `Insufficient` → `BlockingGap`, return to `reddit-phase-brief`
51
53
  - a main-post row has no `PostingTrigger` assigned by the Brief, or its `Planned` `PostingTrigger` already violates the `PostingTrigger × BrandEntryMode` cross-constraint → `BlockingGap` (the post-draft re-derive of `PostingTrigger` vs `Planned` is checked after drafting, in the Writing Rules, not here)
54
+ - a main-post row has no `PersonaCardRef` assigned by the Brief → `BlockingGap`, return to `reddit-phase-brief`. **Never author a Card here**: a writer that supplies its own knowledge boundary grants itself exactly the boundary the draft needs, which is the bypass pre-assignment exists to close
52
55
  - a target subreddit has no baseline card, or it is `Expired` / `Incomplete` → `BlockingGap: BaselineMissingOrStale`
53
56
  - `memory/phase4-叙事指纹索引.md` has not been read, or the rolling-window collision check has not been run
54
57
  - the collision check fails and no sample-based exception is on record → `BlockingGap: StructureCollision`, return to `reddit-phase-brief`
@@ -78,6 +81,17 @@ C-B/C-NB, and rows that were previously low risk.
78
81
  persona or inline style guide. Resolve an approved `brandContext` when Hub
79
82
  requires an explicit brand relationship; do not guess a brand or send an
80
83
  arbitrary `stage` value.
84
+ **Never add the Persona Card, any Card field, or a Card-derived score to the
85
+ request, and never pack it into `subredditRules` or the thread context to get
86
+ it through.** The request schemas are strict, so an extra field is a rejected
87
+ call rather than an ignored key, and a free-text slot reserved for rule
88
+ evidence is not a transport for planner text. The Card governs writing only
89
+ (`../reddit-strategy-shared/references/persona-card-contract.md` §7).
90
+ Recording which quadrant the batch was written for is a **local** planner
91
+ declaration (`LocalReviewIntent`); the review call carries no scope field and
92
+ the response states no scope, so never report a server-side scope, and never
93
+ claim the omission of `accountId` was recognised by Hub as a deliberate
94
+ content-only choice.
81
95
  3. Send `rewrite: true, attempt: 1`. Inspect `executionStatus` before
82
96
  `verdict`.
83
97
  For a comment/reply, include `thread.threadPermalink` when you know the
@@ -106,7 +120,14 @@ C-B/C-NB, and rows that were previously low risk.
106
120
  changing the family — only harder to notice. Apply only the local fixes, or
107
121
  resubmit stating the structural constraints. Submit the changed exact draft
108
122
  as `attempt: 2`.
109
- 5. Accept only `completed/pass` for content review. In ContentOnly, record
123
+ 5. Accept only `completed/pass` for content review. Report any dimension the
124
+ result marks `not_assessed` as NotAssessed with its `notAssessedReason` — it
125
+ was excluded from the score, its remaining `score` number is diagnostic only,
126
+ and quoting it as a result or averaging it misrepresents the review. In
127
+ ContentOnly, `persona_consistency` / `persona_voice` coming back
128
+ `not_assessed` is correct behaviour, not a degraded review. Full semantics,
129
+ including why scores are comparable only within one `applicabilitySignature`:
130
+ `../reddit-strategy-shared/references/content-review-gate.md`. In ContentOnly, record
110
131
  `ReviewMode: ContentOnly`, `AccountBindingStatus: Pending`,
111
132
  `ReadyForContentNextSkill: Yes`, and `ReadyForDelivery: No`;
112
133
  `accountCandidateFeedback` is advisory and persona consistency is not
@@ -152,6 +173,24 @@ then hand to rewrite to keep editing".
152
173
  - **Real limitations:** state genuine, decision-relevant limitations when they exist; never invent a safe flaw to look authentic; position and form follow the content, not a fixed paragraph.
153
174
  - **Punctuation and format values follow the subreddit baseline** — no cross-subreddit em-dash ban, no parenthesis quota, no fixed word count.
154
175
  - **ABU (Answer-Bearing Unit, B-group):** 1–2 per post, all 8 hard requirements met, `AnchorSlot` from the 7-value enum, visibility gate satisfied per length band. The old "must sit in paragraph 2 or 3" rule is removed.
176
+ - **The Persona Card is Brief-assigned; write inside it, never widen it.** Read
177
+ the Card behind the row's `PersonaCardRef` before drafting, and before handing
178
+ the draft to Hub review run the local boundary self-check
179
+ (`../reddit-strategy-shared/references/persona-card-contract.md` §8): every
180
+ firsthand experience claim maps to an entry in `permittedExperienceClaims`,
181
+ nothing displays knowledge listed in `doesNotKnow`, the stated relationship
182
+ matches `brandRelationshipDisclosed`, and `postingWhy` still coexists with the
183
+ assigned `PostingTrigger`. Record `PersonaCardSelfCheck: Pass | BlockingGap`
184
+ **per Key** in `PerKeyStructureContract`, alongside the `PersonaCardVersion` and
185
+ `PersonaCardHash` it was checked against, with the contradicting phrase and Card
186
+ field for each gap. A batch-level value alone cannot show that every post was
187
+ checked against its own current Card, which is what delivery verifies. A draft that needs a
188
+ wider boundary goes back to `reddit-phase-brief` for a new Card version — the
189
+ writer never edits the Card and never bumps its version. This check is local: it
190
+ produces no score, is not a review, and never substitutes for the Hub gate.
191
+ The Card does not relax `ClaimEvidenceStatus`; it adds *who may claim it* on top
192
+ of *whether it is sourced*. A firsthand phrase such as "mine arrived" is not an
193
+ offence by itself — it matters only when it contradicts the Card.
155
194
  - **`PostingTrigger` is Brief-pre-assigned, not writer-chosen.** After drafting,
156
195
  re-derive the actual `PostingTrigger` from the finished body and confirm it
157
196
  matches the `Planned` value; a mismatch is a `BlockingGap` (return to Brief),
@@ -214,14 +253,24 @@ then hand to rewrite to keep editing".
214
253
  - AssignedFlairVerified: Yes / No
215
254
  - PostingTriggerPlannedVsFinal: Match / Drift / NotAssigned
216
255
  - PostingTriggerCrossConstraintOK: Yes / No
256
+ - PersonaCardSelfCheck: Pass / BlockingGap / NotApplicable # batch-level = worst across Keys; per-Key values live in PerKeyStructureContract
257
+ - PersonaCardTransportStatus: LocalOnly(ServerContractMissing) # the Card was not sent to Hub
258
+ - PersonaCardAuthorizationStatus: Authorized / DeclaredOnly / NotApplicable # carried through from the Brief, not re-derived here
259
+ - AcceptanceProfile: Default / NativeCopy
260
+ - NativeCopyProfileStatus: NotSelected / Unavailable(ServerContractMissing)
261
+ - LocalReviewIntent: accountBinding=<account_bound|persona_less>, brandTreatment=<managed_brand|organic> # carried through from the Brief; local declaration, not a Hub receipt
262
+ - HubReviewScopeStatus: Unavailable(ServerContractMissing) # never report a server-side scope
263
+ - NotAssessedDimensions: # per row: dimension + notAssessedReason, verbatim from Hub
217
264
  - ClaimEvidenceAllCitedOrExternallyVerified: Yes / No # no Inferred claim reached any publishable text (title/body/comment/reply)
218
265
  - FingerprintIndexUpdated: Yes / No
219
266
  - CollisionCheckPassed: Yes / No / ExceptionRecorded
220
267
  - PerKeyStructureContract: |
221
268
  <one row per DraftKey — batch-level single values are not valid, a batch normally
222
269
  spans several families, anchor slots and hashes>
223
- | Key | ClientContentPrototype | ContentFunction | PrimaryPostType | StructureFamily | AssignedFlair | FlairCoverageStatus | PostingTrigger | SampleSetID | AnchorSlot | SecondaryAnchorSlot | AllowedStructuralChanges | ForbiddenStructuralChanges | DraftHash |
224
- |---|---|---|---|---|---|---|---|---|---|---|---|---|---|
270
+ <comment rows take N/A for PersonaCardRef / PersonaCardVersion /
271
+ PersonaCardHash / PersonaCardSelfCheck>
272
+ | Key | ClientContentPrototype | ContentFunction | PrimaryPostType | StructureFamily | AssignedFlair | FlairCoverageStatus | PostingTrigger | PersonaCardRef | PersonaCardVersion | PersonaCardHash | PersonaCardSelfCheck | SampleSetID | AnchorSlot | SecondaryAnchorSlot | AllowedStructuralChanges | ForbiddenStructuralChanges | DraftHash |
273
+ |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
225
274
  - SKUBindingsPreserved: Yes / No
226
275
  - BrandWordStatusPreserved: Yes / No
227
276
  - ComplementRulesPassed: Yes / No