instar 0.7.24 → 0.7.26
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/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/cli.js +0 -0
- package/dist/commands/init.js +14 -9
- package/dist/commands/server.js +14 -3
- package/dist/core/AutoDispatcher.d.ts +100 -0
- package/dist/core/AutoDispatcher.js +267 -0
- package/dist/core/DispatchExecutor.d.ts +127 -0
- package/dist/core/DispatchExecutor.js +346 -0
- package/dist/core/DispatchManager.d.ts +1 -1
- package/dist/core/SessionManager.js +8 -6
- package/dist/core/types.d.ts +1 -1
- package/dist/scaffold/templates.js +92 -32
- package/dist/scheduler/JobScheduler.js +10 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +25 -1
- package/package.json +1 -1
- package/skills/instar-feedback/SKILL.md +49 -8
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
> Why do I have a folder named ".vercel" in my project?
|
|
2
|
+
The ".vercel" folder is created when you link a directory to a Vercel project.
|
|
3
|
+
|
|
4
|
+
> What does the "project.json" file contain?
|
|
5
|
+
The "project.json" file contains:
|
|
6
|
+
- The ID of the Vercel project that you linked ("projectId")
|
|
7
|
+
- The ID of the user or team your Vercel project is owned by ("orgId")
|
|
8
|
+
|
|
9
|
+
> Should I commit the ".vercel" folder?
|
|
10
|
+
No, you should not share the ".vercel" folder with anyone.
|
|
11
|
+
Upon creation, it will be automatically added to your ".gitignore" file.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"projectId":"prj_evM5LcItYL3IAmw8zNvEPGrHeaya","orgId":"team_dHctwIDcV3X9ydapQlCPHFGI","projectName":"claude-agent-kit"}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/commands/init.js
CHANGED
|
@@ -1027,16 +1027,16 @@ function getDefaultJobs(port) {
|
|
|
1027
1027
|
{
|
|
1028
1028
|
slug: 'dispatch-check',
|
|
1029
1029
|
name: 'Dispatch Check',
|
|
1030
|
-
description: '
|
|
1030
|
+
description: 'Legacy dispatch check job — disabled because the built-in AutoDispatcher handles polling, evaluation, and execution automatically. See GET /dispatches/auto for status.',
|
|
1031
1031
|
schedule: '*/30 * * * *',
|
|
1032
1032
|
priority: 'medium',
|
|
1033
1033
|
expectedDurationMinutes: 2,
|
|
1034
1034
|
model: 'haiku',
|
|
1035
|
-
enabled:
|
|
1035
|
+
enabled: false,
|
|
1036
1036
|
gate: `curl -sf http://localhost:${port}/dispatches 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('newCount',0) > 0 else 1)"`,
|
|
1037
1037
|
execute: {
|
|
1038
|
-
type: '
|
|
1039
|
-
value: `
|
|
1038
|
+
type: 'script',
|
|
1039
|
+
value: `curl -s http://localhost:${port}/dispatches/auto`,
|
|
1040
1040
|
},
|
|
1041
1041
|
tags: ['coherence', 'default'],
|
|
1042
1042
|
},
|
|
@@ -1052,20 +1052,25 @@ function getDefaultJobs(port) {
|
|
|
1052
1052
|
gate: `curl -sf http://localhost:${port}/health >/dev/null 2>&1`,
|
|
1053
1053
|
execute: {
|
|
1054
1054
|
type: 'prompt',
|
|
1055
|
-
value: `You are your own QA team. Scan for issues with your instar infrastructure and submit feedback for anything wrong.
|
|
1055
|
+
value: `You are your own QA team. Scan for issues with your instar infrastructure and submit feedback for anything wrong.
|
|
1056
|
+
|
|
1057
|
+
FIRST: Read your auth token for API calls:
|
|
1058
|
+
AUTH=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null)
|
|
1059
|
+
|
|
1060
|
+
Then check each area:
|
|
1056
1061
|
|
|
1057
|
-
1. **Server health**: curl -s http://localhost:${port}/health — is it responding? Are all fields present?
|
|
1062
|
+
1. **Server health**: curl -s -H "Authorization: Bearer $AUTH" http://localhost:${port}/health — is it responding? Are all fields present? Is status "ok" or "degraded"?
|
|
1058
1063
|
2. **State files**: Check .instar/state/ — are JSON files parseable? Any empty or corrupted? Try: for f in .instar/state/*.json; do python3 -c "import json; json.load(open('$f'))" 2>&1 || echo "CORRUPT: $f"; done
|
|
1059
1064
|
3. **Hook files**: Do all hooks in .instar/hooks/ exist and have execute permissions? ls -la .instar/hooks/
|
|
1060
|
-
4. **Job execution**: curl -s http://localhost:${port}/jobs — are any jobs failing repeatedly? Check lastRun and lastError fields.
|
|
1061
|
-
5. **Quota**: curl -s http://localhost:${port}/quota — is usage approaching limits?
|
|
1065
|
+
4. **Job execution**: curl -s -H "Authorization: Bearer $AUTH" http://localhost:${port}/jobs — are any jobs failing repeatedly? Check lastRun and lastError fields.
|
|
1066
|
+
5. **Quota**: curl -s -H "Authorization: Bearer $AUTH" http://localhost:${port}/quota — is usage approaching limits?
|
|
1062
1067
|
6. **Logs**: Check .instar/logs/server.log for recent errors: tail -50 .instar/logs/server.log | grep -i error
|
|
1063
1068
|
7. **Settings coherence**: Are hooks in .claude/settings.json pointing to files that exist?
|
|
1064
1069
|
8. **Design friction**: During your recent work, did anything feel unnecessarily difficult, confusing, or broken? Did you work around any issues?
|
|
1065
1070
|
9. **CI health**: Check if the project has a GitHub repo and if CI is passing. Run: REPO=$(git remote get-url origin 2>/dev/null | sed 's/.*github.com[:/]//;s/.git$//'); if [ -n "$REPO" ]; then FAILURES=$(gh run list --repo "$REPO" --status failure --limit 3 --json databaseId,conclusion,headBranch,name,createdAt 2>/dev/null); if echo "$FAILURES" | python3 -c "import sys,json; runs=json.load(sys.stdin); exit(0 if runs else 1)" 2>/dev/null; then echo "CI FAILURES DETECTED in $REPO"; echo "$FAILURES"; echo ""; echo "FIX THESE NOW: Read the failure logs with 'gh run view RUN_ID --repo $REPO --log-failed', diagnose the issue, fix it, run tests locally, commit and push."; fi; fi
|
|
1066
1071
|
|
|
1067
1072
|
For EACH issue found, submit feedback immediately:
|
|
1068
|
-
curl -s -X POST http://localhost:${port}/feedback -H 'Content-Type: application/json' -d '{"type":"bug","title":"TITLE","description":"FULL_CONTEXT"}'
|
|
1073
|
+
curl -s -X POST http://localhost:${port}/feedback -H "Authorization: Bearer $AUTH" -H 'Content-Type: application/json' -d '{"type":"bug","title":"TITLE","description":"FULL_CONTEXT"}'
|
|
1069
1074
|
|
|
1070
1075
|
For improvements (not bugs), use type "improvement" instead.
|
|
1071
1076
|
|
package/dist/commands/server.js
CHANGED
|
@@ -24,6 +24,8 @@ import { FeedbackManager } from '../core/FeedbackManager.js';
|
|
|
24
24
|
import { DispatchManager } from '../core/DispatchManager.js';
|
|
25
25
|
import { UpdateChecker } from '../core/UpdateChecker.js';
|
|
26
26
|
import { AutoUpdater } from '../core/AutoUpdater.js';
|
|
27
|
+
import { AutoDispatcher } from '../core/AutoDispatcher.js';
|
|
28
|
+
import { DispatchExecutor } from '../core/DispatchExecutor.js';
|
|
27
29
|
import { registerPort, unregisterPort, startHeartbeat } from '../core/PortRegistry.js';
|
|
28
30
|
import { TelegraphService } from '../publishing/TelegraphService.js';
|
|
29
31
|
import { PrivateViewer } from '../publishing/PrivateViewer.js';
|
|
@@ -444,14 +446,22 @@ export async function startServer(options) {
|
|
|
444
446
|
});
|
|
445
447
|
console.log(pc.green(' Feedback loop enabled'));
|
|
446
448
|
}
|
|
447
|
-
// Set up dispatch system
|
|
449
|
+
// Set up dispatch system with auto-dispatcher
|
|
448
450
|
let dispatches;
|
|
451
|
+
let autoDispatcher;
|
|
449
452
|
if (config.dispatches) {
|
|
450
453
|
dispatches = new DispatchManager({
|
|
451
454
|
...config.dispatches,
|
|
452
455
|
version: config.version,
|
|
453
456
|
});
|
|
454
|
-
|
|
457
|
+
const dispatchExecutor = new DispatchExecutor(config.projectDir, sessionManager);
|
|
458
|
+
autoDispatcher = new AutoDispatcher(dispatches, dispatchExecutor, state, config.stateDir, {
|
|
459
|
+
pollIntervalMinutes: 30,
|
|
460
|
+
autoApplyPassive: config.dispatches.autoApply ?? true,
|
|
461
|
+
autoExecuteActions: true,
|
|
462
|
+
}, telegram);
|
|
463
|
+
autoDispatcher.start();
|
|
464
|
+
console.log(pc.green(' Dispatch system enabled (auto-polling active)'));
|
|
455
465
|
}
|
|
456
466
|
const updateChecker = new UpdateChecker({
|
|
457
467
|
stateDir: config.stateDir,
|
|
@@ -510,7 +520,7 @@ export async function startServer(options) {
|
|
|
510
520
|
...(config.evolution || {}),
|
|
511
521
|
});
|
|
512
522
|
console.log(pc.green(' Evolution system enabled'));
|
|
513
|
-
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, quotaTracker, publisher, viewer, tunnel, evolution });
|
|
523
|
+
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, publisher, viewer, tunnel, evolution });
|
|
514
524
|
await server.start();
|
|
515
525
|
// Start tunnel AFTER server is listening
|
|
516
526
|
if (tunnel) {
|
|
@@ -527,6 +537,7 @@ export async function startServer(options) {
|
|
|
527
537
|
const shutdown = async () => {
|
|
528
538
|
console.log('\nShutting down...');
|
|
529
539
|
autoUpdater.stop();
|
|
540
|
+
autoDispatcher?.stop();
|
|
530
541
|
if (tunnel)
|
|
531
542
|
await tunnel.stop();
|
|
532
543
|
stopHeartbeat();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto Dispatcher — built-in periodic dispatch polling and execution.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the server process (no Claude session needed for most
|
|
5
|
+
* dispatches). Periodically polls the Portal API for new intelligence
|
|
6
|
+
* dispatches, processes them based on type:
|
|
7
|
+
*
|
|
8
|
+
* - lesson/strategy: Auto-applied to context file (passive)
|
|
9
|
+
* - configuration: Executed programmatically via DispatchExecutor
|
|
10
|
+
* - action: Executed programmatically or agentically via DispatchExecutor
|
|
11
|
+
* - behavioral: Applied to context file (passive)
|
|
12
|
+
* - security: Never auto-applied (requires agent review)
|
|
13
|
+
*
|
|
14
|
+
* This replaces the heavyweight prompt-based dispatch-check job.
|
|
15
|
+
* Dispatches are the intelligent layer that complements npm updates —
|
|
16
|
+
* they tell agents HOW to update themselves beyond just code changes.
|
|
17
|
+
*
|
|
18
|
+
* The full update cycle:
|
|
19
|
+
* 1. Agent sends feedback (FeedbackManager)
|
|
20
|
+
* 2. Dawn fixes the issue
|
|
21
|
+
* 3. Dawn publishes npm update (code) + dispatch (instructions)
|
|
22
|
+
* 4. AutoUpdater applies npm update
|
|
23
|
+
* 5. AutoDispatcher applies dispatch instructions
|
|
24
|
+
* 6. Agent is fully updated — code AND behavior
|
|
25
|
+
*/
|
|
26
|
+
import type { DispatchManager } from './DispatchManager.js';
|
|
27
|
+
import type { DispatchExecutor } from './DispatchExecutor.js';
|
|
28
|
+
import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
|
|
29
|
+
import type { StateManager } from './StateManager.js';
|
|
30
|
+
export interface AutoDispatcherConfig {
|
|
31
|
+
/** How often to poll for dispatches, in minutes. Default: 30 */
|
|
32
|
+
pollIntervalMinutes?: number;
|
|
33
|
+
/** Whether to auto-apply safe dispatches (lesson, strategy). Default: true */
|
|
34
|
+
autoApplyPassive?: boolean;
|
|
35
|
+
/** Whether to auto-execute action/configuration dispatches. Default: true */
|
|
36
|
+
autoExecuteActions?: boolean;
|
|
37
|
+
/** Telegram topic ID for notifications (uses Agent Attention if not set) */
|
|
38
|
+
notificationTopicId?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface AutoDispatcherStatus {
|
|
41
|
+
running: boolean;
|
|
42
|
+
lastPoll: string | null;
|
|
43
|
+
lastExecution: string | null;
|
|
44
|
+
config: Required<AutoDispatcherConfig>;
|
|
45
|
+
pendingDispatches: number;
|
|
46
|
+
executedDispatches: number;
|
|
47
|
+
lastError: string | null;
|
|
48
|
+
}
|
|
49
|
+
export declare class AutoDispatcher {
|
|
50
|
+
private dispatches;
|
|
51
|
+
private executor;
|
|
52
|
+
private telegram;
|
|
53
|
+
private state;
|
|
54
|
+
private config;
|
|
55
|
+
private interval;
|
|
56
|
+
private stateFile;
|
|
57
|
+
private lastPoll;
|
|
58
|
+
private lastExecution;
|
|
59
|
+
private executedCount;
|
|
60
|
+
private lastError;
|
|
61
|
+
private isProcessing;
|
|
62
|
+
constructor(dispatches: DispatchManager, executor: DispatchExecutor, state: StateManager, stateDir: string, config?: AutoDispatcherConfig, telegram?: TelegramAdapter | null);
|
|
63
|
+
/**
|
|
64
|
+
* Start periodic dispatch polling.
|
|
65
|
+
* Idempotent — calling start() when already running is a no-op.
|
|
66
|
+
*/
|
|
67
|
+
start(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Stop polling.
|
|
70
|
+
*/
|
|
71
|
+
stop(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Get current status.
|
|
74
|
+
*/
|
|
75
|
+
getStatus(): AutoDispatcherStatus;
|
|
76
|
+
/**
|
|
77
|
+
* Set Telegram adapter (may be wired after construction).
|
|
78
|
+
*/
|
|
79
|
+
setTelegram(telegram: TelegramAdapter): void;
|
|
80
|
+
/**
|
|
81
|
+
* One tick of the dispatch loop.
|
|
82
|
+
*/
|
|
83
|
+
private tick;
|
|
84
|
+
/**
|
|
85
|
+
* Execute a single action/configuration dispatch.
|
|
86
|
+
*/
|
|
87
|
+
private executeDispatch;
|
|
88
|
+
/**
|
|
89
|
+
* Record the result of executing a dispatch.
|
|
90
|
+
*/
|
|
91
|
+
private recordResult;
|
|
92
|
+
/**
|
|
93
|
+
* Send notification via Telegram.
|
|
94
|
+
*/
|
|
95
|
+
private notify;
|
|
96
|
+
private getAttentionTopicId;
|
|
97
|
+
private loadState;
|
|
98
|
+
private saveState;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=AutoDispatcher.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto Dispatcher — built-in periodic dispatch polling and execution.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the server process (no Claude session needed for most
|
|
5
|
+
* dispatches). Periodically polls the Portal API for new intelligence
|
|
6
|
+
* dispatches, processes them based on type:
|
|
7
|
+
*
|
|
8
|
+
* - lesson/strategy: Auto-applied to context file (passive)
|
|
9
|
+
* - configuration: Executed programmatically via DispatchExecutor
|
|
10
|
+
* - action: Executed programmatically or agentically via DispatchExecutor
|
|
11
|
+
* - behavioral: Applied to context file (passive)
|
|
12
|
+
* - security: Never auto-applied (requires agent review)
|
|
13
|
+
*
|
|
14
|
+
* This replaces the heavyweight prompt-based dispatch-check job.
|
|
15
|
+
* Dispatches are the intelligent layer that complements npm updates —
|
|
16
|
+
* they tell agents HOW to update themselves beyond just code changes.
|
|
17
|
+
*
|
|
18
|
+
* The full update cycle:
|
|
19
|
+
* 1. Agent sends feedback (FeedbackManager)
|
|
20
|
+
* 2. Dawn fixes the issue
|
|
21
|
+
* 3. Dawn publishes npm update (code) + dispatch (instructions)
|
|
22
|
+
* 4. AutoUpdater applies npm update
|
|
23
|
+
* 5. AutoDispatcher applies dispatch instructions
|
|
24
|
+
* 6. Agent is fully updated — code AND behavior
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
export class AutoDispatcher {
|
|
29
|
+
dispatches;
|
|
30
|
+
executor;
|
|
31
|
+
telegram;
|
|
32
|
+
state;
|
|
33
|
+
config;
|
|
34
|
+
interval = null;
|
|
35
|
+
stateFile;
|
|
36
|
+
// Persisted state
|
|
37
|
+
lastPoll = null;
|
|
38
|
+
lastExecution = null;
|
|
39
|
+
executedCount = 0;
|
|
40
|
+
lastError = null;
|
|
41
|
+
isProcessing = false;
|
|
42
|
+
constructor(dispatches, executor, state, stateDir, config, telegram) {
|
|
43
|
+
this.dispatches = dispatches;
|
|
44
|
+
this.executor = executor;
|
|
45
|
+
this.state = state;
|
|
46
|
+
this.telegram = telegram ?? null;
|
|
47
|
+
this.stateFile = path.join(stateDir, 'state', 'auto-dispatcher.json');
|
|
48
|
+
this.config = {
|
|
49
|
+
pollIntervalMinutes: config?.pollIntervalMinutes ?? 30,
|
|
50
|
+
autoApplyPassive: config?.autoApplyPassive ?? true,
|
|
51
|
+
autoExecuteActions: config?.autoExecuteActions ?? true,
|
|
52
|
+
notificationTopicId: config?.notificationTopicId ?? 0,
|
|
53
|
+
};
|
|
54
|
+
this.loadState();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start periodic dispatch polling.
|
|
58
|
+
* Idempotent — calling start() when already running is a no-op.
|
|
59
|
+
*/
|
|
60
|
+
start() {
|
|
61
|
+
if (this.interval)
|
|
62
|
+
return;
|
|
63
|
+
const intervalMs = this.config.pollIntervalMinutes * 60 * 1000;
|
|
64
|
+
console.log(`[AutoDispatcher] Started (every ${this.config.pollIntervalMinutes}m, ` +
|
|
65
|
+
`passive: ${this.config.autoApplyPassive}, actions: ${this.config.autoExecuteActions})`);
|
|
66
|
+
// First poll after a short delay
|
|
67
|
+
setTimeout(() => this.tick(), 15_000);
|
|
68
|
+
// Then poll periodically
|
|
69
|
+
this.interval = setInterval(() => this.tick(), intervalMs);
|
|
70
|
+
this.interval.unref();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Stop polling.
|
|
74
|
+
*/
|
|
75
|
+
stop() {
|
|
76
|
+
if (this.interval) {
|
|
77
|
+
clearInterval(this.interval);
|
|
78
|
+
this.interval = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get current status.
|
|
83
|
+
*/
|
|
84
|
+
getStatus() {
|
|
85
|
+
return {
|
|
86
|
+
running: this.interval !== null,
|
|
87
|
+
lastPoll: this.lastPoll,
|
|
88
|
+
lastExecution: this.lastExecution,
|
|
89
|
+
config: { ...this.config },
|
|
90
|
+
pendingDispatches: this.dispatches.pending().length,
|
|
91
|
+
executedDispatches: this.executedCount,
|
|
92
|
+
lastError: this.lastError,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set Telegram adapter (may be wired after construction).
|
|
97
|
+
*/
|
|
98
|
+
setTelegram(telegram) {
|
|
99
|
+
this.telegram = telegram;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One tick of the dispatch loop.
|
|
103
|
+
*/
|
|
104
|
+
async tick() {
|
|
105
|
+
if (this.isProcessing) {
|
|
106
|
+
console.log('[AutoDispatcher] Skipping tick — already processing');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
this.isProcessing = true;
|
|
111
|
+
// Step 1: Poll for new dispatches
|
|
112
|
+
const result = this.config.autoApplyPassive
|
|
113
|
+
? await this.dispatches.checkAndAutoApply()
|
|
114
|
+
: await this.dispatches.check();
|
|
115
|
+
this.lastPoll = new Date().toISOString();
|
|
116
|
+
this.lastError = null;
|
|
117
|
+
if (result.error) {
|
|
118
|
+
this.lastError = result.error;
|
|
119
|
+
this.saveState();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Report passive auto-applications
|
|
123
|
+
if (result.autoApplied && result.autoApplied > 0) {
|
|
124
|
+
console.log(`[AutoDispatcher] Auto-applied ${result.autoApplied} passive dispatch(es)`);
|
|
125
|
+
await this.notify(`Applied ${result.autoApplied} intelligence dispatch(es) to context.\n` +
|
|
126
|
+
result.dispatches
|
|
127
|
+
.filter(d => d.applied)
|
|
128
|
+
.map(d => ` - ${d.title} (${d.type})`)
|
|
129
|
+
.join('\n'));
|
|
130
|
+
}
|
|
131
|
+
if (result.newCount === 0) {
|
|
132
|
+
this.saveState();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`[AutoDispatcher] ${result.newCount} new dispatch(es) received`);
|
|
136
|
+
// Step 2: Process action and configuration dispatches
|
|
137
|
+
if (this.config.autoExecuteActions) {
|
|
138
|
+
const actionDispatches = result.dispatches.filter(d => (d.type === 'action' || d.type === 'configuration') && !d.applied);
|
|
139
|
+
for (const dispatch of actionDispatches) {
|
|
140
|
+
await this.executeDispatch(dispatch);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Step 3: Notify about any remaining unapplied dispatches
|
|
144
|
+
const remaining = this.dispatches.pending();
|
|
145
|
+
if (remaining.length > 0) {
|
|
146
|
+
const securityDispatches = remaining.filter(d => d.type === 'security');
|
|
147
|
+
if (securityDispatches.length > 0) {
|
|
148
|
+
await this.notify(`⚠️ ${securityDispatches.length} security dispatch(es) require manual review:\n` +
|
|
149
|
+
securityDispatches.map(d => ` - ${d.title}`).join('\n'));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.saveState();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
156
|
+
this.saveState();
|
|
157
|
+
console.error(`[AutoDispatcher] Tick error: ${this.lastError}`);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
this.isProcessing = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Execute a single action/configuration dispatch.
|
|
165
|
+
*/
|
|
166
|
+
async executeDispatch(dispatch) {
|
|
167
|
+
console.log(`[AutoDispatcher] Executing dispatch: ${dispatch.title} (${dispatch.type})`);
|
|
168
|
+
// Try to parse as structured action
|
|
169
|
+
const action = this.executor.parseAction(dispatch.content);
|
|
170
|
+
if (!action) {
|
|
171
|
+
// Not structured JSON — treat as agentic prompt
|
|
172
|
+
console.log(`[AutoDispatcher] Dispatch is not structured — spawning agentic session`);
|
|
173
|
+
const agenticAction = {
|
|
174
|
+
description: dispatch.title,
|
|
175
|
+
steps: [{ type: 'agentic', prompt: dispatch.content }],
|
|
176
|
+
};
|
|
177
|
+
const result = await this.executor.execute(agenticAction);
|
|
178
|
+
await this.recordResult(dispatch, result);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Execute structured action
|
|
182
|
+
const result = await this.executor.execute(action);
|
|
183
|
+
await this.recordResult(dispatch, result);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Record the result of executing a dispatch.
|
|
187
|
+
*/
|
|
188
|
+
async recordResult(dispatch, result) {
|
|
189
|
+
if (result.success) {
|
|
190
|
+
this.dispatches.evaluate(dispatch.dispatchId, 'accepted', result.message);
|
|
191
|
+
this.executedCount++;
|
|
192
|
+
this.lastExecution = new Date().toISOString();
|
|
193
|
+
console.log(`[AutoDispatcher] Dispatch executed successfully: ${dispatch.title}`);
|
|
194
|
+
await this.notify(`Executed dispatch: ${dispatch.title}\n` +
|
|
195
|
+
`${result.completedSteps}/${result.totalSteps} steps completed` +
|
|
196
|
+
(result.verified ? ' (verified)' : ''));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.error(`[AutoDispatcher] Dispatch execution failed: ${result.message}`);
|
|
200
|
+
// Don't reject — mark as deferred so it can be retried
|
|
201
|
+
this.dispatches.evaluate(dispatch.dispatchId, 'deferred', `Auto-execution failed: ${result.message}. ${result.rolledBack ? 'Rolled back.' : 'Manual intervention may be needed.'}`);
|
|
202
|
+
await this.notify(`⚠️ Dispatch execution failed: ${dispatch.title}\n` +
|
|
203
|
+
`${result.message}` +
|
|
204
|
+
(result.rolledBack ? '\nChanges were rolled back.' : ''));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Send notification via Telegram.
|
|
209
|
+
*/
|
|
210
|
+
async notify(message) {
|
|
211
|
+
const formatted = `📡 *Intelligence Dispatch*\n\n${message}`;
|
|
212
|
+
if (this.telegram) {
|
|
213
|
+
try {
|
|
214
|
+
const topicId = this.config.notificationTopicId || this.getAttentionTopicId();
|
|
215
|
+
if (topicId) {
|
|
216
|
+
await this.telegram.sendToTopic(topicId, formatted);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error(`[AutoDispatcher] Telegram notification failed: ${err}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
console.log(`[AutoDispatcher] Notification: ${message}`);
|
|
225
|
+
}
|
|
226
|
+
getAttentionTopicId() {
|
|
227
|
+
return this.state.get('agent-attention-topic') ?? 0;
|
|
228
|
+
}
|
|
229
|
+
// ── State persistence ──────────────────────────────────────────────
|
|
230
|
+
loadState() {
|
|
231
|
+
try {
|
|
232
|
+
if (fs.existsSync(this.stateFile)) {
|
|
233
|
+
const data = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
|
|
234
|
+
this.lastPoll = data.lastPoll ?? null;
|
|
235
|
+
this.lastExecution = data.lastExecution ?? null;
|
|
236
|
+
this.executedCount = data.executedCount ?? 0;
|
|
237
|
+
this.lastError = data.lastError ?? null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Start fresh
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
saveState() {
|
|
245
|
+
const dir = path.dirname(this.stateFile);
|
|
246
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
247
|
+
const data = {
|
|
248
|
+
lastPoll: this.lastPoll,
|
|
249
|
+
lastExecution: this.lastExecution,
|
|
250
|
+
executedCount: this.executedCount,
|
|
251
|
+
lastError: this.lastError,
|
|
252
|
+
savedAt: new Date().toISOString(),
|
|
253
|
+
};
|
|
254
|
+
const tmpPath = this.stateFile + `.${process.pid}.tmp`;
|
|
255
|
+
try {
|
|
256
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
257
|
+
fs.renameSync(tmpPath, this.stateFile);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
try {
|
|
261
|
+
fs.unlinkSync(tmpPath);
|
|
262
|
+
}
|
|
263
|
+
catch { /* ignore */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=AutoDispatcher.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch Executor — executes action dispatches programmatically and agentically.
|
|
3
|
+
*
|
|
4
|
+
* Two layers of execution:
|
|
5
|
+
*
|
|
6
|
+
* Layer 1 (Programmatic): Structured actions in JSON — shell commands, file
|
|
7
|
+
* operations, config merges. Executed mechanically without Claude.
|
|
8
|
+
*
|
|
9
|
+
* Layer 2 (Agentic): Complex instructions that require interpretation.
|
|
10
|
+
* Spawns a lightweight Claude session to execute them.
|
|
11
|
+
*
|
|
12
|
+
* Action dispatch content format:
|
|
13
|
+
* The dispatch `content` field contains a JSON object with:
|
|
14
|
+
* - description: Human-readable explanation of what this action does
|
|
15
|
+
* - steps: Array of action steps to execute in order
|
|
16
|
+
* - verify: Optional verification command (must exit 0 for success)
|
|
17
|
+
* - rollback: Optional array of steps to undo on failure
|
|
18
|
+
* - conditions: Optional preconditions (version, file existence, etc.)
|
|
19
|
+
*
|
|
20
|
+
* Step types:
|
|
21
|
+
* - { type: "shell", command: string } — run a shell command
|
|
22
|
+
* - { type: "file_write", path: string, content: string } — write a file
|
|
23
|
+
* - { type: "file_patch", path: string, find: string, replace: string } — search/replace
|
|
24
|
+
* - { type: "config_merge", path: string, merge: object } — deep merge into JSON config
|
|
25
|
+
* - { type: "agentic", prompt: string } — spawn Claude to handle complex logic
|
|
26
|
+
*
|
|
27
|
+
* Security:
|
|
28
|
+
* - Shell commands are run in the project directory with a 60s timeout
|
|
29
|
+
* - File paths are resolved relative to the project directory
|
|
30
|
+
* - Path traversal (../) is rejected
|
|
31
|
+
* - Destructive commands (rm -rf, etc.) are blocked
|
|
32
|
+
*/
|
|
33
|
+
import type { SessionManager } from './SessionManager.js';
|
|
34
|
+
export interface ActionStep {
|
|
35
|
+
type: 'shell' | 'file_write' | 'file_patch' | 'config_merge' | 'agentic';
|
|
36
|
+
/** Shell command to run */
|
|
37
|
+
command?: string;
|
|
38
|
+
/** File path (relative to project dir) */
|
|
39
|
+
path?: string;
|
|
40
|
+
/** Content for file_write, or replacement string for file_patch */
|
|
41
|
+
content?: string;
|
|
42
|
+
/** Search string for file_patch */
|
|
43
|
+
find?: string;
|
|
44
|
+
/** Replacement string for file_patch */
|
|
45
|
+
replace?: string;
|
|
46
|
+
/** JSON object to deep-merge for config_merge */
|
|
47
|
+
merge?: Record<string, unknown>;
|
|
48
|
+
/** Prompt for agentic execution */
|
|
49
|
+
prompt?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ActionPayload {
|
|
52
|
+
/** Human-readable description */
|
|
53
|
+
description: string;
|
|
54
|
+
/** Steps to execute in order */
|
|
55
|
+
steps: ActionStep[];
|
|
56
|
+
/** Optional verification command (must exit 0) */
|
|
57
|
+
verify?: string;
|
|
58
|
+
/** Optional rollback steps on failure */
|
|
59
|
+
rollback?: ActionStep[];
|
|
60
|
+
/** Optional preconditions */
|
|
61
|
+
conditions?: {
|
|
62
|
+
minVersion?: string;
|
|
63
|
+
maxVersion?: string;
|
|
64
|
+
fileExists?: string;
|
|
65
|
+
fileNotExists?: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export interface ExecutionResult {
|
|
69
|
+
success: boolean;
|
|
70
|
+
/** Which steps completed successfully */
|
|
71
|
+
completedSteps: number;
|
|
72
|
+
/** Total steps attempted */
|
|
73
|
+
totalSteps: number;
|
|
74
|
+
/** Human-readable summary */
|
|
75
|
+
message: string;
|
|
76
|
+
/** Output from each step */
|
|
77
|
+
stepResults: StepResult[];
|
|
78
|
+
/** Whether verification passed */
|
|
79
|
+
verified: boolean;
|
|
80
|
+
/** Whether rollback was attempted */
|
|
81
|
+
rolledBack: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface StepResult {
|
|
84
|
+
step: number;
|
|
85
|
+
type: string;
|
|
86
|
+
success: boolean;
|
|
87
|
+
output?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
}
|
|
90
|
+
export declare class DispatchExecutor {
|
|
91
|
+
private projectDir;
|
|
92
|
+
private sessionManager;
|
|
93
|
+
constructor(projectDir: string, sessionManager?: SessionManager | null);
|
|
94
|
+
/**
|
|
95
|
+
* Parse an action payload from dispatch content.
|
|
96
|
+
* Returns null if the content is not valid action JSON.
|
|
97
|
+
*/
|
|
98
|
+
parseAction(content: string): ActionPayload | null;
|
|
99
|
+
/**
|
|
100
|
+
* Execute an action dispatch.
|
|
101
|
+
*
|
|
102
|
+
* 1. Check preconditions
|
|
103
|
+
* 2. Execute steps in order
|
|
104
|
+
* 3. Verify success
|
|
105
|
+
* 4. Rollback on failure (if rollback steps provided)
|
|
106
|
+
*/
|
|
107
|
+
execute(payload: ActionPayload): Promise<ExecutionResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Execute a single step.
|
|
110
|
+
*/
|
|
111
|
+
private executeStep;
|
|
112
|
+
private runShell;
|
|
113
|
+
private writeFile;
|
|
114
|
+
private patchFile;
|
|
115
|
+
private mergeConfig;
|
|
116
|
+
private runAgentic;
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a path relative to the project directory.
|
|
119
|
+
* Returns null if the path escapes the project dir.
|
|
120
|
+
*/
|
|
121
|
+
private resolvePath;
|
|
122
|
+
/**
|
|
123
|
+
* Check preconditions for an action dispatch.
|
|
124
|
+
*/
|
|
125
|
+
private checkConditions;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=DispatchExecutor.d.ts.map
|