takomi 2.1.37 → 2.1.39

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 (90) hide show
  1. package/.agents/plugins/marketplace.json +32 -0
  2. package/.pi/agents/architect.md +4 -2
  3. package/.pi/agents/designer.md +2 -0
  4. package/.pi/agents/orchestrator.md +4 -0
  5. package/.pi/prompts/design-prompt.md +3 -1
  6. package/.pi/prompts/genesis-prompt.md +2 -0
  7. package/.pi/prompts/takomi-prompt.md +6 -3
  8. package/.pi/takomi/policies/subagent-routing.md +9 -1
  9. package/.pi/takomi/policies/takomi-lifecycle-routing.md +3 -1
  10. package/README.md +27 -5
  11. package/assets/.agent/skills/takomi-flow/SKILL.md +235 -0
  12. package/assets/.agent/workflows/README.md +3 -3
  13. package/assets/.agent/workflows/mode-architect.md +7 -5
  14. package/assets/.agent/workflows/vibe-design.md +3 -1
  15. package/assets/.agent/workflows/vibe-genesis.md +2 -0
  16. package/assets/Takomi-Agents/custom_modes.yaml +1253 -1252
  17. package/assets/Takomi-Agents/vibe-architect.yaml +11 -6
  18. package/assets/Takomi-Agents/vibe-orchestrator.yaml +2 -2
  19. package/docs/features/TakomiFlow_Portable_Plugin.md +78 -0
  20. package/docs/takomi-flow-onboarding.md +76 -0
  21. package/docs/takomi-flow-public-distribution.md +94 -0
  22. package/package.json +19 -2
  23. package/plugins/takomi-codex/.codex-plugin/plugin.json +37 -0
  24. package/plugins/takomi-codex/assets/composer-icon.png +0 -0
  25. package/plugins/takomi-codex/assets/logo-dark.png +0 -0
  26. package/plugins/takomi-codex/assets/logo.png +0 -0
  27. package/plugins/takomi-codex/scripts/takomi-board.ps1 +197 -0
  28. package/plugins/takomi-codex/scripts/takomi-detect.ps1 +125 -0
  29. package/plugins/takomi-codex/scripts/takomi-doctor.ps1 +88 -0
  30. package/plugins/takomi-codex/scripts/takomi-harness.ps1 +52 -0
  31. package/plugins/takomi-codex/scripts/takomi-pi-dispatch.ps1 +93 -0
  32. package/plugins/takomi-codex/scripts/takomi-policy.ps1 +108 -0
  33. package/plugins/takomi-codex/skills/takomi-codex/SKILL.md +194 -0
  34. package/plugins/takomi-flow/.codex-plugin/plugin.json +36 -0
  35. package/plugins/takomi-flow/.mcp.json +11 -0
  36. package/plugins/takomi-flow/assets/capabilities.json +171 -0
  37. package/plugins/takomi-flow/assets/collection.schema.json +29 -0
  38. package/plugins/takomi-flow/assets/composer-icon.png +0 -0
  39. package/plugins/takomi-flow/assets/examples.json +94 -0
  40. package/plugins/takomi-flow/assets/logo-dark.png +0 -0
  41. package/plugins/takomi-flow/assets/logo.png +0 -0
  42. package/plugins/takomi-flow/assets/request.schema.json +67 -0
  43. package/plugins/takomi-flow/assets/result.schema.json +45 -0
  44. package/plugins/takomi-flow/assets/templates/image-request.json +13 -0
  45. package/plugins/takomi-flow/assets/templates/video-request.json +15 -0
  46. package/plugins/takomi-flow/package.json +38 -0
  47. package/plugins/takomi-flow/pnpm-lock.yaml +813 -0
  48. package/plugins/takomi-flow/references/flow-provider-contract.md +326 -0
  49. package/plugins/takomi-flow/scripts/lib/agent-plan.mjs +130 -0
  50. package/plugins/takomi-flow/scripts/lib/api.mjs +113 -0
  51. package/plugins/takomi-flow/scripts/lib/args.mjs +57 -0
  52. package/plugins/takomi-flow/scripts/lib/audit.mjs +65 -0
  53. package/plugins/takomi-flow/scripts/lib/browser.mjs +39 -0
  54. package/plugins/takomi-flow/scripts/lib/capabilities.mjs +11 -0
  55. package/plugins/takomi-flow/scripts/lib/collect.mjs +53 -0
  56. package/plugins/takomi-flow/scripts/lib/commands.mjs +175 -0
  57. package/plugins/takomi-flow/scripts/lib/doctor.mjs +77 -0
  58. package/plugins/takomi-flow/scripts/lib/examples.mjs +17 -0
  59. package/plugins/takomi-flow/scripts/lib/flow-outcome.mjs +90 -0
  60. package/plugins/takomi-flow/scripts/lib/flow-ui.mjs +165 -0
  61. package/plugins/takomi-flow/scripts/lib/generation.mjs +90 -0
  62. package/plugins/takomi-flow/scripts/lib/inspect.mjs +81 -0
  63. package/plugins/takomi-flow/scripts/lib/mcp-prompts.mjs +39 -0
  64. package/plugins/takomi-flow/scripts/lib/mcp-resources.mjs +16 -0
  65. package/plugins/takomi-flow/scripts/lib/mcp-tools.mjs +150 -0
  66. package/plugins/takomi-flow/scripts/lib/media.mjs +78 -0
  67. package/plugins/takomi-flow/scripts/lib/observe.mjs +54 -0
  68. package/plugins/takomi-flow/scripts/lib/paths.mjs +37 -0
  69. package/plugins/takomi-flow/scripts/lib/playwright-loader.mjs +23 -0
  70. package/plugins/takomi-flow/scripts/lib/prompts.mjs +72 -0
  71. package/plugins/takomi-flow/scripts/lib/report.mjs +141 -0
  72. package/plugins/takomi-flow/scripts/lib/request-validator.mjs +58 -0
  73. package/plugins/takomi-flow/scripts/lib/request.mjs +55 -0
  74. package/plugins/takomi-flow/scripts/lib/resource-files.mjs +69 -0
  75. package/plugins/takomi-flow/scripts/lib/result.mjs +31 -0
  76. package/plugins/takomi-flow/scripts/lib/review.mjs +44 -0
  77. package/plugins/takomi-flow/scripts/lib/selftest.mjs +153 -0
  78. package/plugins/takomi-flow/scripts/lib/settings-plan.mjs +39 -0
  79. package/plugins/takomi-flow/scripts/lib/templates.mjs +37 -0
  80. package/plugins/takomi-flow/scripts/lib/trusted-chrome.mjs +67 -0
  81. package/plugins/takomi-flow/scripts/lib/workflow.mjs +56 -0
  82. package/plugins/takomi-flow/scripts/mcp-server.mjs +18 -0
  83. package/plugins/takomi-flow/scripts/mcp-smoke.mjs +142 -0
  84. package/plugins/takomi-flow/scripts/takomi-flow.mjs +17 -0
  85. package/plugins/takomi-flow/skills/takomi-flow/SKILL.md +235 -0
  86. package/plugins/takomi-flow/skills/takomi-flow/agents/openai.yaml +7 -0
  87. package/scripts/install-takomi-flow.ps1 +85 -0
  88. package/src/pi-takomi-core/orchestration.ts +1 -1
  89. package/src/pi-takomi-core/workflows.ts +4 -4
  90. package/src/skills-catalog.js +11 -7
@@ -0,0 +1,141 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { defaultRunsDir, ensureDir, resolvePath } from './paths.mjs';
4
+ import { inspectRun, listRuns, resolveRun } from './inspect.mjs';
5
+
6
+ export function createReport(args = {}) {
7
+ if (args.run) return createRunReport(args);
8
+ return createHistoryReport(args);
9
+ }
10
+
11
+ function createRunReport(args) {
12
+ const run = resolveRun(args.run);
13
+ const summary = inspectRun(args.run);
14
+ const catalog = readOptionalJson(path.join(run.runDir, 'assets.json'));
15
+ const markdown = renderRunReport(summary, run.runDir, catalog);
16
+ const reportPath = resolvePath(args.reportPath, path.join(run.runDir, 'report.md'));
17
+ writeText(reportPath, markdown);
18
+ return {
19
+ schemaVersion: 1,
20
+ status: 'ok',
21
+ mode: 'run',
22
+ runId: summary.runId,
23
+ reportPath,
24
+ markdown,
25
+ };
26
+ }
27
+
28
+ function createHistoryReport(args) {
29
+ const outputDir = resolvePath(args.outputDir, defaultRunsDir());
30
+ const history = listRuns(outputDir, args.limit || 10);
31
+ const markdown = renderHistoryReport(history);
32
+ const reportPath = resolvePath(args.reportPath, path.join(outputDir, 'takomi-flow-report.md'));
33
+ writeText(reportPath, markdown);
34
+ return {
35
+ schemaVersion: 1,
36
+ status: 'ok',
37
+ mode: 'history',
38
+ outputDir,
39
+ reportPath,
40
+ runCount: history.runs.length,
41
+ markdown,
42
+ };
43
+ }
44
+
45
+ function renderRunReport(summary, runDir, catalog) {
46
+ const lines = [
47
+ '# TakomiFlow Run Report',
48
+ '',
49
+ `Generated: ${new Date().toISOString()}`,
50
+ `Run: ${summary.runId || 'unknown'}`,
51
+ `Status: ${summary.status || 'unknown'}`,
52
+ `Command: ${summary.command || 'unknown'}`,
53
+ `Kind: ${summary.kind || 'unknown'}`,
54
+ `Project URL: ${summary.projectUrl || 'unknown'}`,
55
+ `Run directory: ${runDir}`,
56
+ `Metadata: ${summary.metadataPath || 'unknown'}`,
57
+ '',
58
+ ];
59
+ pushList(lines, 'Manual Actions', summary.manualActions);
60
+ pushList(lines, 'Errors', summary.errors);
61
+ pushList(lines, 'Screenshots', summary.screenshots);
62
+ pushList(lines, 'Assets', summary.assets);
63
+ if (catalog) pushCatalog(lines, catalog);
64
+ return `${lines.join('\n')}\n`;
65
+ }
66
+
67
+ function renderHistoryReport(history) {
68
+ const lines = [
69
+ '# TakomiFlow Run History',
70
+ '',
71
+ `Generated: ${new Date().toISOString()}`,
72
+ `Output directory: ${history.outputDir}`,
73
+ `Runs: ${history.runs.length}`,
74
+ '',
75
+ ];
76
+ if (!history.runs.length) {
77
+ lines.push('No TakomiFlow runs were found.', '');
78
+ return `${lines.join('\n')}\n`;
79
+ }
80
+ lines.push('| Run | Status | Command | Kind | Assets | Manual Actions | Errors |');
81
+ lines.push('| --- | --- | --- | --- | ---: | ---: | ---: |');
82
+ for (const run of history.runs) {
83
+ lines.push([
84
+ cell(run.runId),
85
+ cell(run.status),
86
+ cell(run.command),
87
+ cell(run.kind),
88
+ run.assets?.length || 0,
89
+ run.manualActions?.length || 0,
90
+ run.errors?.length || 0,
91
+ ].join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
92
+ }
93
+ lines.push('');
94
+ lines.push('## Latest Details', '');
95
+ for (const run of history.runs.slice(0, 5)) {
96
+ lines.push(`### ${run.runId || 'unknown run'}`, '');
97
+ lines.push(`- Status: ${run.status || 'unknown'}`);
98
+ if (run.projectUrl) lines.push(`- Project URL: ${run.projectUrl}`);
99
+ lines.push(`- Metadata: ${run.metadataPath || 'unknown'}`);
100
+ if (run.errors?.length) lines.push(`- Errors: ${run.errors.join('; ')}`);
101
+ if (run.manualActions?.length) lines.push(`- Manual actions: ${run.manualActions.join('; ')}`);
102
+ lines.push('');
103
+ }
104
+ return `${lines.join('\n')}\n`;
105
+ }
106
+
107
+ function pushList(lines, title, values = []) {
108
+ lines.push(`## ${title}`, '');
109
+ if (!values.length) {
110
+ lines.push('None.', '');
111
+ return;
112
+ }
113
+ for (const value of values) lines.push(`- ${value}`);
114
+ lines.push('');
115
+ }
116
+
117
+ function pushCatalog(lines, catalog) {
118
+ lines.push('## Asset Catalog', '');
119
+ lines.push(`Catalog: ${catalog.assetCatalogPath || catalog.catalogPath || 'unknown'}`);
120
+ lines.push(`Assets cataloged: ${catalog.assets?.length || 0}`);
121
+ const frames = (catalog.assets || []).flatMap(asset => asset.frames || []);
122
+ if (frames.length) {
123
+ lines.push('', '### Review Frames', '');
124
+ for (const frame of frames) lines.push(`- ${frame}`);
125
+ }
126
+ lines.push('');
127
+ }
128
+
129
+ function readOptionalJson(filePath) {
130
+ if (!fs.existsSync(filePath)) return null;
131
+ return { catalogPath: filePath, ...JSON.parse(fs.readFileSync(filePath, 'utf8')) };
132
+ }
133
+
134
+ function writeText(filePath, text) {
135
+ ensureDir(path.dirname(filePath));
136
+ fs.writeFileSync(filePath, text);
137
+ }
138
+
139
+ function cell(value) {
140
+ return String(value || '').replace(/\|/g, '\\|');
141
+ }
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadCapabilities } from './capabilities.mjs';
4
+ import { normalizeRequest } from './request.mjs';
5
+
6
+ export function validateRequestFile(requestPath) {
7
+ if (!requestPath) throw new Error('validate requires --request <path>');
8
+ const absolutePath = path.resolve(requestPath);
9
+ const raw = JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
10
+ return validateRequest(raw, absolutePath);
11
+ }
12
+
13
+ export function validateRequest(raw, requestPath = undefined) {
14
+ const capabilities = loadCapabilities();
15
+ const errors = [];
16
+ const warnings = [];
17
+ let normalized;
18
+ try {
19
+ normalized = normalizeRequest(raw);
20
+ } catch (error) {
21
+ return {
22
+ schemaVersion: 1,
23
+ status: 'failed',
24
+ requestPath,
25
+ errors: [error.message],
26
+ warnings,
27
+ };
28
+ }
29
+ if (!capabilities.kinds.includes(normalized.kind)) {
30
+ errors.push(`Unsupported kind: ${normalized.kind}`);
31
+ }
32
+ const allowedModes = capabilities.modes[normalized.kind] || [];
33
+ if (normalized.mode && !allowedModes.includes(normalized.mode)) {
34
+ warnings.push(`Mode "${normalized.mode}" is not listed for kind "${normalized.kind}".`);
35
+ }
36
+ if (normalized.variations > capabilities.variations.max) {
37
+ warnings.push(`Variations ${normalized.variations} exceeds listed max ${capabilities.variations.max}.`);
38
+ }
39
+ if (normalized.aspectRatio && !capabilities.aspectRatios.includes(normalized.aspectRatio)) {
40
+ warnings.push(`Aspect ratio "${normalized.aspectRatio}" is not in listed options.`);
41
+ }
42
+ for (const asset of normalized.sourceAssets) {
43
+ if (!fs.existsSync(path.resolve(asset))) {
44
+ errors.push(`Source asset does not exist: ${asset}`);
45
+ }
46
+ }
47
+ if (!normalized.allowSpend) {
48
+ warnings.push('Spend guard is off. Generation will not submit until allowSpend=true or TAKOMI_FLOW_ALLOW_SPEND=true.');
49
+ }
50
+ return {
51
+ schemaVersion: 1,
52
+ status: errors.length ? 'failed' : 'ok',
53
+ requestPath,
54
+ normalized,
55
+ errors,
56
+ warnings,
57
+ };
58
+ }
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { defaultRunsDir, ensureDir, timestampId, writeJson } from './paths.mjs';
3
+
4
+ const VALID_KINDS = new Set(['video', 'image']);
5
+
6
+ export function normalizeRequest(input) {
7
+ const kind = String(input.kind || 'video').toLowerCase();
8
+ if (!VALID_KINDS.has(kind)) {
9
+ throw new Error(`Invalid kind "${kind}". Expected video or image.`);
10
+ }
11
+ const prompt = String(input.prompt || '').trim();
12
+ if (!prompt) {
13
+ throw new Error('A non-empty prompt is required.');
14
+ }
15
+ const variations = Math.max(1, Number.parseInt(input.variations || '1', 10) || 1);
16
+ return {
17
+ schemaVersion: 1,
18
+ kind,
19
+ prompt,
20
+ variations,
21
+ aspectRatio: input.aspectRatio || input['aspect-ratio'] || undefined,
22
+ durationSeconds: toOptionalNumber(input.durationSeconds || input.duration),
23
+ mode: input.mode || (kind === 'video' ? 'text-to-video' : 'text-to-image'),
24
+ modelHint: input.modelHint || input.model || 'best-available',
25
+ outputDir: path.resolve(input.outputDir || input['output-dir'] || defaultRunsDir()),
26
+ allowSpend: Boolean(input.allowSpend || input['allow-spend'] || false),
27
+ extractFrames: Math.max(0, Number.parseInt(input.extractFrames || input['extract-frames'] || '0', 10) || 0),
28
+ sourceAssets: normalizeList(input.sourceAssets || input.assets),
29
+ notes: input.notes || undefined,
30
+ };
31
+ }
32
+
33
+ export function createPreparedRequest(args) {
34
+ const request = normalizeRequest(args);
35
+ const requestDir = ensureDir(path.join(request.outputDir, 'requests'));
36
+ const requestPath = path.join(requestDir, `${timestampId('request')}.json`);
37
+ writeJson(requestPath, { ...request, status: 'prepared', requestPath });
38
+ return { request, requestPath };
39
+ }
40
+
41
+ export function canSpend(request) {
42
+ return request.allowSpend === true || process.env.TAKOMI_FLOW_ALLOW_SPEND === 'true';
43
+ }
44
+
45
+ function toOptionalNumber(value) {
46
+ if (value === undefined || value === null || value === '') return undefined;
47
+ const parsed = Number(value);
48
+ return Number.isFinite(parsed) ? parsed : undefined;
49
+ }
50
+
51
+ function normalizeList(value) {
52
+ if (!value) return [];
53
+ if (Array.isArray(value)) return value;
54
+ return String(value).split(',').map(item => item.trim()).filter(Boolean);
55
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
6
+ const PLUGIN_ROOT = path.resolve(MODULE_DIR, '..', '..');
7
+
8
+ export const RESOURCES = [
9
+ {
10
+ name: 'takomi-flow-contract',
11
+ uri: 'takomi-flow://contract',
12
+ file: path.join(PLUGIN_ROOT, 'references', 'flow-provider-contract.md'),
13
+ mimeType: 'text/markdown',
14
+ description: 'TakomiFlow provider contract and agent workflow.',
15
+ },
16
+ {
17
+ name: 'takomi-flow-capabilities',
18
+ uri: 'takomi-flow://capabilities',
19
+ file: path.join(PLUGIN_ROOT, 'assets', 'capabilities.json'),
20
+ mimeType: 'application/json',
21
+ description: 'Machine-readable TakomiFlow capabilities.',
22
+ },
23
+ {
24
+ name: 'takomi-flow-examples',
25
+ uri: 'takomi-flow://examples',
26
+ file: path.join(PLUGIN_ROOT, 'assets', 'examples.json'),
27
+ mimeType: 'application/json',
28
+ description: 'Machine-readable TakomiFlow request and handoff examples.',
29
+ },
30
+ {
31
+ name: 'takomi-flow-request-schema',
32
+ uri: 'takomi-flow://schemas/request',
33
+ file: path.join(PLUGIN_ROOT, 'assets', 'request.schema.json'),
34
+ mimeType: 'application/schema+json',
35
+ description: 'JSON Schema for TakomiFlow request files.',
36
+ },
37
+ {
38
+ name: 'takomi-flow-result-schema',
39
+ uri: 'takomi-flow://schemas/result',
40
+ file: path.join(PLUGIN_ROOT, 'assets', 'result.schema.json'),
41
+ mimeType: 'application/schema+json',
42
+ description: 'JSON Schema for TakomiFlow run result metadata.',
43
+ },
44
+ {
45
+ name: 'takomi-flow-collection-schema',
46
+ uri: 'takomi-flow://schemas/collection',
47
+ file: path.join(PLUGIN_ROOT, 'assets', 'collection.schema.json'),
48
+ mimeType: 'application/schema+json',
49
+ description: 'JSON Schema for TakomiFlow collection manifests.',
50
+ },
51
+ {
52
+ name: 'takomi-flow-video-template',
53
+ uri: 'takomi-flow://templates/video',
54
+ file: path.join(PLUGIN_ROOT, 'assets', 'templates', 'video-request.json'),
55
+ mimeType: 'application/json',
56
+ description: 'Video request template.',
57
+ },
58
+ {
59
+ name: 'takomi-flow-image-template',
60
+ uri: 'takomi-flow://templates/image',
61
+ file: path.join(PLUGIN_ROOT, 'assets', 'templates', 'image-request.json'),
62
+ mimeType: 'application/json',
63
+ description: 'Image request template.',
64
+ },
65
+ ];
66
+
67
+ export function readResourceFile(filePath) {
68
+ return fs.readFileSync(filePath, 'utf8');
69
+ }
@@ -0,0 +1,31 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, timestampId, writeJson } from './paths.mjs';
3
+
4
+ export function createRun(requestOrArgs, label = 'flow') {
5
+ const outputDir = path.resolve(requestOrArgs.outputDir || requestOrArgs['output-dir']);
6
+ const runId = timestampId(label);
7
+ const runDir = ensureDir(path.join(outputDir, runId));
8
+ const downloadsDir = ensureDir(path.join(runDir, 'downloads'));
9
+ const screenshotsDir = ensureDir(path.join(runDir, 'screenshots'));
10
+ return { runId, runDir, downloadsDir, screenshotsDir };
11
+ }
12
+
13
+ export function baseResult(status, run, data = {}) {
14
+ return {
15
+ schemaVersion: 1,
16
+ status,
17
+ runId: run.runId,
18
+ assets: [],
19
+ screenshots: [],
20
+ errors: [],
21
+ manualActions: [],
22
+ ...data,
23
+ metadataPath: path.join(run.runDir, 'run.json'),
24
+ };
25
+ }
26
+
27
+ export function saveResult(run, result) {
28
+ const finalResult = { ...result, metadataPath: path.join(run.runDir, 'run.json') };
29
+ writeJson(finalResult.metadataPath, finalResult);
30
+ return finalResult;
31
+ }
@@ -0,0 +1,44 @@
1
+ import { inspectRun, resolveRun } from './inspect.mjs';
2
+ import { catalogAssets } from './media.mjs';
3
+ import { createReport } from './report.mjs';
4
+
5
+ export async function reviewRun(args = {}) {
6
+ if (!args.run) throw new Error('review requires --run <run.json|run-dir>');
7
+ const summary = inspectRun(args.run);
8
+ const run = resolveRun(args.run);
9
+ const catalog = await catalogAssets(run, run.data.assets || [], {
10
+ frames: args.frames || 0,
11
+ });
12
+ const report = createReport({
13
+ run: args.run,
14
+ reportPath: args.reportPath,
15
+ });
16
+ return {
17
+ schemaVersion: 1,
18
+ status: reviewStatus(summary, catalog),
19
+ runId: summary.runId,
20
+ runDir: run.runDir,
21
+ summary,
22
+ catalog,
23
+ reportPath: report.reportPath,
24
+ nextActions: nextActions(summary, catalog),
25
+ };
26
+ }
27
+
28
+ function reviewStatus(summary, catalog) {
29
+ if (summary.errors?.length) return 'attention_required';
30
+ if (summary.manualActions?.length) return 'manual_action_required';
31
+ if (catalog.assets?.some(asset => asset.errors?.length || !asset.exists)) return 'attention_required';
32
+ if (catalog.assets?.length) return 'review_ready';
33
+ return summary.status || 'unknown';
34
+ }
35
+
36
+ function nextActions(summary, catalog) {
37
+ const actions = [];
38
+ if (summary.errors?.length) actions.push('Review run errors before reusing assets.');
39
+ if (summary.manualActions?.length) actions.push(...summary.manualActions);
40
+ if (catalog.assets?.some(asset => asset.frames?.length)) actions.push('Review extracted frames for video quality.');
41
+ if (catalog.assets?.length) actions.push('Use reportPath for the human handoff.');
42
+ if (!catalog.assets?.length) actions.push('No assets were cataloged; inspect screenshots and run metadata.');
43
+ return actions;
44
+ }
@@ -0,0 +1,153 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { defaultRunsDir, ensureDir, readJson, writeJson } from './paths.mjs';
6
+ import { runDoctor } from './doctor.mjs';
7
+ import { loadCapabilities } from './capabilities.mjs';
8
+ import { loadTemplate } from './templates.mjs';
9
+ import { createPreparedRequest } from './request.mjs';
10
+ import { validateRequestFile } from './request-validator.mjs';
11
+ import { createRun, saveResult, baseResult } from './result.mjs';
12
+ import { catalogAssets } from './media.mjs';
13
+ import { createReport } from './report.mjs';
14
+ import { runWorkflow } from './workflow.mjs';
15
+ import { reviewRun } from './review.mjs';
16
+ import { runAudit } from './audit.mjs';
17
+ import { collectRun } from './collect.mjs';
18
+ import { loadExamples } from './examples.mjs';
19
+ import { createAgentPlan } from './agent-plan.mjs';
20
+
21
+ const execFileAsync = promisify(execFile);
22
+
23
+ export async function runSelfTest(options = {}) {
24
+ const outputDir = path.resolve(options.outputDir || defaultRunsDir(), 'selftest');
25
+ ensureDir(outputDir);
26
+ const steps = [];
27
+ steps.push(await step('doctor', () => runDoctor({ outputDir })));
28
+ steps.push(await step('audit', () => runAudit({ outputDir })));
29
+ steps.push(await step('capabilities', () => loadCapabilities()));
30
+ steps.push(await step('examples', () => loadExamples('cinematic-video')));
31
+ steps.push(await step('agentPlan', () => createAgentPlan({
32
+ kind: 'video',
33
+ prompt: 'TakomiFlow self-test planned video prompt',
34
+ variations: 2,
35
+ aspectRatio: '16:9',
36
+ durationSeconds: 8,
37
+ extractFrames: 2,
38
+ })));
39
+ steps.push(await step('videoTemplate', () => loadTemplate('video')));
40
+ steps.push(await step('imageTemplate', () => loadTemplate('image')));
41
+ const prepared = await step('prepareAndValidate', () => prepareAndValidate(outputDir));
42
+ steps.push(prepared);
43
+ steps.push(await step('workflow', () => noSpendWorkflow(outputDir)));
44
+ steps.push(await step('settingsPlan', () => verifySettingsPlan(outputDir)));
45
+ steps.push(await step('guardedGenerate', () => guardedGenerate(prepared.data?.requestPath)));
46
+ const catalog = await step('assetCatalog', () => syntheticAssetCatalog(outputDir));
47
+ steps.push(catalog);
48
+ steps.push(await step('report', () => selftestReport(catalog.data?.runDir)));
49
+ steps.push(await step('review', () => selftestReview(catalog.data?.runDir)));
50
+ steps.push(await step('collect', () => selftestCollect(catalog.data?.runDir, outputDir)));
51
+ const failed = steps.some(item => item.status !== 'ok');
52
+ return {
53
+ schemaVersion: 1,
54
+ status: failed ? 'failed' : 'ok',
55
+ outputDir,
56
+ steps,
57
+ };
58
+ }
59
+
60
+ async function step(name, fn) {
61
+ try {
62
+ return { name, status: 'ok', data: await fn() };
63
+ } catch (error) {
64
+ return { name, status: 'failed', error: error.message };
65
+ }
66
+ }
67
+
68
+ function prepareAndValidate(outputDir) {
69
+ const { requestPath, request } = createPreparedRequest({
70
+ kind: 'video',
71
+ prompt: 'TakomiFlow self-test video prompt',
72
+ variations: 1,
73
+ 'output-dir': outputDir,
74
+ 'extract-frames': 2,
75
+ });
76
+ return { requestPath, request, validation: validateRequestFile(requestPath) };
77
+ }
78
+
79
+ function guardedGenerate(requestPath) {
80
+ if (!requestPath) throw new Error('No request path available.');
81
+ const request = readJson(requestPath);
82
+ if (request.allowSpend) throw new Error('Self-test request unexpectedly allows spend.');
83
+ return {
84
+ requestPath,
85
+ wouldSubmit: false,
86
+ reason: 'allowSpend is false, so generate must be blocked by the spend guard.',
87
+ };
88
+ }
89
+
90
+ async function noSpendWorkflow(outputDir) {
91
+ return runWorkflow({
92
+ kind: 'image',
93
+ prompt: 'TakomiFlow self-test image prompt',
94
+ variations: 1,
95
+ outputDir,
96
+ });
97
+ }
98
+
99
+ async function verifySettingsPlan(outputDir) {
100
+ const result = await runWorkflow({
101
+ kind: 'video',
102
+ prompt: 'TakomiFlow settings plan prompt',
103
+ variations: 2,
104
+ aspectRatio: '16:9',
105
+ durationSeconds: 8,
106
+ outputDir,
107
+ });
108
+ if (!result.settingsPlan?.selectorDependent?.length) {
109
+ throw new Error('Expected selector-dependent settings in workflow result.');
110
+ }
111
+ return result.settingsPlan;
112
+ }
113
+
114
+ async function syntheticAssetCatalog(outputDir) {
115
+ const run = createRun({ outputDir }, 'selftest-asset');
116
+ const videoPath = path.join(run.downloadsDir, 'sample.mp4');
117
+ await execFileAsync('ffmpeg', [
118
+ '-y',
119
+ '-f', 'lavfi',
120
+ '-i', 'testsrc=duration=2:size=320x180:rate=24',
121
+ '-c:v', 'libx264',
122
+ '-pix_fmt', 'yuv420p',
123
+ videoPath,
124
+ ]);
125
+ const result = baseResult('downloaded', run, {
126
+ command: 'selftest',
127
+ kind: 'video',
128
+ assets: [videoPath],
129
+ });
130
+ saveResult(run, result);
131
+ const catalog = await catalogAssets(run, [videoPath], { frames: 2 });
132
+ return { ...catalog, runDir: run.runDir };
133
+ }
134
+
135
+ function selftestReport(runDir) {
136
+ if (!runDir) throw new Error('No self-test run directory available.');
137
+ return createReport({ run: runDir });
138
+ }
139
+
140
+ async function selftestReview(runDir) {
141
+ if (!runDir) throw new Error('No self-test run directory available.');
142
+ return reviewRun({ run: runDir, frames: 2 });
143
+ }
144
+
145
+ async function selftestCollect(runDir, outputDir) {
146
+ if (!runDir) throw new Error('No self-test run directory available.');
147
+ return collectRun({
148
+ run: runDir,
149
+ targetDir: path.join(outputDir, 'collected'),
150
+ frames: 2,
151
+ includeFrames: true,
152
+ });
153
+ }
@@ -0,0 +1,39 @@
1
+ export function createSettingsPlan(request) {
2
+ const requested = {
3
+ kind: request.kind,
4
+ mode: request.mode,
5
+ variations: request.variations,
6
+ aspectRatio: request.aspectRatio,
7
+ durationSeconds: request.durationSeconds,
8
+ modelHint: request.modelHint,
9
+ extractFrames: request.extractFrames,
10
+ sourceAssets: request.sourceAssets || [],
11
+ };
12
+ const automatic = ['prompt', 'download folder', 'metadata', 'asset catalog'];
13
+ if (request.extractFrames > 0) automatic.push('video review frame extraction after download');
14
+ return {
15
+ schemaVersion: 1,
16
+ requested,
17
+ automatic,
18
+ selectorDependent: selectorDependent(request),
19
+ notes: [
20
+ 'Prompt submission and downloads are automated through public Flow UI selectors.',
21
+ 'Visual settings remain explicit in metadata until live Flow controls are observed and selector coverage is tuned.',
22
+ ],
23
+ };
24
+ }
25
+
26
+ function selectorDependent(request) {
27
+ const items = [];
28
+ if (request.mode) items.push({ field: 'mode', value: request.mode });
29
+ if (request.aspectRatio) items.push({ field: 'aspectRatio', value: request.aspectRatio });
30
+ if (request.durationSeconds) items.push({ field: 'durationSeconds', value: request.durationSeconds });
31
+ if (request.modelHint && request.modelHint !== 'best-available') {
32
+ items.push({ field: 'modelHint', value: request.modelHint });
33
+ }
34
+ if (request.sourceAssets?.length) {
35
+ items.push({ field: 'sourceAssets', value: request.sourceAssets });
36
+ }
37
+ if (request.variations > 1) items.push({ field: 'variations', value: request.variations });
38
+ return items;
39
+ }
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { ensureDir, writeJson } from './paths.mjs';
5
+
6
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
+ const PLUGIN_ROOT = path.resolve(MODULE_DIR, '..', '..');
8
+ const TEMPLATE_DIR = path.join(PLUGIN_ROOT, 'assets', 'templates');
9
+ const TEMPLATES = {
10
+ video: 'video-request.json',
11
+ image: 'image-request.json',
12
+ };
13
+
14
+ export function listTemplates() {
15
+ return Object.keys(TEMPLATES).map(kind => ({ kind, path: templatePath(kind) }));
16
+ }
17
+
18
+ export function loadTemplate(kind) {
19
+ const filePath = templatePath(kind);
20
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
21
+ }
22
+
23
+ export function writeTemplate(kind, outputDir) {
24
+ const template = loadTemplate(kind);
25
+ const destinationDir = ensureDir(path.resolve(outputDir));
26
+ const destination = path.join(destinationDir, TEMPLATES[kind]);
27
+ writeJson(destination, template);
28
+ return { kind, destination, template };
29
+ }
30
+
31
+ function templatePath(kind) {
32
+ const fileName = TEMPLATES[String(kind || '').toLowerCase()];
33
+ if (!fileName) {
34
+ throw new Error(`Unknown template "${kind}". Expected image or video.`);
35
+ }
36
+ return path.join(TEMPLATE_DIR, fileName);
37
+ }