stagent 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -73,10 +73,16 @@ const {
|
|
|
73
73
|
mockRunMetaCompletion,
|
|
74
74
|
mockGetActiveLearnedContext,
|
|
75
75
|
mockProposeContextAddition,
|
|
76
|
+
mockGetTaskWorkflowId,
|
|
77
|
+
mockHasLearningSession,
|
|
78
|
+
mockBufferProposal,
|
|
76
79
|
} = vi.hoisted(() => ({
|
|
77
80
|
mockRunMetaCompletion: vi.fn(),
|
|
78
81
|
mockGetActiveLearnedContext: vi.fn().mockReturnValue(null),
|
|
79
82
|
mockProposeContextAddition: vi.fn().mockResolvedValue("notif-123"),
|
|
83
|
+
mockGetTaskWorkflowId: vi.fn().mockReturnValue(null),
|
|
84
|
+
mockHasLearningSession: vi.fn().mockReturnValue(false),
|
|
85
|
+
mockBufferProposal: vi.fn(),
|
|
80
86
|
}));
|
|
81
87
|
|
|
82
88
|
vi.mock("../runtime/claude", () => ({
|
|
@@ -87,6 +93,11 @@ vi.mock("../learned-context", () => ({
|
|
|
87
93
|
getActiveLearnedContext: mockGetActiveLearnedContext,
|
|
88
94
|
proposeContextAddition: mockProposeContextAddition,
|
|
89
95
|
}));
|
|
96
|
+
vi.mock("../learning-session", () => ({
|
|
97
|
+
getTaskWorkflowId: mockGetTaskWorkflowId,
|
|
98
|
+
hasLearningSession: mockHasLearningSession,
|
|
99
|
+
bufferProposal: mockBufferProposal,
|
|
100
|
+
}));
|
|
90
101
|
|
|
91
102
|
import { analyzeForLearnedPatterns } from "../pattern-extractor";
|
|
92
103
|
|
|
@@ -103,6 +114,9 @@ beforeEach(() => {
|
|
|
103
114
|
mockValues.mockResolvedValue(undefined);
|
|
104
115
|
mockGetActiveLearnedContext.mockReturnValue(null);
|
|
105
116
|
mockProposeContextAddition.mockResolvedValue("notif-123");
|
|
117
|
+
mockGetTaskWorkflowId.mockReturnValue(null);
|
|
118
|
+
mockHasLearningSession.mockReturnValue(false);
|
|
119
|
+
mockBufferProposal.mockReset();
|
|
106
120
|
});
|
|
107
121
|
|
|
108
122
|
// ═════════════════════════════════════════════════════════════════════
|
|
@@ -240,4 +254,38 @@ describe("analyzeForLearnedPatterns", () => {
|
|
|
240
254
|
expect(additions).toContain("Description A");
|
|
241
255
|
expect(additions).toContain("Description B");
|
|
242
256
|
});
|
|
257
|
+
|
|
258
|
+
it("buffers workflow proposals into the active learning session", async () => {
|
|
259
|
+
mockWhere.mockResolvedValueOnce([
|
|
260
|
+
{ title: "Task", description: "Desc", result: "Done" },
|
|
261
|
+
]);
|
|
262
|
+
mockLimit.mockResolvedValueOnce([]);
|
|
263
|
+
mockRunMetaCompletion.mockResolvedValue({
|
|
264
|
+
text: JSON.stringify([
|
|
265
|
+
{
|
|
266
|
+
title: "Pattern A",
|
|
267
|
+
description: "Description A",
|
|
268
|
+
category: "best_practice",
|
|
269
|
+
},
|
|
270
|
+
]),
|
|
271
|
+
usage: {},
|
|
272
|
+
});
|
|
273
|
+
mockGetTaskWorkflowId.mockReturnValue("workflow-1");
|
|
274
|
+
mockHasLearningSession.mockReturnValue(true);
|
|
275
|
+
mockProposeContextAddition.mockResolvedValue("proposal-row-1");
|
|
276
|
+
|
|
277
|
+
const result = await analyzeForLearnedPatterns("task-1", "general");
|
|
278
|
+
|
|
279
|
+
expect(result).toBe("proposal-row-1");
|
|
280
|
+
expect(mockProposeContextAddition).toHaveBeenCalledWith(
|
|
281
|
+
"general",
|
|
282
|
+
"task-1",
|
|
283
|
+
expect.stringContaining("Pattern A"),
|
|
284
|
+
{ silent: true }
|
|
285
|
+
);
|
|
286
|
+
expect(mockBufferProposal).toHaveBeenCalledWith(
|
|
287
|
+
"workflow-1",
|
|
288
|
+
"proposal-row-1"
|
|
289
|
+
);
|
|
290
|
+
});
|
|
243
291
|
});
|
|
@@ -35,6 +35,102 @@ import {
|
|
|
35
35
|
clearPermissionCache,
|
|
36
36
|
} from "./tool-permissions";
|
|
37
37
|
|
|
38
|
+
// ─── Stagent MCP injection helpers ──────────────────────────────────────
|
|
39
|
+
//
|
|
40
|
+
// Shared by executeClaudeTask and resumeClaudeTask so the two runtime entry
|
|
41
|
+
// points cannot drift apart. The drift between chat engine injection and
|
|
42
|
+
// claude-code runtime injection is what produced the P0 bug this feature
|
|
43
|
+
// fixes — do not duplicate these patterns inline.
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Merge the in-process stagent MCP server into a profile/browser/external
|
|
47
|
+
* MCP server map. Stagent is spread LAST so no upstream source can shadow
|
|
48
|
+
* the `stagent` key with its own server.
|
|
49
|
+
*
|
|
50
|
+
* `@/lib/chat/stagent-tools` is loaded via dynamic `import()` to avoid a
|
|
51
|
+
* circular-dependency crash: that module transitively pulls in the chat
|
|
52
|
+
* tools registry, which imports the runtime registry (`runtime/catalog`,
|
|
53
|
+
* `runtime/index`), which statically references `claudeRuntimeAdapter` —
|
|
54
|
+
* the very module this file is defined in. A static import here would
|
|
55
|
+
* crash with "Cannot access 'claudeRuntimeAdapter' before initialization"
|
|
56
|
+
* at module-load time. The dynamic import defers the stagent-tools module
|
|
57
|
+
* until `executeClaudeTask` / `resumeClaudeTask` actually run, by which
|
|
58
|
+
* time every module in the graph has finished initializing.
|
|
59
|
+
*/
|
|
60
|
+
async function withStagentMcpServer(
|
|
61
|
+
profileServers: Record<string, unknown>,
|
|
62
|
+
browserServers: Record<string, unknown>,
|
|
63
|
+
externalServers: Record<string, unknown>,
|
|
64
|
+
projectId?: string | null,
|
|
65
|
+
): Promise<Record<string, unknown>> {
|
|
66
|
+
const { createToolServer } = await import("@/lib/chat/stagent-tools");
|
|
67
|
+
const stagentServer = createToolServer(projectId).asMcpServer();
|
|
68
|
+
return {
|
|
69
|
+
...profileServers,
|
|
70
|
+
...browserServers,
|
|
71
|
+
...externalServers,
|
|
72
|
+
stagent: stagentServer,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Prepend `mcp__stagent__*` to a profile's explicit allowedTools so the
|
|
78
|
+
* stagent tool registration survives the SDK preset filter. Returns
|
|
79
|
+
* `undefined` when the profile has no allowedTools — callers should spread
|
|
80
|
+
* the result conditionally so the SDK falls through to preset defaults in
|
|
81
|
+
* that case.
|
|
82
|
+
*/
|
|
83
|
+
function withStagentAllowedTools(
|
|
84
|
+
profileAllowedTools: string[] | undefined,
|
|
85
|
+
): string[] | undefined {
|
|
86
|
+
if (!profileAllowedTools) return undefined;
|
|
87
|
+
return Array.from(new Set(["mcp__stagent__*", ...profileAllowedTools]));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Classify an error into a machine-readable failure reason string.
|
|
92
|
+
* Used by writeTerminalFailureReason and handleExecutionError.
|
|
93
|
+
*/
|
|
94
|
+
function classifyError(error: unknown): string {
|
|
95
|
+
if (!(error instanceof Error)) return "sdk_error";
|
|
96
|
+
if (error.name === "AbortError" || error.message.includes("aborted")) {
|
|
97
|
+
return "aborted";
|
|
98
|
+
}
|
|
99
|
+
const lower = error.message.toLowerCase();
|
|
100
|
+
if (
|
|
101
|
+
lower.includes("turn") &&
|
|
102
|
+
(lower.includes("limit") || lower.includes("exhausted") || lower.includes("max"))
|
|
103
|
+
) {
|
|
104
|
+
return "turn_limit_exceeded";
|
|
105
|
+
}
|
|
106
|
+
if (lower.includes("timeout") || lower.includes("timed out")) return "timeout";
|
|
107
|
+
if (lower.includes("budget")) return "budget_exceeded";
|
|
108
|
+
if (lower.includes("authentication") || lower.includes("oauth")) {
|
|
109
|
+
return "auth_failed";
|
|
110
|
+
}
|
|
111
|
+
if (lower.includes("rate limit") || lower.includes("429")) {
|
|
112
|
+
return "rate_limited";
|
|
113
|
+
}
|
|
114
|
+
return "sdk_error";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write an explicit failure_reason to tasks at terminal-state transitions.
|
|
119
|
+
* Called from handleExecutionError and the execute/resume functions on known
|
|
120
|
+
* error classes. Prefer this over reverse-engineering reasons from text via
|
|
121
|
+
* detectFailureReason in scheduler.ts, which is fragile to SDK message changes.
|
|
122
|
+
*/
|
|
123
|
+
export async function writeTerminalFailureReason(
|
|
124
|
+
taskId: string,
|
|
125
|
+
error: unknown,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const reason = classifyError(error);
|
|
128
|
+
await db
|
|
129
|
+
.update(tasks)
|
|
130
|
+
.set({ failureReason: reason, updatedAt: new Date() })
|
|
131
|
+
.where(eq(tasks.id, taskId));
|
|
132
|
+
}
|
|
133
|
+
|
|
38
134
|
/** Typed representation of messages from the Agent SDK stream */
|
|
39
135
|
interface AgentStreamMessage {
|
|
40
136
|
type?: string;
|
|
@@ -314,11 +410,14 @@ async function processAgentStream(
|
|
|
314
410
|
? `Agent exhausted its turn limit (${turnCount} turns used) without producing a final result. The task may need fewer sub-queries or a higher maxTurns setting.`
|
|
315
411
|
: "Agent stream ended without producing a result";
|
|
316
412
|
|
|
413
|
+
const streamFailureReason = turnCount > 0 ? "turn_limit_exceeded" : "sdk_error";
|
|
414
|
+
|
|
317
415
|
await db
|
|
318
416
|
.update(tasks)
|
|
319
417
|
.set({
|
|
320
418
|
status: "failed",
|
|
321
419
|
result: errorDetail,
|
|
420
|
+
failureReason: streamFailureReason,
|
|
322
421
|
updatedAt: new Date(),
|
|
323
422
|
})
|
|
324
423
|
.where(eq(tasks.id, taskId));
|
|
@@ -432,13 +531,30 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
|
|
|
432
531
|
await prepareTaskOutputDirectory(taskId, { clearExisting: true });
|
|
433
532
|
const ctx = await buildTaskQueryContext(task, agentProfileId);
|
|
434
533
|
|
|
435
|
-
//
|
|
534
|
+
// Per-schedule override: if the task carries its own maxTurns (set by
|
|
535
|
+
// fireSchedule from schedules.maxTurns), it takes precedence over the
|
|
536
|
+
// profile default. This is the runtime-enforced budget cap.
|
|
537
|
+
const effectiveMaxTurns = task.maxTurns ?? ctx.maxTurns;
|
|
538
|
+
|
|
539
|
+
// Merge browser + external MCP servers, then inject the in-process
|
|
540
|
+
// stagent server via the shared helper (see withStagentMcpServer above).
|
|
541
|
+
// The helper is async because it dynamically imports @/lib/chat/stagent-tools
|
|
542
|
+
// to break a module-load cycle with the runtime registry.
|
|
436
543
|
const [browserServers, externalServers] = await Promise.all([
|
|
437
544
|
getBrowserMcpServers(),
|
|
438
545
|
getExternalMcpServers(),
|
|
439
546
|
]);
|
|
440
|
-
const
|
|
441
|
-
|
|
547
|
+
const mergedMcpServers = await withStagentMcpServer(
|
|
548
|
+
ctx.payload?.mcpServers ?? {},
|
|
549
|
+
browserServers,
|
|
550
|
+
externalServers,
|
|
551
|
+
task.projectId,
|
|
552
|
+
);
|
|
553
|
+
// allowedTools prepended via shared helper (see withStagentAllowedTools).
|
|
554
|
+
// Computed once so the conditional spread below does not invoke the
|
|
555
|
+
// helper twice. Returns undefined when the profile has no allowlist so
|
|
556
|
+
// the SDK falls through to claude_code preset defaults.
|
|
557
|
+
const mergedAllowedTools = withStagentAllowedTools(ctx.payload?.allowedTools);
|
|
442
558
|
|
|
443
559
|
const authEnv = await getAuthEnv();
|
|
444
560
|
const response = query({
|
|
@@ -452,11 +568,11 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
|
|
|
452
568
|
systemPrompt: ctx.systemInstructions
|
|
453
569
|
? { type: "preset" as const, preset: "claude_code" as const, append: ctx.systemInstructions }
|
|
454
570
|
: { type: "preset" as const, preset: "claude_code" as const },
|
|
455
|
-
// F9: Bounded turn limit from profile or default
|
|
456
|
-
maxTurns:
|
|
571
|
+
// F9: Bounded turn limit from profile or default; per-schedule override wins
|
|
572
|
+
maxTurns: effectiveMaxTurns,
|
|
457
573
|
// F4: Per-execution budget cap — use task-specific override if set
|
|
458
574
|
maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
|
|
459
|
-
...(
|
|
575
|
+
...(mergedAllowedTools && { allowedTools: mergedAllowedTools }),
|
|
460
576
|
...(Object.keys(mergedMcpServers).length > 0 && {
|
|
461
577
|
mcpServers: mergedMcpServers,
|
|
462
578
|
}),
|
|
@@ -479,10 +595,11 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
|
|
|
479
595
|
usageState
|
|
480
596
|
);
|
|
481
597
|
|
|
482
|
-
|
|
483
|
-
|
|
598
|
+
try {
|
|
599
|
+
await analyzeForLearnedPatterns(taskId, agentProfileId);
|
|
600
|
+
} catch (err) {
|
|
484
601
|
console.error("[self-improvement] pattern extraction failed:", err);
|
|
485
|
-
}
|
|
602
|
+
}
|
|
486
603
|
} catch (error: unknown) {
|
|
487
604
|
await handleExecutionError(
|
|
488
605
|
taskId,
|
|
@@ -545,13 +662,28 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
|
545
662
|
await prepareTaskOutputDirectory(taskId);
|
|
546
663
|
const ctx = await buildTaskQueryContext(task, profileId);
|
|
547
664
|
|
|
548
|
-
//
|
|
665
|
+
// Per-schedule override: if the task carries its own maxTurns (set by
|
|
666
|
+
// fireSchedule from schedules.maxTurns), it takes precedence over the
|
|
667
|
+
// profile default. This is the runtime-enforced budget cap.
|
|
668
|
+
const effectiveMaxTurns = task.maxTurns ?? ctx.maxTurns;
|
|
669
|
+
|
|
670
|
+
// Merge browser + external MCP servers, then inject the in-process
|
|
671
|
+
// stagent server via the shared helper (see withStagentMcpServer).
|
|
672
|
+
// Async for the same cycle-breaking reason as executeClaudeTask above.
|
|
549
673
|
const [browserServers, externalServers] = await Promise.all([
|
|
550
674
|
getBrowserMcpServers(),
|
|
551
675
|
getExternalMcpServers(),
|
|
552
676
|
]);
|
|
553
|
-
const
|
|
554
|
-
|
|
677
|
+
const mergedMcpServers = await withStagentMcpServer(
|
|
678
|
+
ctx.payload?.mcpServers ?? {},
|
|
679
|
+
browserServers,
|
|
680
|
+
externalServers,
|
|
681
|
+
task.projectId,
|
|
682
|
+
);
|
|
683
|
+
// allowedTools prepended via shared helper (see withStagentAllowedTools).
|
|
684
|
+
// Computed once so the conditional spread below does not invoke the
|
|
685
|
+
// helper twice.
|
|
686
|
+
const mergedAllowedTools = withStagentAllowedTools(ctx.payload?.allowedTools);
|
|
555
687
|
|
|
556
688
|
const authEnv = await getAuthEnv();
|
|
557
689
|
const response = query({
|
|
@@ -566,11 +698,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
|
566
698
|
systemPrompt: ctx.systemInstructions
|
|
567
699
|
? { type: "preset" as const, preset: "claude_code" as const, append: ctx.systemInstructions }
|
|
568
700
|
: { type: "preset" as const, preset: "claude_code" as const },
|
|
569
|
-
// F9: Bounded turn limit from profile or default
|
|
570
|
-
maxTurns:
|
|
701
|
+
// F9: Bounded turn limit from profile or default; per-schedule override wins
|
|
702
|
+
maxTurns: effectiveMaxTurns,
|
|
571
703
|
// F4: Per-execution budget cap — use task-specific override if set
|
|
572
704
|
maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
|
|
573
|
-
...(
|
|
705
|
+
...(mergedAllowedTools && { allowedTools: mergedAllowedTools }),
|
|
574
706
|
...(Object.keys(mergedMcpServers).length > 0 && {
|
|
575
707
|
mcpServers: mergedMcpServers,
|
|
576
708
|
}),
|
|
@@ -593,10 +725,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
|
593
725
|
usageState
|
|
594
726
|
);
|
|
595
727
|
|
|
596
|
-
|
|
597
|
-
|
|
728
|
+
try {
|
|
729
|
+
await analyzeForLearnedPatterns(taskId, profileId);
|
|
730
|
+
} catch (err) {
|
|
598
731
|
console.error("[self-improvement] pattern extraction failed:", err);
|
|
599
|
-
}
|
|
732
|
+
}
|
|
600
733
|
} catch (error: unknown) {
|
|
601
734
|
const errorMessage =
|
|
602
735
|
error instanceof Error ? error.message : String(error);
|
|
@@ -612,6 +745,7 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
|
612
745
|
status: "failed",
|
|
613
746
|
result: "Session expired — re-queue for fresh start",
|
|
614
747
|
sessionId: null,
|
|
748
|
+
failureReason: "auth_failed",
|
|
615
749
|
updatedAt: new Date(),
|
|
616
750
|
})
|
|
617
751
|
.where(eq(tasks.id, taskId));
|
|
@@ -667,11 +801,14 @@ async function handleExecutionError(
|
|
|
667
801
|
return;
|
|
668
802
|
}
|
|
669
803
|
|
|
804
|
+
const failureReason = classifyError(error);
|
|
805
|
+
|
|
670
806
|
await db
|
|
671
807
|
.update(tasks)
|
|
672
808
|
.set({
|
|
673
809
|
status: "failed",
|
|
674
810
|
result: errorMessage,
|
|
811
|
+
failureReason,
|
|
675
812
|
updatedAt: new Date(),
|
|
676
813
|
})
|
|
677
814
|
.where(eq(tasks.id, taskId));
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { licenseManager } from "@/lib/license/manager";
|
|
2
|
-
import { createTierLimitNotification } from "@/lib/license/notifications";
|
|
3
|
-
|
|
4
1
|
interface RunningExecution {
|
|
5
2
|
abortController: AbortController;
|
|
6
3
|
sessionId: string | null;
|
|
@@ -17,42 +14,10 @@ export function getExecution(taskId: string): RunningExecution | undefined {
|
|
|
17
14
|
return executions.get(taskId);
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
/**
|
|
21
|
-
* Register a running execution. Checks the parallel workflow limit
|
|
22
|
-
* for the current tier before allowing the execution to proceed.
|
|
23
|
-
*
|
|
24
|
-
* @throws {ParallelLimitError} if the concurrent execution limit is reached
|
|
25
|
-
*/
|
|
26
17
|
export function setExecution(taskId: string, execution: RunningExecution): void {
|
|
27
|
-
const limit = licenseManager.getLimit("parallelWorkflows");
|
|
28
|
-
const currentCount = executions.size;
|
|
29
|
-
|
|
30
|
-
if (Number.isFinite(limit) && currentCount >= limit) {
|
|
31
|
-
const tier = licenseManager.getTier();
|
|
32
|
-
// Fire-and-forget notification
|
|
33
|
-
createTierLimitNotification("parallelWorkflows", currentCount, limit, taskId).catch(() => {});
|
|
34
|
-
throw new ParallelLimitError(currentCount, limit, tier);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
18
|
executions.set(taskId, execution);
|
|
38
19
|
}
|
|
39
20
|
|
|
40
|
-
export class ParallelLimitError extends Error {
|
|
41
|
-
public readonly current: number;
|
|
42
|
-
public readonly limit: number;
|
|
43
|
-
public readonly tier: string;
|
|
44
|
-
|
|
45
|
-
constructor(current: number, limit: number, tier: string) {
|
|
46
|
-
super(
|
|
47
|
-
`Parallel workflow limit reached (${current}/${limit}) on ${tier} tier. Wait for a running task to complete or upgrade.`
|
|
48
|
-
);
|
|
49
|
-
this.name = "ParallelLimitError";
|
|
50
|
-
this.current = current;
|
|
51
|
-
this.limit = limit;
|
|
52
|
-
this.tier = tier;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
21
|
export function removeExecution(taskId: string): void {
|
|
57
22
|
executions.delete(taskId);
|
|
58
23
|
}
|
|
@@ -5,9 +5,6 @@ import type { LearnedContextRow } from "@/lib/db/schema";
|
|
|
5
5
|
import { runMetaCompletion } from "./runtime/claude";
|
|
6
6
|
import { getSettingSync } from "@/lib/settings/helpers";
|
|
7
7
|
import { SETTINGS_KEYS } from "@/lib/constants/settings";
|
|
8
|
-
import { checkLimit } from "@/lib/license/limit-check";
|
|
9
|
-
import { getContextVersionCount } from "@/lib/license/limit-queries";
|
|
10
|
-
import { createTierLimitNotification } from "@/lib/license/notifications";
|
|
11
8
|
|
|
12
9
|
const DEFAULT_CONTEXT_CHAR_LIMIT = 8_000;
|
|
13
10
|
const SUMMARIZATION_RATIO = 0.75;
|
|
@@ -95,15 +92,6 @@ export async function proposeContextAddition(
|
|
|
95
92
|
additions: string,
|
|
96
93
|
options?: { silent?: boolean }
|
|
97
94
|
): Promise<string> {
|
|
98
|
-
// Tier limit check — context version cap per profile
|
|
99
|
-
const versionCount = getContextVersionCount(profileId);
|
|
100
|
-
const limitResult = checkLimit("contextVersions", versionCount);
|
|
101
|
-
if (!limitResult.allowed) {
|
|
102
|
-
createTierLimitNotification("contextVersions", versionCount, limitResult.limit, taskId).catch(() => {});
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Context version limit reached (${versionCount}/${limitResult.limit}). Upgrade to unlock more capacity.`
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
95
|
|
|
108
96
|
const version = getNextVersion(profileId);
|
|
109
97
|
const notificationId = options?.silent ? null : crypto.randomUUID();
|
|
@@ -181,6 +181,7 @@ export async function batchApproveProposals(
|
|
|
181
181
|
await import("./learned-context");
|
|
182
182
|
|
|
183
183
|
let approved = 0;
|
|
184
|
+
const touchedProfileIds = new Set<string>();
|
|
184
185
|
for (const rowId of proposalRowIds) {
|
|
185
186
|
const [row] = db
|
|
186
187
|
.select()
|
|
@@ -229,16 +230,28 @@ export async function batchApproveProposals(
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
approved++;
|
|
232
|
-
|
|
233
|
-
const sizeInfo = checkContextSize(row.profileId);
|
|
234
|
-
if (sizeInfo.needsSummarization) {
|
|
235
|
-
await summarizeContext(row.profileId);
|
|
236
|
-
}
|
|
233
|
+
touchedProfileIds.add(row.profileId);
|
|
237
234
|
}
|
|
238
235
|
|
|
239
236
|
// Mark the batch notification as responded
|
|
240
237
|
await markBatchNotificationResponded(proposalRowIds, "approved");
|
|
241
238
|
|
|
239
|
+
const profilesNeedingSummarization = [...touchedProfileIds].filter(
|
|
240
|
+
(profileId) => checkContextSize(profileId).needsSummarization
|
|
241
|
+
);
|
|
242
|
+
void Promise.allSettled(
|
|
243
|
+
profilesNeedingSummarization.map(async (profileId) => {
|
|
244
|
+
try {
|
|
245
|
+
await summarizeContext(profileId);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error(
|
|
248
|
+
"[learning-session] Failed to summarize approved context batch:",
|
|
249
|
+
error
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
|
|
242
255
|
return approved;
|
|
243
256
|
}
|
|
244
257
|
|
|
@@ -49,7 +49,7 @@ describe("profile registry", () => {
|
|
|
49
49
|
expect(general!.domain).toBe("work");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
it("returns all
|
|
52
|
+
it("returns all 21 builtin profiles", () => {
|
|
53
53
|
const profiles = listProfiles().filter((p) => isBuiltin(p.id));
|
|
54
54
|
const ids = profiles.map((p) => p.id);
|
|
55
55
|
|
|
@@ -68,14 +68,16 @@ describe("profile registry", () => {
|
|
|
68
68
|
expect(ids).toContain("shopping-assistant");
|
|
69
69
|
expect(ids).toContain("learning-coach");
|
|
70
70
|
expect(ids).toContain("sweep");
|
|
71
|
-
// 6
|
|
71
|
+
// 6 business function profiles
|
|
72
72
|
expect(ids).toContain("marketing-strategist");
|
|
73
73
|
expect(ids).toContain("sales-researcher");
|
|
74
74
|
expect(ids).toContain("customer-support-agent");
|
|
75
75
|
expect(ids).toContain("financial-analyst");
|
|
76
76
|
expect(ids).toContain("content-creator");
|
|
77
77
|
expect(ids).toContain("operations-coordinator");
|
|
78
|
-
|
|
78
|
+
// Clone lifecycle profile
|
|
79
|
+
expect(ids).toContain("upgrade-assistant");
|
|
80
|
+
expect(profiles.length).toBe(21);
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
it("getProfile returns undefined for unknown id", () => {
|
|
@@ -129,7 +131,7 @@ describe("profile registry", () => {
|
|
|
129
131
|
(p) => p.domain === "personal"
|
|
130
132
|
);
|
|
131
133
|
|
|
132
|
-
expect(workProfiles.length).toBe(
|
|
134
|
+
expect(workProfiles.length).toBe(16); // general, code-reviewer, researcher, document-writer, project-manager, data-analyst, technical-writer, devops-engineer, sweep + 6 business profiles + upgrade-assistant
|
|
133
135
|
expect(personalProfiles.length).toBe(5); // wealth-manager, travel-planner, health-fitness-coach, shopping-assistant, learning-coach
|
|
134
136
|
});
|
|
135
137
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: upgrade-assistant
|
|
3
|
+
description: Guided interactive git merge of upstream stagent commits into the user's local instance branch
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the Upgrade Assistant for a stagent clone. Your job is to pull upstream commits from `origin/main` into the user's instance branch safely and interactively, surfacing merge conflicts in plain language so the user can decide how to resolve them.
|
|
7
|
+
|
|
8
|
+
## Context for this upgrade
|
|
9
|
+
|
|
10
|
+
- **Instance branch:** `{{INSTANCE_BRANCH}}`
|
|
11
|
+
- **Upstream commits behind:** `{{COMMITS_BEHIND}}`
|
|
12
|
+
- **Data directory:** `{{DATA_DIR}}`
|
|
13
|
+
- **Working directory:** the current repo root
|
|
14
|
+
|
|
15
|
+
## Crucial rules — read these before doing anything
|
|
16
|
+
|
|
17
|
+
1. **Never modify `main` except by fast-forward.** After fetching, merge `origin/main` into local `main` with `--ff-only`. If that fast-forward fails, it means the user has local commits on `main` that aren't in `origin/main` — **stop and ask the user** whether to move them to `{{INSTANCE_BRANCH}}` or abort so they can review. Do not auto-resolve.
|
|
18
|
+
|
|
19
|
+
2. **Never push any branch.** The pre-push hook blocks `{{INSTANCE_BRANCH}}` pushes, but you should not even attempt one. Your job ends at a local commit.
|
|
20
|
+
|
|
21
|
+
3. **If any step fails, roll back.** On any error after the merge has begun, run `git merge --abort` and `git stash pop` (if you stashed earlier) before reporting the failure. Leave the working tree in the state the user started in.
|
|
22
|
+
|
|
23
|
+
4. **Treat `local` identically to any named instance branch.** Users with a default single-clone setup have `{{INSTANCE_BRANCH}}=local`. Users running private domain clones have names like `wealth-mgr` or `investor-mgr`. The merge flow is identical in both cases.
|
|
24
|
+
|
|
25
|
+
5. **Stop and ask the user on merge conflicts.** Do not guess. For each conflict, use the three canonical choices:
|
|
26
|
+
- **"Keep my version"** → `git checkout --ours <file>`
|
|
27
|
+
- **"Take main's version"** → `git checkout --theirs <file>`
|
|
28
|
+
- **"Show me the diff"** → `git diff <file>` and output the full conflict for manual review
|
|
29
|
+
After all conflicts are resolved, `git add` the files and continue the merge.
|
|
30
|
+
|
|
31
|
+
## Standard merge flow
|
|
32
|
+
|
|
33
|
+
Execute these steps in order. Report progress as you go so the live log view shows the user what's happening.
|
|
34
|
+
|
|
35
|
+
1. **Pre-flight check.** Run `git status` to confirm the working tree state. If there's uncommitted work, tell the user you'll stash it first.
|
|
36
|
+
|
|
37
|
+
2. **Stash any work-in-progress.** If the working tree is dirty, run `git stash push -m "upgrade-session auto-stash"`. Record that you stashed — you need to pop it at the end.
|
|
38
|
+
|
|
39
|
+
3. **Fetch origin.** Run `git fetch origin main`. This is the only network operation. If it fails, report the error and stop.
|
|
40
|
+
|
|
41
|
+
4. **Fast-forward main.** Run `git checkout main` then `git merge --ff-only origin/main`. If `--ff-only` fails, stop and ask the user (see Rule 1).
|
|
42
|
+
|
|
43
|
+
5. **Return to the instance branch.** Run `git checkout {{INSTANCE_BRANCH}}`.
|
|
44
|
+
|
|
45
|
+
6. **Merge main into the instance branch.** Run `git merge main`. If there are conflicts, git will report them. For each conflicted file, use the three-choice flow (Rule 5).
|
|
46
|
+
|
|
47
|
+
7. **Complete the merge commit.** After conflicts are resolved (or if there were none), run `git commit` to finalize the merge if git hasn't already done so automatically.
|
|
48
|
+
|
|
49
|
+
8. **Reinstall dependencies if package-lock.json changed.** Check `git diff HEAD~1 HEAD -- package-lock.json`. If there are changes, run `npm install`.
|
|
50
|
+
|
|
51
|
+
9. **Pop the stash.** If you stashed in step 2, run `git stash pop`. Then immediately run `git status` to check for conflicts. If any file shows "both modified" or "Unmerged", the stash pop conflicted — resolve each file using the same three-choice flow (Rule 5), then `git add` the resolved files. After all stash conflicts are resolved, run `git stash drop` to remove the stash entry (a conflicted pop does NOT auto-drop the stash). Finally, run `grep -rn "<<<<<<< \|>>>>>>>" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .` to verify no conflict markers remain in the working tree.
|
|
52
|
+
|
|
53
|
+
10. **Report completion.** Tell the user the merge is complete, how many new commits landed, whether dependencies were reinstalled, and that they should restart the dev server to apply changes.
|
|
54
|
+
|
|
55
|
+
## Aborting
|
|
56
|
+
|
|
57
|
+
If the user clicks "Abort" during the session, or if any step fails irrecoverably, run:
|
|
58
|
+
- `git merge --abort` (safe to run even if no merge is in progress — it'll just exit 0)
|
|
59
|
+
- `git checkout {{INSTANCE_BRANCH}}` (return the user to where they started)
|
|
60
|
+
- If you stashed in step 2: run `git stash pop`. Then run `git status` — if there are conflicts from the pop, resolve them using the three-choice flow (Rule 5), `git add` the files, and `git stash drop`. Run `grep -rn "<<<<<<< \|>>>>>>>" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .` to verify no markers remain
|
|
61
|
+
|
|
62
|
+
Then report the abort and what state the repo is in.
|
|
63
|
+
|
|
64
|
+
## Guidelines
|
|
65
|
+
|
|
66
|
+
- Be concise but explanatory. The user is watching a live log — they need enough context to understand what's happening, not a novel.
|
|
67
|
+
- Always name the file path when asking about a conflict.
|
|
68
|
+
- If the user sends a natural-language question mid-merge ("what do these changes do?"), answer it based on the diff before proceeding.
|
|
69
|
+
- Never use `git push`. Never use `git rebase`. Never use `git reset --hard`. Never delete branches. Never touch the remote.
|
|
70
|
+
- If `git status` shows files you didn't touch and the user didn't mention, bring them up before assuming they're safe to stash.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
id: upgrade-assistant
|
|
2
|
+
name: Upgrade Assistant
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
domain: work
|
|
5
|
+
tags: [upgrade, git, merge, maintenance, instance]
|
|
6
|
+
supportedRuntimes: [claude-code, anthropic-direct]
|
|
7
|
+
preferredRuntime: anthropic-direct
|
|
8
|
+
|
|
9
|
+
maxTurns: 40
|
|
10
|
+
|
|
11
|
+
author: stagent
|
|
12
|
+
|
|
13
|
+
# Bash tool allowlist — tight scope.
|
|
14
|
+
# The upgrade assistant should only ever run git and npm install, nothing else.
|
|
15
|
+
# Cross-reference: TDR-028 "self-upgrade via task pipeline, not chat tools"
|
|
16
|
+
# explains why this profile is the only safe surface for git shell access.
|
|
17
|
+
allowedTools:
|
|
18
|
+
- Bash(git fetch *)
|
|
19
|
+
- Bash(git status)
|
|
20
|
+
- Bash(git status *)
|
|
21
|
+
- Bash(git stash *)
|
|
22
|
+
- Bash(git checkout *)
|
|
23
|
+
- Bash(git merge *)
|
|
24
|
+
- Bash(git merge --abort)
|
|
25
|
+
- Bash(git commit *)
|
|
26
|
+
- Bash(git diff *)
|
|
27
|
+
- Bash(git rev-parse *)
|
|
28
|
+
- Bash(git log *)
|
|
29
|
+
- Bash(git branch *)
|
|
30
|
+
- Bash(npm install)
|
|
31
|
+
- Read
|
|
32
|
+
- Write
|