aimeat 1.33.2 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locales/en.json +437 -15
- package/dist/locales/fi.json +437 -15
- package/dist/public/cortex-bundled/aimeat-charts.js +3 -3
- package/dist/public/cortex-bundled/aimeat-charts.yaml +3 -3
- package/dist/public/cortex-bundled/aimeat-i18n.js +123 -0
- package/dist/public/cortex-bundled/aimeat-i18n.yaml +64 -0
- package/dist/public/css/views/landing.css +2 -0
- package/dist/public/css/views/secretary.css +464 -2
- package/dist/public/js/i18n.js +9 -0
- package/dist/public/js/services/apps.js +4 -2
- package/dist/public/js/services/memory.js +9 -3
- package/dist/public/js/services/offers.js +1 -1
- package/dist/public/js/services/organisms.js +49 -17
- package/dist/public/js/services/secretary-helpers.js +266 -17
- package/dist/public/js/services/secretary-policy.js +28 -2
- package/dist/public/js/services/secretary-rules.js +70 -0
- package/dist/public/js/theme.js +20 -0
- package/dist/public/lib/VENDORED-STYLING.md +31 -0
- package/dist/public/lib/aimeat-daisyui-bridge.css +48 -0
- package/dist/public/lib/chartjs@4.js +14 -0
- package/dist/public/lib/daisyui@5.css +7 -0
- package/dist/public/lib/tailwindcss@4.js +8 -0
- package/dist/public/llms-template.txt +9 -6
- package/dist/public/showroom.html +104 -0
- package/dist/public/showroom.js +247 -0
- package/dist/public/spa.html +18 -16
- package/dist/public/views/aimeat-os.js +3 -2
- package/dist/public/views/landing.js +50 -6
- package/dist/public/views/profile/agents/agent-card.js +13 -4
- package/dist/public/views/profile/agents/state-detector.js +6 -0
- package/dist/public/views/profile/agents/tab-integration.js +11 -2
- package/dist/public/views/profile/apps-tab.js +46 -0
- package/dist/public/views/profile/extensions-tab.js +4 -0
- package/dist/public/views/profile/inbox-tab.js +1 -1
- package/dist/public/views/profile/landing-page.js +8 -3
- package/dist/public/views/profile/memory-tab.js +37 -6
- package/dist/public/views/profile/organisms/home.js +32 -0
- package/dist/public/views/profile/organisms/workspace-list.js +36 -5
- package/dist/public/views/profile/organisms/workspace.js +29 -4
- package/dist/public/views/profile/organisms-tab.js +61 -8
- package/dist/public/views/secretary/calendar.js +327 -0
- package/dist/public/views/secretary/cards-reach.js +16 -17
- package/dist/public/views/secretary/cards.js +217 -79
- package/dist/public/views/secretary/dashboard.js +617 -0
- package/dist/public/views/secretary/quality.js +156 -0
- package/dist/public/views/secretary/use-autonomy.js +17 -2
- package/dist/public/views/secretary/use-freshness.js +50 -0
- package/dist/public/views/secretary/use-intake.js +23 -4
- package/dist/public/views/secretary/use-layout.js +122 -0
- package/dist/public/views/secretary/use-quick-actions.js +98 -0
- package/dist/public/views/secretary/use-specialists.js +103 -0
- package/dist/public/views/secretary/use-triggers.js +159 -0
- package/dist/public/views/secretary/use-whats-next.js +340 -0
- package/dist/public/views/secretary/workflow-design.js +112 -0
- package/dist/public/views/secretary.js +836 -44
- package/dist/scripts/check-no-max-tokens.d.ts +2 -0
- package/dist/scripts/check-no-max-tokens.d.ts.map +1 -0
- package/dist/scripts/check-no-max-tokens.js +71 -0
- package/dist/scripts/check-no-max-tokens.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js +9 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
- package/dist/src/cli/connect/onboarding-prompt.d.ts +3 -0
- package/dist/src/cli/connect/onboarding-prompt.d.ts.map +1 -1
- package/dist/src/cli/connect/onboarding-prompt.js +10 -3
- package/dist/src/cli/connect/onboarding-prompt.js.map +1 -1
- package/dist/src/data/aimeat-iam-package.d.ts +25 -0
- package/dist/src/data/aimeat-iam-package.d.ts.map +1 -0
- package/dist/src/data/aimeat-iam-package.js +312 -0
- package/dist/src/data/aimeat-iam-package.js.map +1 -0
- package/dist/src/data/aimeat-marketplace-package.d.ts +25 -0
- package/dist/src/data/aimeat-marketplace-package.d.ts.map +1 -0
- package/dist/src/data/aimeat-marketplace-package.js +231 -0
- package/dist/src/data/aimeat-marketplace-package.js.map +1 -0
- package/dist/src/data/app-templates.d.ts +48 -0
- package/dist/src/data/app-templates.d.ts.map +1 -0
- package/dist/src/data/app-templates.js +856 -0
- package/dist/src/data/app-templates.js.map +1 -0
- package/dist/src/data/example-packages.d.ts.map +1 -1
- package/dist/src/data/example-packages.js +3 -1
- package/dist/src/data/example-packages.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +689 -7
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +10 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +7 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +205 -2
- package/dist/src/generated/prisma-postgres/index.js +10 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +23 -13
- package/dist/src/generated/prisma-postgres/wasm.js +10 -3
- package/dist/src/mcp/agent-onboarding.d.ts +4 -0
- package/dist/src/mcp/agent-onboarding.d.ts.map +1 -1
- package/dist/src/mcp/agent-onboarding.js +28 -2
- package/dist/src/mcp/agent-onboarding.js.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +1 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +16 -1
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +3 -3
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/index.d.ts +3 -0
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +11 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/mcp/organisms.d.ts.map +1 -1
- package/dist/src/mcp/organisms.js +48 -8
- package/dist/src/mcp/organisms.js.map +1 -1
- package/dist/src/mcp/workspaces.d.ts.map +1 -1
- package/dist/src/mcp/workspaces.js +6 -4
- package/dist/src/mcp/workspaces.js.map +1 -1
- package/dist/src/models/agent-onboarding-schemas.d.ts +47 -0
- package/dist/src/models/agent-onboarding-schemas.d.ts.map +1 -1
- package/dist/src/models/agent-onboarding-schemas.js +117 -0
- package/dist/src/models/agent-onboarding-schemas.js.map +1 -1
- package/dist/src/models/offer-schemas.d.ts +8 -8
- package/dist/src/routes/agent-onboarding.d.ts +5 -0
- package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
- package/dist/src/routes/agent-onboarding.js +21 -2
- package/dist/src/routes/agent-onboarding.js.map +1 -1
- package/dist/src/routes/ai.d.ts +8 -4
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +38 -14
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts +2 -0
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +7 -1
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/app-templates.d.ts +16 -0
- package/dist/src/routes/app-templates.d.ts.map +1 -0
- package/dist/src/routes/app-templates.js +40 -0
- package/dist/src/routes/app-templates.js.map +1 -0
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +55 -5
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/auth.d.ts +3 -0
- package/dist/src/routes/auth.d.ts.map +1 -1
- package/dist/src/routes/auth.js +9 -4
- package/dist/src/routes/auth.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +1 -0
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/generator.d.ts.map +1 -1
- package/dist/src/routes/generator.js +3 -2
- package/dist/src/routes/generator.js.map +1 -1
- package/dist/src/routes/lib-ai.d.ts +0 -1
- package/dist/src/routes/lib-ai.d.ts.map +1 -1
- package/dist/src/routes/lib-ai.js +13 -6
- package/dist/src/routes/lib-ai.js.map +1 -1
- package/dist/src/routes/lib-header.d.ts +2 -0
- package/dist/src/routes/lib-header.d.ts.map +1 -1
- package/dist/src/routes/lib-header.js +7 -16
- package/dist/src/routes/lib-header.js.map +1 -1
- package/dist/src/routes/lib-markdown.d.ts +18 -0
- package/dist/src/routes/lib-markdown.d.ts.map +1 -0
- package/dist/src/routes/lib-markdown.js +236 -0
- package/dist/src/routes/lib-markdown.js.map +1 -0
- package/dist/src/routes/libs.d.ts +22 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +133 -10
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +21 -2
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +4 -0
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +17 -0
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +107 -7
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/portal.d.ts +9 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +1 -1
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +3 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -1
- package/dist/src/routes/public-stats.js +9 -2
- package/dist/src/routes/public-stats.js.map +1 -1
- package/dist/src/routes/secretary.d.ts +34 -0
- package/dist/src/routes/secretary.d.ts.map +1 -0
- package/dist/src/routes/secretary.js +311 -0
- package/dist/src/routes/secretary.js.map +1 -0
- package/dist/src/routes/specialists.d.ts.map +1 -1
- package/dist/src/routes/specialists.js +21 -0
- package/dist/src/routes/specialists.js.map +1 -1
- package/dist/src/routes/subdomains.js +1 -1
- package/dist/src/routes/subdomains.js.map +1 -1
- package/dist/src/routes/workflows.d.ts +9 -3
- package/dist/src/routes/workflows.d.ts.map +1 -1
- package/dist/src/routes/workflows.js +9 -3
- package/dist/src/routes/workflows.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +4 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +7 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server-bootstrap/static-files.d.ts +4 -0
- package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
- package/dist/src/server-bootstrap/static-files.js +18 -0
- package/dist/src/server-bootstrap/static-files.js.map +1 -1
- package/dist/src/services/archive.d.ts +74 -0
- package/dist/src/services/archive.d.ts.map +1 -0
- package/dist/src/services/archive.js +132 -0
- package/dist/src/services/archive.js.map +1 -0
- package/dist/src/services/cortex-seeder.d.ts +4 -1
- package/dist/src/services/cortex-seeder.d.ts.map +1 -1
- package/dist/src/services/cortex-seeder.js +25 -6
- package/dist/src/services/cortex-seeder.js.map +1 -1
- package/dist/src/services/directory.d.ts.map +1 -1
- package/dist/src/services/directory.js +3 -2
- package/dist/src/services/directory.js.map +1 -1
- package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
- package/dist/src/services/generator-prompt-seeds.js +5 -4
- package/dist/src/services/generator-prompt-seeds.js.map +1 -1
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +14 -0
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/onboarding-guide.d.ts +53 -0
- package/dist/src/services/onboarding-guide.d.ts.map +1 -0
- package/dist/src/services/onboarding-guide.js +81 -0
- package/dist/src/services/onboarding-guide.js.map +1 -0
- package/dist/src/services/organism-search.d.ts +6 -2
- package/dist/src/services/organism-search.d.ts.map +1 -1
- package/dist/src/services/organism-search.js +2 -2
- package/dist/src/services/organism-search.js.map +1 -1
- package/dist/src/services/owner-memory.d.ts +1 -0
- package/dist/src/services/owner-memory.d.ts.map +1 -1
- package/dist/src/services/owner-memory.js.map +1 -1
- package/dist/src/services/owner-session.d.ts +1 -0
- package/dist/src/services/owner-session.d.ts.map +1 -1
- package/dist/src/services/owner-session.js +10 -2
- package/dist/src/services/owner-session.js.map +1 -1
- package/dist/src/services/package-seeder.d.ts +22 -0
- package/dist/src/services/package-seeder.d.ts.map +1 -0
- package/dist/src/services/package-seeder.js +30 -0
- package/dist/src/services/package-seeder.js.map +1 -0
- package/dist/src/services/presence.d.ts +3 -0
- package/dist/src/services/presence.d.ts.map +1 -1
- package/dist/src/services/presence.js +10 -0
- package/dist/src/services/presence.js.map +1 -1
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +8 -3
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/scheduler.d.ts +50 -2
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +454 -26
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/secretary-authoring.d.ts +71 -0
- package/dist/src/services/secretary-authoring.d.ts.map +1 -0
- package/dist/src/services/secretary-authoring.js +336 -0
- package/dist/src/services/secretary-authoring.js.map +1 -0
- package/dist/src/services/secretary-clarify.d.ts +57 -0
- package/dist/src/services/secretary-clarify.d.ts.map +1 -0
- package/dist/src/services/secretary-clarify.js +104 -0
- package/dist/src/services/secretary-clarify.js.map +1 -0
- package/dist/src/services/secretary-feed.d.ts +11 -0
- package/dist/src/services/secretary-feed.d.ts.map +1 -0
- package/dist/src/services/secretary-feed.js +28 -0
- package/dist/src/services/secretary-feed.js.map +1 -0
- package/dist/src/services/secretary-tick.d.ts +243 -3
- package/dist/src/services/secretary-tick.d.ts.map +1 -1
- package/dist/src/services/secretary-tick.js +273 -3
- package/dist/src/services/secretary-tick.js.map +1 -1
- package/dist/src/services/secretary-workbrief.d.ts +144 -0
- package/dist/src/services/secretary-workbrief.d.ts.map +1 -0
- package/dist/src/services/secretary-workbrief.js +315 -0
- package/dist/src/services/secretary-workbrief.js.map +1 -0
- package/dist/src/services/secretary-worktier.d.ts +118 -0
- package/dist/src/services/secretary-worktier.d.ts.map +1 -0
- package/dist/src/services/secretary-worktier.js +157 -0
- package/dist/src/services/secretary-worktier.js.map +1 -0
- package/dist/src/services/skill-bundle/generator.d.ts +3 -0
- package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
- package/dist/src/services/skill-bundle/generator.js +19 -0
- package/dist/src/services/skill-bundle/generator.js.map +1 -1
- package/dist/src/services/specialist-runner.d.ts +36 -0
- package/dist/src/services/specialist-runner.d.ts.map +1 -0
- package/dist/src/services/specialist-runner.js +91 -0
- package/dist/src/services/specialist-runner.js.map +1 -0
- package/dist/src/services/specialist.d.ts +9 -0
- package/dist/src/services/specialist.d.ts.map +1 -1
- package/dist/src/services/specialist.js +42 -0
- package/dist/src/services/specialist.js.map +1 -1
- package/dist/src/services/structure-overview.d.ts +13 -0
- package/dist/src/services/structure-overview.d.ts.map +1 -1
- package/dist/src/services/structure-overview.js +34 -11
- package/dist/src/services/structure-overview.js.map +1 -1
- package/dist/src/services/system-agent-responder.d.ts +36 -0
- package/dist/src/services/system-agent-responder.d.ts.map +1 -0
- package/dist/src/services/system-agent-responder.js +107 -0
- package/dist/src/services/system-agent-responder.js.map +1 -0
- package/dist/src/services/system-agent.d.ts +34 -1
- package/dist/src/services/system-agent.d.ts.map +1 -1
- package/dist/src/services/system-agent.js +73 -0
- package/dist/src/services/system-agent.js.map +1 -1
- package/dist/src/storage/interface.d.ts +33 -0
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +37 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +132 -12
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +23 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +60 -8
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +29 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +109 -10
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts +3 -0
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +64 -9
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/app.repository.d.ts +15 -0
- package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +41 -1
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/organism.repository.d.ts +2 -1
- package/dist/src/storage/repositories/organism.repository.d.ts.map +1 -1
- package/dist/static/app-catalog.html +469 -122
- package/package.json +2 -1
- package/prisma/schema.postgres.prisma +10 -0
- package/prisma/schema.prisma +12 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file secretary/use-specialists.js
|
|
3
|
+
* @description Secretary-side specialist management (Phase 3 surface). List the owner's specialists,
|
|
4
|
+
* create one from a role template (+ a brain purpose), run its queued tasks now, or remove it. A
|
|
5
|
+
* specialist is a stripped-down Secretary (node-run, own brain) that publishes a workflow-compatible
|
|
6
|
+
* offer on creation — so creating one here gives the workflow designer a new building block.
|
|
7
|
+
* @structure useSpecialists({ showToast }) -> { list, form, openForm, closeForm, setField, create, busy, remove, runNow }
|
|
8
|
+
* · specialistsCard(p) -> section
|
|
9
|
+
* @usage const spec = useSpecialists({ showToast }); specialistsCard({ ...spec })
|
|
10
|
+
* @version-history
|
|
11
|
+
* v0.1.0 — 2026-06-28 — Phase 3: specialist create/list/run/delete card driven by /v1/specialists.
|
|
12
|
+
*/
|
|
13
|
+
import { h } from 'preact';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
import { useState, useEffect, useCallback } from 'preact/hooks';
|
|
16
|
+
import { apiGet, apiPost, apiDelete } from '/js/api.js';
|
|
17
|
+
import { t } from '/js/i18n.js';
|
|
18
|
+
|
|
19
|
+
const html = htm.bind(h);
|
|
20
|
+
/** Role templates a specialist can be created from (each selects a scope profile server-side). */
|
|
21
|
+
export const SPECIALIST_ROLES = ['specialist', 'sdr', 'prep', 'finance', 'recruiter'];
|
|
22
|
+
|
|
23
|
+
export function useSpecialists({ showToast }) {
|
|
24
|
+
const [list, setList] = useState([]);
|
|
25
|
+
const [form, setForm] = useState(null); // null = closed; { name, role, purpose }
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
|
|
28
|
+
const load = useCallback(async () => {
|
|
29
|
+
const r = await apiGet('/v1/specialists').catch(() => null);
|
|
30
|
+
setList((r && r.data && r.data.specialists) || []);
|
|
31
|
+
}, []);
|
|
32
|
+
useEffect(() => { load(); }, [load]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handler = () => load();
|
|
35
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
36
|
+
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
37
|
+
}, [load]);
|
|
38
|
+
|
|
39
|
+
const openForm = useCallback(() => setForm({ name: '', role: 'specialist', purpose: '' }), []);
|
|
40
|
+
const closeForm = useCallback(() => setForm(null), []);
|
|
41
|
+
const setField = useCallback((k, v) => setForm((f) => (f ? { ...f, [k]: v } : f)), []);
|
|
42
|
+
|
|
43
|
+
const create = useCallback(async () => {
|
|
44
|
+
if (!form || !form.name.trim()) return;
|
|
45
|
+
setBusy(true);
|
|
46
|
+
try {
|
|
47
|
+
const body = { name: form.name.trim(), role: form.role || 'specialist' };
|
|
48
|
+
if ((form.purpose || '').trim()) body.brain = { purpose: form.purpose.trim() };
|
|
49
|
+
await apiPost('/v1/specialists', body);
|
|
50
|
+
showToast(t('secretary.spec.created'));
|
|
51
|
+
setForm(null);
|
|
52
|
+
await load();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
showToast(`${t('secretary.spec.error')}: ${e.message}`, true);
|
|
55
|
+
} finally {
|
|
56
|
+
setBusy(false);
|
|
57
|
+
}
|
|
58
|
+
}, [form, load, showToast]);
|
|
59
|
+
|
|
60
|
+
const remove = useCallback(async (name) => {
|
|
61
|
+
await apiDelete(`/v1/specialists/${encodeURIComponent(name)}`).catch(() => {});
|
|
62
|
+
await load();
|
|
63
|
+
}, [load]);
|
|
64
|
+
|
|
65
|
+
const runNow = useCallback(async (name) => {
|
|
66
|
+
try {
|
|
67
|
+
const r = await apiPost(`/v1/specialists/${encodeURIComponent(name)}/run`, {});
|
|
68
|
+
showToast(t('secretary.spec.ran', { n: (r && r.data && r.data.ran) || 0 }));
|
|
69
|
+
await load();
|
|
70
|
+
} catch (e) {
|
|
71
|
+
showToast(`${t('secretary.spec.error')}: ${e.message}`, true);
|
|
72
|
+
}
|
|
73
|
+
}, [load, showToast]);
|
|
74
|
+
|
|
75
|
+
return { list, form, openForm, closeForm, setField, create, busy, remove, runNow };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Specialists card: list + create-from-role-template + run-now / remove. */
|
|
79
|
+
export function specialistsCard(p) {
|
|
80
|
+
return html`
|
|
81
|
+
<section class="sec-card sec-specialists">
|
|
82
|
+
<div class="sec-card-head">
|
|
83
|
+
<h2 class="sec-h2">${t('secretary.spec.title')}</h2>
|
|
84
|
+
<button class="btn-outline btn-sm" onClick=${p.form ? p.closeForm : p.openForm}>${p.form ? t('secretary.spec.cancel') : t('secretary.spec.add')}</button>
|
|
85
|
+
</div>
|
|
86
|
+
${p.list.length === 0 && !p.form ? html`<p class="sec-hint">${t('secretary.spec.empty')}</p>` : null}
|
|
87
|
+
${p.list.length ? html`<ul class="sec-goal-list">
|
|
88
|
+
${p.list.map((s) => html`<li class="sec-goal-row" key=${s.name}>
|
|
89
|
+
<div class="sec-goal-main"><span class="sec-goal-title">${s.display_name || s.name}</span> <span class="sec-hint">· ${s.role}</span></div>
|
|
90
|
+
<button class="sec-linkbtn" onClick=${() => p.runNow(s.name)}>${t('secretary.spec.run')}</button>
|
|
91
|
+
<button class="sec-linkbtn sec-next-discard" onClick=${() => p.remove(s.name)}>${t('secretary.spec.remove')}</button>
|
|
92
|
+
</li>`)}
|
|
93
|
+
</ul>` : null}
|
|
94
|
+
${p.form ? html`<div class="sec-form">
|
|
95
|
+
<input class="sec-input" placeholder=${t('secretary.spec.namePh')} value=${p.form.name} onInput=${(e) => p.setField('name', e.target.value)} />
|
|
96
|
+
<select class="sec-input" value=${p.form.role} onChange=${(e) => p.setField('role', e.target.value)}>
|
|
97
|
+
${SPECIALIST_ROLES.map((r) => html`<option value=${r} selected=${p.form.role === r}>${r}</option>`)}
|
|
98
|
+
</select>
|
|
99
|
+
<textarea class="sec-paste" placeholder=${t('secretary.spec.purposePh')} value=${p.form.purpose} onInput=${(e) => p.setField('purpose', e.target.value)}></textarea>
|
|
100
|
+
<button class="btn-primary btn-sm" disabled=${p.busy || !p.form.name.trim()} onClick=${p.create}>${p.busy ? t('secretary.saving') : t('secretary.spec.create')}</button>
|
|
101
|
+
</div>` : null}
|
|
102
|
+
</section>`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file secretary/use-triggers.js
|
|
3
|
+
* @description Triggers (Slice 3) — proactive follow-ups the Secretary binds to what it tracks. CRUD over
|
|
4
|
+
* `secretary.trigger.{id}` records (kind: time | recurring | completion | condition). The autonomous
|
|
5
|
+
* tick is the engine that evaluates + fires them (services/scheduler.ts + the pure helpers in
|
|
6
|
+
* services/secretary-tick.ts); this hook only manages the records the owner creates/pauses/deletes.
|
|
7
|
+
* @structure useTriggers({ active, showToast }) -> { triggers, armed, form, openForm, createTrigger, togglePause, removeTrigger, ... }
|
|
8
|
+
* @usage const trig = useTriggers({ active, showToast }); triggersCard({ ...trig, goals, routines })
|
|
9
|
+
* @version-history
|
|
10
|
+
* v0.2.0 — 2026-06-28 — Bound actions: a trigger can BIND a workflow or a specialist to its fire
|
|
11
|
+
* (action.{kind,workflowId|specialistName,prompt}); loads /v1/workflows + buildAction() composes it.
|
|
12
|
+
* v0.1.0 — 2026-06-28 — Slice 3: trigger CRUD hook (create all 4 kinds, pause/resume, delete).
|
|
13
|
+
*/
|
|
14
|
+
import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';
|
|
15
|
+
import { api, apiGet, apiPost, apiDelete } from '/js/api.js';
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { extractJson } from '/js/services/secretary-helpers.js';
|
|
18
|
+
|
|
19
|
+
const PREFIX = 'secretary.trigger.';
|
|
20
|
+
const CADENCE_MS = { daily: 86400000, weekly: 604800000, monthly: 2592000000 };
|
|
21
|
+
function genId() { return 'tr-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
|
|
22
|
+
|
|
23
|
+
/** Cheap gate (Slice 4): only bother the model when text smells recurring or deadline-y (FI + EN). */
|
|
24
|
+
const TRIG_HINTS = /(joka\s+(päivä|viikko|kuukausi|aamu|ilta|maanantai|tiistai|keskiviikko|torstai|perjantai)|viikoittain|päivittäin|kuukausittain|säännöllisesti|muistuta|deadline|määräaika|mennessä|ennen\s+\d|weekly|daily|monthly|every\s+(day|week|month|morning|monday|tuesday|wednesday|thursday|friday)|remind|by\s+\w+day|each\s+(day|week|month))/i;
|
|
25
|
+
|
|
26
|
+
const EMPTY_FORM = { kind: 'recurring', label: '', cadence: 'weekly', when: '', targetId: '', condType: 'memory_count', condPrefix: '', condValue: 5, condDays: 7, actionKind: 'remind', actionWorkflowId: '', actionSpecialistName: '', actionPrompt: '' };
|
|
27
|
+
|
|
28
|
+
/** Build the trigger's bound `action` from the form. 'remind' (default) → feed + action-item; 'workflow'
|
|
29
|
+
* → start the chosen workflow; 'specialist' → queue + run a task (the prompt) for the chosen specialist. */
|
|
30
|
+
function buildAction(form) {
|
|
31
|
+
const summary = form.label.trim();
|
|
32
|
+
if (form.actionKind === 'workflow' && form.actionWorkflowId) return { kind: 'workflow', summary, workflowId: form.actionWorkflowId };
|
|
33
|
+
if (form.actionKind === 'specialist' && form.actionSpecialistName) return { kind: 'specialist', summary, specialistName: form.actionSpecialistName, prompt: (form.actionPrompt || '').trim() || summary };
|
|
34
|
+
return { kind: 'remind', summary };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useTriggers({ active, showToast }) {
|
|
38
|
+
const [triggers, setTriggers] = useState([]);
|
|
39
|
+
const [workflows, setWorkflows] = useState([]); // saved workflows a trigger can bind to (run-on-fire)
|
|
40
|
+
const [form, setForm] = useState(null); // null = closed
|
|
41
|
+
const [adding, setAdding] = useState(false);
|
|
42
|
+
|
|
43
|
+
const load = useCallback(async () => {
|
|
44
|
+
// Cache-bust: the tick fires/settles triggers server-side, so always read fresh.
|
|
45
|
+
const r = await apiGet(`/v1/memory?prefix=${encodeURIComponent(PREFIX)}&_=${Date.now()}`).catch(() => null);
|
|
46
|
+
const items = (r && r.data && r.data.items) || [];
|
|
47
|
+
setTriggers(items.map((it) => it.value).filter((v) => v && v.id).sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || ''))));
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => { load(); }, [load]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
apiGet('/v1/workflows').then((r) => setWorkflows((r && r.data && r.data.workflows) || [])).catch(() => {});
|
|
52
|
+
}, []);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const h = () => load();
|
|
55
|
+
window.addEventListener('aimeat-live-update', h);
|
|
56
|
+
return () => window.removeEventListener('aimeat-live-update', h);
|
|
57
|
+
}, [load]);
|
|
58
|
+
|
|
59
|
+
const writeTrigger = useCallback(async (tr) => {
|
|
60
|
+
await apiPost('/v1/memory', { key: PREFIX + tr.id, value: tr, visibility: 'private', tags: ['secretary', 'trigger', tr.status || ''] });
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const openForm = useCallback(() => setForm({ ...EMPTY_FORM }), []);
|
|
64
|
+
const closeForm = useCallback(() => setForm(null), []);
|
|
65
|
+
const setField = useCallback((k, v) => setForm((f) => (f ? { ...f, [k]: v } : f)), []);
|
|
66
|
+
|
|
67
|
+
const createTrigger = useCallback(async () => {
|
|
68
|
+
if (!form || !form.label.trim()) return;
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const tr = {
|
|
71
|
+
id: genId(), kind: form.kind, label: form.label.trim().slice(0, 200),
|
|
72
|
+
contextId: (active && active.id) || '', status: 'armed', createdBy: 'owner',
|
|
73
|
+
createdAt: new Date().toISOString(), action: buildAction(form),
|
|
74
|
+
};
|
|
75
|
+
if (form.kind === 'time') {
|
|
76
|
+
if (!form.when) { showToast(t('secretary.trig.needWhen'), true); return; }
|
|
77
|
+
tr.when = new Date(form.when).toISOString(); tr.nextFireAt = tr.when;
|
|
78
|
+
} else if (form.kind === 'recurring') {
|
|
79
|
+
tr.cadence = form.cadence; tr.nextFireAt = new Date(now + (CADENCE_MS[form.cadence] || CADENCE_MS.weekly)).toISOString();
|
|
80
|
+
} else if (form.kind === 'completion') {
|
|
81
|
+
if (!form.targetId) { showToast(t('secretary.trig.needTarget'), true); return; }
|
|
82
|
+
tr.target = { type: 'item', id: form.targetId };
|
|
83
|
+
} else if (form.kind === 'condition') {
|
|
84
|
+
if (!form.condPrefix.trim()) { showToast(t('secretary.trig.needPrefix'), true); return; }
|
|
85
|
+
tr.condition = { type: form.condType, prefix: form.condPrefix.trim(), op: '>=', value: Number(form.condValue) || 0, days: Number(form.condDays) || 7 };
|
|
86
|
+
}
|
|
87
|
+
setAdding(true);
|
|
88
|
+
try {
|
|
89
|
+
await writeTrigger(tr);
|
|
90
|
+
showToast(t('secretary.trig.created'));
|
|
91
|
+
setForm(null);
|
|
92
|
+
await load();
|
|
93
|
+
} catch (e) {
|
|
94
|
+
showToast(`${t('secretary.trig.error')}: ${e.message}`, true);
|
|
95
|
+
} finally {
|
|
96
|
+
setAdding(false);
|
|
97
|
+
}
|
|
98
|
+
}, [form, active, writeTrigger, load, showToast]);
|
|
99
|
+
|
|
100
|
+
const togglePause = useCallback(async (tr) => {
|
|
101
|
+
await writeTrigger({ ...tr, status: tr.status === 'paused' ? 'armed' : 'paused' }).catch(() => {});
|
|
102
|
+
await load();
|
|
103
|
+
}, [writeTrigger, load]);
|
|
104
|
+
|
|
105
|
+
/** Edit a trigger in place (label/cadence) — writes the memory record. Recomputes nextFireAt when a
|
|
106
|
+
* recurring trigger's cadence changes. Used by the calendar's inline editor + the triggers card. */
|
|
107
|
+
const updateTrigger = useCallback(async (id, patch) => {
|
|
108
|
+
const cur = triggers.find((x) => x.id === id);
|
|
109
|
+
if (!cur) return;
|
|
110
|
+
const next = { ...cur, ...patch };
|
|
111
|
+
if (patch.label) { next.label = String(patch.label).slice(0, 200); next.action = { ...(next.action || {}), summary: next.label }; }
|
|
112
|
+
if (patch.cadence && next.kind === 'recurring' && patch.cadence !== cur.cadence) {
|
|
113
|
+
next.cadence = patch.cadence;
|
|
114
|
+
next.nextFireAt = new Date(Date.now() + (CADENCE_MS[patch.cadence] || CADENCE_MS.weekly)).toISOString();
|
|
115
|
+
}
|
|
116
|
+
await writeTrigger(next).catch(() => {});
|
|
117
|
+
await load();
|
|
118
|
+
}, [triggers, writeTrigger, load]);
|
|
119
|
+
|
|
120
|
+
const removeTrigger = useCallback(async (tr) => {
|
|
121
|
+
await apiDelete(`/v1/memory/${encodeURIComponent(PREFIX + tr.id)}`).catch(() => {});
|
|
122
|
+
await load();
|
|
123
|
+
}, [load]);
|
|
124
|
+
|
|
125
|
+
/** Arm a proposed trigger (Slice 4): status → armed, computing nextFireAt for recurring if unset. */
|
|
126
|
+
const armTrigger = useCallback(async (tr) => {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const armedTr = { ...tr, status: 'armed' };
|
|
129
|
+
if (tr.kind === 'recurring' && !tr.nextFireAt) armedTr.nextFireAt = new Date(now + (CADENCE_MS[tr.cadence] || CADENCE_MS.weekly)).toISOString();
|
|
130
|
+
await writeTrigger(armedTr).catch(() => {});
|
|
131
|
+
await load();
|
|
132
|
+
}, [writeTrigger, load]);
|
|
133
|
+
|
|
134
|
+
/** Slice 4 auto-detect: if `text` clearly implies a recurring follow-up or a deadline, create a
|
|
135
|
+
* PROPOSED trigger the owner can Arm or Dismiss. Cheap keyword gate first, then one AI call. No-op
|
|
136
|
+
* when nothing clearly recurring/deadline is present, so it never nags. */
|
|
137
|
+
const proposeTriggerFromText = useCallback(async (text) => {
|
|
138
|
+
const s = String(text || '').trim();
|
|
139
|
+
if (!s || !TRIG_HINTS.test(s)) return;
|
|
140
|
+
if (triggers.some((tr) => tr.status === 'proposed' && tr.fromText === s)) return; // no duplicate proposal
|
|
141
|
+
try {
|
|
142
|
+
const sys = `You decide whether a note implies a recurring follow-up or a one-off deadline the Secretary should track. Return ONLY JSON: {"trigger": false} OR {"trigger": true, "kind": "recurring", "label": "short label", "cadence": "daily"|"weekly"|"monthly"} OR {"trigger": true, "kind": "time", "label": "short label", "when": "ISO datetime"}. Say true ONLY if there is a CLEAR recurrence or deadline. Write the label in the user's language.`;
|
|
143
|
+
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: s, systemPrompt: sys, app_id: 'secretary-trigger-detect' }), timeoutMs: 1_800_000, retries: 0 });
|
|
144
|
+
let j = null;
|
|
145
|
+
try { j = extractJson((r && r.data && r.data.content) || ''); } catch { /* ignore */ }
|
|
146
|
+
if (!j || !j.trigger || !j.label) return;
|
|
147
|
+
const tr = { id: genId(), kind: j.kind === 'time' ? 'time' : 'recurring', label: String(j.label).slice(0, 200), contextId: (active && active.id) || '', status: 'proposed', createdBy: 'secretary', fromText: s, createdAt: new Date().toISOString(), action: { kind: 'remind', summary: String(j.label).slice(0, 200) } };
|
|
148
|
+
if (tr.kind === 'time' && j.when) { tr.when = new Date(j.when).toISOString(); tr.nextFireAt = tr.when; }
|
|
149
|
+
else { tr.cadence = ['daily', 'weekly', 'monthly'].includes(j.cadence) ? j.cadence : 'weekly'; }
|
|
150
|
+
await writeTrigger(tr);
|
|
151
|
+
showToast(t('secretary.trig.proposed'));
|
|
152
|
+
await load();
|
|
153
|
+
} catch { /* best-effort — detection never blocks the user */ }
|
|
154
|
+
}, [triggers, active, writeTrigger, load, showToast]);
|
|
155
|
+
|
|
156
|
+
const armed = useMemo(() => triggers.filter((tr) => tr.status === 'armed'), [triggers]);
|
|
157
|
+
const proposed = useMemo(() => triggers.filter((tr) => tr.status === 'proposed'), [triggers]);
|
|
158
|
+
return { triggers, workflows, armed, proposed, form, openForm, closeForm, setField, createTrigger, adding, togglePause, updateTrigger, removeTrigger, armTrigger, proposeTriggerFromText };
|
|
159
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file secretary/use-whats-next.js
|
|
3
|
+
* @description B2 — Routines + "What's next". Generalises the Phase-3c guided plan into the Secretary's
|
|
4
|
+
* forward-driver: it either PROPOSES a new Routine (a named, recurring, multi-step workflow it tracks)
|
|
5
|
+
* from a goal via the owner's OpenRouter key, or ADVANCES an active one — approving each step
|
|
6
|
+
* band-gated (act → run · draft|ask → approve first + log a decision contract · off → skip), executing
|
|
7
|
+
* the automatable part (discover = scout, file/briefing/reminders = file a note into the self-organism),
|
|
8
|
+
* recording the result, and persisting the routine under `secretary.config.contexts[i].routines[]`.
|
|
9
|
+
* Delegation/create steps are proposed + band-gated but their execution is DEFERRED to B4 (marked
|
|
10
|
+
* deferred, never fake-acted). Band routing is the pure `routeRoutineStep` helper (services/secretary-tick),
|
|
11
|
+
* shared with the autonomous tick + asserted in e2e-secretary. Redesign: docs/internal/2026-06-25-secretary-view-redesign.md (B2).
|
|
12
|
+
* @structure ROUTINE_CAPABILITIES · buildRoutinePrompt · useWhatsNext({ active, config, persistConfig, policy, wsList, suggestedWsId, showToast })
|
|
13
|
+
* @usage const next = useWhatsNext({...}); whatsNextCard(next) / routinesCard(next)
|
|
14
|
+
* @version-history
|
|
15
|
+
* v0.6.0 — 2026-06-28 — G7: a delegate step can run an Agent Workflow (chaining specialists) instead of a
|
|
16
|
+
* single agent task — loads the owner's workflows, POST /v1/workflows/:id/run, tracks the runId; checkDelegateResult polls it.
|
|
17
|
+
* v0.5.0 — 2026-06-28 — G4: split FILE_CAPS into NOTE_CAPS (file a note) + FEED_CAPS (briefing/reminders
|
|
18
|
+
* surface in the Home feed), mirroring actionKind() — consistent with the tick + legacy action-loop.
|
|
19
|
+
* v0.4.0 — 2026-06-28 — G2: recurring routines — setRoutineCadence (none/daily/weekly); the tick
|
|
20
|
+
* re-arms a completed routine once its cadence elapses (services/secretary-tick.ts).
|
|
21
|
+
* v0.3.0 — 2026-06-28 — B5: action-items — handle (advance routine / check delegate) + dismiss the
|
|
22
|
+
* follow-ups the tick derived (active context's actionItems[]); delegated steps stay active until done.
|
|
23
|
+
* v0.2.0 — 2026-06-28 — B4: a delegate step creates an agent task for an owner-picked target agent
|
|
24
|
+
* (POST /v1/agents/:name/tasks, queued, parent_task_id = routine id); checkDelegateResult pulls the
|
|
25
|
+
* task status back into the routine. delegate removed from the deferred set.
|
|
26
|
+
* v0.1.0 — 2026-06-27 — B2: Routine entity + "What's next" propose/advance with band-gated, persisted steps.
|
|
27
|
+
*/
|
|
28
|
+
import { useState, useCallback, useMemo, useEffect } from 'preact/hooks';
|
|
29
|
+
import { api, apiGet, apiPost } from '/js/api.js';
|
|
30
|
+
import { t } from '/js/i18n.js';
|
|
31
|
+
import { extractJson, buildDecisionRecord } from '/js/services/secretary-helpers.js';
|
|
32
|
+
import { routeRoutineStep } from '/js/services/secretary-rules.js';
|
|
33
|
+
import { SPECIALIST_CONTRACT } from '/views/secretary/quality.js';
|
|
34
|
+
|
|
35
|
+
/** Capabilities a routine step may use (subset of the policy taxonomy the Secretary can plan with). */
|
|
36
|
+
export const ROUTINE_CAPABILITIES = ['discover', 'file_intake', 'briefing', 'reminders', 'curate_knowledge', 'create_resource', 'delegate'];
|
|
37
|
+
// G4: the capability→kind split MIRRORS actionKind() in services/secretary-tick.ts — keep in sync.
|
|
38
|
+
// NOTE caps file a note into the self-organism; FEED caps (briefing/reminders are nudges, not documents)
|
|
39
|
+
// surface in the Home feed — exactly how the tick + the legacy action-loop treat them.
|
|
40
|
+
const NOTE_CAPS = new Set(['file_intake', 'curate_knowledge']);
|
|
41
|
+
const FEED_CAPS = new Set(['briefing', 'reminders']);
|
|
42
|
+
/** Steps proposed + band-gated but whose execution is still deferred (create lands later; spend/messaging are Enterprise). */
|
|
43
|
+
const DEFERRED_CAPS = new Set(['create_resource', 'resource_invoke', 'third_party_message', 'spend']);
|
|
44
|
+
|
|
45
|
+
function genId(p) { return p + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
|
|
46
|
+
|
|
47
|
+
export function buildRoutinePrompt(goal, contextName) {
|
|
48
|
+
return `In the "${contextName || 'personal'}" context, the user's goal is:
|
|
49
|
+
"""
|
|
50
|
+
${goal}
|
|
51
|
+
"""
|
|
52
|
+
Design a short, repeatable ROUTINE (a named, multi-step workflow the Secretary can run and track) that makes progress on this goal. Output ONLY a JSON object inside a single code block, EXACTLY this shape:
|
|
53
|
+
|
|
54
|
+
\`\`\`json
|
|
55
|
+
{ "title": "short routine name", "purpose": "one line — what this routine achieves", "steps": [ { "capability": "discover", "summary": "what this step does, one sentence" } ] }
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
Rules:
|
|
59
|
+
- 3–6 steps, ordered, concrete and specific to the goal.
|
|
60
|
+
- Each step's "capability" MUST be one of: ${ROUTINE_CAPABILITIES.join(', ')}.
|
|
61
|
+
- Prefer starting with "discover" (scout what already exists) before any "create_resource"/"delegate".
|
|
62
|
+
- Output ONLY the JSON code block — no commentary.`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useWhatsNext({ active, config, persistConfig, policy, wsList, suggestedWsId, showToast }) {
|
|
66
|
+
const [goal, setGoal] = useState('');
|
|
67
|
+
const [proposing, setProposing] = useState(false);
|
|
68
|
+
const [selectedId, setSelectedId] = useState(null); // routine being walked in the card
|
|
69
|
+
const [busyStepId, setBusyStepId] = useState(null);
|
|
70
|
+
const [delegateAgent, setDelegateAgent] = useState(''); // owner's chosen agent target for the next delegate step
|
|
71
|
+
const [delegateWorkflow, setDelegateWorkflow] = useState(''); // G7: owner's chosen workflow to run for a delegate step
|
|
72
|
+
const [checkingStepId, setCheckingStepId] = useState(null);
|
|
73
|
+
const [workflows, setWorkflows] = useState([]); // G7: the owner's Agent Workflows (delegate can run one)
|
|
74
|
+
|
|
75
|
+
// G7: load the owner's workflows so a delegate step can run one (chaining specialists) instead of a single agent task.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
let cancelled = false;
|
|
78
|
+
apiGet('/v1/workflows').then((r) => { if (!cancelled) setWorkflows((r && r.data && r.data.workflows) || []); }).catch(() => {});
|
|
79
|
+
return () => { cancelled = true; };
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const routines = useMemo(() => (active && Array.isArray(active.routines)) ? active.routines : [], [active]);
|
|
83
|
+
const activeRoutines = useMemo(() => routines.filter((r) => r.status === 'active'), [routines]);
|
|
84
|
+
const selected = useMemo(() => routines.find((r) => r.id === selectedId) || null, [routines, selectedId]);
|
|
85
|
+
const bands = useMemo(() => (policy && policy.bands) || {}, [policy]);
|
|
86
|
+
|
|
87
|
+
/** Read-modify-write the active context's routines list into a fresh config and persist. */
|
|
88
|
+
const writeRoutines = useCallback(async (nextRoutines) => {
|
|
89
|
+
if (!active) return;
|
|
90
|
+
const next = { ...config, contexts: (config.contexts || []).map((c) => (c.id === active.id ? { ...c, routines: nextRoutines } : c)) };
|
|
91
|
+
await persistConfig(next);
|
|
92
|
+
}, [active, config, persistConfig]);
|
|
93
|
+
|
|
94
|
+
const patchRoutine = useCallback(async (routineId, updater) => {
|
|
95
|
+
const nextRoutines = routines.map((r) => (r.id === routineId ? updater(r) : r));
|
|
96
|
+
await writeRoutines(nextRoutines);
|
|
97
|
+
}, [routines, writeRoutines]);
|
|
98
|
+
|
|
99
|
+
/** Propose (and save) a NEW routine from the goal — the "What's next: something new" path. */
|
|
100
|
+
const proposeRoutine = useCallback(async () => {
|
|
101
|
+
const g = goal.trim();
|
|
102
|
+
if (!g || !active) return;
|
|
103
|
+
setProposing(true);
|
|
104
|
+
try {
|
|
105
|
+
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: buildRoutinePrompt(g, active.name), app_id: 'secretary-routine' }), timeoutMs: 1_800_000, retries: 0 });
|
|
106
|
+
const json = extractJson((r && r.data && r.data.content) || '');
|
|
107
|
+
if (!json || !Array.isArray(json.steps) || json.steps.length === 0) throw new Error(t('secretary.next.bad'));
|
|
108
|
+
const steps = json.steps.slice(0, 8).map((s) => {
|
|
109
|
+
const capability = ROUTINE_CAPABILITIES.includes(s.capability) ? s.capability : 'file_intake';
|
|
110
|
+
return { id: genId('s'), capability, summary: String(s.summary || '').slice(0, 200), band: (typeof bands[capability] === 'string' ? bands[capability] : 'ask'), status: 'pending', result: null };
|
|
111
|
+
});
|
|
112
|
+
const routine = {
|
|
113
|
+
id: genId('r'), title: String(json.title || g).slice(0, 80), purpose: String(json.purpose || '').slice(0, 200),
|
|
114
|
+
steps, cadence: null, status: 'active', lastRunAt: null, nextRunAt: null,
|
|
115
|
+
results: [], createdBy: 'owner', createdAt: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
await writeRoutines([routine, ...routines]);
|
|
118
|
+
setSelectedId(routine.id);
|
|
119
|
+
setGoal('');
|
|
120
|
+
showToast(t('secretary.next.proposed'));
|
|
121
|
+
} catch (e) {
|
|
122
|
+
showToast(`${t('secretary.next.error')}: ${e.message}`, true);
|
|
123
|
+
} finally {
|
|
124
|
+
setProposing(false);
|
|
125
|
+
}
|
|
126
|
+
}, [goal, active, bands, routines, writeRoutines, showToast]);
|
|
127
|
+
|
|
128
|
+
/** Carry out one step's capability (B2: discover scouts, file caps file a note; B4: delegate creates an agent task). */
|
|
129
|
+
const performStep = useCallback(async (step, opts = {}) => {
|
|
130
|
+
if (step.capability === 'delegate' && opts.workflowId) {
|
|
131
|
+
// G7: run an Agent Workflow (chaining specialists) instead of a single agent task. The run executes
|
|
132
|
+
// server-side over the workflow's steps; the routine tracks its runId (checkDelegateResult polls it).
|
|
133
|
+
const resp = await apiPost(`/v1/workflows/${encodeURIComponent(opts.workflowId)}/run`, { mode: 'full' });
|
|
134
|
+
const runId = resp && resp.data && resp.data.runId;
|
|
135
|
+
return { status: 'delegated', summary: t('secretary.next.resultWorkflow', { wf: opts.workflowName || opts.workflowId }), workflowId: opts.workflowId, runId: runId || null };
|
|
136
|
+
}
|
|
137
|
+
if (step.capability === 'delegate') {
|
|
138
|
+
// B4: hand the step off to one of the owner's other agents as an agent task (queued so it's runnable;
|
|
139
|
+
// parent_task_id links it back to this routine). The owner picked the target when approving.
|
|
140
|
+
const agentName = String(opts.agentName || '').trim();
|
|
141
|
+
if (!agentName) throw new Error(t('secretary.next.delegateNoAgent'));
|
|
142
|
+
const ctxLine = opts.routinePurpose ? `${opts.routinePurpose}\n\n` : '';
|
|
143
|
+
// Specialist parity: every delegated task carries the same quality contract the Secretary works to
|
|
144
|
+
// (grounded, no fabrication, ask via aimeat_dm_ask) + a pointer to where the facts live.
|
|
145
|
+
const grounding = active.organismId
|
|
146
|
+
? `\n\nFacts live in organism ${active.organismId}${wsList.length ? `, workspaces: ${wsList.map((w) => w.name).slice(0, 6).join(', ')}` : ''} — read them with your memory/workspace tools; do not guess.`
|
|
147
|
+
: '';
|
|
148
|
+
const description = `${SPECIALIST_CONTRACT}\n\nTASK: ${ctxLine}${step.summary}${grounding}`.slice(0, 8000);
|
|
149
|
+
const resp = await apiPost(`/v1/agents/${encodeURIComponent(agentName)}/tasks`, {
|
|
150
|
+
title: step.summary.slice(0, 200),
|
|
151
|
+
description,
|
|
152
|
+
status: 'queued',
|
|
153
|
+
parent_task_id: opts.routineId || undefined,
|
|
154
|
+
});
|
|
155
|
+
const taskId = resp && resp.data && resp.data.task && resp.data.task.id;
|
|
156
|
+
return { status: 'delegated', summary: t('secretary.next.resultDelegated', { agent: agentName }), taskId: taskId || null, agentName };
|
|
157
|
+
}
|
|
158
|
+
if (step.capability === 'discover') {
|
|
159
|
+
const params = new URLSearchParams({ scope: 'public', per_page: '10' });
|
|
160
|
+
if (step.summary) params.set('q', step.summary);
|
|
161
|
+
let found = 0;
|
|
162
|
+
try {
|
|
163
|
+
const d = await apiGet('/v1/discover?' + params.toString());
|
|
164
|
+
found = (d && d.data && Array.isArray(d.data.entries)) ? d.data.entries.length : 0;
|
|
165
|
+
} catch { /* discovery is best-effort */ }
|
|
166
|
+
return { status: 'done', summary: t('secretary.next.resultFound', { n: found }) || `Found ${found}` };
|
|
167
|
+
}
|
|
168
|
+
if (NOTE_CAPS.has(step.capability)) {
|
|
169
|
+
const wsId = suggestedWsId || (wsList[0] && wsList[0].id);
|
|
170
|
+
if (active.organismId && wsId) {
|
|
171
|
+
const id = genId('n');
|
|
172
|
+
await apiPost('/v1/memory', {
|
|
173
|
+
key: `organism.${active.organismId}.w.${wsId}.notes.${id}`,
|
|
174
|
+
value: { id, title: step.summary.slice(0, 80), body: step.summary, createdAt: new Date().toISOString(), via: 'secretary-routine' },
|
|
175
|
+
visibility: 'private',
|
|
176
|
+
});
|
|
177
|
+
const w = wsList.find((x) => x.id === wsId);
|
|
178
|
+
return { status: 'done', summary: t('secretary.next.resultFiled', { ws: (w && w.name) || '' }) || `Filed into ${(w && w.name) || ''}` };
|
|
179
|
+
}
|
|
180
|
+
return { status: 'done', summary: t('secretary.next.resultNoted') || 'Noted' };
|
|
181
|
+
}
|
|
182
|
+
if (FEED_CAPS.has(step.capability)) {
|
|
183
|
+
// G4: briefing/reminders surface in the Home feed (newest first, cap 50) — same shape as the tick's appendFeed.
|
|
184
|
+
const r = await apiGet('/v1/memory/secretary.feed').catch(() => null);
|
|
185
|
+
const existing = (r && r.data && r.data.value && Array.isArray(r.data.value.items)) ? r.data.value.items : [];
|
|
186
|
+
const entry = { id: genId('f'), ts: new Date().toISOString(), kind: 'act', contextId: active.id, contextName: active.name || '', text: step.summary };
|
|
187
|
+
await apiPost('/v1/memory', { key: 'secretary.feed', value: { items: [entry, ...existing].slice(0, 50) }, visibility: 'private' });
|
|
188
|
+
return { status: 'done', summary: t('secretary.next.resultSurfaced') || 'Surfaced in the feed' };
|
|
189
|
+
}
|
|
190
|
+
if (DEFERRED_CAPS.has(step.capability)) {
|
|
191
|
+
return { status: 'deferred', summary: t('secretary.next.resultDeferred') || 'Deferred' };
|
|
192
|
+
}
|
|
193
|
+
// Unknown capability → safest is to note it rather than fake-act.
|
|
194
|
+
return { status: 'done', summary: t('secretary.next.resultNoted') || 'Noted' };
|
|
195
|
+
}, [active, wsList, suggestedWsId]);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Approve + run a step, band-gated. Skip (off) marks it skipped without acting; confirm (draft|ask)
|
|
199
|
+
* also logs a decision contract so the learning loop later scores how the routine turned out.
|
|
200
|
+
*/
|
|
201
|
+
const approveStep = useCallback(async (routine, step, opts = {}) => {
|
|
202
|
+
if (!active || busyStepId) return;
|
|
203
|
+
// Band routing via the shared rules module (proven equal to the server's secretary-tick.ts by the G6
|
|
204
|
+
// parity test) — recomputed from the live policy so a band change applies before approval.
|
|
205
|
+
const { disposition } = routeRoutineStep(step, bands);
|
|
206
|
+
setBusyStepId(step.id);
|
|
207
|
+
try {
|
|
208
|
+
let outcome;
|
|
209
|
+
if (disposition === 'skip') {
|
|
210
|
+
outcome = { status: 'skipped', summary: t('secretary.next.resultSkipped') || 'Skipped (off)' };
|
|
211
|
+
} else {
|
|
212
|
+
outcome = await performStep(step, { ...opts, routineId: routine.id, routinePurpose: routine.purpose });
|
|
213
|
+
if (disposition === 'confirm') {
|
|
214
|
+
// The approval is a real choice — log a decision contract (reuses the Phase-5 learning loop).
|
|
215
|
+
try {
|
|
216
|
+
const rec = buildDecisionRecord({
|
|
217
|
+
decision: `${routine.title}: ${step.summary}`.slice(0, 200), options: [],
|
|
218
|
+
chosen: t('secretary.next.decisionChosen') || 'Approved this routine step',
|
|
219
|
+
rationale: routine.purpose || '', expectedOutcome: step.summary, revisitDays: 7, active,
|
|
220
|
+
});
|
|
221
|
+
await apiPost('/v1/memory', { key: `secretary.decision.${rec.id}`, value: rec, visibility: 'private', tags: ['secretary', 'decision', 'open', active.id || ''] });
|
|
222
|
+
} catch { /* decision logging is best-effort */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const ts = new Date().toISOString();
|
|
226
|
+
// Delegated steps carry the agent-task ref (taskId + agent) OR the workflow run ref (G7) for later checking.
|
|
227
|
+
const resultObj = { summary: outcome.summary, ts,
|
|
228
|
+
...(outcome.taskId ? { taskId: outcome.taskId, agentName: outcome.agentName } : {}),
|
|
229
|
+
...(outcome.workflowId ? { workflowId: outcome.workflowId, runId: outcome.runId } : {}) };
|
|
230
|
+
const nextSteps = routine.steps.map((s) => (s.id === step.id ? { ...s, status: outcome.status, result: resultObj } : s));
|
|
231
|
+
// B4/B5 fix: a 'delegated' step is NOT settled (its task is still out) — keep the routine active
|
|
232
|
+
// (on the dashboard) until checkDelegateResult marks it done once the delegated task completes.
|
|
233
|
+
const allSettled = nextSteps.every((s) => s.status !== 'pending' && s.status !== 'running' && s.status !== 'delegated');
|
|
234
|
+
const updater = (r) => ({
|
|
235
|
+
...r, steps: nextSteps, lastRunAt: ts,
|
|
236
|
+
results: outcome.status === 'skipped' ? r.results : [{ ts, summary: outcome.summary }, ...(r.results || [])].slice(0, 20),
|
|
237
|
+
status: allSettled ? 'done' : r.status,
|
|
238
|
+
});
|
|
239
|
+
await patchRoutine(routine.id, updater);
|
|
240
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
241
|
+
} catch (e) {
|
|
242
|
+
showToast(`${t('secretary.next.error')}: ${e.message}`, true);
|
|
243
|
+
} finally {
|
|
244
|
+
setBusyStepId(null);
|
|
245
|
+
}
|
|
246
|
+
}, [active, bands, busyStepId, performStep, patchRoutine, showToast]);
|
|
247
|
+
|
|
248
|
+
const setRoutineStatus = useCallback((routine, status) => patchRoutine(routine.id, (r) => ({ ...r, status })), [patchRoutine]);
|
|
249
|
+
const deleteRoutine = useCallback((routine) => writeRoutines(routines.filter((r) => r.id !== routine.id)), [routines, writeRoutines]);
|
|
250
|
+
// G2: set a routine's cadence (recurrence). The tick re-arms a completed routine once its cadence
|
|
251
|
+
// elapses (services/secretary-tick.ts routineDueForRearm); '' clears it back to run-once.
|
|
252
|
+
const setRoutineCadence = useCallback((routine, cadence) => patchRoutine(routine.id, (r) => ({ ...r, cadence: cadence || null })), [patchRoutine]);
|
|
253
|
+
|
|
254
|
+
/** "Advance" from the dashboard: select the routine + scroll the working card into view. */
|
|
255
|
+
const advance = useCallback((routine) => {
|
|
256
|
+
setSelectedId(routine.id);
|
|
257
|
+
const el = document.querySelector('.sec-next');
|
|
258
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
const nextPendingStep = useCallback((routine) => (routine.steps || []).find((s) => s.status === 'pending') || null, []);
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* B4: pull a delegated step's agent-task status back into the routine (the result flowing home).
|
|
265
|
+
* `closeItemId` (B5) optionally marks an action-item done in the SAME config write — one persist, so a
|
|
266
|
+
* handled "check result" action-item can't clobber the routine update with a stale second write.
|
|
267
|
+
*/
|
|
268
|
+
const checkDelegateResult = useCallback(async (routine, step, closeItemId = null) => {
|
|
269
|
+
const ref = step.result || {};
|
|
270
|
+
const isWorkflow = !!(ref.workflowId && ref.runId);
|
|
271
|
+
if ((!isWorkflow && (!ref.taskId || !ref.agentName)) || checkingStepId) return;
|
|
272
|
+
setCheckingStepId(step.id);
|
|
273
|
+
try {
|
|
274
|
+
let status; let summary;
|
|
275
|
+
if (isWorkflow) {
|
|
276
|
+
// G7: poll the workflow run's outcome.
|
|
277
|
+
const r = await apiGet(`/v1/workflows/${encodeURIComponent(ref.workflowId)}/runs/${encodeURIComponent(ref.runId)}`);
|
|
278
|
+
const run = (r && r.data) || {};
|
|
279
|
+
status = run.status || run.outcome || 'unknown';
|
|
280
|
+
summary = t('secretary.next.resultWorkflowStatus', { wf: ref.workflowId, status });
|
|
281
|
+
} else {
|
|
282
|
+
const r = await apiGet(`/v1/agents/${encodeURIComponent(ref.agentName)}/tasks/${encodeURIComponent(ref.taskId)}`);
|
|
283
|
+
const task = r && r.data && r.data.task;
|
|
284
|
+
status = (task && task.status) || 'unknown';
|
|
285
|
+
summary = t('secretary.next.resultDelegateStatus', { agent: ref.agentName, status });
|
|
286
|
+
}
|
|
287
|
+
const ts = new Date().toISOString();
|
|
288
|
+
// B4/B5/G7: once the delegated task / workflow run reaches a terminal state, settle the step.
|
|
289
|
+
const done = ['completed', 'done', 'closed', 'failed', 'cancelled', 'partial', 'error'].includes(String(status).toLowerCase());
|
|
290
|
+
const next = { ...config, contexts: (config.contexts || []).map((c) => {
|
|
291
|
+
if (!active || c.id !== active.id) return c;
|
|
292
|
+
const nextRoutines = (c.routines || []).map((rt) => {
|
|
293
|
+
if (rt.id !== routine.id) return rt;
|
|
294
|
+
const steps = (rt.steps || []).map((s) => (s.id === step.id ? { ...s, status: done ? 'done' : s.status, result: { ...s.result, summary, checkedAt: ts } } : s));
|
|
295
|
+
const allSettled = steps.every((s) => s.status !== 'pending' && s.status !== 'running' && s.status !== 'delegated');
|
|
296
|
+
return { ...rt, steps, results: [{ ts, summary }, ...(rt.results || [])].slice(0, 20), status: allSettled ? 'done' : rt.status };
|
|
297
|
+
});
|
|
298
|
+
const nextItems = closeItemId ? (c.actionItems || []).map((a) => (a.id === closeItemId ? { ...a, status: 'done' } : a)) : c.actionItems;
|
|
299
|
+
return { ...c, routines: nextRoutines, actionItems: nextItems };
|
|
300
|
+
}) };
|
|
301
|
+
await persistConfig(next);
|
|
302
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
303
|
+
} catch (e) {
|
|
304
|
+
showToast(`${t('secretary.next.error')}: ${e.message}`, true);
|
|
305
|
+
} finally {
|
|
306
|
+
setCheckingStepId(null);
|
|
307
|
+
}
|
|
308
|
+
}, [active, config, persistConfig, checkingStepId, showToast]);
|
|
309
|
+
|
|
310
|
+
// ── B5: action-items (the tick-derived follow-ups) live on the active context; handle/dismiss them ──
|
|
311
|
+
const allActionItems = useMemo(() => (active && Array.isArray(active.actionItems)) ? active.actionItems : [], [active]);
|
|
312
|
+
const actionItems = useMemo(() => allActionItems.filter((a) => a.status === 'open'), [allActionItems]);
|
|
313
|
+
const writeActionItems = useCallback(async (nextItems) => {
|
|
314
|
+
if (!active) return;
|
|
315
|
+
const next = { ...config, contexts: (config.contexts || []).map((c) => (c.id === active.id ? { ...c, actionItems: nextItems } : c)) };
|
|
316
|
+
await persistConfig(next);
|
|
317
|
+
}, [active, config, persistConfig]);
|
|
318
|
+
const dismissActionItem = useCallback((item) => writeActionItems(allActionItems.map((a) => (a.id === item.id ? { ...a, status: 'done' } : a))), [allActionItems, writeActionItems]);
|
|
319
|
+
/** One-click handle: 'advance' opens the routine here (local state) then dismisses the item; 'check-delegate'
|
|
320
|
+
* pulls the task result home AND closes the item in one config write (no stale-config clobber). */
|
|
321
|
+
const handleActionItem = useCallback(async (item) => {
|
|
322
|
+
const sa = item.suggestedAction || {};
|
|
323
|
+
const routine = routines.find((r) => r.id === sa.routineId);
|
|
324
|
+
if (sa.kind === 'check-delegate' && routine) {
|
|
325
|
+
const step = (routine.steps || []).find((s) => s.id === sa.stepId);
|
|
326
|
+
if (step && step.result && step.result.taskId) { await checkDelegateResult(routine, step, item.id); return; }
|
|
327
|
+
}
|
|
328
|
+
if (sa.kind === 'advance' && routine) advance(routine); // local state only — no persist before the dismiss
|
|
329
|
+
await dismissActionItem(item);
|
|
330
|
+
}, [routines, advance, checkDelegateResult, dismissActionItem]);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
goal, setGoal, proposing, proposeRoutine,
|
|
334
|
+
routines, activeRoutines, selected, selectedId, setSelectedId,
|
|
335
|
+
busyStepId, approveStep, setRoutineStatus, deleteRoutine, setRoutineCadence, advance, nextPendingStep,
|
|
336
|
+
delegateAgent, setDelegateAgent, checkingStepId, checkDelegateResult,
|
|
337
|
+
workflows, delegateWorkflow, setDelegateWorkflow,
|
|
338
|
+
actionItems, handleActionItem, dismissActionItem,
|
|
339
|
+
};
|
|
340
|
+
}
|