agent-sin 0.1.12 → 0.1.16

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 (97) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +2 -1
  3. package/builtin-skills/_shared/_todo_lib.py +290 -0
  4. package/builtin-skills/even-g2-setup/main.ts +896 -0
  5. package/builtin-skills/even-g2-setup/skill.yaml +133 -0
  6. package/builtin-skills/memo-delete/main.py +28 -107
  7. package/builtin-skills/memo-delete/skill.yaml +10 -21
  8. package/builtin-skills/memo-index/main.py +96 -64
  9. package/builtin-skills/memo-index/skill.yaml +4 -10
  10. package/builtin-skills/memo-list/main.py +126 -72
  11. package/builtin-skills/memo-list/skill.yaml +8 -14
  12. package/builtin-skills/memo-save/main.py +191 -25
  13. package/builtin-skills/memo-save/skill.yaml +29 -5
  14. package/builtin-skills/memo-search/main.py +38 -18
  15. package/builtin-skills/memo-vector-search/main.py +11 -6
  16. package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
  17. package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
  18. package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
  19. package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
  20. package/builtin-skills/schedule-add/main.py +26 -0
  21. package/builtin-skills/service-restart/main.ts +249 -0
  22. package/builtin-skills/service-restart/skill.yaml +49 -0
  23. package/builtin-skills/todo-add/main.py +3 -1
  24. package/builtin-skills/todo-delete/main.py +3 -1
  25. package/builtin-skills/todo-done/main.py +3 -1
  26. package/builtin-skills/todo-list/main.py +4 -1
  27. package/builtin-skills/todo-tick/main.py +3 -1
  28. package/builtin-skills/topic-knowledge-read/main.py +118 -0
  29. package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
  30. package/dist/builder/build-action-classifier.d.ts +18 -0
  31. package/dist/builder/build-action-classifier.js +82 -1
  32. package/dist/builder/build-flow.d.ts +33 -4
  33. package/dist/builder/build-flow.js +251 -89
  34. package/dist/builder/builder-session.d.ts +1 -1
  35. package/dist/builder/builder-session.js +112 -7
  36. package/dist/builder/conversation-router.d.ts +4 -2
  37. package/dist/builder/conversation-router.js +19 -2
  38. package/dist/cli/index.js +323 -20
  39. package/dist/core/ai-provider.d.ts +1 -0
  40. package/dist/core/ai-provider.js +8 -3
  41. package/dist/core/chat-engine.d.ts +9 -3
  42. package/dist/core/chat-engine.js +1263 -146
  43. package/dist/core/config.d.ts +4 -0
  44. package/dist/core/config.js +82 -0
  45. package/dist/core/daily-memory-promotion.d.ts +7 -0
  46. package/dist/core/daily-memory-promotion.js +596 -18
  47. package/dist/core/image-attachments.d.ts +31 -0
  48. package/dist/core/image-attachments.js +237 -0
  49. package/dist/core/logger.d.ts +2 -1
  50. package/dist/core/logger.js +77 -1
  51. package/dist/core/memo-migration.d.ts +3 -0
  52. package/dist/core/memo-migration.js +422 -0
  53. package/dist/core/native-modules.d.ts +24 -0
  54. package/dist/core/native-modules.js +99 -0
  55. package/dist/core/notifier.d.ts +8 -3
  56. package/dist/core/notifier.js +191 -17
  57. package/dist/core/obsidian-vault.d.ts +19 -0
  58. package/dist/core/obsidian-vault.js +477 -0
  59. package/dist/core/operating-model.d.ts +2 -0
  60. package/dist/core/operating-model.js +15 -0
  61. package/dist/core/output-writer.d.ts +3 -2
  62. package/dist/core/output-writer.js +108 -7
  63. package/dist/core/profile-memory.js +22 -1
  64. package/dist/core/runtime.d.ts +2 -0
  65. package/dist/core/runtime.js +9 -1
  66. package/dist/core/secrets.d.ts +4 -0
  67. package/dist/core/secrets.js +34 -0
  68. package/dist/core/skill-history.d.ts +44 -0
  69. package/dist/core/skill-history.js +329 -0
  70. package/dist/core/skill-registry.d.ts +5 -0
  71. package/dist/core/skill-registry.js +11 -0
  72. package/dist/discord/bot.d.ts +1 -0
  73. package/dist/discord/bot.js +181 -10
  74. package/dist/even-g2/gateway.d.ts +15 -0
  75. package/dist/even-g2/gateway.js +868 -0
  76. package/dist/runtimes/codex-app-server.d.ts +5 -1
  77. package/dist/runtimes/codex-app-server.js +147 -8
  78. package/dist/runtimes/python-runner.js +82 -0
  79. package/dist/runtimes/typescript-runner.js +13 -1
  80. package/dist/skills-sdk/types.d.ts +19 -4
  81. package/dist/telegram/bot.d.ts +1 -0
  82. package/dist/telegram/bot.js +115 -7
  83. package/package.json +3 -1
  84. package/templates/even-g2-agent/README.md +83 -0
  85. package/templates/even-g2-agent/app.json +20 -0
  86. package/templates/even-g2-agent/index.html +31 -0
  87. package/templates/even-g2-agent/package-lock.json +1836 -0
  88. package/templates/even-g2-agent/package.json +22 -0
  89. package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
  90. package/templates/even-g2-agent/src/embedded-config.ts +4 -0
  91. package/templates/even-g2-agent/src/main.ts +539 -0
  92. package/templates/even-g2-agent/src/style.css +70 -0
  93. package/templates/even-g2-agent/tsconfig.json +11 -0
  94. package/templates/skill-python/main.py +20 -2
  95. package/templates/skill-python/skill.yaml +9 -0
  96. package/templates/skill-typescript/main.ts +40 -5
  97. package/templates/skill-typescript/skill.yaml +9 -0
@@ -6,10 +6,11 @@ import { getAiProvider, } from "../core/ai-provider.js";
6
6
  import { findMissingRequiredEnv, runSkill } from "../core/runtime.js";
7
7
  import { loadKnownModelIds, validateSkillDirectory, validateSkillId, } from "../core/skill-scaffold.js";
8
8
  import { appendConversationLog } from "../core/logger.js";
9
- import { dotenvPath, loadDotenv } from "../core/secrets.js";
9
+ import { dotenvPath, loadDotenv, removeFromDotenv, upsertDotenv } from "../core/secrets.js";
10
10
  import { formatProfileMemoryPromptSection, readProfileMemoryForPrompt, } from "../core/profile-memory.js";
11
11
  import { maybePromoteDailyMemory } from "../core/daily-memory-promotion.js";
12
12
  import { l } from "../core/i18n.js";
13
+ import { agentSinOperatingModelLines } from "../core/operating-model.js";
13
14
  export async function createBuildSession(config, skillId, options = {}) {
14
15
  const id = skillId || `new-skill-${timestampId()}`;
15
16
  validateSkillId(id);
@@ -507,7 +508,80 @@ async function runBuilderTurn(config, session, state, options) {
507
508
  catch {
508
509
  // No fenced blocks — agent wrote (or didn't) directly via cwd.
509
510
  }
510
- return { response, parsedSummary };
511
+ const envSaved = await applyBuilderEnvAssignments(config, response.text, options.onProgress);
512
+ return { response, parsedSummary, envSaved };
513
+ }
514
+ function parseBuilderEnvAssignments(text) {
515
+ const upserts = [];
516
+ const deletes = [];
517
+ const seenUpsert = new Set();
518
+ const seenDelete = new Set();
519
+ const blockPattern = /```env\s*\n([\s\S]*?)\n```/g;
520
+ let block;
521
+ while ((block = blockPattern.exec(text)) !== null) {
522
+ for (const raw of block[1].split(/\r?\n/)) {
523
+ const line = raw.trim();
524
+ if (!line || line.startsWith("#"))
525
+ continue;
526
+ const removeMatch = line.match(/^(?:-|delete\s+|remove\s+)([A-Za-z_][A-Za-z0-9_]*)\s*$/i);
527
+ if (removeMatch) {
528
+ const key = removeMatch[1];
529
+ if (/^AGENT_SIN_/i.test(key))
530
+ continue;
531
+ if (seenDelete.has(key))
532
+ continue;
533
+ seenDelete.add(key);
534
+ deletes.push(key);
535
+ continue;
536
+ }
537
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
538
+ if (!match)
539
+ continue;
540
+ const key = match[1];
541
+ if (/^AGENT_SIN_/i.test(key))
542
+ continue;
543
+ let value = match[2].trim();
544
+ if (value.length >= 2) {
545
+ const first = value.charAt(0);
546
+ const last = value.charAt(value.length - 1);
547
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
548
+ value = value.slice(1, -1);
549
+ }
550
+ }
551
+ if (!value || seenUpsert.has(key))
552
+ continue;
553
+ seenUpsert.add(key);
554
+ upserts.push({ key, value });
555
+ }
556
+ }
557
+ return { upserts, deletes };
558
+ }
559
+ async function applyBuilderEnvAssignments(config, text, onProgress) {
560
+ const { upserts, deletes } = parseBuilderEnvAssignments(text);
561
+ const saved = [];
562
+ if (upserts.length > 0) {
563
+ try {
564
+ await upsertDotenv(config.workspace, upserts);
565
+ for (const entry of upserts)
566
+ saved.push(entry.key);
567
+ emitBuildProgress(onProgress, l(`Saving ${upserts.length} env value(s)`, `${upserts.length}件の環境変数を保存しています`));
568
+ }
569
+ catch {
570
+ // ignore — best effort
571
+ }
572
+ }
573
+ if (deletes.length > 0) {
574
+ try {
575
+ const result = await removeFromDotenv(config.workspace, deletes);
576
+ if (result.removed.length > 0) {
577
+ emitBuildProgress(onProgress, l(`Removing ${result.removed.length} env value(s)`, `${result.removed.length}件の環境変数を削除しています`));
578
+ }
579
+ }
580
+ catch {
581
+ // ignore — best effort
582
+ }
583
+ }
584
+ return saved;
511
585
  }
512
586
  export async function prepareEditDraft(config, skillId) {
513
587
  let manifest;
@@ -697,7 +771,7 @@ export function formatDiscordSlashGuidance(context) {
697
771
  "- Shape:\n```\ninvocation:\n phrases: [...] # required (keep)\n discord_slash: # optional (add)\n description: Flip a coin\n options:\n - name: count\n type: integer # only string|integer|number|boolean\n description: number of flips\n required: false\n```",
698
772
  "- Rules: the command name is taken from skill.id (one skill = one command, flat options, no subcommands). `options[*].type` must be string/integer/number/boolean. `choices` must be `[{name, value}]` for string/integer/number only. `required` defaults to false. A Japanese description can be placed in `description_ja`.",
699
773
  "- Keep `input.schema` and `discord_slash.options` consistent. Use matching names/meanings (e.g. if the schema has `text`, the option should also be `name: text`).",
700
- "- Activation timing: when a skill that adds, changes, or removes `discord_slash` is registered, the Discord slash-command list will only update after the bot restarts (reconnects). Always mention this in the completion message (e.g. \"Restart the bot to use it as a slash command. The chat trigger (phrases) is available now.\"). A skill that only uses `phrases`, or an edit that does not touch `discord_slash`, does not require a restart.",
774
+ "- Activation timing: when a skill that adds, changes, or removes `discord_slash` is registered, the Discord slash-command list will only update after the bot restarts (reconnects). Do not tell the user only to restart manually. Say that the chat trigger is available now, and that if they want Agent-Sin to restart, they can send \"Agent-Sinを再起動して\" / \"restart Agent-Sin\" next. A skill that only uses `phrases`, or an edit that does not touch `discord_slash`, does not require a restart.",
701
775
  "",
702
776
  ];
703
777
  }
@@ -713,6 +787,9 @@ function formatBuilderRuntimeContext(context) {
713
787
  `- agent-sin source tree (read-only, for reference): ${context.install_root}`,
714
788
  `- agent-sin workspace: ${context.workspace_dir}`,
715
789
  `- Other skills (read-only, for reference): ${context.skills_dir}`,
790
+ `- Run logs (read-only): ${context.workspace_dir}/logs/runs/<run-id>.json — each file holds one skill execution's input.args, result.summary, result.data and ctx_logs. Useful when the user says "didn't return X" / "was wrong" — read recent runs of this skill to see exactly what the previous version produced vs. what the user wanted.`,
791
+ `- Conversation logs (read-only): ${context.workspace_dir}/logs/conversations/<YYYY-MM-DD>.jsonl — daily chat history with user, assistant, and skill turns. Useful when you need the surrounding intent that triggered the gap.`,
792
+ `- Skill data store (read-only, for reference): ${context.workspace_dir}/data/<skill-id>.db — per-skill SQLite history. Open it to see what data the skill has actually accumulated before deciding what to change.`,
716
793
  `- .env (update only the env vars this skill needs): ${context.dotenv_path}`,
717
794
  "",
718
795
  "# Areas you must not touch directly, and how to handle them",
@@ -744,6 +821,8 @@ function buildBuilderMessages(session, state, runtimeContext, profileMemory) {
744
821
  "- Your job is to build a skill that runs on agent-sin from the user's request, and to leave it in a state where it can be used immediately after writing.",
745
822
  "- The user is not an engineer. Do not ask about function names, commands, or technical jargon. Take requirements in plain language.",
746
823
  "",
824
+ ...agentSinOperatingModelLines("builder"),
825
+ "",
747
826
  "# How to proceed",
748
827
  "1. Read the user's requirements. Only when something truly cannot be built without clarification, ask at most three questions at a time. You may ask another batch of up to three if the answer is still insufficient. Iterate as many times as needed. Conversely, when you can decide on your own, go straight to implementation.",
749
828
  "2. Once requirements are clear, you *must* emit file blocks in the response body to write the files. The format is `\\`\\`\\`file:<relative-path>` to open, the raw file content, and `\\`\\`\\`` to close. Always emit both skill.yaml and main.py (main.ts if TypeScript). If needed, emit fixtures/input.json in the same format. Do not use language fences like `\\`\\`\\`yaml` / `\\`\\`\\`python`, and do not use plain labels like \"File:\" alone. If you write only a completion report and omit the file blocks, agent-sin writes nothing and the skill stays unfinished. Example:\n```file:skill.yaml\nid: example\nname: Example\ndescription: ...\nruntime: python\ninvocation:\n phrases:\n - run the example\noutput_mode: raw\n```\n```file:main.py\nasync def run(ctx, input):\n return {\"status\": \"ok\", \"summary\": \"hello\"}\n```",
@@ -755,6 +834,7 @@ function buildBuilderMessages(session, state, runtimeContext, profileMemory) {
755
834
  "- When the user says \"Discord notification\", they mean the agent-sin notification feature. Call `ctx.notify({ channel: \"discord\", ... })` from the skill. Do not create your own `DISCORD_WEBHOOK_URL` or POST to webhooks directly.",
756
835
  "- `AGENT_SIN_DISCORD_*` for Discord notifications is an agent-sin runtime setting. Do not put it in the skill's `required_env` or .env examples. Only if it is not yet configured, briefly tell the user that agent-sin's Discord notification setup is needed.",
757
836
  "- When the user says \"Telegram notification\", use `ctx.notify({ channel: \"telegram\", ... })` the same way. `AGENT_SIN_TELEGRAM_*` is also a runtime setting and must not appear in skill.yaml.",
837
+ "- To send a local image or file from a skill to Discord/Telegram, call `ctx.notify({ channel: \"discord\"|\"telegram\"|\"auto\", title, body, filePath })` or `filePaths`. `imagePath` / `imagePaths` are accepted aliases for image files. The path must point to a readable local file; use an absolute path or a workspace-relative path. Do not upload to Discord/Telegram directly.",
758
838
  "",
759
839
  "# Hard rules",
760
840
  "- Never finish with only a completion report and no skill.yaml / main.*. Don't stop at requirement confirmation.",
@@ -778,6 +858,7 @@ function buildBuilderMessages(session, state, runtimeContext, profileMemory) {
778
858
  "",
779
859
  "Do include (completion-report template)",
780
860
  "1-2 sentences of \"what is now possible\" (e.g. \"Classifies unread Gmail and summarizes it at 3pm.\"). If there is setup the user must do, follow with a brief description. Do not add confirmation prompts like \"Want to test it?\" or \"Should I run it?\". When the user replies with \"run it\" / \"try it\", the system auto-switches back to chat mode and runs the skill, so you do not need to prompt.",
861
+ "If a restart is required to apply settings, avoid a yes/no confirmation inside build mode. Tell the user they can send \"Agent-Sinを再起動して\" / \"restart Agent-Sin\" and Agent-Sin will ask or perform the restart from chat mode.",
781
862
  "",
782
863
  "## When credentials are required (API key / OAuth / token / cookie / etc.)",
783
864
  "Skills that integrate with external services almost always need credentials. It is normal that the skill cannot run immediately after implementation, so always include the following:",
@@ -785,20 +866,43 @@ function buildBuilderMessages(session, state, runtimeContext, profileMemory) {
785
866
  " Example for Gmail: \"In Google Cloud Console (https://console.cloud.google.com/) create a new project → enable the Gmail API → set up the OAuth consent screen → Credentials → create an OAuth client ID (desktop) → download the JSON\".",
786
867
  "- Where to save what: the key names and formats to write to `~/.agent-sin/.env`, one per line.",
787
868
  " Example: `GMAIL_CREDENTIALS_PATH=~/credentials.json`, `OPENAI_API_KEY=sk-...`.",
788
- "- If the user pastes a value into chat and you can tell which `required_env` entry it belongs to, you may update `~/.agent-sin/.env` directly. After saving, just reply \"Saved.\" briefly. Only when you cannot tell, ask the user to send it as `env NAME=value`.",
869
+ "- Always look at the existing .env keys provided in the runtime context. Do not propose keys that are already configured (treat them as present). Propose only what is genuinely missing for this skill.",
870
+ "- When the user pastes one or more credential values (in any format — `KEY=value`, prose like \"トークンは abc です\", a bare token after asking for it, JSON, etc.), do not stay silent. Compare the values against the skill's `required_env` plus the .env keys you already see, and decide:",
871
+ " (a) If the mapping is clear (only one missing slot fits, the user explicitly named the slot, or the value's shape unambiguously matches a documented format like `sk-...`, `GOCSPX-...`, `xoxb-...`, `1//...`), save it by emitting a fenced env block (see below) and tell the user in one short sentence which key was saved.",
872
+ " (b) If the mapping is ambiguous (multiple required slots could match, value shape is unclear, or the user mentioned a key that is not in `required_env`), ask the user a short confirmation question before saving anything. Do not guess.",
873
+ "- To save credentials to `~/.agent-sin/.env`, include a fenced env block in your reply. The block itself is stripped from what the user sees, so still write a short natural-language confirmation in the body. The same block can also tidy up stale keys: prefix a key with `-` (or `delete `) to remove it from `.env`.",
874
+ " ```env",
875
+ " GOOGLE_CALENDAR_WORK_CLIENT_ID=123-abc.apps.googleusercontent.com",
876
+ " GOOGLE_CALENDAR_WORK_CLIENT_SECRET=GOCSPX-1234567890abcd",
877
+ " -GOOGLE_CLIENT_ID",
878
+ " -GOOGLE_CLIENT_SECRET",
879
+ " ```",
880
+ "- When the skill's `required_env` no longer needs a key that is currently in `.env` (e.g. you renamed `GOOGLE_CLIENT_ID` to `GOOGLE_CALENDAR_WORK_CLIENT_ID`), include the obsolete key with a `-` prefix in the same env block so `.env` stays in sync. Do not just say \"please delete it\" to the user — perform the cleanup yourself.",
881
+ "- Only put real credential values inside the env block — no quotes around obvious tokens, no placeholders like `<your-key>`, no `AGENT_SIN_*` runtime keys, no commentary lines. Empty or placeholder values are dropped. Never invent or paraphrase a credential value; only emit env entries for values the user actually provided.",
882
+ "- Never tell the user that .env edits were \"blocked by permissions\" or similar host-internal phrases. The env block is the supported way to read, update, and prune `.env` — use it instead of describing failures.",
789
883
  "- Do not push for \"test it\" until setup is finished. Instead write something like \"once you have obtained X and saved it to .env, let me know and I'll verify.\"",
790
884
  "",
791
885
  "# Skill authoring rules",
792
886
  "- skill.yaml requires `id` (kebab-case), `runtime` (python|typescript), `name`, `description`, and `invocation.phrases`. `invocation.phrases` is required for chat invocation; list 3-6 entries combining the skill name, aliases, and example utterances (e.g. for id `flip-coin`: `[\"flip a coin\", \"toss a coin\", \"heads or tails\", \"flip-coin\"]`). `entry` defaults to `main.py` / `main.ts` per runtime, and `handler` defaults to `run`; both can be omitted. `invocation.command`, `input.schema`, `outputs` (only when leaving notes/reports the user reads back), `memory` (only when keeping state), `ai_steps` (only when calling AI), and `required_env` are optional. Do not write empty `outputs: []` / `ai_steps: []` / `retry: max_attempts: 0`. `schema_version` / `type` / `security` / `triggers` are deprecated.",
793
887
  "- Handler signature: Python is `async def run(ctx, input)`, TS is `export async function run(ctx, input)`. `input` is a dict of `{args, trigger, sources, memory}`, and `input.args` is already validated against skill.yaml's `input.schema`.",
794
888
  "- Return value: `{status: 'ok'|'skipped'|'error', title, summary, outputs, data, suggestions}`.",
795
- "- The available ctx surface is: `log.info/warn/error`, `memory.get` (async) / `memory.set` (async), `ai.run(step_id, payload)`, `notify(args)`, and `now()`. Do not touch env or fs directly.",
796
- "- File output is done by returning `{content, frontmatter}` under `outputs[id]`. The Runtime saves it according to skill.yaml's `outputs[].path/filename` (you can use `{{yyyy}}/{{MM}}/{{dd}}/{{date}}/{{datetime}}`; `append: true` appends). The skill itself must not open/write files. `outputs[].type` is either `markdown` or `json`.",
889
+ "- The available ctx surface is: `log.info/warn/error`, `memory.get` (async) / `memory.set` (async), `ai.run(step_id, payload)`, `notify(args)`, `history.append/list/read` (async), and `now()`. Do not touch env or fs directly.",
890
+ "- Use `ctx.history` whenever the skill should accumulate a *queryable log* the user (or the skill itself) will look back at later — meals, weight, training, mood, expenses, study minutes, watering plants, anything that benefits from \"show me the last week / last month\". Each entry is one SQLite row in `~/.agent-sin/data/<skill-id>.db` (table `entries(id, time, meta JSON, content)`), hidden from the user — they don't see the raw DB. To enable it, declare `history: { read: true, write: true }` in skill.yaml (read-only consumers may set only `read: true`).",
891
+ "- `ctx.history.append({ time?, meta?, content, replace? })`: insert/upsert one row. `time` defaults to now; `meta` is an arbitrary JSON-serialisable object (typed fields go here — e.g. `{ calories: 500, type: \"lunch\", record: {...} }`); `content` is a short text body; pass `replace: true` together with `meta.id` to upsert the same row on later corrections.",
892
+ "- `ctx.history.list({ from?, to?, limit? })`: read past rows as a sorted array of `{ time, meta, content }`. Use this when the skill needs to compute aggregates (totals, averages, streaks).",
893
+ "- `ctx.history.read({ from?, to? })`: read the same range as one concatenated text string. Pipe it into an ai_steps payload when the skill should ask the LLM to *interpret* the history (e.g. \"suggest tomorrow's menu based on the last 7 days\").",
894
+ "- `outputs` (Markdown reports the user reads back in Obsidian) and `history` (structured per-skill SQLite log queryable by date) are complementary. `history` is the data store; `outputs` is the optional, opt-in human view. For \"log diet meals\" use history. For \"weekly health digest\" add an output that renders a summary into notes/. Do not duplicate the same data in both.",
895
+ "- **Markdown outputs must be rendered, human-readable content** — a narrative, a summary, a structured report someone would actually open and *read top-to-bottom in Obsidian*. They are NOT an event log: do not append `## 2026-05-22T07:38:07` blocks one per call, do not dump every API row as a markdown table per execution, do not write per-call frontmatter (`type:` / `tags:`) for each entry. Raw structured/event data, per-entry records, and anything the skill itself queries back later belongs in `ctx.history` (SQLite). The litmus test: if the file is mostly machine-shaped data that only makes sense as a sequence of records, it should be `history`, not an output. If it is a *document* (weekly digest, generated script, analysis writeup, review), it can be an output. When the user wants both raw records and a readable summary, store records in `history` and render the summary as a separate `outputs[]` entry.",
896
+ "- File output is done by returning `{content, frontmatter}` under `outputs[id]`. The Runtime saves it according to skill.yaml's `outputs[].path/filename` (you can use `{{yyyy}}/{{MM}}/{{dd}}/{{date}}/{{datetime}}`; `append: true` appends). The skill itself must not open/write files. `outputs[].type` is either `markdown` or `json`. For per-skill user-visible md, prefer `path: skill-outputs/<this-skill-id>` with a plain `filename` (no `{{yyyy}}/{{MM}}` subdirs) — that path is symlinked into the user's Obsidian Vault as `06 Skills/<skill-id>/`. Do NOT put outputs under `marketing/`, `news/`, `reports/`, `reviews/`, `youtube/`, `health/`, or any other ad-hoc top-level workspace dir; those used to be tolerated and are now considered legacy.",
897
+ "- As an escape hatch for when the runtime `outputs` block doesn't fit (rotating logs the skill itself rewrites, file lists the skill enumerates, etc.), the skill MAY open and write its own file under `input.sources.skill_output_dir` (TS: `input.sources.skillOutputDir`). The runtime creates that dir (= `~/.agent-sin/skill-outputs/<skill-id>/`) before invoking the skill, and Obsidian exposes it as `06 Skills/<skill-id>/`. Use a plain filename (`marketing-log.md`), never date subdirectories (`2026/05/2026-05-22.md`). This dir is for user-visible markdown only — internal state the user shouldn't see goes in `ctx.memory` / `ctx.history`, not here. Writing to any other absolute path (e.g. `~/.agent-sin/marketing/...`) is forbidden.",
797
898
  "- To call AI, first declare it in skill.yaml's `ai_steps` with `id / purpose / model` (optionally `optional: true`), then call `ctx.ai.run(id, payload)`. Ids that were not declared cannot be called.",
899
+ "- If a skill has local files that should be attached to the current chat reply, return their paths in `data.filePath` / `data.filePaths` (or `data.imagePath` / `data.imagePaths` for image-only results). The host will attach readable local files on Discord/Telegram. If an AI/helper response already contains local path strings, copy those strings into `data.filePaths` or pass them to `ctx.notify({ filePaths })`.",
900
+ "- If the user explicitly asks for Codex image generation, make the AI step instruction say to use `gpt-image-2` and to actually call the image generation tool, not just describe the image. Then read the returned local image path strings and pass them as `filePaths` / `imagePaths`.",
901
+ "- If the user asked for a separate notification, use `ctx.notify({ filePath })` / `ctx.notify({ filePaths })` instead of relying on the chat reply attachment.",
798
902
  "- Environment variables must be declared in skill.yaml's `required_env: [{name, description, optional}]`. The Runtime checks them before execution and blocks the run if any are missing.",
799
903
  "- For simple CRUD-style skills (add todo, list todos, mark done, etc.) where the result should be shown to the user verbatim, add `output_mode: raw` to skill.yaml. The summary then bypasses the LLM reformatting turn and is shown directly. Omit it when complex summarization/rewording is needed (the default behaviour lets the LLM format the result).",
800
904
  "- Do not write recurring schedules into skill.yaml (the builder must not touch schedules.yaml). Instead, tell the user that the finished skill can be scheduled by calling the `schedule-add` built-in skill from chat or CLI (`agent-sin run schedule-add --payload '{\"id\":\"...\",\"cron\":\"min hour dom month dow\",\"skill\":\"<this-skill>\"}'`). If the user prefers to edit manually, mention that they may add `- id / cron / skill / args / approve` to the `schedules:` list in `~/.agent-sin/schedules.yaml`.",
801
- "- Writable paths are only inside cwd: `skill.yaml` / `main.py` or `main.ts` / `README.md` / `fixtures/` / `tests` / `prompts/`. As an exception, you may add or update only the keys this skill needs in `~/.agent-sin/.env`. Do not write anywhere else outside cwd.",
905
+ "- Writable paths are only inside cwd: `skill.yaml` / `main.py` or `main.ts` / `README.md` / `fixtures/` / `tests` / `prompts/`. As an exception, you may add or update only the keys this skill needs in `~/.agent-sin/.env`, and at runtime the skill may write user-visible markdown under `input.sources.skill_output_dir`. Do not write anywhere else outside cwd. `outputs` and `ctx.history` are handled by the Runtime — the skill never opens those files itself.",
802
906
  "- Reading is unrestricted: feel free to read from the agent-sin source or other skills. A useful reference implementation is `~/.agent-sin/skills/memo-save/`.",
803
907
  "- If you feel an urge to edit cross-cutting files (schedules.yaml, other skills, models.yaml, config.toml, etc.), do not touch them yourself. Add a one-line request in the completion report (\"please do XYZ\") and let the host handle it.",
804
908
  "",
@@ -945,6 +1049,7 @@ function stripBuilderArtifacts(text) {
945
1049
  .replace(/```summary\s*\n([\s\S]*?)\n```/g, "$1")
946
1050
  .replace(/```builder-files[^\n]*\n[\s\S]*?\n```/gi, "")
947
1051
  .replace(/```file:[^\n]*\n[\s\S]*?\n```/g, "")
1052
+ .replace(/```env\s*\n[\s\S]*?\n```/g, "")
948
1053
  .trim();
949
1054
  }
950
1055
  async function listDraftFiles(draftDir) {
@@ -1,7 +1,7 @@
1
1
  import { type ChatProgressEvent, type ChatTurn } from "../core/chat-engine.js";
2
2
  import type { AppConfig } from "../core/config.js";
3
3
  import type { AiImagePart, AiProgressHandler } from "../core/ai-provider.js";
4
- import { type IntentRuntime } from "./build-flow.js";
4
+ import { type BuilderEventSource, type IntentRuntime } from "./build-flow.js";
5
5
  export interface BuildProgressReporter {
6
6
  onProgress: AiProgressHandler;
7
7
  flush(): Promise<void>;
@@ -11,12 +11,14 @@ export interface RouteConversationMessageOptions {
11
11
  text: string;
12
12
  history: ChatTurn[];
13
13
  intentRuntime: IntentRuntime;
14
- eventSource: "discord" | "telegram";
14
+ eventSource: BuilderEventSource;
15
15
  images?: AiImagePart[];
16
16
  createBuildProgress(): BuildProgressReporter;
17
17
  onBuildStart?(): Promise<void>;
18
18
  onBuildDone?(): Promise<void>;
19
19
  onChatProgress?(event: ChatProgressEvent): void;
20
20
  onAiProgress?: AiProgressHandler;
21
+ onLocalAttachments?: (paths: string[]) => void;
22
+ onGeneratedImages?: (paths: string[]) => void;
21
23
  }
22
24
  export declare function routeConversationMessage(options: RouteConversationMessageOptions): Promise<string[]>;
@@ -1,5 +1,5 @@
1
1
  import { chatRespond, } from "../core/chat-engine.js";
2
- import { classifyPendingHandoff, enterBuildMode, handleBuildModeMessage, } from "./build-flow.js";
2
+ import { classifyPendingHandoff, composeBuildSuggestionReply, enterBuildMode, handleBuildModeMessage, isExplicitBuildModeStartRequest, } from "./build-flow.js";
3
3
  import { l } from "../core/i18n.js";
4
4
  export async function routeConversationMessage(options) {
5
5
  const { config, text, history, intentRuntime, eventSource, images = [], } = options;
@@ -37,6 +37,7 @@ export async function routeConversationMessage(options) {
37
37
  // "discuss" → keep pending, fall through to chatRespond.
38
38
  }
39
39
  let modelFailed = false;
40
+ let buildSuggestion = null;
40
41
  const lines = await chatRespond(config, text, history, {
41
42
  eventSource,
42
43
  preferredSkillId: intentRuntime.preferred_skill_id || undefined,
@@ -48,12 +49,28 @@ export async function routeConversationMessage(options) {
48
49
  options.onChatProgress?.(event);
49
50
  },
50
51
  onAiProgress: options.onAiProgress,
51
- onBuildSuggestion: (suggestion) => setPendingBuildSuggestion(intentRuntime, suggestion, text),
52
+ onLocalAttachments: options.onLocalAttachments || options.onGeneratedImages,
53
+ onBuildSuggestion: (suggestion) => {
54
+ buildSuggestion = suggestion;
55
+ setPendingBuildSuggestion(intentRuntime, suggestion, text);
56
+ },
52
57
  });
53
58
  intentRuntime.preferred_skill_id = null;
54
59
  if (modelFailed && lines.length === 0) {
55
60
  return [l("The model call failed.", "モデル呼び出しでエラーになりました。")];
56
61
  }
62
+ const suggestion = buildSuggestion;
63
+ if (suggestion && intentRuntime.pending && isExplicitBuildModeStartRequest(text)) {
64
+ await options.onBuildStart?.();
65
+ const buildProgress = options.createBuildProgress();
66
+ const buildLines = await enterBuildMode(config, history, intentRuntime, { onProgress: buildProgress.onProgress }, undefined, eventSource);
67
+ await buildProgress.flush();
68
+ await options.onBuildDone?.();
69
+ return buildLines;
70
+ }
71
+ if (suggestion && intentRuntime.pending) {
72
+ return composeBuildSuggestionReply(lines, suggestion.type);
73
+ }
57
74
  return lines;
58
75
  }
59
76
  function setPendingBuildSuggestion(intentRuntime, suggestion, userText) {