akari-video 1.5.0-beta.5 → 1.5.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +2 -0
  3. package/src/kits.mjs +35 -0
  4. package/src/messages.mjs +1 -1
  5. package/src/playbook-command.mjs +16 -6
  6. package/src/recipe-command.mjs +128 -0
  7. package/src/status-command.mjs +11 -2
  8. package/src/status-core/events.mjs +34 -0
  9. package/src/status-core/review.mjs +3 -1
  10. package/src/status-core/status.mjs +3 -1
  11. package/src/store-command.mjs +4 -0
  12. package/vendor/.akari-capability-sources.json +3 -0
  13. package/vendor/docs/contract-2026-07-20-review-json-v1-annotation-model.md +4 -2
  14. package/vendor/docs/contract-2026-08-23-stroke-persistence.md +2 -0
  15. package/vendor/docs/contract-2026-10-15-playbook-v0.md +1 -1
  16. package/vendor/docs/contract-2026-10-16-recipes-and-connectors-v0.md +14 -7
  17. package/vendor/docs/contract-2026-10-16-showcase-v0.md +150 -0
  18. package/vendor/docs/contract-2026-10-16-template-v0.md +129 -0
  19. package/vendor/docs/contract-2026-10-16-vocabulary-v0.md +175 -0
  20. package/vendor/packages/akari-launcher/package.json +1 -1
  21. package/vendor/packages/edit-lint/src/lint/review.mjs +1 -1
  22. package/vendor/packages/i18n/locales/en/annotations.json +3 -3
  23. package/vendor/packages/i18n/locales/en/annotationscommon.json +1 -0
  24. package/vendor/packages/i18n/locales/en/cli.json +17 -1
  25. package/vendor/packages/i18n/locales/en/preview.json +15 -6
  26. package/vendor/packages/i18n/locales/en/project.json +6 -4
  27. package/vendor/packages/i18n/locales/en/surfaces.json +28 -0
  28. package/vendor/packages/i18n/locales/ja/annotations.json +3 -3
  29. package/vendor/packages/i18n/locales/ja/annotationscommon.json +1 -0
  30. package/vendor/packages/i18n/locales/ja/cli.json +17 -1
  31. package/vendor/packages/i18n/locales/ja/preview.json +13 -4
  32. package/vendor/packages/i18n/locales/ja/project.json +8 -6
  33. package/vendor/packages/i18n/locales/ja/surfaces.json +28 -0
  34. package/vendor/packages/project-scaffold/src/index.mjs +313 -4
  35. package/vendor/packages/schemas/akari-recipe.schema.json +19 -0
  36. package/vendor/packages/schemas/bin/validate-kit-manifest.mjs +38 -1
  37. package/vendor/packages/schemas/bin/validate-review.mjs +3 -3
  38. package/vendor/packages/schemas/capabilities.json +9 -0
  39. package/vendor/packages/schemas/connections.schema.json +15 -0
  40. package/vendor/packages/schemas/fixtures/review/invalid-strokes-content-rect-empty-sessionref/review.json +6 -0
  41. package/vendor/packages/schemas/fixtures/review/valid-strokes-direct/review.json +6 -0
  42. package/vendor/packages/schemas/kit-manifest.schema.json +11 -0
  43. package/vendor/packages/schemas/playbook.schema.json +27 -2
  44. package/vendor/packages/schemas/review.schema.json +2 -2
  45. package/vendor/packages/schemas/test/akari-recipe.test.mjs +52 -0
  46. package/vendor/packages/schemas/test/fixtures/akari-recipes/music.json +11 -0
  47. package/vendor/packages/schemas/test/fixtures/akari-recipes/three-d.json +11 -0
  48. package/vendor/packages/schemas/test/kit-manifest.test.mjs +28 -1
  49. package/vendor/packages/schemas/test/kit-recipes.test.mjs +35 -0
  50. package/vendor/packages/schemas/test/validate-review.test.mjs +66 -0
  51. package/vendor/packages/schemas/test/validate-template.test.mjs +55 -0
  52. package/vendor/packages/schemas/validate-akari-recipe.mjs +60 -0
  53. package/vendor/packages/schemas/validate-playbook.mjs +172 -12
  54. package/vendor/skills/akari/SKILL.md +7 -0
  55. package/vendor/templates/project-default/.akari/workflow.json +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.5.0-beta.5",
3
+ "version": "1.5.0",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -32,6 +32,7 @@ import { runWordBookCommand } from './word-book-command.mjs';
32
32
  import { runConnectCommand } from './connect-command.mjs';
33
33
  import { runProjectCommand } from './project-command.mjs';
34
34
  import { runPlaybookCommand } from './playbook-command.mjs';
35
+ import { runRecipeCommand } from './recipe-command.mjs';
35
36
  import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
36
37
  import { cliText } from './i18n.mjs';
37
38
 
@@ -47,6 +48,7 @@ export const runSubcommands = {
47
48
  connect: runConnectCommand,
48
49
  project: runProjectCommand,
49
50
  playbook: runPlaybookCommand,
51
+ recipe: runRecipeCommand,
50
52
  };
51
53
 
52
54
  /**
package/src/kits.mjs CHANGED
@@ -293,6 +293,37 @@ export function linkKitPlaybooks(kitDir, manifest, home, options = {}) {
293
293
  return { linked, warnings };
294
294
  }
295
295
 
296
+ export function linkKitRecipes(kitDir, manifest, home, options = {}) {
297
+ const linked = [];
298
+ const warnings = [];
299
+ for (const entry of manifest.recipes ?? []) {
300
+ if (typeof entry?.dir !== 'string' || !/^recipes\/[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(entry.dir)) {
301
+ throw new Error(cliText('cli.kits.error.pathOutside', { path: String(entry?.dir) }));
302
+ }
303
+ const id = path.posix.basename(entry.dir);
304
+ const source = safeKitPath(kitDir, entry.dir);
305
+ const realKit = realpathSync(kitDir);
306
+ const realSource = realpathSync(source);
307
+ if (!realSource.startsWith(`${realKit}${path.sep}`) || !statSync(realSource).isDirectory()) {
308
+ throw new Error(cliText('cli.kits.error.pathOutside', { path: entry.dir }));
309
+ }
310
+ const destination = path.join(home, 'tool-recipes', id);
311
+ try {
312
+ const stat = lstatSync(destination);
313
+ let owned = false;
314
+ if (stat.isSymbolicLink()) {
315
+ try { owned = realpathSync(destination) === realSource; } catch { /* Broken links are not owned. */ }
316
+ }
317
+ if (!owned) { warnings.push(cliText('cli.kits.warning.directoryKept', { path: destination })); continue; }
318
+ } catch (error) { if (error?.code !== 'ENOENT') throw error; }
319
+ const result = replaceSymlink(source, destination, options);
320
+ if (result.status === 'linked') linked.push(id);
321
+ else warnings.push(cliText(result.status === 'permission-denied'
322
+ ? 'cli.kits.warning.symlinkFailed' : 'cli.kits.warning.directoryKept', { path: destination }));
323
+ }
324
+ return { linked, warnings };
325
+ }
326
+
296
327
  export function readKitsLedger(home) {
297
328
  const ledgerPath = path.join(home, 'kits', 'installed.json');
298
329
  if (!existsSync(ledgerPath)) return { schema: KITS_SCHEMA, kits: [] };
@@ -311,6 +342,9 @@ export function writeKitsLedger(home, entry) {
311
342
  removeOwnedSymlink(path.join(home, 'playbooks', id), previous.kitDir);
312
343
  }
313
344
  }
345
+ for (const id of previous?.recipes ?? []) {
346
+ if (!(entry.recipes ?? []).includes(id)) removeOwnedSymlink(path.join(home, 'tool-recipes', id), previous.kitDir);
347
+ }
314
348
  ledger.kits = [...ledger.kits.filter((kit) => kit.id !== entry.id), entry];
315
349
  const ledgerPath = path.join(home, 'kits', 'installed.json');
316
350
  mkdirSync(path.dirname(ledgerPath), { recursive: true });
@@ -326,6 +360,7 @@ export function removeKit(home, productId, env = process.env) {
326
360
  if (!entry) return false;
327
361
  for (const name of entry.skills ?? []) removeOwnedSymlink(path.join(home, 'kits', 'plugin', 'skills', name), entry.kitDir);
328
362
  for (const id of entry.playbooks ?? []) removeOwnedSymlink(path.join(home, 'playbooks', id), entry.kitDir);
363
+ for (const id of entry.recipes ?? []) removeOwnedSymlink(path.join(home, 'tool-recipes', id), entry.kitDir);
329
364
  for (const asset of entry.assets ?? []) {
330
365
  for (const root of resolveAssetLibraryRoots({ ...env, AKARI_HOME: home }).read) removeOwnedSymlink(path.join(root, asset.category, asset.id), entry.kitDir);
331
366
  }
package/src/messages.mjs CHANGED
@@ -335,7 +335,7 @@ export function describeCliHelp() {
335
335
  cliText('cli.help.header.usage'),
336
336
  '',
337
337
  cliText('cli.help.commands.heading'),
338
- ...['launch', 'store', 'sounds', 'doctor', 'speakers', 'toolsVoice', 'update', 'status', 'migrate', 'migrateLook', 'generate', 'storyboard', 'project', 'playbook', 'clean'].map((name) => cliText(`cli.help.commands.${name}`)),
338
+ ...['launch', 'store', 'sounds', 'doctor', 'speakers', 'toolsVoice', 'update', 'status', 'migrate', 'migrateLook', 'generate', 'storyboard', 'project', 'playbook', 'recipe', 'clean'].map((name) => cliText(`cli.help.commands.${name}`)),
339
339
  '',
340
340
  cliText('cli.help.developer.heading'),
341
341
  ...['opencode', 'claude', 'yes', 'version', 'new', 'init', 'other', 'help'].map((name) => cliText(`cli.help.developer.${name}`)),
@@ -5,27 +5,30 @@ import { pathToFileURL } from 'node:url';
5
5
  import { cliText } from './i18n.mjs';
6
6
  import { resolveLauncherAssets } from './repo-assets.mjs';
7
7
 
8
- const usage = () => cliText('cli.playbook.help.usage');
8
+ const usage = () => `${cliText('cli.playbook.help.usage')}\n${cliText('cli.playbook.template.usage')}`;
9
9
  const safeChannel = value => typeof value === 'string' && value.trim() && value.length <= 64
10
10
  && value !== '.' && !value.includes('..') && !/[/\\<>:"|?*\x00-\x1f]/.test(value);
11
11
 
12
12
  function parse(args) {
13
13
  const action = args[0];
14
14
  if (action === '--help' || action === '-h' || args.includes('--help') || args.includes('-h')) return { action: 'help' };
15
- if (!['list', 'apply'].includes(action)) throw new Error(usage());
16
- const result = { action, id: action === 'apply' ? args[1] : null };
15
+ if (!['list', 'apply', 'template'].includes(action)) throw new Error(usage());
16
+ const result = { action, id: action === 'apply' ? args[1] : null, project: action === 'template' ? args[1] : null };
17
17
  if (action === 'apply' && (!result.id || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(result.id))) throw new Error(usage());
18
- for (let i = action === 'apply' ? 2 : 1; i < args.length; i++) {
18
+ if (action === 'template' && !result.project) throw new Error(usage());
19
+ for (let i = action === 'apply' || action === 'template' ? 2 : 1; i < args.length; i++) {
19
20
  const arg = args[i];
20
21
  if ((action === 'list' && arg === '--json') || (action === 'apply' && arg === '--dry-run')) {
21
22
  result[arg.slice(2).replace('-', '')] = true;
22
23
  } else if ((action === 'list' && ['--category', '--tag', '--channel'].includes(arg))
23
- || (action === 'apply' && arg === '--into')) {
24
+ || (action === 'apply' && arg === '--into')
25
+ || (action === 'template' && ['--item', '--id', '--name', '--out', '--license-own'].includes(arg))) {
24
26
  if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(usage());
25
27
  result[arg.slice(2)] = args[++i];
26
28
  } else throw new Error(usage());
27
29
  }
28
30
  if (action === 'apply' && !result.into) throw new Error(usage());
31
+ if (action === 'template' && (!result.item || !result.id || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(result.id))) throw new Error(usage());
29
32
  if (result.channel && !safeChannel(result.channel)) throw new Error(usage());
30
33
  return result;
31
34
  }
@@ -41,9 +44,16 @@ export async function runPlaybookCommand(args, options = {}) {
41
44
  const env = options.env ?? process.env;
42
45
  const cwd = options.cwd ?? process.cwd();
43
46
  const assets = options.assets ?? resolveLauncherAssets();
47
+ const akariHome = env.AKARI_HOME || path.join(options.home ?? os.homedir(), '.akari');
44
48
  if (!assets.scaffoldModulePath) throw new Error(cliText('cli.playbook.error.scaffoldMissing'));
45
49
  const scaffold = await import(pathToFileURL(assets.scaffoldModulePath).href);
46
- const akariHome = env.AKARI_HOME || path.join(options.home ?? os.homedir(), '.akari');
50
+ if (parsed.action === 'template') {
51
+ const result = await scaffold.exportPlaybookTemplate({ projectDir: path.resolve(cwd, parsed.project),
52
+ itemId: parsed.item, id: parsed.id, name: parsed.name, out: parsed.out ?? path.join(akariHome, 'playbooks'),
53
+ licenseOwn: parsed['license-own'] });
54
+ log(result.dir);
55
+ return { exitCode: 0, ...result };
56
+ }
47
57
  const projectDir = parsed.into ? path.resolve(cwd, parsed.into) : null;
48
58
  let root = null;
49
59
  if (assets.creatorRootModulePath) {
@@ -0,0 +1,128 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ import { cliText } from './i18n.mjs';
7
+ import { resolveLauncherAssets } from './repo-assets.mjs';
8
+
9
+ const KIND_CAPABILITIES = Object.freeze({
10
+ image: ['image-generation'], video: ['video-generation'], music: ['music-generation'],
11
+ tts: ['speech-synthesis'], sns: ['publish']
12
+ });
13
+ const idPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
14
+
15
+ function parse(args) {
16
+ if (args.length === 0 || ['--help', '-h'].includes(args[0])) return { action: 'help' };
17
+ const action = args[0];
18
+ if (!['list', 'plan'].includes(action)) throw new Error(cliText('cli.recipe.help.usage'));
19
+ const id = action === 'plan' ? args[1] : null;
20
+ if (action === 'plan' && !idPattern.test(id ?? '')) throw new Error(cliText('cli.recipe.help.usage'));
21
+ const parsed = { action, id, json: false, project: null };
22
+ for (let i = action === 'plan' ? 2 : 1; i < args.length; i++) {
23
+ if (args[i] === '--json' && !parsed.json) parsed.json = true;
24
+ else if (args[i] === '--project' && !parsed.project && args[i + 1] && !args[i + 1].startsWith('-')) parsed.project = args[++i];
25
+ else throw new Error(cliText('cli.recipe.help.usage'));
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ async function readJson(file) {
31
+ return JSON.parse(await fs.readFile(file, 'utf8'));
32
+ }
33
+
34
+ function akariHome(options) {
35
+ const env = options.env ?? process.env;
36
+ return env.AKARI_HOME || path.join(options.home ?? env.HOME ?? os.homedir(), '.akari');
37
+ }
38
+
39
+ export async function listRecipes({ projectDir, home, schemasDir }) {
40
+ const { validateAkariRecipe } = await import(pathToFileURL(path.join(schemasDir, 'validate-akari-recipe.mjs')).href);
41
+ const result = new Map();
42
+ for (const [source, shelf] of [['project', path.join(projectDir, 'recipes')], ['local', path.join(home, 'tool-recipes')]]) {
43
+ let entries;
44
+ try { entries = await fs.readdir(shelf, { withFileTypes: true }); }
45
+ catch (error) { if (error.code === 'ENOENT') continue; throw error; }
46
+ for (const entry of entries) {
47
+ if (!idPattern.test(entry.name) || result.has(entry.name)) continue;
48
+ const file = path.join(shelf, entry.name, 'recipe.json');
49
+ let recipe;
50
+ try { recipe = await readJson(file); }
51
+ catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR' || error instanceof SyntaxError) continue; throw error; }
52
+ if (recipe?.kind !== 'akari-recipe' || recipe.id !== entry.name || !validateAkariRecipe(recipe).ok) continue;
53
+ result.set(entry.name, { id: recipe.id, name: recipe.name, needs: recipe.needs,
54
+ source: source === 'local' && entry.isSymbolicLink() ? 'kit' : source, recipe });
55
+ }
56
+ }
57
+ return [...result.values()].sort((a, b) => a.id.localeCompare(b.id));
58
+ }
59
+
60
+ function candidateGates(recipe, provider) {
61
+ const gates = new Set(recipe.gates);
62
+ const billing = provider.notes?.billing ?? '';
63
+ const paidSignal = /(?:paid|purchase|per (?:image|minute|generation|request)|\$|usd|\u6709\u6599|\u8ab2\u91d1|\u5f93\u91cf)/iu.test(billing);
64
+ const freeSignal = /(?:free|no charge|\u7121\u6599|\u7121\u511f)/iu.test(billing);
65
+ if (billing && (paidSignal || !freeSignal)) gates.add('paid');
66
+ if (provider.adapter?.type === 'official-api' || provider.adapter?.type === 'custom'
67
+ || (!provider.adapter && provider.auth !== 'none')) gates.add('external-send');
68
+ if (!provider.terms?.checked) gates.add('license');
69
+ return [...gates];
70
+ }
71
+
72
+ export function planRecipe(recipe, connections) {
73
+ const providers = Array.isArray(connections?.providers) ? connections.providers : [];
74
+ const requirements = recipe.needs.map(capability => {
75
+ const candidates = providers.filter(provider =>
76
+ (provider.provides ?? KIND_CAPABILITIES[provider.kind] ?? []).includes(capability))
77
+ .map(provider => ({ id: provider.id, kind: provider.kind,
78
+ auth: provider.doctor?.status ?? 'unchecked', gates: candidateGates(recipe, provider) }));
79
+ return { capability, candidates, ...(candidates.length ? {} : { message: cliText('cli.recipe.plan.noCandidate') }) };
80
+ });
81
+ return { id: recipe.id, name: recipe.name, gates: recipe.gates, requirements,
82
+ missing: requirements.filter(item => item.candidates.length === 0).map(item => item.capability) };
83
+ }
84
+
85
+ export async function runRecipeCommand(args, options = {}) {
86
+ const log = options.log ?? console.log;
87
+ const error = options.error ?? console.error;
88
+ try {
89
+ const parsed = parse(args);
90
+ if (parsed.action === 'help') { log(cliText('cli.recipe.help.usage')); return { exitCode: 0 }; }
91
+ const projectDir = path.resolve(options.cwd ?? process.cwd(), parsed.project ?? options.projectRoot ?? '.');
92
+ const assets = options.assets ?? resolveLauncherAssets();
93
+ const schemasDir = options.schemasDir ?? assets.schemasSourceDir;
94
+ if (!schemasDir) throw new Error(cliText('cli.recipe.error.schemasMissing'));
95
+ const recipes = await listRecipes({ projectDir, home: akariHome(options), schemasDir });
96
+ if (parsed.action === 'list') {
97
+ const entries = recipes.map(({ recipe, ...entry }) => entry);
98
+ if (parsed.json) log(JSON.stringify(entries));
99
+ else for (const entry of entries) log(cliText('cli.recipe.list.row', { id: entry.id, name: entry.name,
100
+ needs: entry.needs.join(', '), source: entry.source }));
101
+ return { exitCode: 0, entries };
102
+ }
103
+ const selected = recipes.find(entry => entry.id === parsed.id);
104
+ if (!selected) throw new Error(cliText('cli.recipe.error.notFound', { id: parsed.id }));
105
+ const projectConnections = path.join(projectDir, '.akari', 'connections.json');
106
+ let connections;
107
+ try { connections = await readJson(projectConnections); }
108
+ catch (cause) {
109
+ if (cause.code !== 'ENOENT') throw cause;
110
+ if (!assets.templateDir) throw new Error(cliText('cli.recipe.error.connectionsMissing'));
111
+ connections = await readJson(path.join(assets.templateDir, '.akari', 'connections.json'));
112
+ }
113
+ const plan = planRecipe(selected.recipe, connections);
114
+ if (parsed.json) log(JSON.stringify(plan));
115
+ else {
116
+ log(cliText('cli.recipe.plan.header', { id: plan.id, name: plan.name }));
117
+ if (plan.gates.length) log(cliText('cli.recipe.plan.recipeGates', { gates: plan.gates.join(', ') }));
118
+ for (const item of plan.requirements) {
119
+ if (!item.candidates.length) log(cliText('cli.recipe.plan.missingRow', { capability: item.capability, message: item.message }));
120
+ else for (const candidate of item.candidates) log(cliText('cli.recipe.plan.row', {
121
+ capability: item.capability, id: candidate.id,
122
+ auth: candidate.auth === 'unchecked' ? cliText('cli.recipe.auth.unchecked') : candidate.auth,
123
+ gates: candidate.gates.join(', ') || cliText('cli.recipe.plan.noGates') }));
124
+ }
125
+ }
126
+ return { exitCode: plan.missing.length ? 1 : 0, plan };
127
+ } catch (cause) { error(cause.message); return { exitCode: 1 }; }
128
+ }
@@ -11,6 +11,7 @@ import {
11
11
  import { describeVersionStatus } from "./messages.mjs";
12
12
  import { readCacheSync, readOwnVersion, resolveCachePath, resolveInstalledVersionInfo } from "./update-check.mjs";
13
13
  import { resolveRuntimePaths } from "./runtime-diagnostics.mjs";
14
+ import { cliText } from "./i18n.mjs";
14
15
 
15
16
  export async function runStatusCommand(argv, options = {}) {
16
17
  const log = options.log ?? ((line) => process.stdout.write(line));
@@ -48,9 +49,17 @@ export async function runStatusCommand(argv, options = {}) {
48
49
 
49
50
  export function formatStatusOutput(status) {
50
51
  const summary = formatStatusSummary(status);
51
- if (status.state_health !== "inconclusive" || status.problems.length === 0) return summary;
52
+ const activity = [];
53
+ if (status.automation?.action_count) activity.push(cliText("cli.status.automation.actions", {
54
+ count: status.automation.action_count, summary: status.automation.latest_action,
55
+ }));
56
+ if (status.automation?.pending_gate_count) activity.push(cliText("cli.status.automation.pending", {
57
+ count: status.automation.pending_gate_count,
58
+ }));
59
+ const output = [summary, ...activity].join("\n");
60
+ if (status.state_health !== "inconclusive" || status.problems.length === 0) return output;
52
61
  const problems = status.problems.map((problem) => ` - ${problem}`).join("\n");
53
- return `${summary}\nCould not determine:\n${problems}\nNext: run \`akari status --json\` for the full machine-readable report, or resolve the items above and re-run \`akari status\`.`;
62
+ return `${output}\nCould not determine:\n${problems}\nNext: run \`akari status --json\` for the full machine-readable report, or resolve the items above and re-run \`akari status\`.`;
54
63
  }
55
64
 
56
65
  export function parseStatusArguments(argv, cwd = process.cwd()) {
@@ -2,6 +2,10 @@ import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
2
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
 
4
4
  const FINAL_EVENT_TYPES = new Set(["final-acceptance", "final-acceptance-revoked"]);
5
+ const AUTOMATION_GATES = new Set([
6
+ "publish", "lab-submit", "paid", "external-send", "delete-original",
7
+ "license", "playbook-checkpoint", "human-asked",
8
+ ]);
5
9
 
6
10
  export function readProjectEvents(projectRoot, { gateTypes = [] } = {}) {
7
11
  let realRoot;
@@ -147,6 +151,36 @@ export function resolveActiveAcceptance(events) {
147
151
  };
148
152
  }
149
153
 
154
+ /** Automation requests are separate from milestone gateTypes. Unknown events stay harmless. */
155
+ export function resolveAutomationSummary(events) {
156
+ const validBase = (payload) => payload.version === 1
157
+ && isNonEmptyString(payload.id) && isNonEmptyString(payload.occurredAt);
158
+ const actions = events.filter(({ type, payload }) => type === "agent-action" && validBase(payload)
159
+ && isNonEmptyString(payload.summary)
160
+ && !/[\r\n]/u.test(payload.summary)
161
+ && ["partner", "external", "unattended"].includes(payload.actor)
162
+ && Array.isArray(payload.files) && payload.files.every(isNonEmptyString));
163
+ const resolved = new Set(events.filter(({ type, payload }) => type === "gate-resolved"
164
+ && validBase(payload) && isNonEmptyString(payload.requestId)
165
+ && ["approved", "declined"].includes(payload.decision) && payload.by === "human")
166
+ .map(({ payload }) => payload.requestId));
167
+ const pendingIds = new Set();
168
+ for (const { type, payload } of events) {
169
+ if (type === "gate-requested" && validBase(payload)
170
+ && isNonEmptyString(payload.requestId) && isNonEmptyString(payload.summary)
171
+ && AUTOMATION_GATES.has(payload.gate)
172
+ && (payload.detail === undefined || typeof payload.detail === "string")
173
+ && !resolved.has(payload.requestId)) {
174
+ pendingIds.add(payload.requestId);
175
+ }
176
+ }
177
+ return {
178
+ action_count: actions.length,
179
+ latest_action: actions.at(-1)?.payload.summary ?? null,
180
+ pending_gate_count: pendingIds.size,
181
+ };
182
+ }
183
+
150
184
  function validateFinalEventShape(payload, type) {
151
185
  if (!FINAL_EVENT_TYPES.has(type)) return null;
152
186
  if (!isNonEmptyString(payload.id)) return "has no id";
@@ -139,7 +139,9 @@ function validateStroke(stroke, label, problems) {
139
139
  && (!Number.isInteger(stroke.frame.cutIndex) || stroke.frame.cutIndex < 0))) {
140
140
  problems.push(`${label}.frame is invalid for content-rect`);
141
141
  }
142
- if (!isNonEmptyString(stroke.sessionRef)) problems.push(`${label}.sessionRef is required for content-rect`);
142
+ if (hasOwn(stroke, "sessionRef") && !isNonEmptyString(stroke.sessionRef)) {
143
+ problems.push(`${label}.sessionRef must be non-empty when present`);
144
+ }
143
145
  } else if (hasOwn(stroke, "frame")) {
144
146
  problems.push(`${label}.frame is forbidden for ${stroke.space}`);
145
147
  }
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
 
5
- import { readProjectEvents, resolveActiveAcceptance } from "./events.mjs";
5
+ import { readProjectEvents, resolveActiveAcceptance, resolveAutomationSummary } from "./events.mjs";
6
6
  import { resolveProjectDisplayName } from "./display-name.mjs";
7
7
  import { inspectFullIntegrity } from "./integrity.mjs";
8
8
  import { validateAndCountReview } from "./review.mjs";
@@ -76,6 +76,7 @@ export function resolveProjectStatus(input = process.cwd(), { mode = "fast" } =
76
76
  });
77
77
  problems.push(...eventProblems);
78
78
  const acceptanceState = resolveActiveAcceptance(events);
79
+ const automation = resolveAutomationSummary(events);
79
80
  problems.push(...acceptanceState.problems);
80
81
 
81
82
  const materials = scaffolded
@@ -120,6 +121,7 @@ export function resolveProjectStatus(input = process.cwd(), { mode = "fast" } =
120
121
  waiting_on: routing.waiting_on,
121
122
  next_skill: routing.next_skill,
122
123
  review: reviewCounts,
124
+ ...(automation.action_count || automation.pending_gate_count ? { automation } : {}),
123
125
  release: { state: releaseState, accepted: false },
124
126
  problems: [...new Set(problems)].sort((a, b) => a.localeCompare(b, "en")),
125
127
  warnings: [...new Set(warnings)].sort((a, b) => a.localeCompare(b, "en")),
@@ -34,6 +34,7 @@ import {
34
34
  ensureKitsMarketplace,
35
35
  linkKitAssets,
36
36
  linkKitPlaybooks,
37
+ linkKitRecipes,
37
38
  linkKitSkills,
38
39
  readKitManifest,
39
40
  readKitsLedger,
@@ -630,6 +631,8 @@ export async function runStoreCommand(args, options = {}) {
630
631
  }
631
632
  const playbookLinks = linkKitPlaybooks(kitDir, manifest, home, { platform: options.platform });
632
633
  for (const warning of playbookLinks.warnings) log(cliText('cli.store.install.warning', { warning }));
634
+ const recipeLinks = linkKitRecipes(kitDir, manifest, home, { platform: options.platform });
635
+ for (const warning of recipeLinks.warnings) log(cliText('cli.store.install.warning', { warning }));
633
636
  registerKitAssets(home, manifest, kitDir, assetLinks.items, env);
634
637
  writeKitsLedger(home, {
635
638
  id: manifest.id,
@@ -638,6 +641,7 @@ export async function runStoreCommand(args, options = {}) {
638
641
  kitDir,
639
642
  skills: skillLinks.linked,
640
643
  playbooks: playbookLinks.linked,
644
+ recipes: recipeLinks.linked,
641
645
  assets: assetLinks.linked
642
646
  });
643
647
  ensureKitsMarketplace(home);
@@ -95,6 +95,9 @@
95
95
  "docs/contract-2026-10-15-script-lines-v0.md",
96
96
  "docs/contract-2026-10-16-automation-gates-v0.md",
97
97
  "docs/contract-2026-10-16-recipes-and-connectors-v0.md",
98
+ "docs/contract-2026-10-16-showcase-v0.md",
99
+ "docs/contract-2026-10-16-template-v0.md",
100
+ "docs/contract-2026-10-16-vocabulary-v0.md",
98
101
  "packages/akari-ear/package.json",
99
102
  "packages/akari-ear/README.md",
100
103
  "packages/akari-launcher/package.json",
@@ -92,8 +92,10 @@
92
92
  - `target` が `doc:<path>#<block-id>`、`image:<path>`、`canvas:<c-NNNN>` のときだけ
93
93
  `sourceT:null` を許容する。動画面では従来どおり 0 以上の source 秒が必要である。
94
94
  - `strokes[]` は `{tool:"pen", space, points, ...}` の object。`content-rect` は
95
- `frame:{sourceT,cutIndex?}` と `sessionRef` が必須、`image-rect` / `canvas-rect` は
96
- `frame` を持たない。後二者の `sessionRef` / `canvasRef` は任意である。
95
+ `frame:{sourceT,cutIndex?}` が必須。`sessionRef` は録音から作った線だけが持ち、録音なしで
96
+ プレビューへ直接描いた線では省略する。`image-rect` / `canvas-rect` は `frame` を持たない。
97
+ `content-rect` / `image-rect` の `sessionRef` と `canvas-rect` の `canvasRef` は
98
+ いずれも任意である。
97
99
 
98
100
  実行可能仕様は `packages/schemas/fixtures/review/` に一元化し、`validate-review` と
99
101
  `edit-lint` が同じ valid / invalid 全件を消費する。片方だけの専用 fixture で契約差を
@@ -73,6 +73,8 @@ review.json の data `version` は 0 のまま据え置く。compile-review-sess
73
73
  ```
74
74
 
75
75
  - `strokeRefs` は `null`、省略、または 1 件以上の配列。欠落は従来データとして正常である。
76
+ - `strokeRefs` は録音の原本への参照なので、録音なしで直接描いた線には付かない。その線は
77
+ `strokes[].points`(描いた順)がそのまま原本になる。
76
78
  - pen は従来どおり最大 100 点の `strokes[].points` も埋め込み、`strokeRefs` から無加工の原本へ
77
79
  戻れる。rect は従来どおり `targetKind:"region"` + `region.box` を埋め込み、同じ `strokeRefs`
78
80
  から `strokes.json` 内の rect 原本へ戻れる。
@@ -185,7 +185,7 @@ akari new <target-dir> --from <playbook-dir | id> [--lang <bcp47>]
185
185
 
186
186
  ## 8. 非スコープ
187
187
 
188
- - 深さ 3(元データごと)。edit.json そのものと素材の実体の配布
188
+ - 深さ 3(元データごと)。edit.json そのものと素材の実体の配布 — **ただし「まとまり 1 つ + その素材」の深さ 3 = テンプレは `contract-2026-10-16-template-v0.md` で範囲内にした(2026-10-16)**
189
189
  - Lab での公開・入手・検索・タグ俯瞰・クリエイター登録・還元
190
190
  - URL を貼って作り方を起こす分析
191
191
  - 画面の最終デザイン(ポップアップの出だしは別の実装票。本契約は形と CLI と確認シートまで)
@@ -1,5 +1,5 @@
1
1
  ---
2
- lifecycle: draft
2
+ lifecycle: accepted
3
3
  created: 2026-10-16
4
4
  updated: 2026-10-16
5
5
  ---
@@ -7,7 +7,7 @@ updated: 2026-10-16
7
7
  # recipes-and-connectors-v0(レシピと接続の 2 層)契約
8
8
 
9
9
  - 日付: 2026-10-16
10
- - 状態: **ドラフト・語の裁定待ち**(§6。形と層の分け方は確定方向、「レシピ」の語の割り当てが未裁定)
10
+ - 状態: **accepted**(2026-10-16。語と置き場は §6 の既定で確定。語は後から改名できるよう、ファイルの見分けは `kind` で行う)
11
11
  - 前提: `contract-2026-07-14-3d-bake-recipe.md`(道具のレシピ = 正本、焼いた出力 = 再生成できるキャッシュ)/
12
12
  `contract-2026-09-13-extension-kit-v0.md`(キットの manifest。v0 の `kind` は `kit` のみ)/
13
13
  `packages/schemas/connections.schema.json`(接続の台帳。`kind`: genai / image / video / tts / music / sns / analytics / notify)/
@@ -73,10 +73,17 @@ updated: 2026-10-16
73
73
  - 公式の API が無いサービスを非公式に叩くアダプタは**同梱しない**。配る場合は個別に審査する(相手の規約に触れうる)
74
74
  - 有償・外部送信は門(`paid` / `external-send`)。どのおまかせ度でも止まる
75
75
  - MCP サーバーは作らない(2 種類目のエージェントか外部アプリが要ったとき)。外から叩く口はファイル契約と `akari` の副コマンド
76
- - 個々のアダプタの実装・レシピの GUI
76
+ - 個々のアダプタの実装・レシピの GUI・レシピの実行エンジン(実行はエージェント)
77
77
 
78
- ## 6. 開けた問い(オーナー裁定待ち)
78
+ ## 6. 語と置き場(2026-10-16 の既定)
79
79
 
80
- 1. **語**: 「レシピ」は既に `contract-2026-07-25-recipe-v0.md`(好みの凍結 `recipe.json`。ドラフト・スキル 2 本が参照)と
81
- 3D ベイクのレシピで使われている。シチュエーションの層を「レシピ」と呼ぶなら、好みの凍結の側を「好み」などへ改名するか
82
- 2. 置き場: レシピの宣言を キット(`recipes/<id>/recipe.json`)と手元(`~/.akari/…`)のどこに置くか(1 の語が決まってから)
80
+ - 画面の語: シチュエーションの層を **レシピ** と呼ぶ。既存の `contract-2026-07-25-recipe-v0.md` の `recipe.json`
81
+ (確認済みの好みの凍結。`~/.akari/recipes/<name>.json`)は**そのまま**で、画面では「好みの記憶」と呼ぶ。2 つはファイルの `kind` で見分ける
82
+ (本契約の宣言は `"kind": "akari-recipe"`。好みの凍結は `kind` を持たない)
83
+ - 置き場(作り方と同じ形の棚): 手元 = `~/.akari/tool-recipes/<id>/recipe.json`(`~/.akari/recipes/` には置かない)/
84
+ キット = `recipes/<id>/recipe.json`(キットの manifest に `recipes: [{ "dir": "recipes/<id>" }]`。`akari store install` で手元の棚へ symlink)/
85
+ プロジェクト = `<project>/recipes/<id>/recipe.json`(そのプロジェクトだけで使うもの)。同じ id はプロジェクト → 手元 → キットの順で先を使う
86
+ - 動詞(CLI): `akari recipe list [--json]`(棚の一覧)/ `akari recipe plan <id> [--project <dir>]`(要る能力 → 使える接続の候補・
87
+ 止まる門〈`paid` / `external-send` / `terms.checked` が無ければ `license`〉を出す。**実行はしない** — 実行はエージェントがスキルと既存の動詞
88
+ 〈`akari generate` など〉で行い、出力を manifest の tool-output に recipe / connector つきで載せる)
89
+ - 改名するときは、画面の語と CLI の別名を足すだけで、ファイルの `kind` と置き場は変えない