@zzclub/pipeline 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +366 -170
- package/package.json +4 -7
- package/src/adapter-loader.ts +138 -0
- package/src/adapter-types.ts +172 -0
- package/src/adapters/builtin-image-renderer.ts +353 -0
- package/src/adapters/builtin-markdown-renderer.ts +63 -0
- package/src/adapters/index.ts +2 -0
- package/src/args.test.ts +118 -0
- package/src/args.ts +90 -0
- package/src/cli.ts +51 -0
- package/src/commands/abandon.test.ts +104 -0
- package/src/commands/abandon.ts +202 -0
- package/src/commands/attach-body-images.ts +88 -0
- package/src/commands/attach-body.ts +86 -0
- package/src/commands/attach-newspic-spec.ts +41 -0
- package/src/commands/checkpoint.test.ts +91 -0
- package/src/commands/checkpoint.ts +66 -0
- package/src/commands/config.ts +106 -0
- package/src/commands/cos-upload.ts +62 -0
- package/src/commands/doctor.ts +66 -0
- package/src/commands/find-run.ts +55 -0
- package/src/commands/hermes-metrics.ts +226 -0
- package/src/commands/imgx.ts +60 -0
- package/src/commands/ingest-handoff.ts +509 -0
- package/src/commands/init.ts +126 -0
- package/src/commands/prepare-finalize.ts +348 -0
- package/src/commands/prepare.ts +310 -0
- package/src/commands/publish.ts +147 -0
- package/src/commands/reconcile.ts +36 -0
- package/src/commands/render.ts +244 -0
- package/src/commands/reset.test.ts +151 -0
- package/src/commands/reset.ts +159 -0
- package/src/commands/review.ts +77 -0
- package/src/commands/status.ts +68 -0
- package/src/commands/sync-blog.ts +39 -0
- package/src/commands/tasks.ts +50 -0
- package/src/commands/wechat-export.ts +68 -0
- package/src/config.ts +0 -0
- package/src/imgx/ascii-portrait.ts +77 -0
- package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
- package/src/imgx/assets/icons/logo.png +0 -0
- package/src/imgx/assets/icons/logo.svg +8 -0
- package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
- package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
- package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
- package/src/imgx/cli.ts +57 -0
- package/src/imgx/geometry.ts +68 -0
- package/src/imgx/index.ts +5 -0
- package/src/imgx/longform-theme.ts +243 -0
- package/src/imgx/poster-recipe.ts +81 -0
- package/src/imgx/pretext-adapter.ts +14 -0
- package/src/imgx/pretext-runtime.ts +61 -0
- package/src/imgx/references/ascii-portrait-3-4.md +59 -0
- package/src/imgx/references/longform-3-4.md +173 -0
- package/src/imgx/references/poster-3-4.md +87 -0
- package/src/imgx/references/tips-3-4.md +69 -0
- package/src/imgx/references/wechat-cover-split.md +89 -0
- package/src/imgx/references/x-like-posts.md +139 -0
- package/src/imgx/render-article.test.ts +53 -0
- package/src/imgx/render-article.ts +1364 -0
- package/src/imgx/render-ascii-portrait.ts +53 -0
- package/src/imgx/render-card.ts +232 -0
- package/src/imgx/render-x-like-posts.ts +235 -0
- package/src/imgx/runtime.ts +403 -0
- package/src/imgx/typographic-scale.test.ts +56 -0
- package/src/imgx/typographic-scale.ts +44 -0
- package/src/output.ts +632 -0
- package/src/plugins.ts +101 -0
- package/src/profiles.ts +149 -0
- package/src/providers/blog.test.ts +58 -0
- package/src/providers/blog.ts +154 -0
- package/src/providers/cos.ts +183 -0
- package/src/providers/index.test.ts +26 -0
- package/src/providers/index.ts +185 -0
- package/src/providers/wechat.test.ts +215 -0
- package/src/providers/wechat.ts +548 -0
- package/src/routes.ts +287 -0
- package/src/runtime-paths.ts +179 -0
- package/src/schema/config.ts +133 -0
- package/src/schema/state.ts +376 -0
- package/src/spawn.ts +103 -0
- package/src/state.ts +265 -0
- package/src/task-manager.ts +657 -0
- package/src/task-views.ts +222 -0
- package/src/text.test.ts +689 -0
- package/src/text.ts +683 -0
- package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
- package/src/wechat-preview/browser/editor-export.css +241 -0
- package/src/wechat-preview/browser/editor-export.ts +103 -0
- package/src/wechat-preview/frontmatter-handler.ts +73 -0
- package/src/wechat-preview/index.test.ts +154 -0
- package/src/wechat-preview/index.ts +294 -0
- package/src/wechat-preview/themes.ts +141 -0
- package/src/wechat-preview/wechat-formatter.test.ts +288 -0
- package/src/wechat-preview/wechat-formatter.ts +1506 -0
- package/src/wechat-preview.test.ts +73 -0
- package/src/workflow-materials.test.ts +225 -0
- package/src/workflow-materials.ts +217 -0
- package/src/workflow.test.ts +2087 -0
- package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
- package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
- package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
- package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
- package/dist/cli.js +0 -431
- package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
- package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
- package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
- package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
- package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
- package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
- package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
- package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
- package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
- package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
- /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { readdir, stat } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
import { loadConfig, resolveWorkspacePaths, resolveWorkspaceRoot } from "./config";
|
|
5
|
+
import {
|
|
6
|
+
readState,
|
|
7
|
+
type PhaseName,
|
|
8
|
+
type ValidationError,
|
|
9
|
+
validateForPhase,
|
|
10
|
+
type WorkflowState,
|
|
11
|
+
} from "./state";
|
|
12
|
+
import { reconcileStateArtifacts } from "./workflow-materials";
|
|
13
|
+
|
|
14
|
+
export type TaskFileSource = "run" | "canonical";
|
|
15
|
+
|
|
16
|
+
export interface TaskGap {
|
|
17
|
+
code: string;
|
|
18
|
+
field: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TaskNextAction {
|
|
23
|
+
action: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
executor: "cli" | "worker" | "await-input" | "repair" | "complete" | "none";
|
|
26
|
+
command: string | null;
|
|
27
|
+
params?: {
|
|
28
|
+
/** Absolute path to the state JSON file. Always populated. */
|
|
29
|
+
state_path?: string;
|
|
30
|
+
/** True when the action should be executed as a spawned worker (e.g. editor). */
|
|
31
|
+
spawn?: boolean;
|
|
32
|
+
/** True when a research pass is needed before writing. */
|
|
33
|
+
requires_research?: boolean;
|
|
34
|
+
/** Path to the existing source body file, when already attached. */
|
|
35
|
+
source_body_path?: string;
|
|
36
|
+
/** Path to upstream material bundle/file, when writing should start from materials. */
|
|
37
|
+
source_materials_path?: string;
|
|
38
|
+
/** Path to the most recent formatted body file, when prepare already ran. */
|
|
39
|
+
formatted_body_path?: string;
|
|
40
|
+
/** Review feedback text, populated for revise-content action. */
|
|
41
|
+
feedback?: string | null;
|
|
42
|
+
/** Named worker expected to handle the step when executor=worker. */
|
|
43
|
+
worker_profile?: string;
|
|
44
|
+
/** High-level worker mode or purpose. */
|
|
45
|
+
worker_mode?: string;
|
|
46
|
+
/** Effective handoff research policy, when upstream explicitly set it. */
|
|
47
|
+
research_policy?: WorkflowState["handoff"]["research_policy"];
|
|
48
|
+
/** Effective handoff authoring policy, when upstream explicitly set it. */
|
|
49
|
+
authoring_policy?: WorkflowState["handoff"]["authoring_policy"];
|
|
50
|
+
/** Effective handoff review policy, when upstream explicitly set it. */
|
|
51
|
+
review_policy?: WorkflowState["handoff"]["review_policy"];
|
|
52
|
+
/** Inputs the caller must still provide before the command can run. */
|
|
53
|
+
required_inputs?: string[];
|
|
54
|
+
/** Whether body text must be generated or can be supplied by the user. */
|
|
55
|
+
body_source?: "needs_production" | "user_provided";
|
|
56
|
+
/** Post-execution instruction for the orchestrator. */
|
|
57
|
+
on_complete?: string;
|
|
58
|
+
/** Alternative CLI command when user already has the body as a file (no placeholder). */
|
|
59
|
+
handoff_alt?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TaskSummary {
|
|
64
|
+
run_id: string;
|
|
65
|
+
mode: WorkflowState["mode"];
|
|
66
|
+
phase: {
|
|
67
|
+
current: WorkflowState["phase"]["current"];
|
|
68
|
+
prepare: WorkflowState["phase"]["prepare"]["status"];
|
|
69
|
+
render: WorkflowState["phase"]["render"]["status"];
|
|
70
|
+
publish: WorkflowState["phase"]["publish"]["status"];
|
|
71
|
+
};
|
|
72
|
+
created_at: string | null;
|
|
73
|
+
updated_at: string | null;
|
|
74
|
+
state_path: string;
|
|
75
|
+
asset_path: string | null;
|
|
76
|
+
source: TaskFileSource;
|
|
77
|
+
task_kind: WorkflowState["intent"]["task_kind"];
|
|
78
|
+
content_form: WorkflowState["intent"]["content_form"];
|
|
79
|
+
targets: WorkflowState["intent"]["targets"];
|
|
80
|
+
content_origin: WorkflowState["intent"]["content_origin"];
|
|
81
|
+
route: {
|
|
82
|
+
primary: WorkflowState["route"]["primary"];
|
|
83
|
+
extras: WorkflowState["route"]["extras"];
|
|
84
|
+
account: string;
|
|
85
|
+
};
|
|
86
|
+
metadata: {
|
|
87
|
+
title: string | null;
|
|
88
|
+
slug: string | null;
|
|
89
|
+
date: string | null;
|
|
90
|
+
description: string | null;
|
|
91
|
+
};
|
|
92
|
+
content_review: WorkflowState["content_review"];
|
|
93
|
+
artifacts: WorkflowState["artifacts"];
|
|
94
|
+
images: {
|
|
95
|
+
plan_status: WorkflowState["images"]["plan"]["status"];
|
|
96
|
+
body_inputs: WorkflowState["images"]["body_inputs"];
|
|
97
|
+
render_assets_count: number;
|
|
98
|
+
};
|
|
99
|
+
publish: WorkflowState["publish"];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TaskStatusReport {
|
|
103
|
+
summary: TaskSummary;
|
|
104
|
+
state: WorkflowState;
|
|
105
|
+
validation: {
|
|
106
|
+
phase_checked: PhaseName;
|
|
107
|
+
valid: boolean;
|
|
108
|
+
errors: ValidationError[];
|
|
109
|
+
};
|
|
110
|
+
gaps: TaskGap[];
|
|
111
|
+
blockers: TaskGap[];
|
|
112
|
+
next_action: TaskNextAction;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ListedTask extends TaskStatusReport {
|
|
116
|
+
file_path: string;
|
|
117
|
+
source: TaskFileSource;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseDateParts(runId: string): Date | null {
|
|
121
|
+
const match = runId.match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})/);
|
|
122
|
+
if (!match) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const [, y, m, d, hh, mm, ss] = match;
|
|
126
|
+
const date = new Date(`${y}-${m}-${d}T${hh}:${mm}:${ss}`);
|
|
127
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fallbackIsoFromRunId(runId: string): string | null {
|
|
131
|
+
const parsed = parseDateParts(runId);
|
|
132
|
+
return parsed ? parsed.toISOString() : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function fallbackIsoFromFile(path: string): Promise<string | null> {
|
|
136
|
+
try {
|
|
137
|
+
const info = await stat(path);
|
|
138
|
+
return info.mtime.toISOString();
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getValidationPhase(state: WorkflowState): PhaseName {
|
|
145
|
+
if (state.phase.current === "done") {
|
|
146
|
+
if (state.intent.requires.publish) {
|
|
147
|
+
return "publish";
|
|
148
|
+
}
|
|
149
|
+
if (state.intent.requires.render) {
|
|
150
|
+
return "render";
|
|
151
|
+
}
|
|
152
|
+
return "prepare";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (state.phase.current === "failed") {
|
|
156
|
+
if (state.phase.publish.status === "failed" || state.phase.publish.status === "handoff") {
|
|
157
|
+
return "publish";
|
|
158
|
+
}
|
|
159
|
+
if (state.phase.render.status === "failed" || state.phase.render.status === "handoff") {
|
|
160
|
+
return "render";
|
|
161
|
+
}
|
|
162
|
+
return "prepare";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return state.phase.current;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function summarizeState(
|
|
169
|
+
state: WorkflowState,
|
|
170
|
+
filePath: string,
|
|
171
|
+
source: TaskFileSource,
|
|
172
|
+
createdAt: string | null,
|
|
173
|
+
updatedAt: string | null,
|
|
174
|
+
): TaskSummary {
|
|
175
|
+
return {
|
|
176
|
+
run_id: state.run_id,
|
|
177
|
+
mode: state.mode,
|
|
178
|
+
phase: {
|
|
179
|
+
current: state.phase.current,
|
|
180
|
+
prepare: state.phase.prepare.status,
|
|
181
|
+
render: state.phase.render.status,
|
|
182
|
+
publish: state.phase.publish.status,
|
|
183
|
+
},
|
|
184
|
+
created_at: createdAt,
|
|
185
|
+
updated_at: updatedAt,
|
|
186
|
+
state_path: state.state_path || filePath,
|
|
187
|
+
asset_path: state.asset_path || null,
|
|
188
|
+
source,
|
|
189
|
+
task_kind: state.intent.task_kind,
|
|
190
|
+
content_form: state.intent.content_form,
|
|
191
|
+
targets: state.intent.targets,
|
|
192
|
+
content_origin: state.intent.content_origin,
|
|
193
|
+
route: {
|
|
194
|
+
primary: state.route.primary,
|
|
195
|
+
extras: state.route.extras,
|
|
196
|
+
account: state.route.account,
|
|
197
|
+
},
|
|
198
|
+
metadata: {
|
|
199
|
+
title: state.metadata.title || null,
|
|
200
|
+
slug: state.metadata.slug || null,
|
|
201
|
+
date: state.metadata.date || null,
|
|
202
|
+
description: state.metadata.description || null,
|
|
203
|
+
},
|
|
204
|
+
content_review: state.content_review,
|
|
205
|
+
artifacts: state.artifacts,
|
|
206
|
+
images: {
|
|
207
|
+
plan_status: state.images.plan.status,
|
|
208
|
+
body_inputs: state.images.body_inputs,
|
|
209
|
+
render_assets_count: state.images.render_assets.length,
|
|
210
|
+
},
|
|
211
|
+
publish: state.publish,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildTaskStatus(summary: TaskSummary, state: WorkflowState): TaskStatusReport {
|
|
216
|
+
const phaseChecked = getValidationPhase(state);
|
|
217
|
+
const validationErrors = validateForPhase(state, phaseChecked);
|
|
218
|
+
const gaps: TaskGap[] = validationErrors.map((error) => ({
|
|
219
|
+
code: "validation",
|
|
220
|
+
field: error.field,
|
|
221
|
+
message: error.message,
|
|
222
|
+
}));
|
|
223
|
+
const blockers: TaskGap[] = [];
|
|
224
|
+
|
|
225
|
+
if (!state.source_body_path && !state.asset_path && !state.handoff.source_materials_path) {
|
|
226
|
+
gaps.unshift({
|
|
227
|
+
code: "missing-body",
|
|
228
|
+
field: "source_body_path",
|
|
229
|
+
message: "Body source is not attached yet.",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (state.content_review.status === "unchecked" && state.source_body_path) {
|
|
234
|
+
gaps.push({
|
|
235
|
+
code: "content-review",
|
|
236
|
+
field: "content_review.status",
|
|
237
|
+
message: "Content review decision is still missing.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (state.content_review.status === "needs_revision") {
|
|
242
|
+
blockers.push({
|
|
243
|
+
code: "needs-revision",
|
|
244
|
+
field: "content_review.status",
|
|
245
|
+
message: state.content_review.feedback || "Content review requires revision.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (state.images.body_inputs.status === "pending") {
|
|
250
|
+
blockers.push({
|
|
251
|
+
code: "missing-body-images",
|
|
252
|
+
field: "images.body_inputs",
|
|
253
|
+
message: `Waiting for ${state.images.body_inputs.expected - state.images.body_inputs.received.length} more body image(s).`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (state.mode === "failed") {
|
|
258
|
+
blockers.push({
|
|
259
|
+
code: "failed",
|
|
260
|
+
field: "mode",
|
|
261
|
+
message: "Task is marked as failed and needs manual intervention or reset.",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (state.mode === "handoff") {
|
|
266
|
+
blockers.push({
|
|
267
|
+
code: "handoff",
|
|
268
|
+
field: "mode",
|
|
269
|
+
message: "Task is currently waiting in handoff mode.",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let nextAction: TaskNextAction = {
|
|
274
|
+
action: "noop",
|
|
275
|
+
reason: "No next action computed.",
|
|
276
|
+
executor: "none",
|
|
277
|
+
command: null,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
if (state.mode === "done" || state.phase.current === "done") {
|
|
281
|
+
nextAction = {
|
|
282
|
+
action: "complete",
|
|
283
|
+
reason: "Task is already complete.",
|
|
284
|
+
executor: "complete",
|
|
285
|
+
command: null,
|
|
286
|
+
params: {
|
|
287
|
+
state_path: summary.state_path,
|
|
288
|
+
on_complete: "Task is done. No further action needed.",
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
} else if (state.mode === "failed") {
|
|
292
|
+
nextAction = {
|
|
293
|
+
action: "reset-or-repair",
|
|
294
|
+
reason: "Task failed; inspect state and run reset or repair missing assets.",
|
|
295
|
+
executor: "repair",
|
|
296
|
+
command: `zzhub-pipeline checkpoint --state "${summary.state_path}"`,
|
|
297
|
+
params: {
|
|
298
|
+
state_path: summary.state_path,
|
|
299
|
+
on_complete: "After repair, re-run status to confirm the task is active again.",
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
} else if (state.mode === "handoff") {
|
|
303
|
+
if (state.images.body_inputs.status === "pending") {
|
|
304
|
+
nextAction = {
|
|
305
|
+
action: "attach-body-images",
|
|
306
|
+
reason: "Body image inputs are still missing.",
|
|
307
|
+
executor: "await-input",
|
|
308
|
+
command: `zzhub-pipeline attach-body-images --state "${summary.state_path}" --images-file "{images_json_path}"`,
|
|
309
|
+
params: {
|
|
310
|
+
state_path: summary.state_path,
|
|
311
|
+
required_inputs: ["images_json_path"],
|
|
312
|
+
on_complete: "After user provides image inputs, call the suggested command, then re-run status.",
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
} else {
|
|
316
|
+
nextAction = {
|
|
317
|
+
action: "resolve-handoff",
|
|
318
|
+
reason: "Task is waiting for an external dependency or manual resolution.",
|
|
319
|
+
executor: "repair",
|
|
320
|
+
command: `zzhub-pipeline checkpoint --state "${summary.state_path}"`,
|
|
321
|
+
params: {
|
|
322
|
+
state_path: summary.state_path,
|
|
323
|
+
on_complete: "After resolving the external dependency, re-run status to proceed.",
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
} else if (!state.source_body_path && !state.asset_path) {
|
|
328
|
+
const hasMaterials = Boolean(state.handoff.source_materials_path);
|
|
329
|
+
const requiresResearch = state.intent.requires.research;
|
|
330
|
+
nextAction = {
|
|
331
|
+
action: "attach-body",
|
|
332
|
+
reason: hasMaterials
|
|
333
|
+
? "Upstream materials are attached; write the body from materials before continuing."
|
|
334
|
+
: "No body is attached yet.",
|
|
335
|
+
executor: "worker",
|
|
336
|
+
command: hasMaterials
|
|
337
|
+
? null
|
|
338
|
+
: `zzhub-pipeline attach-body --state "${summary.state_path}" --body-text "{body_text}"`,
|
|
339
|
+
params: {
|
|
340
|
+
state_path: summary.state_path,
|
|
341
|
+
spawn: true,
|
|
342
|
+
requires_research: requiresResearch || undefined,
|
|
343
|
+
source_materials_path: state.handoff.source_materials_path ?? undefined,
|
|
344
|
+
research_policy: state.handoff.research_policy,
|
|
345
|
+
authoring_policy: state.handoff.authoring_policy,
|
|
346
|
+
review_policy: state.handoff.review_policy,
|
|
347
|
+
worker_profile: requiresResearch ? "research+editor" : "editor",
|
|
348
|
+
worker_mode: hasMaterials
|
|
349
|
+
? requiresResearch
|
|
350
|
+
? "research-then-write-from-materials"
|
|
351
|
+
: "write-from-materials"
|
|
352
|
+
: requiresResearch
|
|
353
|
+
? "research-then-write"
|
|
354
|
+
: "write",
|
|
355
|
+
required_inputs: hasMaterials ? [] : ["body_text"],
|
|
356
|
+
body_source: "needs_production",
|
|
357
|
+
on_complete: hasMaterials
|
|
358
|
+
? "Produce body text from the upstream materials, then call: zzhub-pipeline attach-body --state ... --body-text \"...\" and re-run status."
|
|
359
|
+
: "Produce the body text, then call the suggested command with --body-text to hand off, and re-run status. If the user already has a body file, use --body <path> instead.",
|
|
360
|
+
handoff_alt: `zzhub-pipeline attach-body --state "${summary.state_path}" --body "{file_path}"`,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
} else if (state.images.body_inputs.status === "pending") {
|
|
364
|
+
nextAction = {
|
|
365
|
+
action: "attach-body-images",
|
|
366
|
+
reason: "Body image inputs are still missing.",
|
|
367
|
+
executor: "await-input",
|
|
368
|
+
command: `zzhub-pipeline attach-body-images --state "${summary.state_path}" --images-file "{images_json_path}"`,
|
|
369
|
+
params: {
|
|
370
|
+
state_path: summary.state_path,
|
|
371
|
+
required_inputs: ["images_json_path"],
|
|
372
|
+
on_complete: "After user provides image inputs, call the suggested command, then re-run status.",
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
} else if (!state.metadata.title || !state.metadata.slug || !state.metadata.date) {
|
|
376
|
+
nextAction = {
|
|
377
|
+
action: "prepare",
|
|
378
|
+
reason: "Body exists, but deterministic prepare metadata is still incomplete.",
|
|
379
|
+
executor: "cli",
|
|
380
|
+
command: `zzhub-pipeline prepare --state "${summary.state_path}"`,
|
|
381
|
+
params: {
|
|
382
|
+
state_path: summary.state_path,
|
|
383
|
+
source_body_path: state.source_body_path ?? undefined,
|
|
384
|
+
source_materials_path: state.handoff.source_materials_path ?? undefined,
|
|
385
|
+
authoring_policy: state.handoff.authoring_policy,
|
|
386
|
+
review_policy: state.handoff.review_policy,
|
|
387
|
+
on_complete: "After the command succeeds, re-run status to confirm metadata is ready.",
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
} else if (state.content_review.status === "needs_revision") {
|
|
391
|
+
nextAction = {
|
|
392
|
+
action: "revise-content",
|
|
393
|
+
reason: "Content review requested a revision.",
|
|
394
|
+
executor: "worker",
|
|
395
|
+
command: null,
|
|
396
|
+
params: {
|
|
397
|
+
state_path: summary.state_path,
|
|
398
|
+
feedback: state.content_review.feedback ?? null,
|
|
399
|
+
source_body_path: state.source_body_path ?? undefined,
|
|
400
|
+
source_materials_path: state.handoff.source_materials_path ?? undefined,
|
|
401
|
+
worker_profile: "editor",
|
|
402
|
+
worker_mode: "revise",
|
|
403
|
+
required_inputs: ["body_text"],
|
|
404
|
+
body_source: "needs_production",
|
|
405
|
+
on_complete: "Revise the body text based on the feedback. Then call: zzhub-pipeline attach-body --state ... --body-text \"...\" and re-run status.",
|
|
406
|
+
handoff_alt: `zzhub-pipeline attach-body --state "${summary.state_path}" --body "{file_path}"`,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
} else if (!state.asset_path) {
|
|
410
|
+
if (state.content_review.status !== "passed") {
|
|
411
|
+
nextAction = {
|
|
412
|
+
action: "review-content",
|
|
413
|
+
reason: "Prepare data exists, but content review has not passed yet.",
|
|
414
|
+
executor: "worker",
|
|
415
|
+
command: `zzhub-pipeline review --state "${summary.state_path}" --status "{passed|needs_revision}"`,
|
|
416
|
+
params: {
|
|
417
|
+
state_path: summary.state_path,
|
|
418
|
+
source_body_path: state.source_body_path ?? undefined,
|
|
419
|
+
formatted_body_path: state.formatted_body_path ?? undefined,
|
|
420
|
+
review_policy: state.handoff.review_policy,
|
|
421
|
+
worker_profile: "editor",
|
|
422
|
+
worker_mode: "review",
|
|
423
|
+
required_inputs: ["review_status"],
|
|
424
|
+
on_complete: "Review the content, then call the suggested command with --status passed or --status needs_revision, and re-run status.",
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
} else {
|
|
428
|
+
nextAction = {
|
|
429
|
+
action: "prepare-finalize",
|
|
430
|
+
reason: "Content review passed; task can be finalized into canonical assets.",
|
|
431
|
+
executor: "cli",
|
|
432
|
+
command:
|
|
433
|
+
state.formatted_body_path
|
|
434
|
+
? `zzhub-pipeline prepare-finalize --state "${summary.state_path}"`
|
|
435
|
+
: `zzhub-pipeline prepare-finalize --state "${summary.state_path}" --body "{formatted_body_path}"`,
|
|
436
|
+
params: {
|
|
437
|
+
state_path: summary.state_path,
|
|
438
|
+
formatted_body_path: state.formatted_body_path ?? undefined,
|
|
439
|
+
on_complete: "After the command succeeds, re-run status to confirm the phase advanced to render or publish.",
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
} else if (state.intent.requires.render && state.phase.render.status !== "done") {
|
|
444
|
+
nextAction = {
|
|
445
|
+
action: "render",
|
|
446
|
+
reason: "Render phase is still pending.",
|
|
447
|
+
executor: "cli",
|
|
448
|
+
command: `zzhub-pipeline render --state "${summary.state_path}"`,
|
|
449
|
+
params: {
|
|
450
|
+
state_path: summary.state_path,
|
|
451
|
+
on_complete: "After the command succeeds, re-run status to confirm the phase advanced to publish or done.",
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
} else if (state.intent.requires.publish && state.phase.publish.status !== "done") {
|
|
455
|
+
nextAction = {
|
|
456
|
+
action: "publish",
|
|
457
|
+
reason: "Publish phase is still pending.",
|
|
458
|
+
executor: "cli",
|
|
459
|
+
command: `zzhub-pipeline publish --state "${summary.state_path}"`,
|
|
460
|
+
params: {
|
|
461
|
+
state_path: summary.state_path,
|
|
462
|
+
on_complete: "After the command succeeds, re-run status to confirm the task is complete. If publish fails, it stays retryable — re-run publish.",
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
} else {
|
|
466
|
+
nextAction = {
|
|
467
|
+
action: "prepare",
|
|
468
|
+
reason: "Task is active and should continue from the current phase.",
|
|
469
|
+
executor: "cli",
|
|
470
|
+
command: `zzhub-pipeline prepare --state "${summary.state_path}"`,
|
|
471
|
+
params: {
|
|
472
|
+
state_path: summary.state_path,
|
|
473
|
+
on_complete: "After the command succeeds, re-run status to determine the next step.",
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
summary,
|
|
480
|
+
state,
|
|
481
|
+
validation: {
|
|
482
|
+
phase_checked: phaseChecked,
|
|
483
|
+
valid: validationErrors.length === 0,
|
|
484
|
+
errors: validationErrors,
|
|
485
|
+
},
|
|
486
|
+
gaps,
|
|
487
|
+
blockers,
|
|
488
|
+
next_action: nextAction,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function walkFiles(
|
|
493
|
+
root: string,
|
|
494
|
+
matcher: (name: string) => boolean,
|
|
495
|
+
): Promise<string[]> {
|
|
496
|
+
const matches: string[] = [];
|
|
497
|
+
|
|
498
|
+
async function visit(dir: string): Promise<void> {
|
|
499
|
+
let entries;
|
|
500
|
+
try {
|
|
501
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
502
|
+
} catch {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const fullPath = join(dir, entry.name);
|
|
508
|
+
if (entry.isDirectory()) {
|
|
509
|
+
await visit(fullPath);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (entry.isFile() && matcher(entry.name)) {
|
|
513
|
+
matches.push(fullPath);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await visit(root);
|
|
519
|
+
return matches;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function compareIsoDesc(a: string | null, b: string | null): number {
|
|
523
|
+
if (a && b) {
|
|
524
|
+
return b.localeCompare(a);
|
|
525
|
+
}
|
|
526
|
+
if (a) {
|
|
527
|
+
return -1;
|
|
528
|
+
}
|
|
529
|
+
if (b) {
|
|
530
|
+
return 1;
|
|
531
|
+
}
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function choosePreferredTask(a: ListedTask, b: ListedTask): ListedTask {
|
|
536
|
+
const aCanonical = a.file_path === a.summary.state_path || a.source === "canonical";
|
|
537
|
+
const bCanonical = b.file_path === b.summary.state_path || b.source === "canonical";
|
|
538
|
+
if (aCanonical !== bCanonical) {
|
|
539
|
+
return aCanonical ? a : b;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const byUpdated = compareIsoDesc(a.summary.updated_at, b.summary.updated_at);
|
|
543
|
+
if (byUpdated !== 0) {
|
|
544
|
+
return byUpdated < 0 ? a : b;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return a.file_path.localeCompare(b.file_path) <= 0 ? a : b;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function getTaskByStatePath(
|
|
551
|
+
path: string,
|
|
552
|
+
source: TaskFileSource = path.endsWith("workflow-state.json") ? "canonical" : "run",
|
|
553
|
+
): Promise<ListedTask> {
|
|
554
|
+
const state = await readState(path);
|
|
555
|
+
await reconcileStateArtifacts(state);
|
|
556
|
+
const createdAt = state.created_at || fallbackIsoFromRunId(state.run_id) || await fallbackIsoFromFile(path);
|
|
557
|
+
const updatedAt = state.updated_at || createdAt || await fallbackIsoFromFile(path);
|
|
558
|
+
const summary = summarizeState(state, path, source, createdAt, updatedAt);
|
|
559
|
+
return {
|
|
560
|
+
file_path: path,
|
|
561
|
+
source,
|
|
562
|
+
...buildTaskStatus(summary, state),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export async function listTasks(workspaceRoot?: string): Promise<ListedTask[]> {
|
|
567
|
+
const config = loadConfig();
|
|
568
|
+
const workspace = resolveWorkspaceRoot(workspaceRoot, config);
|
|
569
|
+
const paths = resolveWorkspacePaths(workspace, config);
|
|
570
|
+
const runRoot = join(workspace, ".zzhub-media", "runs");
|
|
571
|
+
const runFiles = await walkFiles(runRoot, (name) => name.endsWith(".json")).catch(() => []);
|
|
572
|
+
const canonicalFiles = await walkFiles(paths.postsRoot, (name) => name === "workflow-state.json");
|
|
573
|
+
|
|
574
|
+
const candidates = [
|
|
575
|
+
...runFiles.filter((path) => path.endsWith(".json")),
|
|
576
|
+
...canonicalFiles,
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
const byRunId = new Map<string, ListedTask>();
|
|
580
|
+
for (const filePath of candidates) {
|
|
581
|
+
try {
|
|
582
|
+
const task = await getTaskByStatePath(
|
|
583
|
+
filePath,
|
|
584
|
+
filePath.endsWith("/workflow-state.json") ? "canonical" : "run",
|
|
585
|
+
);
|
|
586
|
+
const existing = byRunId.get(task.summary.run_id);
|
|
587
|
+
byRunId.set(task.summary.run_id, existing ? choosePreferredTask(existing, task) : task);
|
|
588
|
+
} catch {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return [...byRunId.values()].sort((a, b) => compareIsoDesc(a.summary.updated_at, b.summary.updated_at));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function isActiveTask(task: ListedTask): boolean {
|
|
597
|
+
if (task.summary.phase.current === "done" || task.summary.phase.current === "failed") {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
return task.summary.mode === "active" || task.summary.mode === "handoff";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function findTask(workspaceRoot: string | undefined, filters: {
|
|
604
|
+
run_id?: string;
|
|
605
|
+
route?: string;
|
|
606
|
+
account?: string;
|
|
607
|
+
mode?: string;
|
|
608
|
+
phase?: string;
|
|
609
|
+
title_contains?: string;
|
|
610
|
+
active_only?: boolean;
|
|
611
|
+
}): Promise<ListedTask | null> {
|
|
612
|
+
const tasks = await listTasks(workspaceRoot);
|
|
613
|
+
const filtered = tasks.filter((task) => {
|
|
614
|
+
if (filters.run_id && task.summary.run_id !== filters.run_id) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
if (filters.route && task.summary.route.primary !== filters.route) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
if (filters.account && task.summary.route.account !== filters.account) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
if (filters.mode) {
|
|
624
|
+
if (task.summary.mode !== filters.mode) return false;
|
|
625
|
+
} else {
|
|
626
|
+
if (task.summary.mode === "abandoned") return false;
|
|
627
|
+
}
|
|
628
|
+
if (filters.phase && task.summary.phase.current !== filters.phase) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
if (
|
|
632
|
+
filters.title_contains &&
|
|
633
|
+
!(task.summary.metadata.title || "").includes(filters.title_contains)
|
|
634
|
+
) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
if (filters.active_only && !isActiveTask(task)) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
return true;
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
if (filtered.length > 0) {
|
|
644
|
+
return filtered[0];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!filters.run_id && !filters.route && !filters.account && !filters.mode && !filters.phase && !filters.title_contains && !filters.active_only) {
|
|
648
|
+
const nonAbandoned = tasks.filter((t) => t.summary.mode !== "abandoned");
|
|
649
|
+
return nonAbandoned.find((task) => isActiveTask(task)) ?? nonAbandoned[0] ?? null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function filterActiveTasks(tasks: ListedTask[]): ListedTask[] {
|
|
656
|
+
return tasks.filter((task) => isActiveTask(task));
|
|
657
|
+
}
|