autocrew 0.4.0 → 0.4.2
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/adapters/openclaw/index.ts +7 -8
- package/package.json +1 -1
- package/skills/configure/SKILL.md +63 -5
- package/skills/write-script/SKILL.md +58 -576
- package/skills/write-script/SKILL.md.bak +601 -0
- package/src/cli/bootstrap.ts +3 -0
- package/src/cli/commands/logs.ts +26 -0
- package/src/e2e.test.ts +6 -9
- package/src/modules/workflow/templates.ts +20 -0
- package/src/runtime/context.ts +5 -1
- package/src/runtime/logger.test.ts +76 -0
- package/src/runtime/logger.ts +84 -0
- package/src/runtime/tool-runner.ts +34 -0
- package/src/runtime/workflow-engine.test.ts +69 -0
- package/src/runtime/workflow-engine.ts +20 -0
- package/src/tools/content-save.ts +11 -29
- package/templates/AGENTS.md +10 -64
|
@@ -12,6 +12,17 @@ const TEMPLATES: WorkflowDefinition[] = [
|
|
|
12
12
|
id: "xiaohongshu_full",
|
|
13
13
|
name: "小红书一键发布",
|
|
14
14
|
description: "从选题调研到发布的完整小红书内容流水线。包含 AI 写作、去 AI 痕迹、敏感词审查、封面设计等全流程。",
|
|
15
|
+
restatement: {
|
|
16
|
+
intervalSteps: 3,
|
|
17
|
+
context: [
|
|
18
|
+
"## Restatement — 质量提醒",
|
|
19
|
+
"- 标题必须 ≤20 中文字符",
|
|
20
|
+
"- 风格必须符合 STYLE.md",
|
|
21
|
+
"- 每段必须制造或解决张力,不能只是罗列信息",
|
|
22
|
+
"- 保持创作者第一人称视角",
|
|
23
|
+
"- 不要使用 AI 套话:值得一提的是、综上所述、首先其次最后",
|
|
24
|
+
].join("\n"),
|
|
25
|
+
},
|
|
15
26
|
steps: [
|
|
16
27
|
{
|
|
17
28
|
id: "research",
|
|
@@ -70,6 +81,15 @@ const TEMPLATES: WorkflowDefinition[] = [
|
|
|
70
81
|
id: "quick_publish",
|
|
71
82
|
name: "快速发布",
|
|
72
83
|
description: "已有内容的快速发布流程。去 AI 痕迹 → 审查 → 平台适配 → 封面 → 发布。",
|
|
84
|
+
restatement: {
|
|
85
|
+
intervalSteps: 3,
|
|
86
|
+
context: [
|
|
87
|
+
"## Restatement — 质量提醒",
|
|
88
|
+
"- 风格必须符合 STYLE.md",
|
|
89
|
+
"- 不要使用 AI 套话",
|
|
90
|
+
"- 发布前确保封面和内容匹配",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
},
|
|
73
93
|
steps: [
|
|
74
94
|
{
|
|
75
95
|
id: "humanize",
|
package/src/runtime/context.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Audit trail for debugging
|
|
8
8
|
*/
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import type { SessionLogger } from "./logger.js";
|
|
10
11
|
|
|
11
12
|
// --- Types ---
|
|
12
13
|
|
|
@@ -51,6 +52,8 @@ export interface ToolContext {
|
|
|
51
52
|
workspace: WorkspaceState;
|
|
52
53
|
/** Audit log for this session */
|
|
53
54
|
audit: AuditEntry[];
|
|
55
|
+
/** Persistent session logger (writes to ~/.autocrew/logs/) */
|
|
56
|
+
logger?: SessionLogger;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
// --- Factory ---
|
|
@@ -70,13 +73,14 @@ function resolveDataDir(config?: PluginConfig): string {
|
|
|
70
73
|
/**
|
|
71
74
|
* Create a new ToolContext. Called once per plugin registration or MCP session.
|
|
72
75
|
*/
|
|
73
|
-
export function createContext(config?: PluginConfig): ToolContext {
|
|
76
|
+
export function createContext(config?: PluginConfig, logger?: SessionLogger): ToolContext {
|
|
74
77
|
const ctx: ToolContext = {
|
|
75
78
|
sessionId: generateSessionId(),
|
|
76
79
|
dataDir: resolveDataDir(config),
|
|
77
80
|
config: config || {},
|
|
78
81
|
workspace: {},
|
|
79
82
|
audit: [],
|
|
83
|
+
logger,
|
|
80
84
|
};
|
|
81
85
|
_activeContext = ctx;
|
|
82
86
|
return ctx;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { SessionLogger } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
describe("SessionLogger", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let logger: SessionLogger;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "autocrew-log-"));
|
|
13
|
+
logger = new SessionLogger(tmpDir, "test-session-001");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("writes audit entries as JSONL", async () => {
|
|
21
|
+
await logger.audit({
|
|
22
|
+
tool: "autocrew_content",
|
|
23
|
+
action: "save",
|
|
24
|
+
timestamp: "2026-04-09T10:00:00Z",
|
|
25
|
+
durationMs: 150,
|
|
26
|
+
ok: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const logPath = path.join(tmpDir, "logs", "test-session-001", "audit.jsonl");
|
|
30
|
+
const raw = await fs.readFile(logPath, "utf-8");
|
|
31
|
+
const lines = raw.trim().split("\n");
|
|
32
|
+
expect(lines).toHaveLength(1);
|
|
33
|
+
expect(JSON.parse(lines[0]).tool).toBe("autocrew_content");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("writes events as JSONL", async () => {
|
|
37
|
+
await logger.event({
|
|
38
|
+
type: "content:created",
|
|
39
|
+
timestamp: "2026-04-09T10:00:00Z",
|
|
40
|
+
data: { contentId: "abc" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const logPath = path.join(tmpDir, "logs", "test-session-001", "events.jsonl");
|
|
44
|
+
const raw = await fs.readFile(logPath, "utf-8");
|
|
45
|
+
const lines = raw.trim().split("\n");
|
|
46
|
+
expect(lines).toHaveLength(1);
|
|
47
|
+
expect(JSON.parse(lines[0]).type).toBe("content:created");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("writes tool IO as JSONL", async () => {
|
|
51
|
+
await logger.toolIO({
|
|
52
|
+
tool: "autocrew_content",
|
|
53
|
+
action: "draft",
|
|
54
|
+
input: { topic_title: "AI编程", platform: "xhs" },
|
|
55
|
+
output: { ok: true, writingInstructions: "..." },
|
|
56
|
+
durationMs: 200,
|
|
57
|
+
timestamp: "2026-04-09T10:00:00Z",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const logPath = path.join(tmpDir, "logs", "test-session-001", "tool-io.jsonl");
|
|
61
|
+
const raw = await fs.readFile(logPath, "utf-8");
|
|
62
|
+
const entry = JSON.parse(raw.trim());
|
|
63
|
+
expect(entry.tool).toBe("autocrew_content");
|
|
64
|
+
expect(entry.input.topic_title).toBe("AI编程");
|
|
65
|
+
expect(entry.output.ok).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("appends multiple entries without overwriting", async () => {
|
|
69
|
+
await logger.audit({ tool: "a", timestamp: "t1", durationMs: 1, ok: true });
|
|
70
|
+
await logger.audit({ tool: "b", timestamp: "t2", durationMs: 2, ok: false, error: "boom" });
|
|
71
|
+
|
|
72
|
+
const logPath = path.join(tmpDir, "logs", "test-session-001", "audit.jsonl");
|
|
73
|
+
const lines = (await fs.readFile(logPath, "utf-8")).trim().split("\n");
|
|
74
|
+
expect(lines).toHaveLength(2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionLogger — persistent JSONL logging for audit entries, events, and tool IO.
|
|
3
|
+
*
|
|
4
|
+
* Writes to {dataDir}/logs/{sessionId}/ with separate files:
|
|
5
|
+
* - audit.jsonl — tool execution audit trail
|
|
6
|
+
* - events.jsonl — workflow events
|
|
7
|
+
* - tool-io.jsonl — full tool input/output for debugging
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import type { AuditEntry } from "./context.js";
|
|
12
|
+
import type { AutoCrewEvent } from "./events.js";
|
|
13
|
+
|
|
14
|
+
export interface ToolIOEntry {
|
|
15
|
+
tool: string;
|
|
16
|
+
action?: string;
|
|
17
|
+
input: Record<string, unknown>;
|
|
18
|
+
output: Record<string, unknown>;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SessionLogger {
|
|
24
|
+
private sessionDir: string;
|
|
25
|
+
private initialized = false;
|
|
26
|
+
|
|
27
|
+
constructor(dataDir: string, sessionId: string) {
|
|
28
|
+
this.sessionDir = path.join(dataDir, "logs", sessionId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private async ensureDir(): Promise<void> {
|
|
32
|
+
if (!this.initialized) {
|
|
33
|
+
await fs.mkdir(this.sessionDir, { recursive: true });
|
|
34
|
+
this.initialized = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async append(filename: string, data: unknown): Promise<void> {
|
|
39
|
+
await this.ensureDir();
|
|
40
|
+
const line = JSON.stringify(data) + "\n";
|
|
41
|
+
await fs.appendFile(path.join(this.sessionDir, filename), line, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async audit(entry: AuditEntry): Promise<void> {
|
|
45
|
+
await this.append("audit.jsonl", entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async event(event: AutoCrewEvent): Promise<void> {
|
|
49
|
+
await this.append("events.jsonl", event);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async toolIO(entry: ToolIOEntry): Promise<void> {
|
|
53
|
+
await this.append("tool-io.jsonl", entry);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** List all session log directories, newest first */
|
|
58
|
+
export async function listLogSessions(dataDir: string): Promise<string[]> {
|
|
59
|
+
const logsDir = path.join(dataDir, "logs");
|
|
60
|
+
try {
|
|
61
|
+
const entries = await fs.readdir(logsDir);
|
|
62
|
+
return entries.sort().reverse();
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Delete session logs older than N days */
|
|
69
|
+
export async function cleanOldLogs(dataDir: string, maxAgeDays: number): Promise<number> {
|
|
70
|
+
const logsDir = path.join(dataDir, "logs");
|
|
71
|
+
const cutoff = Date.now() - maxAgeDays * 86400000;
|
|
72
|
+
let deleted = 0;
|
|
73
|
+
try {
|
|
74
|
+
const entries = await fs.readdir(logsDir);
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const stat = await fs.stat(path.join(logsDir, entry));
|
|
77
|
+
if (stat.mtimeMs < cutoff) {
|
|
78
|
+
await fs.rm(path.join(logsDir, entry), { recursive: true });
|
|
79
|
+
deleted++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch { /* logs dir may not exist */ }
|
|
83
|
+
return deleted;
|
|
84
|
+
}
|
|
@@ -172,6 +172,39 @@ const auditMiddleware: Middleware = async (ctx, toolName, params, next) => {
|
|
|
172
172
|
return result;
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
/** Persist tool IO to disk via SessionLogger */
|
|
176
|
+
const persistentLogMiddleware: Middleware = async (ctx, toolName, params, next) => {
|
|
177
|
+
const start = Date.now();
|
|
178
|
+
const { _dataDir, _gatewayUrl, _geminiApiKey, _geminiModel, ...loggedInput } = params;
|
|
179
|
+
let result: ToolResult;
|
|
180
|
+
try {
|
|
181
|
+
result = await next();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (ctx.logger) {
|
|
184
|
+
await ctx.logger.toolIO({
|
|
185
|
+
tool: toolName,
|
|
186
|
+
action: loggedInput.action as string | undefined,
|
|
187
|
+
input: loggedInput,
|
|
188
|
+
output: { ok: false, error: err instanceof Error ? err.message : String(err) },
|
|
189
|
+
durationMs: Date.now() - start,
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
if (ctx.logger) {
|
|
196
|
+
await ctx.logger.toolIO({
|
|
197
|
+
tool: toolName,
|
|
198
|
+
action: loggedInput.action as string | undefined,
|
|
199
|
+
input: loggedInput,
|
|
200
|
+
output: result,
|
|
201
|
+
durationMs: Date.now() - start,
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
};
|
|
207
|
+
|
|
175
208
|
/** Update workspace state based on tool results */
|
|
176
209
|
const workspaceTrackingMiddleware: Middleware = async (ctx, toolName, params, next) => {
|
|
177
210
|
const result = await next();
|
|
@@ -206,6 +239,7 @@ export class ToolRunner {
|
|
|
206
239
|
onboardingGateMiddleware,
|
|
207
240
|
errorBoundaryMiddleware,
|
|
208
241
|
auditMiddleware,
|
|
242
|
+
persistentLogMiddleware,
|
|
209
243
|
workspaceTrackingMiddleware,
|
|
210
244
|
...(options.middleware || []),
|
|
211
245
|
];
|
|
@@ -452,4 +452,73 @@ describe("WorkflowEngine", () => {
|
|
|
452
452
|
it("throws when starting a nonexistent workflow", async () => {
|
|
453
453
|
await expect(engine.start("nonexistent")).rejects.toThrow("not found");
|
|
454
454
|
});
|
|
455
|
+
|
|
456
|
+
describe("restatement", () => {
|
|
457
|
+
it("injects restatement context every N steps", async () => {
|
|
458
|
+
runner.register({
|
|
459
|
+
name: "echo_tool",
|
|
460
|
+
label: "Echo",
|
|
461
|
+
description: "always succeeds",
|
|
462
|
+
parameters: {},
|
|
463
|
+
execute: vi.fn(async () => ({ ok: true, value: "done" })),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const def: WorkflowDefinition = {
|
|
467
|
+
id: "restate-test",
|
|
468
|
+
name: "Restatement Test",
|
|
469
|
+
description: "Tests restatement injection",
|
|
470
|
+
steps: Array.from({ length: 6 }, (_, i) => ({
|
|
471
|
+
id: `step-${i}`,
|
|
472
|
+
name: `Step ${i}`,
|
|
473
|
+
tool: "echo_tool",
|
|
474
|
+
params: {},
|
|
475
|
+
})),
|
|
476
|
+
restatement: {
|
|
477
|
+
intervalSteps: 3,
|
|
478
|
+
context: "IMPORTANT: Follow quality rules.",
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
engine.registerDefinition(def);
|
|
483
|
+
const instance = await engine.create("restate-test");
|
|
484
|
+
const result = await engine.start(instance.id);
|
|
485
|
+
|
|
486
|
+
expect(result.status).toBe("completed");
|
|
487
|
+
// After step index 3 (steps 0,1,2 done), restatement should fire
|
|
488
|
+
expect(result.stepResults["_restatement_after_step-2"]).toBeDefined();
|
|
489
|
+
expect((result.stepResults["_restatement_after_step-2"] as any).context).toBe("IMPORTANT: Follow quality rules.");
|
|
490
|
+
// After step index 6 (steps 3,4,5 done), another restatement
|
|
491
|
+
expect(result.stepResults["_restatement_after_step-5"]).toBeDefined();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("does not inject restatement when not configured", async () => {
|
|
495
|
+
runner.register({
|
|
496
|
+
name: "echo_tool2",
|
|
497
|
+
label: "Echo2",
|
|
498
|
+
description: "always succeeds",
|
|
499
|
+
parameters: {},
|
|
500
|
+
execute: vi.fn(async () => ({ ok: true, value: "done" })),
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const def: WorkflowDefinition = {
|
|
504
|
+
id: "no-restate",
|
|
505
|
+
name: "No Restatement",
|
|
506
|
+
description: "No restatement configured",
|
|
507
|
+
steps: Array.from({ length: 4 }, (_, i) => ({
|
|
508
|
+
id: `s-${i}`,
|
|
509
|
+
name: `Step ${i}`,
|
|
510
|
+
tool: "echo_tool2",
|
|
511
|
+
params: {},
|
|
512
|
+
})),
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
engine.registerDefinition(def);
|
|
516
|
+
const instance = await engine.create("no-restate");
|
|
517
|
+
const result = await engine.start(instance.id);
|
|
518
|
+
|
|
519
|
+
expect(result.status).toBe("completed");
|
|
520
|
+
const restateKeys = Object.keys(result.stepResults).filter(k => k.startsWith("_restatement_"));
|
|
521
|
+
expect(restateKeys).toHaveLength(0);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
455
524
|
});
|
|
@@ -35,6 +35,13 @@ export interface WorkflowDefinition {
|
|
|
35
35
|
name: string;
|
|
36
36
|
description: string;
|
|
37
37
|
steps: WorkflowStep[];
|
|
38
|
+
/** Optional: inject restatement context every N steps to combat attention decay */
|
|
39
|
+
restatement?: {
|
|
40
|
+
/** Inject restatement every N steps */
|
|
41
|
+
intervalSteps: number;
|
|
42
|
+
/** The context string to restate (key rules, current goal, quality constraints) */
|
|
43
|
+
context: string;
|
|
44
|
+
};
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
export type WorkflowStatus = "pending" | "running" | "paused" | "completed" | "failed" | "cancelled";
|
|
@@ -350,6 +357,19 @@ export class WorkflowEngine {
|
|
|
350
357
|
|
|
351
358
|
// Advance
|
|
352
359
|
instance.currentStepIndex++;
|
|
360
|
+
|
|
361
|
+
// Restatement injection — combat attention decay in long workflows
|
|
362
|
+
if (def.restatement && instance.currentStepIndex > 0 && instance.currentStepIndex % def.restatement.intervalSteps === 0) {
|
|
363
|
+
const restateKey = `_restatement_after_${step.id}`;
|
|
364
|
+
instance.stepResults[restateKey] = {
|
|
365
|
+
type: "restatement",
|
|
366
|
+
context: def.restatement.context,
|
|
367
|
+
afterStep: step.id,
|
|
368
|
+
stepIndex: instance.currentStepIndex,
|
|
369
|
+
timestamp: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
353
373
|
await this.persistInstance(instance);
|
|
354
374
|
}
|
|
355
375
|
|
|
@@ -149,39 +149,21 @@ Clock Theory: Bang moments at 12:00(hook), 3:00(escalation), 6:00(payload), 9:00
|
|
|
149
149
|
methodology,
|
|
150
150
|
wikiContext: wikiContext || "(no wiki knowledge available yet)",
|
|
151
151
|
writingInstructions: `
|
|
152
|
-
|
|
152
|
+
## Writing Instructions
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
Think through these questions before writing:
|
|
155
155
|
|
|
156
|
-
1.
|
|
156
|
+
1. WHO is reading this? (Reference the audiencePersona above)
|
|
157
|
+
2. WHAT is the ONE opinion this content argues? (Not a topic — an opinion)
|
|
158
|
+
3. WHY would a reader stop scrolling for this? (The hook)
|
|
159
|
+
4. WHAT evidence makes this believable? (Creator's own experience > external data)
|
|
157
160
|
|
|
158
|
-
|
|
161
|
+
Then:
|
|
162
|
+
- Build a skeleton FIRST (thesis → evidence → twist → action). Present it for confirmation.
|
|
163
|
+
- Write the full draft only after skeleton is confirmed.
|
|
164
|
+
- Every paragraph must either open a question or close one. No "information-only" paragraphs.
|
|
159
165
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
4. TENSION IS OXYGEN — Every paragraph must either OPEN a question or CLOSE one. No paragraph should just "sit there" as information. When energy drops, inject a question, contradiction, or surprise.
|
|
163
|
-
|
|
164
|
-
5. THE CREATOR IS THE PROOF — The creator's own experience is the strongest evidence. Lead with "I did X" before "Company Y did Z." Vulnerability > authority.
|
|
165
|
-
|
|
166
|
-
## TWO-PHASE CREATION
|
|
167
|
-
|
|
168
|
-
PHASE A — Build the skeleton FIRST (do not write prose yet):
|
|
169
|
-
- A1: Core thesis in ONE sentence (an opinion, not a topic)
|
|
170
|
-
- A2: Argument structure (thesis → evidence → twist → action)
|
|
171
|
-
- A3: Clock Theory — plan 4 bang moments (12:00 hook, 3:00 escalation, 6:00 payload, 9:00 climax)
|
|
172
|
-
- A4: HKRR — choose dominant dimension
|
|
173
|
-
- A5: Place 2-3 micro-retention techniques
|
|
174
|
-
|
|
175
|
-
Present the skeleton to the user. Wait for confirmation. Then:
|
|
176
|
-
|
|
177
|
-
PHASE B — Write the full draft based on the skeleton.
|
|
178
|
-
- Fear short, not long. Every case study deserves full detail.
|
|
179
|
-
- After each clock section, simulate the reader's reaction.
|
|
180
|
-
- Ground every factual claim in real sources.
|
|
181
|
-
|
|
182
|
-
## AFTER WRITING
|
|
183
|
-
|
|
184
|
-
Call autocrew_content action="save" with the full body text, title, platform, and hypothesis.
|
|
166
|
+
Save via autocrew_content action="save" with title, body, platform, and hypothesis.
|
|
185
167
|
Do NOT use the Write tool to create draft.md directly.
|
|
186
168
|
`,
|
|
187
169
|
nextAction: {
|
package/templates/AGENTS.md
CHANGED
|
@@ -14,50 +14,9 @@
|
|
|
14
14
|
10. After completing a task, suggest one concrete next step.
|
|
15
15
|
11. When user gives feedback on content, capture it via the memory-distill skill.
|
|
16
16
|
|
|
17
|
-
## Onboarding Protocol
|
|
17
|
+
## Onboarding Protocol
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
### Gate Check
|
|
22
|
-
1. Call `autocrew_pro_status` at session start.
|
|
23
|
-
2. If ANY of these are true → BLOCK all other actions and run onboarding:
|
|
24
|
-
- `profileExists: false`
|
|
25
|
-
- `missingInfo` array is non-empty
|
|
26
|
-
- `styleCalibrated: false`
|
|
27
|
-
|
|
28
|
-
### Onboarding Flow (3 phases, must complete all)
|
|
29
|
-
|
|
30
|
-
**Phase 1: 初始化 + 基础信息(2-3 轮对话)**
|
|
31
|
-
1. Call `autocrew_init` to create data directory
|
|
32
|
-
2. Read host MEMORY.md if it exists (extract known info)
|
|
33
|
-
3. Ask for missing fields ONLY:
|
|
34
|
-
- 行业/领域(必填)
|
|
35
|
-
- 目标平台(必填,可多选:小红书/抖音/公众号/视频号)
|
|
36
|
-
- 目标受众(必填:年龄段、职业、痛点)
|
|
37
|
-
- 变现模式(选填:广告/带货/知识付费/引流)
|
|
38
|
-
4. Save to creator-profile.json
|
|
39
|
-
|
|
40
|
-
**Phase 2: 风格校准(3-5 轮对话)**
|
|
41
|
-
1. 询问用户是否有参考账号或已有内容样本
|
|
42
|
-
2. 如果有 → 分析样本,提取风格特征
|
|
43
|
-
3. 如果没有 → 通过 A/B 对比问题确定风格偏好:
|
|
44
|
-
- 正式 vs 口语
|
|
45
|
-
- 专业术语 vs 大白话
|
|
46
|
-
- 长文深度 vs 短文快节奏
|
|
47
|
-
- 情感共鸣 vs 干货实用
|
|
48
|
-
4. 生成 STYLE.md 写作人格文件
|
|
49
|
-
5. 更新 creator-profile.json 的 `styleCalibrated: true`
|
|
50
|
-
|
|
51
|
-
**Phase 3: 确认 + 过渡**
|
|
52
|
-
1. 展示生成的风格档案摘要
|
|
53
|
-
2. 告诉用户"设置完成,现在可以开始创作了"
|
|
54
|
-
3. 然后继续用户的原始请求(如果有的话)
|
|
55
|
-
|
|
56
|
-
### 重要:不要把 onboarding 做成审讯
|
|
57
|
-
- 语气轻松友好,像朋友聊天
|
|
58
|
-
- 每次最多问 2-3 个问题
|
|
59
|
-
- 已知信息(从 MEMORY.md 读到的)直接确认,不重复问
|
|
60
|
-
- 风格校准用选择题,不要让用户写长文
|
|
19
|
+
Onboarding is enforced by the tool system. If a tool returns `error: "onboarding_required"`, `error: "profile_incomplete"`, or `error: "style_not_calibrated"`, follow the returned instructions to complete setup before proceeding. Keep the tone conversational — like chatting with a friend, not an interrogation.
|
|
61
20
|
|
|
62
21
|
## Memory Protocol
|
|
63
22
|
|
|
@@ -74,26 +33,13 @@ This is the FIRST thing that happens for any new user. No exceptions.
|
|
|
74
33
|
|
|
75
34
|
## Skill Routing
|
|
76
35
|
|
|
77
|
-
| User intent | Skill
|
|
78
|
-
|
|
36
|
+
| User intent | Skill |
|
|
37
|
+
|-------------|-------|
|
|
79
38
|
| First use / profile incomplete | onboarding |
|
|
80
|
-
|
|
|
81
|
-
| "
|
|
82
|
-
| "
|
|
83
|
-
| "
|
|
84
|
-
| "二创" / "换个角度写" / "remix" | remix-content |
|
|
85
|
-
| "写这个" / "帮我写" / "写一篇" | spawn-writer |
|
|
86
|
-
| "批量写" / "都写了" / "写N篇" | spawn-batch-writer |
|
|
87
|
-
| "改写" / "适配" / "发到XX平台" | platform-rewrite |
|
|
88
|
-
| "去AI味" / "润色" | humanizer-zh |
|
|
89
|
-
| "审核" / "检查" / "敏感词" | content-review |
|
|
90
|
-
| "封面" / "生成封面" / "做个封面" | cover-generator |
|
|
91
|
-
| "发布前检查" | pre-publish |
|
|
92
|
-
| "发布" / "发到小红书" | publish-content |
|
|
93
|
-
| "自动化" / "定时" / "pipeline" | manage-pipeline |
|
|
39
|
+
| "设置" / "风格校准" / "calibrate" | setup |
|
|
40
|
+
| "帮我写" / "写一篇" / "写这个" | spawn-writer (loads write-script + title-craft) |
|
|
41
|
+
| "批量写" / "都写了" | spawn-batch-writer |
|
|
42
|
+
| "帮我找选题" / "调研" / "内容规划" | spawn-planner or research (loads title-craft) |
|
|
94
43
|
| User gives feedback on content | memory-distill |
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| Video/note URL + "分析/拆解" | [Pro] video-analysis |
|
|
98
|
-
| Video/note URL (no analysis intent) | [Pro] extract-video-script |
|
|
99
|
-
| "数据" / "分析报告" | [Pro] analytics-report |
|
|
44
|
+
|
|
45
|
+
For other intents (封面, 发布, 审核, 去AI味, 改写, 状态, 对标, 数据), use the corresponding autocrew_* tool directly — the tool names are self-explanatory.
|