autocrew 0.3.4 → 0.3.5
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/package.json
CHANGED
|
@@ -14,11 +14,14 @@ export const cmd: CommandDef = {
|
|
|
14
14
|
|
|
15
15
|
// Try pipeline project first (draft versions from meta.yaml)
|
|
16
16
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const { getProjectMeta, syncUntrackedChanges } = await import("../../storage/pipeline-store.js");
|
|
18
|
+
|
|
19
|
+
// Auto-detect manual edits to draft.md before reading versions
|
|
20
|
+
const syncResult = await syncUntrackedChanges(id);
|
|
21
|
+
if (syncResult.synced) {
|
|
22
|
+
console.log(` (detected external edit: ${syncResult.reason})`);
|
|
23
|
+
}
|
|
24
|
+
|
|
22
25
|
const meta = await getProjectMeta(id);
|
|
23
26
|
if (meta) {
|
|
24
27
|
console.log(`Draft versions for "${meta.title}":`);
|
package/src/e2e.test.ts
CHANGED
|
@@ -181,6 +181,49 @@ describe("E2E: Topic Management", () => {
|
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
describe("E2E: Draft Action (content generation)", () => {
|
|
185
|
+
it("draft returns writing context and instructions for a topic", async () => {
|
|
186
|
+
const result = await runner.execute("autocrew_content", {
|
|
187
|
+
action: "draft",
|
|
188
|
+
topic_title: "vibe-coding 实践者的真实工作流",
|
|
189
|
+
platform: "xiaohongshu",
|
|
190
|
+
});
|
|
191
|
+
expect(result.ok).toBe(true);
|
|
192
|
+
expect(result.action).toBe("draft");
|
|
193
|
+
|
|
194
|
+
// Should return topic info
|
|
195
|
+
expect((result.topic as any).title).toBe("vibe-coding 实践者的真实工作流");
|
|
196
|
+
expect((result.topic as any).platform).toBe("xiaohongshu");
|
|
197
|
+
|
|
198
|
+
// Should return creator context
|
|
199
|
+
expect(result.creatorContext).toBeDefined();
|
|
200
|
+
|
|
201
|
+
// Should return writing instructions with Operating System principles
|
|
202
|
+
const instructions = result.writingInstructions as string;
|
|
203
|
+
expect(instructions).toContain("EMPATHY FIRST");
|
|
204
|
+
expect(instructions).toContain("THEIR WORDS, NOT YOURS");
|
|
205
|
+
expect(instructions).toContain("SHOW THE MOVIE");
|
|
206
|
+
expect(instructions).toContain("TENSION IS OXYGEN");
|
|
207
|
+
expect(instructions).toContain("THE CREATOR IS THE PROOF");
|
|
208
|
+
expect(instructions).toContain("TWO-PHASE CREATION");
|
|
209
|
+
expect(instructions).toContain("PHASE A");
|
|
210
|
+
expect(instructions).toContain("PHASE B");
|
|
211
|
+
|
|
212
|
+
// Should return next action guidance
|
|
213
|
+
expect((result.nextAction as any).tool).toBe("autocrew_content");
|
|
214
|
+
expect((result.nextAction as any).action).toBe("save");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("draft fails without topic_title", async () => {
|
|
218
|
+
const result = await runner.execute("autocrew_content", {
|
|
219
|
+
action: "draft",
|
|
220
|
+
platform: "xiaohongshu",
|
|
221
|
+
});
|
|
222
|
+
expect(result.ok).toBe(false);
|
|
223
|
+
expect(result.error).toContain("topic_title");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
184
227
|
describe("E2E: Topic → Start → Content (cross-system)", () => {
|
|
185
228
|
it("topic created via autocrew_topic can be started via pipeline_ops", async () => {
|
|
186
229
|
// Create topic via local-store (autocrew_topic)
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
listWikiPages,
|
|
31
31
|
regenerateWikiIndex,
|
|
32
32
|
appendWikiLog,
|
|
33
|
+
syncUntrackedChanges,
|
|
33
34
|
type IntelItem,
|
|
34
35
|
type TopicCandidate,
|
|
35
36
|
type WikiPage,
|
|
@@ -471,3 +472,66 @@ describe("Wiki Storage", () => {
|
|
|
471
472
|
expect(lines[1]).toContain("[update]");
|
|
472
473
|
});
|
|
473
474
|
});
|
|
475
|
+
|
|
476
|
+
// ─── Untracked Changes Detection ────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
describe("syncUntrackedChanges", () => {
|
|
479
|
+
it("detects manually edited draft.md and registers version", async () => {
|
|
480
|
+
await saveTopic(makeTopic({ title: "手动编辑测试" }), testDir);
|
|
481
|
+
await startProject("手动编辑测试", testDir);
|
|
482
|
+
|
|
483
|
+
const projectName = slugify("手动编辑测试");
|
|
484
|
+
const projectDir = path.join(stagePath("drafting", testDir), projectName);
|
|
485
|
+
|
|
486
|
+
// Simulate a manual edit: write new content to draft.md directly
|
|
487
|
+
// Wait >1s so mtime exceeds the 1-second detection threshold
|
|
488
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
489
|
+
await fs.writeFile(
|
|
490
|
+
path.join(projectDir, "draft.md"),
|
|
491
|
+
"# 手动编辑测试\n\n这是手动编辑的内容,不是通过 autocrew 工具写入的。包含足够长的文本来触发版本检测。",
|
|
492
|
+
"utf-8",
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Sync should detect the change
|
|
496
|
+
const result = await syncUntrackedChanges(projectName, testDir);
|
|
497
|
+
expect(result.synced).toBe(true);
|
|
498
|
+
expect(result.reason).toContain("external edit");
|
|
499
|
+
|
|
500
|
+
// Meta should now have a version entry
|
|
501
|
+
const meta = await getProjectMeta(projectName, testDir);
|
|
502
|
+
expect(meta!.versions.length).toBe(1);
|
|
503
|
+
expect(meta!.versions[0].note).toBe("detected external edit");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("creates meta.yaml when only draft.md exists", async () => {
|
|
507
|
+
await initPipeline(testDir);
|
|
508
|
+
const projectDir = path.join(stagePath("drafting", testDir), "orphan-project");
|
|
509
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
510
|
+
|
|
511
|
+
// Write draft.md without meta.yaml (simulates manual file copy)
|
|
512
|
+
await fs.writeFile(
|
|
513
|
+
path.join(projectDir, "draft.md"),
|
|
514
|
+
"# 孤儿项目\n\n这个项目的 draft.md 是手动复制进来的,没有 meta.yaml。",
|
|
515
|
+
"utf-8",
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const result = await syncUntrackedChanges("orphan-project", testDir);
|
|
519
|
+
expect(result.synced).toBe(true);
|
|
520
|
+
expect(result.reason).toContain("meta.yaml created");
|
|
521
|
+
|
|
522
|
+
// meta.yaml should now exist
|
|
523
|
+
const meta = await getProjectMeta("orphan-project", testDir);
|
|
524
|
+
expect(meta).not.toBeNull();
|
|
525
|
+
expect(meta!.title).toBe("孤儿项目");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("skips when draft.md has not changed", async () => {
|
|
529
|
+
await saveTopic(makeTopic({ title: "无变更测试" }), testDir);
|
|
530
|
+
await startProject("无变更测试", testDir);
|
|
531
|
+
|
|
532
|
+
const projectName = slugify("无变更测试");
|
|
533
|
+
const result = await syncUntrackedChanges(projectName, testDir);
|
|
534
|
+
expect(result.synced).toBe(false);
|
|
535
|
+
expect(result.reason).toContain("up to date");
|
|
536
|
+
});
|
|
537
|
+
});
|
|
@@ -948,6 +948,106 @@ export async function getProjectMeta(
|
|
|
948
948
|
return readMeta(found.dir);
|
|
949
949
|
}
|
|
950
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Detect if draft.md has been modified outside of AutoCrew tools
|
|
953
|
+
* (e.g., manually edited, copied in). If so, auto-register the change
|
|
954
|
+
* into meta.yaml so version tracking stays accurate.
|
|
955
|
+
*
|
|
956
|
+
* Returns true if an untracked change was detected and registered.
|
|
957
|
+
*/
|
|
958
|
+
export async function syncUntrackedChanges(
|
|
959
|
+
name: string,
|
|
960
|
+
dataDir?: string,
|
|
961
|
+
): Promise<{ synced: boolean; reason?: string }> {
|
|
962
|
+
const found = await findProject(name, dataDir);
|
|
963
|
+
if (!found) return { synced: false, reason: "project not found" };
|
|
964
|
+
|
|
965
|
+
const draftPath = path.join(found.dir, "draft.md");
|
|
966
|
+
const metaPath = path.join(found.dir, "meta.yaml");
|
|
967
|
+
|
|
968
|
+
// Check if draft.md exists
|
|
969
|
+
let draftStat;
|
|
970
|
+
try {
|
|
971
|
+
draftStat = await fs.stat(draftPath);
|
|
972
|
+
} catch {
|
|
973
|
+
return { synced: false, reason: "draft.md not found" };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Check if meta.yaml exists — if not, create one from draft.md
|
|
977
|
+
let meta: ProjectMeta;
|
|
978
|
+
try {
|
|
979
|
+
meta = await readMeta(found.dir);
|
|
980
|
+
} catch {
|
|
981
|
+
// meta.yaml missing — create from scratch based on draft.md
|
|
982
|
+
const draftContent = await fs.readFile(draftPath, "utf-8");
|
|
983
|
+
const titleMatch = draftContent.match(/^#\s+(.+)/m);
|
|
984
|
+
const now = new Date().toISOString();
|
|
985
|
+
meta = {
|
|
986
|
+
title: titleMatch?.[1]?.trim() || name,
|
|
987
|
+
domain: "",
|
|
988
|
+
format: "article",
|
|
989
|
+
createdAt: now,
|
|
990
|
+
sourceTopic: "",
|
|
991
|
+
intelRefs: [],
|
|
992
|
+
versions: [],
|
|
993
|
+
current: "draft.md",
|
|
994
|
+
history: [{ stage: found.stage, entered: now }],
|
|
995
|
+
platforms: [],
|
|
996
|
+
};
|
|
997
|
+
await writeMeta(found.dir, meta);
|
|
998
|
+
return { synced: true, reason: "meta.yaml created from existing draft.md" };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Compare draft.md mtime against meta.yaml's updatedAt or last version timestamp
|
|
1002
|
+
const draftMtime = draftStat.mtime;
|
|
1003
|
+
const lastKnownTime = meta.versions.length > 0
|
|
1004
|
+
? new Date(meta.versions[meta.versions.length - 1].createdAt)
|
|
1005
|
+
: meta.createdAt
|
|
1006
|
+
? new Date(meta.createdAt)
|
|
1007
|
+
: new Date(0);
|
|
1008
|
+
|
|
1009
|
+
// If draft.md is newer than the last tracked version by more than 5 seconds,
|
|
1010
|
+
// there was an untracked edit
|
|
1011
|
+
if (draftMtime.getTime() - lastKnownTime.getTime() > 1000) {
|
|
1012
|
+
// Read current draft content and register as a new version
|
|
1013
|
+
const draftContent = await fs.readFile(draftPath, "utf-8");
|
|
1014
|
+
|
|
1015
|
+
// Only register if draft has substantial content
|
|
1016
|
+
if (draftContent.trim().length < 10) {
|
|
1017
|
+
return { synced: false, reason: "draft.md is too short to register" };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Archive the current state if there's something to archive
|
|
1021
|
+
const versionNum = meta.versions.length + 1;
|
|
1022
|
+
const archiveFilename = `draft-v${versionNum}.md`;
|
|
1023
|
+
|
|
1024
|
+
// If this is the first version and draft has content, just register it
|
|
1025
|
+
// (no need to archive — there's nothing to archive from)
|
|
1026
|
+
if (meta.versions.length > 0) {
|
|
1027
|
+
// There's existing version history — this is a new edit on top
|
|
1028
|
+
// Archive current to draft-vN.md before we record the new state
|
|
1029
|
+
// (but we don't overwrite draft.md — the user already wrote it)
|
|
1030
|
+
await fs.writeFile(
|
|
1031
|
+
path.join(found.dir, archiveFilename),
|
|
1032
|
+
draftContent,
|
|
1033
|
+
"utf-8",
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
meta.versions.push({
|
|
1038
|
+
file: meta.versions.length > 0 ? archiveFilename : "draft.md",
|
|
1039
|
+
createdAt: draftMtime.toISOString(),
|
|
1040
|
+
note: "detected external edit",
|
|
1041
|
+
});
|
|
1042
|
+
meta.current = "draft.md";
|
|
1043
|
+
await writeMeta(found.dir, meta);
|
|
1044
|
+
|
|
1045
|
+
return { synced: true, reason: `registered external edit as version ${versionNum}` };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return { synced: false, reason: "draft.md is up to date with meta.yaml" };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
951
1051
|
export async function listProjects(
|
|
952
1052
|
stage: PipelineStage,
|
|
953
1053
|
dataDir?: string,
|
|
@@ -31,14 +31,17 @@ const ALL_STATUSES = [
|
|
|
31
31
|
] as const;
|
|
32
32
|
|
|
33
33
|
export const contentSaveSchema = Type.Object({
|
|
34
|
-
action: Type.Unsafe<"save" | "list" | "get" | "update" | "transition" | "create_variant" | "siblings" | "allowed_transitions">({
|
|
34
|
+
action: Type.Unsafe<"save" | "list" | "get" | "update" | "transition" | "create_variant" | "siblings" | "allowed_transitions" | "draft">({
|
|
35
35
|
type: "string",
|
|
36
|
-
enum: ["save", "list", "get", "update", "transition", "create_variant", "siblings", "allowed_transitions"],
|
|
36
|
+
enum: ["save", "list", "get", "update", "transition", "create_variant", "siblings", "allowed_transitions", "draft"],
|
|
37
37
|
description:
|
|
38
|
-
"Action: '
|
|
39
|
-
"'
|
|
40
|
-
"'
|
|
38
|
+
"Action: 'draft' GENERATE a content draft (provide topic_title + platform, returns writing context + instructions), " +
|
|
39
|
+
"'save' persist content, 'list' all, 'get' by id, 'update' existing, " +
|
|
40
|
+
"'transition' change status, 'create_variant' platform variant, " +
|
|
41
|
+
"'siblings' list siblings, 'allowed_transitions' valid next statuses.",
|
|
41
42
|
}),
|
|
43
|
+
topic_title: Type.Optional(Type.String({ description: "Topic title for draft action — what to write about" })),
|
|
44
|
+
topic_description: Type.Optional(Type.String({ description: "Topic description/angle for draft action" })),
|
|
42
45
|
id: Type.Optional(Type.String({ description: "Content id (for get/update/transition/siblings/allowed_transitions)" })),
|
|
43
46
|
title: Type.Optional(Type.String({ description: "Content title" })),
|
|
44
47
|
body: Type.Optional(Type.String({ description: "Content body (markdown)" })),
|
|
@@ -78,20 +81,137 @@ export const contentSaveSchema = Type.Object({
|
|
|
78
81
|
)),
|
|
79
82
|
});
|
|
80
83
|
|
|
84
|
+
async function executeDraft(params: Record<string, unknown>) {
|
|
85
|
+
const dataDir = (params._dataDir as string) || undefined;
|
|
86
|
+
const topicTitle = (params.topic_title as string) || (params.title as string) || "";
|
|
87
|
+
const topicDescription = (params.topic_description as string) || "";
|
|
88
|
+
const platform = (params.platform as string) || "xiaohongshu";
|
|
89
|
+
|
|
90
|
+
if (!topicTitle) {
|
|
91
|
+
return { ok: false, error: "topic_title is required for draft action. What should the content be about?" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const effectiveDataDir = dataDir || path.join(process.env.HOME ?? "~", ".autocrew");
|
|
95
|
+
|
|
96
|
+
// Load creator context
|
|
97
|
+
let style = "";
|
|
98
|
+
let profile: Record<string, unknown> = {};
|
|
99
|
+
let methodology = "";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
style = await fs.readFile(path.join(effectiveDataDir, "STYLE.md"), "utf-8");
|
|
103
|
+
} catch { /* no style file */ }
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const raw = await fs.readFile(path.join(effectiveDataDir, "creator-profile.json"), "utf-8");
|
|
107
|
+
profile = JSON.parse(raw);
|
|
108
|
+
} catch { /* no profile */ }
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Load HAMLETDEER methodology summary (not the full 562 lines — key principles only)
|
|
112
|
+
const full = await fs.readFile(path.join(effectiveDataDir, "..", "AutoCrew", "HAMLETDEER.md"), "utf-8");
|
|
113
|
+
// Extract the HKRR + Clock Theory sections
|
|
114
|
+
const hkrrMatch = full.match(/### The HKRR Framework[\s\S]*?(?=###\s|---)/);
|
|
115
|
+
const clockMatch = full.match(/### The Clock Theory[\s\S]*?(?=###\s|---)/);
|
|
116
|
+
methodology = [hkrrMatch?.[0] || "", clockMatch?.[0] || ""].filter(Boolean).join("\n\n");
|
|
117
|
+
} catch {
|
|
118
|
+
// HAMLETDEER not found at expected path — use embedded summary
|
|
119
|
+
methodology = `HKRR Framework: H(Happiness), K(Knowledge), R(Resonance), R(Rhythm). Short-form: pick ONE. Long-form: combine all.
|
|
120
|
+
Clock Theory: Bang moments at 12:00(hook), 3:00(escalation), 6:00(payload), 9:00(climax). Every position must have energy.`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load wiki knowledge if available
|
|
124
|
+
let wikiContext = "";
|
|
125
|
+
try {
|
|
126
|
+
const { listWikiPages, slugify: wikiSlugify } = await import("../storage/pipeline-store.js");
|
|
127
|
+
const pages = await listWikiPages(dataDir);
|
|
128
|
+
const relevant = pages.filter((p) =>
|
|
129
|
+
topicTitle.toLowerCase().includes(p.title.toLowerCase()) ||
|
|
130
|
+
p.aliases.some((a) => topicTitle.toLowerCase().includes(a.toLowerCase())),
|
|
131
|
+
);
|
|
132
|
+
if (relevant.length > 0) {
|
|
133
|
+
wikiContext = relevant.map((p) => `## Wiki: ${p.title}\n${p.body}`).join("\n\n");
|
|
134
|
+
}
|
|
135
|
+
} catch { /* wiki not available */ }
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
action: "draft",
|
|
140
|
+
topic: { title: topicTitle, description: topicDescription, platform },
|
|
141
|
+
creatorContext: {
|
|
142
|
+
industry: profile.industry || "",
|
|
143
|
+
platforms: profile.platforms || [],
|
|
144
|
+
expressionPersona: profile.expressionPersona || "",
|
|
145
|
+
styleBoundaries: profile.styleBoundaries || { never: [], always: [] },
|
|
146
|
+
audiencePersona: profile.audiencePersona || null,
|
|
147
|
+
},
|
|
148
|
+
style: style || "(no style file — write in natural conversational Chinese)",
|
|
149
|
+
methodology,
|
|
150
|
+
wikiContext: wikiContext || "(no wiki knowledge available yet)",
|
|
151
|
+
writingInstructions: `
|
|
152
|
+
You are now writing a content draft for Chinese social media. Follow these instructions EXACTLY.
|
|
153
|
+
|
|
154
|
+
## THE OPERATING SYSTEM — 5 Principles (override everything else)
|
|
155
|
+
|
|
156
|
+
1. EMPATHY FIRST — You are sitting across from ONE person (the audiencePersona). They are scrolling, half-distracted. Every sentence must earn their next 3 seconds. If a sentence triggers no curiosity, recognition, surprise, or relief — delete it.
|
|
157
|
+
|
|
158
|
+
2. THEIR WORDS, NOT YOURS — Every word must pass: would the reader say this to a friend over coffee? If no, replace it. No jargon, no abstractions, no "smart" words the reader wouldn't use.
|
|
159
|
+
|
|
160
|
+
3. SHOW THE MOVIE — Abstractions are invisible. Stories are visible. Every claim needs a scene: a face, a number, a moment. "She built a product in 3 weeks, alone" > "AI improves efficiency."
|
|
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.
|
|
185
|
+
Do NOT use the Write tool to create draft.md directly.
|
|
186
|
+
`,
|
|
187
|
+
nextAction: {
|
|
188
|
+
tool: "autocrew_content",
|
|
189
|
+
action: "save",
|
|
190
|
+
description: "After generating the draft, save it with action='save' providing title, body, platform, hypothesis.",
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
81
195
|
export async function executeContentSave(params: Record<string, unknown>) {
|
|
82
196
|
const action = (params.action as string) || "save";
|
|
83
197
|
const dataDir = (params._dataDir as string) || undefined;
|
|
84
198
|
|
|
199
|
+
if (action === "draft") {
|
|
200
|
+
return executeDraft(params);
|
|
201
|
+
}
|
|
202
|
+
|
|
85
203
|
if (action === "list") {
|
|
86
204
|
const contents = await listContents(dataDir);
|
|
87
205
|
|
|
88
206
|
// Also list pipeline drafting projects (these may not exist in legacy contents/)
|
|
89
207
|
const pipelineProjects: Array<{ slug: string; title: string; stage: string; current: string }> = [];
|
|
90
208
|
try {
|
|
91
|
-
const { listProjects, getProjectMeta,
|
|
209
|
+
const { listProjects, getProjectMeta, syncUntrackedChanges } = await import("../storage/pipeline-store.js");
|
|
92
210
|
for (const stage of ["drafting", "production", "published"] as const) {
|
|
93
211
|
const slugs = await listProjects(stage, dataDir);
|
|
94
212
|
for (const slug of slugs) {
|
|
213
|
+
// Auto-detect manual edits before reading meta
|
|
214
|
+
try { await syncUntrackedChanges(slug, dataDir); } catch { /* ignore */ }
|
|
95
215
|
const meta = await getProjectMeta(slug, dataDir);
|
|
96
216
|
if (meta) {
|
|
97
217
|
pipelineProjects.push({
|
package/src/tools/registry.ts
CHANGED
|
@@ -47,11 +47,11 @@ export function registerAllTools(runner: ToolRunner): void {
|
|
|
47
47
|
name: "autocrew_content",
|
|
48
48
|
label: "AutoCrew Content",
|
|
49
49
|
description:
|
|
50
|
-
"Content creation and lifecycle management.
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"3
|
|
50
|
+
"Content creation and lifecycle management. " +
|
|
51
|
+
"CONTENT CREATION WORKFLOW: " +
|
|
52
|
+
"Step 1: action='draft' with topic_title + platform → returns creator style, methodology, writing instructions, and wiki knowledge. " +
|
|
53
|
+
"Step 2: Generate the draft body following the returned instructions (two-phase: skeleton first, then prose). " +
|
|
54
|
+
"Step 3: action='save' with title + body + platform + hypothesis → persists to pipeline with version tracking + auto-humanization. " +
|
|
55
55
|
"Other actions: list, get, update, transition, create_variant, siblings, allowed_transitions.",
|
|
56
56
|
parameters: contentSaveSchema,
|
|
57
57
|
execute: executeContentSave,
|