@runfusion/fusion 0.7.1 → 0.8.1
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/bin.js +3737 -1373
- package/dist/client/assets/{AgentDetailView-DyzuiJas.js → AgentDetailView-CLzxf6Z7.js} +3 -3
- package/dist/client/assets/{AgentDetailView-C1_lTTET.css → AgentDetailView-DIBOY8V-.css} +1 -1
- package/dist/client/assets/{AgentsView-CgweOTe6.js → AgentsView-CXaYJX_G.js} +3 -3
- package/dist/client/assets/{ChatView-DrY8FMIt.js → ChatView-iXxGAaN1.js} +1 -1
- package/dist/client/assets/DevServerView-BeXfFkF4.js +1 -0
- package/dist/client/assets/{DirectoryPicker-D5KQ-im_.js → DirectoryPicker-BMn5fjn9.js} +1 -1
- package/dist/client/assets/{DocumentsView-D2wK7FYJ.js → DocumentsView-CjrtI3TX.js} +1 -1
- package/dist/client/assets/{InsightsView-DfY3sa1j.js → InsightsView-BkfQ-TV1.js} +1 -1
- package/dist/client/assets/MemoryView-1G0zWu1i.js +2 -0
- package/dist/client/assets/{NodesView-g26-j7rg.js → NodesView-Bn_1R73N.js} +1 -1
- package/dist/client/assets/{NodesView-BYVG2yY-.css → NodesView-DCoS6iYh.css} +1 -1
- package/dist/client/assets/{PiExtensionsManager-DfMr3Gls.js → PiExtensionsManager-CqGOtQnR.js} +3 -3
- package/dist/client/assets/{PluginManager-DiMOD-Kj.js → PluginManager-CM5QGvSG.js} +1 -1
- package/dist/client/assets/{RoadmapsView-DJC4F4CD.js → RoadmapsView-B4VnQP83.js} +1 -1
- package/dist/client/assets/SettingsModal-BiLA-BeG.js +31 -0
- package/dist/client/assets/{SettingsModal-Cx3iMWDs.js → SettingsModal-C3LckzfT.js} +1 -1
- package/dist/client/assets/SettingsModal-D5hLoLXp.css +1 -0
- package/dist/client/assets/SetupWizardModal-Bk_8HfLm.js +1 -0
- package/dist/client/assets/SetupWizardModal-DRF5fOoR.css +1 -0
- package/dist/client/assets/{SkillsView-DTB2cmXQ.js → SkillsView-CRvqF8P1.js} +1 -1
- package/dist/client/assets/{TodoView-CyxdHUdz.js → TodoView-Vzui5Eha.js} +2 -2
- package/dist/client/assets/{folder-open-C3zB1vmh.js → folder-open-CMF89prE.js} +1 -1
- package/dist/client/assets/index-B8kH5y4Q.js +656 -0
- package/dist/client/assets/index-D2fXOwWF.css +1 -0
- package/dist/client/assets/{list-checks-CK3_6p5e.js → list-checks-M95d1uAy.js} +1 -1
- package/dist/client/assets/{star-BQhDgM9V.js → star-DHhJD6ow.js} +1 -1
- package/dist/client/assets/{upload-DDdZveEJ.js → upload-CEq8jic8.js} +1 -1
- package/dist/client/assets/{users-DWWgd19M.js → users-CUA8Tv-d.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/extension.js +2403 -471
- package/dist/pi-claude-cli/index.ts +27 -0
- package/dist/pi-claude-cli/package.json +4 -5
- package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +31 -9
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +122 -8
- package/dist/pi-claude-cli/src/process-manager.ts +25 -7
- package/dist/pi-claude-cli/src/provider.ts +31 -7
- package/dist/pi-claude-cli/src/types/cross-spawn.d.ts +7 -0
- package/package.json +2 -6
- package/skill/fusion/references/extension-tools.md +1 -0
- package/dist/client/assets/DevServerView-fvjo36sF.js +0 -6
- package/dist/client/assets/MemoryView-CyAQgXwO.js +0 -2
- package/dist/client/assets/SettingsModal-BnekMOV2.css +0 -1
- package/dist/client/assets/SettingsModal-DjVE27r5.js +0 -31
- package/dist/client/assets/SetupWizardModal-BMa6p24b.css +0 -1
- package/dist/client/assets/SetupWizardModal-Cow6woq6.js +0 -1
- package/dist/client/assets/index-Belw0PQt.css +0 -1
- package/dist/client/assets/index-DJDWSrju.js +0 -646
package/dist/extension.js
CHANGED
|
@@ -37,7 +37,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
37
37
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
38
38
|
mod
|
|
39
39
|
));
|
|
40
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
41
40
|
|
|
42
41
|
// ../core/src/settings-schema.ts
|
|
43
42
|
function isGlobalSettingsKey(key) {
|
|
@@ -61,8 +60,13 @@ var init_settings_schema = __esm({
|
|
|
61
60
|
ntfyEnabled: false,
|
|
62
61
|
ntfyTopic: void 0,
|
|
63
62
|
ntfyBaseUrl: void 0,
|
|
64
|
-
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "planning-awaiting-input"],
|
|
63
|
+
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "planning-awaiting-input", "gridlock"],
|
|
65
64
|
ntfyDashboardHost: void 0,
|
|
65
|
+
webhookEnabled: false,
|
|
66
|
+
webhookUrl: void 0,
|
|
67
|
+
webhookFormat: "generic",
|
|
68
|
+
webhookEvents: [],
|
|
69
|
+
notificationProviders: [],
|
|
66
70
|
defaultProjectId: void 0,
|
|
67
71
|
setupComplete: void 0,
|
|
68
72
|
favoriteProviders: void 0,
|
|
@@ -115,6 +119,7 @@ var init_settings_schema = __esm({
|
|
|
115
119
|
pushAfterMerge: false,
|
|
116
120
|
pushRemote: "origin",
|
|
117
121
|
unavailableNodePolicy: "block",
|
|
122
|
+
defaultNodeId: void 0,
|
|
118
123
|
worktreeInitCommand: void 0,
|
|
119
124
|
testCommand: void 0,
|
|
120
125
|
buildCommand: void 0,
|
|
@@ -145,6 +150,9 @@ var init_settings_schema = __esm({
|
|
|
145
150
|
smartConflictResolution: true,
|
|
146
151
|
worktreeRebaseBeforeMerge: true,
|
|
147
152
|
worktreeRebaseRemote: "",
|
|
153
|
+
worktreeRebaseLocalBase: true,
|
|
154
|
+
mergeConflictStrategy: "smart-prefer-main",
|
|
155
|
+
workflowStepTimeoutMs: 36e4,
|
|
148
156
|
strictScopeEnforcement: false,
|
|
149
157
|
buildRetryCount: 0,
|
|
150
158
|
verificationFixRetries: 3,
|
|
@@ -210,7 +218,7 @@ var init_settings_schema = __esm({
|
|
|
210
218
|
},
|
|
211
219
|
cloudflare: {
|
|
212
220
|
enabled: false,
|
|
213
|
-
quickTunnel:
|
|
221
|
+
quickTunnel: true,
|
|
214
222
|
tunnelName: "",
|
|
215
223
|
tunnelToken: null,
|
|
216
224
|
ingressUrl: ""
|
|
@@ -728,7 +736,132 @@ var init_error_message = __esm({
|
|
|
728
736
|
}
|
|
729
737
|
});
|
|
730
738
|
|
|
739
|
+
// ../core/src/model-resolution.ts
|
|
740
|
+
function hasCompleteModelPair(pair) {
|
|
741
|
+
return Boolean(pair?.provider && pair?.modelId);
|
|
742
|
+
}
|
|
743
|
+
function pickFirstModelPair(...pairs) {
|
|
744
|
+
for (const pair of pairs) {
|
|
745
|
+
if (hasCompleteModelPair(pair)) {
|
|
746
|
+
return { provider: pair.provider, modelId: pair.modelId };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return {};
|
|
750
|
+
}
|
|
751
|
+
function resolveProjectDefaultModel(settings) {
|
|
752
|
+
return pickFirstModelPair(
|
|
753
|
+
{
|
|
754
|
+
provider: settings?.defaultProviderOverride,
|
|
755
|
+
modelId: settings?.defaultModelIdOverride
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
provider: settings?.defaultProvider,
|
|
759
|
+
modelId: settings?.defaultModelId
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
function resolveExecutionSettingsModel(settings) {
|
|
764
|
+
return pickFirstModelPair(
|
|
765
|
+
{
|
|
766
|
+
provider: settings?.executionProvider,
|
|
767
|
+
modelId: settings?.executionModelId
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
provider: settings?.executionGlobalProvider,
|
|
771
|
+
modelId: settings?.executionGlobalModelId
|
|
772
|
+
},
|
|
773
|
+
resolveProjectDefaultModel(settings)
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
function resolvePlanningSettingsModel(settings) {
|
|
777
|
+
return pickFirstModelPair(
|
|
778
|
+
{
|
|
779
|
+
provider: settings?.planningProvider,
|
|
780
|
+
modelId: settings?.planningModelId
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
provider: settings?.planningGlobalProvider,
|
|
784
|
+
modelId: settings?.planningGlobalModelId
|
|
785
|
+
},
|
|
786
|
+
resolveProjectDefaultModel(settings)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
function resolveValidatorSettingsModel(settings) {
|
|
790
|
+
return pickFirstModelPair(
|
|
791
|
+
{
|
|
792
|
+
provider: settings?.validatorProvider,
|
|
793
|
+
modelId: settings?.validatorModelId
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
provider: settings?.validatorGlobalProvider,
|
|
797
|
+
modelId: settings?.validatorGlobalModelId
|
|
798
|
+
},
|
|
799
|
+
resolveProjectDefaultModel(settings)
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
function resolveTitleSummarizerSettingsModel(settings) {
|
|
803
|
+
return pickFirstModelPair(
|
|
804
|
+
{
|
|
805
|
+
provider: settings?.titleSummarizerProvider,
|
|
806
|
+
modelId: settings?.titleSummarizerModelId
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
provider: settings?.titleSummarizerGlobalProvider,
|
|
810
|
+
modelId: settings?.titleSummarizerGlobalModelId
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
provider: settings?.planningProvider,
|
|
814
|
+
modelId: settings?.planningModelId
|
|
815
|
+
},
|
|
816
|
+
resolveProjectDefaultModel(settings)
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
function resolveTaskExecutionModel(task, settings) {
|
|
820
|
+
return pickFirstModelPair(
|
|
821
|
+
{
|
|
822
|
+
provider: task.modelProvider,
|
|
823
|
+
modelId: task.modelId
|
|
824
|
+
},
|
|
825
|
+
resolveExecutionSettingsModel(settings)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
function resolveTaskValidatorModel(task, settings) {
|
|
829
|
+
return pickFirstModelPair(
|
|
830
|
+
{
|
|
831
|
+
provider: task.validatorModelProvider,
|
|
832
|
+
modelId: task.validatorModelId
|
|
833
|
+
},
|
|
834
|
+
resolveValidatorSettingsModel(settings)
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function resolveTaskPlanningModel(task, settings) {
|
|
838
|
+
return pickFirstModelPair(
|
|
839
|
+
{
|
|
840
|
+
provider: task.planningModelProvider,
|
|
841
|
+
modelId: task.planningModelId
|
|
842
|
+
},
|
|
843
|
+
resolvePlanningSettingsModel(settings)
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
var init_model_resolution = __esm({
|
|
847
|
+
"../core/src/model-resolution.ts"() {
|
|
848
|
+
"use strict";
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
731
852
|
// ../core/src/types.ts
|
|
853
|
+
function normalizeMergeConflictStrategy(value) {
|
|
854
|
+
switch (value) {
|
|
855
|
+
case "smart":
|
|
856
|
+
return "smart-prefer-branch";
|
|
857
|
+
case "prefer-main":
|
|
858
|
+
return "smart-prefer-main";
|
|
859
|
+
case void 0:
|
|
860
|
+
return "smart-prefer-main";
|
|
861
|
+
default:
|
|
862
|
+
return value;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
732
865
|
function validateDocumentKey(key) {
|
|
733
866
|
if (!DOCUMENT_KEY_RE.test(key)) {
|
|
734
867
|
throw new Error(
|
|
@@ -812,13 +945,14 @@ function validateMessageMetadata(metadata) {
|
|
|
812
945
|
throw new Error("metadata.replyTo.messageId must be a non-empty string");
|
|
813
946
|
}
|
|
814
947
|
}
|
|
815
|
-
var THINKING_LEVELS, COLUMNS, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, CheckoutConflictError, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS;
|
|
948
|
+
var THINKING_LEVELS, COLUMNS, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, CheckoutConflictError, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS;
|
|
816
949
|
var init_types = __esm({
|
|
817
950
|
"../core/src/types.ts"() {
|
|
818
951
|
"use strict";
|
|
819
952
|
init_settings_schema();
|
|
820
953
|
init_prompt_overrides();
|
|
821
954
|
init_error_message();
|
|
955
|
+
init_model_resolution();
|
|
822
956
|
THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
823
957
|
COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
|
|
824
958
|
TASK_PRIORITIES = ["low", "normal", "high", "urgent"];
|
|
@@ -882,6 +1016,15 @@ var init_types = __esm({
|
|
|
882
1016
|
"neon-bloom",
|
|
883
1017
|
"sepia"
|
|
884
1018
|
];
|
|
1019
|
+
NOTIFICATION_EVENTS = [
|
|
1020
|
+
"in-review",
|
|
1021
|
+
"merged",
|
|
1022
|
+
"failed",
|
|
1023
|
+
"awaiting-approval",
|
|
1024
|
+
"awaiting-user-review",
|
|
1025
|
+
"planning-awaiting-input",
|
|
1026
|
+
"gridlock"
|
|
1027
|
+
];
|
|
885
1028
|
WORKFLOW_STEP_TEMPLATES = [
|
|
886
1029
|
{
|
|
887
1030
|
id: "documentation-review",
|
|
@@ -2289,7 +2432,7 @@ var init_db = __esm({
|
|
|
2289
2432
|
"use strict";
|
|
2290
2433
|
init_sqlite_adapter();
|
|
2291
2434
|
init_types();
|
|
2292
|
-
SCHEMA_VERSION =
|
|
2435
|
+
SCHEMA_VERSION = 51;
|
|
2293
2436
|
SCHEMA_SQL = `
|
|
2294
2437
|
-- Tasks table with JSON columns for nested data
|
|
2295
2438
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
@@ -2823,6 +2966,74 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
|
|
|
2823
2966
|
get fts5Available() {
|
|
2824
2967
|
return this._fts5Available;
|
|
2825
2968
|
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Rebuild the task FTS5 index and maintenance triggers from scratch.
|
|
2971
|
+
* Returns false when FTS5 is unavailable in this runtime.
|
|
2972
|
+
*/
|
|
2973
|
+
rebuildFts5Index() {
|
|
2974
|
+
if (!this._fts5Available) {
|
|
2975
|
+
return false;
|
|
2976
|
+
}
|
|
2977
|
+
try {
|
|
2978
|
+
this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_ai");
|
|
2979
|
+
this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_au");
|
|
2980
|
+
this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_ad");
|
|
2981
|
+
this.db.exec("DROP TABLE IF EXISTS tasks_fts");
|
|
2982
|
+
this.db.exec(`
|
|
2983
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
2984
|
+
id,
|
|
2985
|
+
title,
|
|
2986
|
+
description,
|
|
2987
|
+
comments,
|
|
2988
|
+
content='tasks',
|
|
2989
|
+
content_rowid='rowid'
|
|
2990
|
+
)
|
|
2991
|
+
`);
|
|
2992
|
+
this.db.exec(`
|
|
2993
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_ai AFTER INSERT ON tasks BEGIN
|
|
2994
|
+
INSERT INTO tasks_fts(rowid, id, title, description, comments)
|
|
2995
|
+
VALUES (new.rowid, new.id, COALESCE(new.title, ''), new.description, COALESCE(new.comments, '[]'));
|
|
2996
|
+
END
|
|
2997
|
+
`);
|
|
2998
|
+
const hasTaskTitle = this.hasColumn("tasks", "title");
|
|
2999
|
+
const updateColumns = hasTaskTitle ? "id, title, description, comments" : "id, description, comments";
|
|
3000
|
+
const oldTitle = hasTaskTitle ? "COALESCE(old.title, '')" : "''";
|
|
3001
|
+
const newTitle = hasTaskTitle ? "COALESCE(new.title, '')" : "''";
|
|
3002
|
+
this.db.exec(`
|
|
3003
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_au AFTER UPDATE OF ${updateColumns} ON tasks BEGIN
|
|
3004
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, id, title, description, comments)
|
|
3005
|
+
VALUES('delete', old.rowid, old.id, ${oldTitle}, old.description, COALESCE(old.comments, '[]'));
|
|
3006
|
+
INSERT INTO tasks_fts(rowid, id, title, description, comments)
|
|
3007
|
+
VALUES (new.rowid, new.id, ${newTitle}, new.description, COALESCE(new.comments, '[]'));
|
|
3008
|
+
END
|
|
3009
|
+
`);
|
|
3010
|
+
this.db.exec(`
|
|
3011
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_ad AFTER DELETE ON tasks BEGIN
|
|
3012
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, id, title, description, comments)
|
|
3013
|
+
VALUES('delete', old.rowid, old.id, COALESCE(old.title, ''), old.description, COALESCE(old.comments, '[]'));
|
|
3014
|
+
END
|
|
3015
|
+
`);
|
|
3016
|
+
this.db.exec("INSERT INTO tasks_fts(tasks_fts) VALUES('rebuild')");
|
|
3017
|
+
return true;
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
console.warn("[fusion:db] Failed to rebuild FTS5 index", error);
|
|
3020
|
+
throw error;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Run FTS5 integrity check. Returns true when healthy or unavailable.
|
|
3025
|
+
*/
|
|
3026
|
+
checkFts5Integrity() {
|
|
3027
|
+
if (!this._fts5Available) {
|
|
3028
|
+
return true;
|
|
3029
|
+
}
|
|
3030
|
+
try {
|
|
3031
|
+
this.db.exec("INSERT INTO tasks_fts(tasks_fts) VALUES('integrity-check')");
|
|
3032
|
+
return true;
|
|
3033
|
+
} catch {
|
|
3034
|
+
return false;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
2826
3037
|
/**
|
|
2827
3038
|
* Initialize the database: create tables if they don't exist
|
|
2828
3039
|
* and seed meta values.
|
|
@@ -3748,6 +3959,19 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
|
|
|
3748
3959
|
this.addColumnIfMissing("tasks", "nodeId", "TEXT");
|
|
3749
3960
|
});
|
|
3750
3961
|
}
|
|
3962
|
+
if (version < 50) {
|
|
3963
|
+
this.applyMigration(50, () => {
|
|
3964
|
+
this.addColumnIfMissing("tasks", "effectiveNodeId", "TEXT");
|
|
3965
|
+
this.addColumnIfMissing("tasks", "effectiveNodeSource", "TEXT");
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
if (version < 51) {
|
|
3969
|
+
this.applyMigration(51, () => {
|
|
3970
|
+
if (this.hasTable("chat_messages")) {
|
|
3971
|
+
this.addColumnIfMissing("chat_messages", "attachments", "TEXT");
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3751
3975
|
}
|
|
3752
3976
|
/**
|
|
3753
3977
|
* Run a single migration step inside a transaction and bump the version.
|
|
@@ -3763,6 +3987,14 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
|
|
|
3763
3987
|
const row = this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
3764
3988
|
return Boolean(row);
|
|
3765
3989
|
}
|
|
3990
|
+
/**
|
|
3991
|
+
* Check whether an error appears to be an FTS5 corruption/integrity failure.
|
|
3992
|
+
*/
|
|
3993
|
+
isFts5CorruptionError(error) {
|
|
3994
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
3995
|
+
const lower = message.toLowerCase();
|
|
3996
|
+
return lower.includes("corruption found reading blob") || lower.includes("database disk image is malformed") || lower.includes("fts5") && lower.includes("corrupt");
|
|
3997
|
+
}
|
|
3766
3998
|
/**
|
|
3767
3999
|
* Check whether a table has a given column.
|
|
3768
4000
|
*/
|
|
@@ -18238,10 +18470,31 @@ var init_task_merge = __esm({
|
|
|
18238
18470
|
"use strict";
|
|
18239
18471
|
BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
18240
18472
|
"failed",
|
|
18473
|
+
// ── User-attention / awaiting-handoff states ─────────────────────────
|
|
18241
18474
|
"awaiting-inspection",
|
|
18242
18475
|
"awaiting-user-review",
|
|
18476
|
+
"awaiting-approval",
|
|
18477
|
+
// triage spec awaiting user approval
|
|
18478
|
+
// ── Active merge in-flight ───────────────────────────────────────────
|
|
18243
18479
|
"merging",
|
|
18244
|
-
"merging-pr"
|
|
18480
|
+
"merging-pr",
|
|
18481
|
+
// ── Re-planning / triage states (scope not finalized) ────────────────
|
|
18482
|
+
// A task in planning/triage hasn't finalized its scope yet — letting it
|
|
18483
|
+
// merge skips the work the user moved it back to plan. Same for the legacy
|
|
18484
|
+
// "specifying" alias migrated to "planning" in db.ts.
|
|
18485
|
+
"planning",
|
|
18486
|
+
"specifying",
|
|
18487
|
+
"needs-replan",
|
|
18488
|
+
// scheduler/executor/triage signaled re-plan
|
|
18489
|
+
// ── Mission-level validation in flight ───────────────────────────────
|
|
18490
|
+
"mission-validation",
|
|
18491
|
+
// ── Scheduler-side transient state ───────────────────────────────────
|
|
18492
|
+
"queued",
|
|
18493
|
+
// scheduler placed the task in line; not finalized
|
|
18494
|
+
// ── Abnormal termination — defensive guard ───────────────────────────
|
|
18495
|
+
// Task was killed by the stuck detector. If it surfaces in in-review,
|
|
18496
|
+
// it needs investigation, not auto-merge.
|
|
18497
|
+
"stuck-killed"
|
|
18245
18498
|
]);
|
|
18246
18499
|
NON_TERMINAL_STEP_STATUSES = /* @__PURE__ */ new Set([
|
|
18247
18500
|
"pending",
|
|
@@ -29462,6 +29715,26 @@ var init_logger = __esm({
|
|
|
29462
29715
|
}
|
|
29463
29716
|
});
|
|
29464
29717
|
|
|
29718
|
+
// ../core/src/node-override-guard.ts
|
|
29719
|
+
function validateNodeOverrideChange(task, newNodeId) {
|
|
29720
|
+
if (newNodeId === void 0) {
|
|
29721
|
+
return { allowed: true };
|
|
29722
|
+
}
|
|
29723
|
+
if (task.column === "in-progress") {
|
|
29724
|
+
return {
|
|
29725
|
+
allowed: false,
|
|
29726
|
+
reason: "task-in-progress",
|
|
29727
|
+
message: `Cannot change node override for ${task.id} while it is in progress. The task is currently executing and routing cannot be changed mid-flight. Wait for the task to complete, or pause/stop it first before changing the node assignment.`
|
|
29728
|
+
};
|
|
29729
|
+
}
|
|
29730
|
+
return { allowed: true };
|
|
29731
|
+
}
|
|
29732
|
+
var init_node_override_guard = __esm({
|
|
29733
|
+
"../core/src/node-override-guard.ts"() {
|
|
29734
|
+
"use strict";
|
|
29735
|
+
}
|
|
29736
|
+
});
|
|
29737
|
+
|
|
29465
29738
|
// ../core/src/store.ts
|
|
29466
29739
|
import { EventEmitter as EventEmitter12 } from "node:events";
|
|
29467
29740
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
@@ -29549,6 +29822,7 @@ var init_store = __esm({
|
|
|
29549
29822
|
init_project_memory();
|
|
29550
29823
|
init_run_command();
|
|
29551
29824
|
init_logger();
|
|
29825
|
+
init_node_override_guard();
|
|
29552
29826
|
TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1e3;
|
|
29553
29827
|
TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4e3;
|
|
29554
29828
|
ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
|
|
@@ -29835,6 +30109,8 @@ var init_store = __esm({
|
|
|
29835
30109
|
assignedAgentId: row.assignedAgentId || void 0,
|
|
29836
30110
|
assigneeUserId: row.assigneeUserId || void 0,
|
|
29837
30111
|
nodeId: row.nodeId || void 0,
|
|
30112
|
+
effectiveNodeId: row.effectiveNodeId || void 0,
|
|
30113
|
+
effectiveNodeSource: row.effectiveNodeSource || void 0,
|
|
29838
30114
|
checkedOutBy: row.checkedOutBy || void 0,
|
|
29839
30115
|
checkedOutAt: row.checkedOutAt || void 0
|
|
29840
30116
|
};
|
|
@@ -30083,6 +30359,8 @@ ${recentText}` : void 0
|
|
|
30083
30359
|
"assignedAgentId",
|
|
30084
30360
|
"assigneeUserId",
|
|
30085
30361
|
"nodeId",
|
|
30362
|
+
"effectiveNodeId",
|
|
30363
|
+
"effectiveNodeSource",
|
|
30086
30364
|
"checkedOutBy",
|
|
30087
30365
|
"checkedOutAt",
|
|
30088
30366
|
// `log` is fetched in slim mode so the server can aggregate
|
|
@@ -30183,6 +30461,8 @@ ${outcome}`;
|
|
|
30183
30461
|
"assignedAgentId",
|
|
30184
30462
|
"assigneeUserId",
|
|
30185
30463
|
"nodeId",
|
|
30464
|
+
"effectiveNodeId",
|
|
30465
|
+
"effectiveNodeSource",
|
|
30186
30466
|
"checkedOutBy",
|
|
30187
30467
|
"checkedOutAt"
|
|
30188
30468
|
];
|
|
@@ -30221,9 +30501,9 @@ ${outcome}`;
|
|
|
30221
30501
|
dependencies, steps, log, attachments, steeringComments,
|
|
30222
30502
|
comments, workflowStepResults, prInfo, issueInfo,
|
|
30223
30503
|
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
|
|
30224
|
-
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, checkedOutBy, checkedOutAt
|
|
30504
|
+
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, checkedOutBy, checkedOutAt
|
|
30225
30505
|
) VALUES (
|
|
30226
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
30506
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
30227
30507
|
)
|
|
30228
30508
|
ON CONFLICT(id) DO UPDATE SET
|
|
30229
30509
|
title = excluded.title,
|
|
@@ -30291,6 +30571,8 @@ ${outcome}`;
|
|
|
30291
30571
|
assignedAgentId = excluded.assignedAgentId,
|
|
30292
30572
|
assigneeUserId = excluded.assigneeUserId,
|
|
30293
30573
|
nodeId = excluded.nodeId,
|
|
30574
|
+
effectiveNodeId = excluded.effectiveNodeId,
|
|
30575
|
+
effectiveNodeSource = excluded.effectiveNodeSource,
|
|
30294
30576
|
checkedOutBy = excluded.checkedOutBy,
|
|
30295
30577
|
checkedOutAt = excluded.checkedOutAt
|
|
30296
30578
|
`).run(
|
|
@@ -30360,11 +30642,36 @@ ${outcome}`;
|
|
|
30360
30642
|
task.assignedAgentId ?? null,
|
|
30361
30643
|
task.assigneeUserId ?? null,
|
|
30362
30644
|
task.nodeId ?? null,
|
|
30645
|
+
task.effectiveNodeId ?? null,
|
|
30646
|
+
task.effectiveNodeSource ?? null,
|
|
30363
30647
|
task.checkedOutBy ?? null,
|
|
30364
30648
|
task.checkedOutAt ?? null
|
|
30365
30649
|
);
|
|
30366
30650
|
this.db.bumpLastModified();
|
|
30367
30651
|
}
|
|
30652
|
+
upsertTaskWithFtsRecovery(task) {
|
|
30653
|
+
try {
|
|
30654
|
+
this.upsertTask(task);
|
|
30655
|
+
return;
|
|
30656
|
+
} catch (error) {
|
|
30657
|
+
if (!this.db.isFts5CorruptionError(error)) {
|
|
30658
|
+
throw error;
|
|
30659
|
+
}
|
|
30660
|
+
console.warn(`[fusion:store] FTS5 corruption detected during upsert for task ${task.id}; rebuilding index and retrying once`);
|
|
30661
|
+
try {
|
|
30662
|
+
this.db.rebuildFts5Index();
|
|
30663
|
+
} catch (rebuildError) {
|
|
30664
|
+
console.warn("[fusion:store] FTS5 rebuild failed; propagating original upsert error", rebuildError);
|
|
30665
|
+
throw error;
|
|
30666
|
+
}
|
|
30667
|
+
try {
|
|
30668
|
+
this.upsertTask(task);
|
|
30669
|
+
} catch (retryError) {
|
|
30670
|
+
console.warn("[fusion:store] Upsert retry after FTS5 rebuild failed; propagating original upsert error", retryError);
|
|
30671
|
+
throw error;
|
|
30672
|
+
}
|
|
30673
|
+
}
|
|
30674
|
+
}
|
|
30368
30675
|
/**
|
|
30369
30676
|
* Read a task from SQLite by ID.
|
|
30370
30677
|
*/
|
|
@@ -30583,7 +30890,7 @@ ${outcome}`;
|
|
|
30583
30890
|
* for backward compatibility and debugging.
|
|
30584
30891
|
*/
|
|
30585
30892
|
async atomicWriteTaskJson(dir, task) {
|
|
30586
|
-
this.
|
|
30893
|
+
this.upsertTaskWithFtsRecovery(task);
|
|
30587
30894
|
await this.writeTaskJsonFile(dir, task);
|
|
30588
30895
|
}
|
|
30589
30896
|
/**
|
|
@@ -30596,7 +30903,7 @@ ${outcome}`;
|
|
|
30596
30903
|
*/
|
|
30597
30904
|
async atomicWriteTaskJsonWithAudit(dir, task, auditInput) {
|
|
30598
30905
|
this.db.transaction(() => {
|
|
30599
|
-
this.
|
|
30906
|
+
this.upsertTaskWithFtsRecovery(task);
|
|
30600
30907
|
if (auditInput) {
|
|
30601
30908
|
const eventId = randomUUID6();
|
|
30602
30909
|
const timestamp = auditInput.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -31544,6 +31851,14 @@ ${newTask.description}
|
|
|
31544
31851
|
if (fromColumn === "in-review" && (toColumn === "todo" || toColumn === "in-progress") || fromColumn === "done" && (toColumn === "todo" || toColumn === "triage")) {
|
|
31545
31852
|
task.workflowStepResults = void 0;
|
|
31546
31853
|
}
|
|
31854
|
+
if (fromColumn === "in-review" && toColumn === "todo") {
|
|
31855
|
+
task.branch = void 0;
|
|
31856
|
+
task.baseBranch = void 0;
|
|
31857
|
+
task.baseCommitSha = void 0;
|
|
31858
|
+
task.summary = void 0;
|
|
31859
|
+
task.recoveryRetryCount = void 0;
|
|
31860
|
+
task.nextRecoveryAt = void 0;
|
|
31861
|
+
}
|
|
31547
31862
|
await this.atomicWriteTaskJson(dir, task);
|
|
31548
31863
|
if (this.isWatching) this.taskCache.set(id, { ...task });
|
|
31549
31864
|
this.emit("task:moved", { task, from: fromColumn, to: toColumn });
|
|
@@ -31557,6 +31872,12 @@ ${newTask.description}
|
|
|
31557
31872
|
}
|
|
31558
31873
|
const dir = this.taskDir(id);
|
|
31559
31874
|
const task = await this.readTaskJson(dir);
|
|
31875
|
+
if (updates.nodeId !== void 0) {
|
|
31876
|
+
const validation = validateNodeOverrideChange(task, updates.nodeId ?? null);
|
|
31877
|
+
if (!validation.allowed) {
|
|
31878
|
+
throw new Error(validation.message);
|
|
31879
|
+
}
|
|
31880
|
+
}
|
|
31560
31881
|
if (!task.log) {
|
|
31561
31882
|
task.log = [];
|
|
31562
31883
|
}
|
|
@@ -31619,6 +31940,16 @@ ${newTask.description}
|
|
|
31619
31940
|
} else if (updates.nodeId !== void 0) {
|
|
31620
31941
|
task.nodeId = updates.nodeId;
|
|
31621
31942
|
}
|
|
31943
|
+
if (updates.effectiveNodeId === null) {
|
|
31944
|
+
task.effectiveNodeId = void 0;
|
|
31945
|
+
} else if (updates.effectiveNodeId !== void 0) {
|
|
31946
|
+
task.effectiveNodeId = updates.effectiveNodeId;
|
|
31947
|
+
}
|
|
31948
|
+
if (updates.effectiveNodeSource === null) {
|
|
31949
|
+
task.effectiveNodeSource = void 0;
|
|
31950
|
+
} else if (updates.effectiveNodeSource !== void 0) {
|
|
31951
|
+
task.effectiveNodeSource = updates.effectiveNodeSource;
|
|
31952
|
+
}
|
|
31622
31953
|
if (updates.checkedOutBy === null) {
|
|
31623
31954
|
task.checkedOutBy = void 0;
|
|
31624
31955
|
task.checkedOutAt = void 0;
|
|
@@ -33852,7 +34183,7 @@ ${stepsSection}`;
|
|
|
33852
34183
|
healthCheck() {
|
|
33853
34184
|
try {
|
|
33854
34185
|
this.db.prepare("SELECT 1").get();
|
|
33855
|
-
return
|
|
34186
|
+
return this.db.checkFts5Integrity();
|
|
33856
34187
|
} catch {
|
|
33857
34188
|
return false;
|
|
33858
34189
|
}
|
|
@@ -34474,6 +34805,10 @@ var init_board = __esm({
|
|
|
34474
34805
|
|
|
34475
34806
|
// ../core/src/gh-cli.ts
|
|
34476
34807
|
import { execFileSync, execFile as execFile2 } from "node:child_process";
|
|
34808
|
+
function normalizeRunGhOptions(opts) {
|
|
34809
|
+
if (typeof opts === "string") return { cwd: opts };
|
|
34810
|
+
return opts ?? {};
|
|
34811
|
+
}
|
|
34477
34812
|
function isGhAvailable() {
|
|
34478
34813
|
try {
|
|
34479
34814
|
execFileSync("gh", ["--version"], {
|
|
@@ -34513,19 +34848,55 @@ function runGh(args, cwd) {
|
|
|
34513
34848
|
throw error;
|
|
34514
34849
|
}
|
|
34515
34850
|
}
|
|
34516
|
-
function runGhAsync(args,
|
|
34851
|
+
function runGhAsync(args, cwdOrOptions) {
|
|
34852
|
+
const { cwd, signal: externalSignal, timeoutMs = DEFAULT_GH_TIMEOUT_MS } = normalizeRunGhOptions(cwdOrOptions);
|
|
34517
34853
|
return new Promise((resolve17, reject) => {
|
|
34854
|
+
if (externalSignal?.aborted) {
|
|
34855
|
+
reject(makeGhError(`gh command aborted: ${describeAbortReason(externalSignal.reason)}`, "ABORT_ERR"));
|
|
34856
|
+
return;
|
|
34857
|
+
}
|
|
34858
|
+
const controller = new AbortController();
|
|
34859
|
+
let timedOut = false;
|
|
34860
|
+
let externalAborted = false;
|
|
34861
|
+
const onExternalAbort = () => {
|
|
34862
|
+
externalAborted = true;
|
|
34863
|
+
controller.abort();
|
|
34864
|
+
};
|
|
34865
|
+
if (externalSignal) {
|
|
34866
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
34867
|
+
}
|
|
34868
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
34869
|
+
timedOut = true;
|
|
34870
|
+
controller.abort();
|
|
34871
|
+
}, timeoutMs) : void 0;
|
|
34872
|
+
const cleanup = () => {
|
|
34873
|
+
if (timer) clearTimeout(timer);
|
|
34874
|
+
if (externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
|
|
34875
|
+
};
|
|
34518
34876
|
execFile2(
|
|
34519
34877
|
"gh",
|
|
34520
34878
|
args,
|
|
34521
34879
|
{
|
|
34522
34880
|
encoding: "utf-8",
|
|
34523
|
-
cwd
|
|
34881
|
+
cwd,
|
|
34882
|
+
signal: controller.signal
|
|
34524
34883
|
},
|
|
34525
34884
|
(error, stdout, stderr) => {
|
|
34885
|
+
cleanup();
|
|
34526
34886
|
if (error) {
|
|
34527
|
-
const
|
|
34528
|
-
|
|
34887
|
+
const isAbort = error.code === "ABORT_ERR" || error.name === "AbortError";
|
|
34888
|
+
let message;
|
|
34889
|
+
if (timedOut) {
|
|
34890
|
+
message = `gh command timed out after ${timeoutMs}ms`;
|
|
34891
|
+
} else if (isAbort && externalAborted) {
|
|
34892
|
+
message = `gh command aborted: ${describeAbortReason(externalSignal?.reason)}`;
|
|
34893
|
+
} else if (isAbort) {
|
|
34894
|
+
message = "gh command aborted";
|
|
34895
|
+
} else {
|
|
34896
|
+
message = `gh command failed: ${error.message}`;
|
|
34897
|
+
}
|
|
34898
|
+
const ghError = new Error(message);
|
|
34899
|
+
ghError.code = error.code ?? (isAbort ? "ABORT_ERR" : null);
|
|
34529
34900
|
ghError.stdout = stdout ?? "";
|
|
34530
34901
|
ghError.stderr = stderr ?? "";
|
|
34531
34902
|
reject(ghError);
|
|
@@ -34536,6 +34907,18 @@ function runGhAsync(args, cwd) {
|
|
|
34536
34907
|
);
|
|
34537
34908
|
});
|
|
34538
34909
|
}
|
|
34910
|
+
function makeGhError(message, code) {
|
|
34911
|
+
const err = new Error(message);
|
|
34912
|
+
err.code = code;
|
|
34913
|
+
err.stdout = "";
|
|
34914
|
+
err.stderr = "";
|
|
34915
|
+
return err;
|
|
34916
|
+
}
|
|
34917
|
+
function describeAbortReason(reason) {
|
|
34918
|
+
if (reason instanceof Error) return reason.message;
|
|
34919
|
+
if (typeof reason === "string") return reason;
|
|
34920
|
+
return "aborted";
|
|
34921
|
+
}
|
|
34539
34922
|
function runGhJson(args, cwd) {
|
|
34540
34923
|
const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
|
|
34541
34924
|
const output = runGh(jsonArgs, cwd);
|
|
@@ -34545,9 +34928,9 @@ function runGhJson(args, cwd) {
|
|
|
34545
34928
|
throw new Error(`Failed to parse gh JSON output: ${err instanceof Error ? err.message : String(err)}`);
|
|
34546
34929
|
}
|
|
34547
34930
|
}
|
|
34548
|
-
async function runGhJsonAsync(args,
|
|
34931
|
+
async function runGhJsonAsync(args, cwdOrOptions) {
|
|
34549
34932
|
const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
|
|
34550
|
-
const output = await runGhAsync(jsonArgs,
|
|
34933
|
+
const output = await runGhAsync(jsonArgs, cwdOrOptions);
|
|
34551
34934
|
try {
|
|
34552
34935
|
return JSON.parse(output);
|
|
34553
34936
|
} catch (err) {
|
|
@@ -34605,9 +34988,11 @@ function getCurrentRepo(cwd) {
|
|
|
34605
34988
|
return null;
|
|
34606
34989
|
}
|
|
34607
34990
|
}
|
|
34991
|
+
var DEFAULT_GH_TIMEOUT_MS;
|
|
34608
34992
|
var init_gh_cli = __esm({
|
|
34609
34993
|
"../core/src/gh-cli.ts"() {
|
|
34610
34994
|
"use strict";
|
|
34995
|
+
DEFAULT_GH_TIMEOUT_MS = 3e4;
|
|
34611
34996
|
}
|
|
34612
34997
|
});
|
|
34613
34998
|
|
|
@@ -35071,6 +35456,83 @@ var init_routine_store = __esm({
|
|
|
35071
35456
|
}
|
|
35072
35457
|
});
|
|
35073
35458
|
|
|
35459
|
+
// ../core/src/notification/dispatcher.ts
|
|
35460
|
+
var NotificationDispatcher;
|
|
35461
|
+
var init_dispatcher = __esm({
|
|
35462
|
+
"../core/src/notification/dispatcher.ts"() {
|
|
35463
|
+
"use strict";
|
|
35464
|
+
NotificationDispatcher = class {
|
|
35465
|
+
constructor(config = {}) {
|
|
35466
|
+
this.config = config;
|
|
35467
|
+
}
|
|
35468
|
+
providers = /* @__PURE__ */ new Map();
|
|
35469
|
+
registerProvider(provider) {
|
|
35470
|
+
this.providers.set(provider.getProviderId(), provider);
|
|
35471
|
+
}
|
|
35472
|
+
unregisterProvider(providerId) {
|
|
35473
|
+
this.providers.delete(providerId);
|
|
35474
|
+
}
|
|
35475
|
+
getProviders() {
|
|
35476
|
+
return [...this.providers.values()];
|
|
35477
|
+
}
|
|
35478
|
+
async dispatch(event, payload) {
|
|
35479
|
+
const providers = this.getProviders().filter(
|
|
35480
|
+
(provider) => provider.isEventSupported(event)
|
|
35481
|
+
);
|
|
35482
|
+
const results = await Promise.all(
|
|
35483
|
+
providers.map(async (provider) => {
|
|
35484
|
+
const providerId = provider.getProviderId();
|
|
35485
|
+
try {
|
|
35486
|
+
return await provider.sendNotification(event, payload);
|
|
35487
|
+
} catch (error) {
|
|
35488
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35489
|
+
console.warn(
|
|
35490
|
+
`[notification-dispatcher] Provider ${providerId} failed for event ${event}: ${message}`
|
|
35491
|
+
);
|
|
35492
|
+
return { success: false, providerId, error: message };
|
|
35493
|
+
}
|
|
35494
|
+
})
|
|
35495
|
+
);
|
|
35496
|
+
return results;
|
|
35497
|
+
}
|
|
35498
|
+
async initializeAll() {
|
|
35499
|
+
await Promise.all(
|
|
35500
|
+
this.getProviders().map(async (provider) => {
|
|
35501
|
+
if (!provider.initialize) {
|
|
35502
|
+
return;
|
|
35503
|
+
}
|
|
35504
|
+
try {
|
|
35505
|
+
await provider.initialize(this.config);
|
|
35506
|
+
} catch (error) {
|
|
35507
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35508
|
+
console.warn(
|
|
35509
|
+
`[notification-dispatcher] Provider ${provider.getProviderId()} initialization failed: ${message}`
|
|
35510
|
+
);
|
|
35511
|
+
}
|
|
35512
|
+
})
|
|
35513
|
+
);
|
|
35514
|
+
}
|
|
35515
|
+
async shutdownAll() {
|
|
35516
|
+
await Promise.all(
|
|
35517
|
+
this.getProviders().map(async (provider) => {
|
|
35518
|
+
if (!provider.shutdown) {
|
|
35519
|
+
return;
|
|
35520
|
+
}
|
|
35521
|
+
try {
|
|
35522
|
+
await provider.shutdown();
|
|
35523
|
+
} catch (error) {
|
|
35524
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35525
|
+
console.warn(
|
|
35526
|
+
`[notification-dispatcher] Provider ${provider.getProviderId()} shutdown failed: ${message}`
|
|
35527
|
+
);
|
|
35528
|
+
}
|
|
35529
|
+
})
|
|
35530
|
+
);
|
|
35531
|
+
}
|
|
35532
|
+
};
|
|
35533
|
+
}
|
|
35534
|
+
});
|
|
35535
|
+
|
|
35074
35536
|
// ../core/src/plugin-loader.ts
|
|
35075
35537
|
import { basename as basename6, dirname as dirname5, extname, isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
|
|
35076
35538
|
import { copyFile, rm } from "node:fs/promises";
|
|
@@ -48046,6 +48508,7 @@ var init_chat_store = __esm({
|
|
|
48046
48508
|
content: row.content,
|
|
48047
48509
|
thinkingOutput: row.thinkingOutput ?? null,
|
|
48048
48510
|
metadata: fromJson(row.metadata) ?? null,
|
|
48511
|
+
attachments: fromJson(row.attachments) ?? void 0,
|
|
48049
48512
|
createdAt: row.createdAt
|
|
48050
48513
|
};
|
|
48051
48514
|
}
|
|
@@ -48264,11 +48727,12 @@ var init_chat_store = __esm({
|
|
|
48264
48727
|
content: input.content,
|
|
48265
48728
|
thinkingOutput: input.thinkingOutput ?? null,
|
|
48266
48729
|
metadata: input.metadata ?? null,
|
|
48730
|
+
attachments: input.attachments,
|
|
48267
48731
|
createdAt: now
|
|
48268
48732
|
};
|
|
48269
48733
|
this.db.prepare(`
|
|
48270
|
-
INSERT INTO chat_messages (id, sessionId, role, content, thinkingOutput, metadata, createdAt)
|
|
48271
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
48734
|
+
INSERT INTO chat_messages (id, sessionId, role, content, thinkingOutput, metadata, attachments, createdAt)
|
|
48735
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
48272
48736
|
`).run(
|
|
48273
48737
|
message.id,
|
|
48274
48738
|
message.sessionId,
|
|
@@ -48276,6 +48740,7 @@ var init_chat_store = __esm({
|
|
|
48276
48740
|
message.content,
|
|
48277
48741
|
message.thinkingOutput,
|
|
48278
48742
|
toJsonNullable(message.metadata),
|
|
48743
|
+
toJsonNullable(message.attachments),
|
|
48279
48744
|
message.createdAt
|
|
48280
48745
|
);
|
|
48281
48746
|
this.db.prepare("UPDATE chat_sessions SET updatedAt = ? WHERE id = ?").run(now, sessionId);
|
|
@@ -48283,6 +48748,28 @@ var init_chat_store = __esm({
|
|
|
48283
48748
|
this.emit("chat:message:added", message);
|
|
48284
48749
|
return message;
|
|
48285
48750
|
}
|
|
48751
|
+
/**
|
|
48752
|
+
* Append a file attachment metadata record to an existing message.
|
|
48753
|
+
*/
|
|
48754
|
+
addMessageAttachment(sessionId, messageId, attachment) {
|
|
48755
|
+
const message = this.getMessage(messageId);
|
|
48756
|
+
if (!message || message.sessionId !== sessionId) {
|
|
48757
|
+
throw new Error(`Message ${messageId} not found in session ${sessionId}`);
|
|
48758
|
+
}
|
|
48759
|
+
const updatedAttachments = [...message.attachments ?? [], attachment];
|
|
48760
|
+
this.db.prepare(`
|
|
48761
|
+
UPDATE chat_messages
|
|
48762
|
+
SET attachments = ?
|
|
48763
|
+
WHERE id = ?
|
|
48764
|
+
`).run(toJsonNullable(updatedAttachments), messageId);
|
|
48765
|
+
const updated = this.getMessage(messageId);
|
|
48766
|
+
if (!updated) {
|
|
48767
|
+
throw new Error(`Failed to update message ${messageId}`);
|
|
48768
|
+
}
|
|
48769
|
+
this.db.bumpLastModified();
|
|
48770
|
+
this.emit("chat:message:updated", updated);
|
|
48771
|
+
return updated;
|
|
48772
|
+
}
|
|
48286
48773
|
/**
|
|
48287
48774
|
* Get messages for a chat session with optional filtering.
|
|
48288
48775
|
*
|
|
@@ -48447,8 +48934,10 @@ __export(src_exports, {
|
|
|
48447
48934
|
MessageStore: () => MessageStore,
|
|
48448
48935
|
MigrationCoordinator: () => MigrationCoordinator,
|
|
48449
48936
|
MissionStore: () => MissionStore,
|
|
48937
|
+
NOTIFICATION_EVENTS: () => NOTIFICATION_EVENTS,
|
|
48450
48938
|
NodeConnection: () => NodeConnection,
|
|
48451
48939
|
NodeDiscovery: () => NodeDiscovery,
|
|
48940
|
+
NotificationDispatcher: () => NotificationDispatcher,
|
|
48452
48941
|
PROJECT_SETTINGS_KEYS: () => PROJECT_SETTINGS_KEYS,
|
|
48453
48942
|
PROMPT_KEY_CATALOG: () => PROMPT_KEY_CATALOG,
|
|
48454
48943
|
PluginLoader: () => PluginLoader,
|
|
@@ -48596,6 +49085,7 @@ __export(src_exports, {
|
|
|
48596
49085
|
mergeInsights: () => mergeInsights,
|
|
48597
49086
|
migrateFromLegacy: () => migrateFromLegacy,
|
|
48598
49087
|
moveRoadmapFeature: () => moveRoadmapFeature,
|
|
49088
|
+
normalizeMergeConflictStrategy: () => normalizeMergeConflictStrategy,
|
|
48599
49089
|
normalizePermissions: () => normalizePermissions,
|
|
48600
49090
|
normalizeRoadmapFeatureOrder: () => normalizeRoadmapFeatureOrder,
|
|
48601
49091
|
normalizeRoadmapMilestoneOrder: () => normalizeRoadmapMilestoneOrder,
|
|
@@ -48635,12 +49125,20 @@ __export(src_exports, {
|
|
|
48635
49125
|
renderMemoryAuditMarkdown: () => renderMemoryAuditMarkdown,
|
|
48636
49126
|
resolveAgentPrompt: () => resolveAgentPrompt,
|
|
48637
49127
|
resolveDependencyOrder: () => resolveDependencyOrder,
|
|
49128
|
+
resolveExecutionSettingsModel: () => resolveExecutionSettingsModel,
|
|
48638
49129
|
resolveGlobalDir: () => resolveGlobalDir,
|
|
48639
49130
|
resolveMemoryBackend: () => resolveMemoryBackend,
|
|
48640
49131
|
resolveMemoryInstructionContext: () => resolveMemoryInstructionContext,
|
|
48641
49132
|
resolvePiExtensionProjectRoot: () => resolvePiExtensionProjectRoot,
|
|
49133
|
+
resolvePlanningSettingsModel: () => resolvePlanningSettingsModel,
|
|
49134
|
+
resolveProjectDefaultModel: () => resolveProjectDefaultModel,
|
|
48642
49135
|
resolvePrompt: () => resolvePrompt,
|
|
48643
49136
|
resolveRolePrompts: () => resolveRolePrompts,
|
|
49137
|
+
resolveTaskExecutionModel: () => resolveTaskExecutionModel,
|
|
49138
|
+
resolveTaskPlanningModel: () => resolveTaskPlanningModel,
|
|
49139
|
+
resolveTaskValidatorModel: () => resolveTaskValidatorModel,
|
|
49140
|
+
resolveTitleSummarizerSettingsModel: () => resolveTitleSummarizerSettingsModel,
|
|
49141
|
+
resolveValidatorSettingsModel: () => resolveValidatorSettingsModel,
|
|
48644
49142
|
runBackupCommand: () => runBackupCommand,
|
|
48645
49143
|
runCommandAsync: () => runCommandAsync,
|
|
48646
49144
|
runGh: () => runGh,
|
|
@@ -48670,6 +49168,7 @@ __export(src_exports, {
|
|
|
48670
49168
|
validateDescription: () => validateDescription,
|
|
48671
49169
|
validateImportData: () => validateImportData,
|
|
48672
49170
|
validateMessageMetadata: () => validateMessageMetadata,
|
|
49171
|
+
validateNodeOverrideChange: () => validateNodeOverrideChange,
|
|
48673
49172
|
validatePluginManifest: () => validatePluginManifest,
|
|
48674
49173
|
validateUnavailableNodePolicy: () => validateUnavailableNodePolicy,
|
|
48675
49174
|
writeAgentMemoryFile: () => writeAgentMemoryFile,
|
|
@@ -48704,15 +49203,19 @@ var init_src = __esm({
|
|
|
48704
49203
|
init_automation();
|
|
48705
49204
|
init_automation_store();
|
|
48706
49205
|
init_run_command();
|
|
49206
|
+
init_node_override_guard();
|
|
48707
49207
|
init_settings_validation();
|
|
48708
49208
|
init_routine();
|
|
48709
49209
|
init_routine_store();
|
|
49210
|
+
init_dispatcher();
|
|
49211
|
+
init_types();
|
|
48710
49212
|
init_plugin_types();
|
|
48711
49213
|
init_plugin_store();
|
|
48712
49214
|
init_plugin_loader();
|
|
48713
49215
|
init_backup();
|
|
48714
49216
|
init_settings_export();
|
|
48715
49217
|
init_ai_summarize();
|
|
49218
|
+
init_model_resolution();
|
|
48716
49219
|
init_memory_compaction();
|
|
48717
49220
|
init_roadmap_ordering();
|
|
48718
49221
|
init_task_priority();
|
|
@@ -50291,17 +50794,6 @@ var init_auth_storage = __esm({
|
|
|
50291
50794
|
});
|
|
50292
50795
|
|
|
50293
50796
|
// ../engine/src/pi.ts
|
|
50294
|
-
var pi_exports = {};
|
|
50295
|
-
__export(pi_exports, {
|
|
50296
|
-
COMPACTION_FALLBACK_INSTRUCTIONS: () => COMPACTION_FALLBACK_INSTRUCTIONS,
|
|
50297
|
-
compactSessionContext: () => compactSessionContext,
|
|
50298
|
-
createFnAgent: () => createFnAgent2,
|
|
50299
|
-
describeModel: () => describeModel,
|
|
50300
|
-
getHostExtensionPaths: () => getHostExtensionPaths,
|
|
50301
|
-
promptWithFallback: () => promptWithFallback,
|
|
50302
|
-
setHostExtensionPaths: () => setHostExtensionPaths,
|
|
50303
|
-
wrapToolsWithBoundary: () => wrapToolsWithBoundary
|
|
50304
|
-
});
|
|
50305
50797
|
import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
|
|
50306
50798
|
import { exec } from "node:child_process";
|
|
50307
50799
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -51225,16 +51717,14 @@ function wrapPluginRuntime(instance, runtimeId, runtimeName) {
|
|
|
51225
51717
|
if (typeof adapter.promptWithFallback === "function") {
|
|
51226
51718
|
return adapter.promptWithFallback(session, prompt, options);
|
|
51227
51719
|
}
|
|
51228
|
-
|
|
51229
|
-
return pwf(session, prompt, options);
|
|
51720
|
+
return promptWithFallback(session, prompt, options);
|
|
51230
51721
|
},
|
|
51231
51722
|
describeModel: (session) => {
|
|
51232
51723
|
const adapter = instance;
|
|
51233
51724
|
if (typeof adapter.describeModel === "function") {
|
|
51234
51725
|
return adapter.describeModel(session);
|
|
51235
51726
|
}
|
|
51236
|
-
|
|
51237
|
-
return dm(session);
|
|
51727
|
+
return describeModel(session);
|
|
51238
51728
|
}
|
|
51239
51729
|
};
|
|
51240
51730
|
}
|
|
@@ -51305,36 +51795,19 @@ var init_runtime_resolution = __esm({
|
|
|
51305
51795
|
"../engine/src/runtime-resolution.ts"() {
|
|
51306
51796
|
"use strict";
|
|
51307
51797
|
init_logger2();
|
|
51798
|
+
init_pi();
|
|
51308
51799
|
runtimeLog2 = createLogger2("runtime-resolver");
|
|
51309
|
-
DefaultPiRuntime = class
|
|
51800
|
+
DefaultPiRuntime = class {
|
|
51310
51801
|
id = "pi";
|
|
51311
51802
|
name = "Default PI Runtime";
|
|
51312
|
-
// Synchronous cached describeModel function
|
|
51313
|
-
static describeModelFn = null;
|
|
51314
|
-
/**
|
|
51315
|
-
* Create an agent session using the default pi implementation.
|
|
51316
|
-
*/
|
|
51317
51803
|
async createSession(options) {
|
|
51318
|
-
|
|
51319
|
-
return createFnAgent5(options);
|
|
51804
|
+
return createFnAgent2(options);
|
|
51320
51805
|
}
|
|
51321
|
-
/**
|
|
51322
|
-
* Prompt with automatic retry and compaction.
|
|
51323
|
-
* Delegates to the existing promptWithFallback implementation.
|
|
51324
|
-
*/
|
|
51325
51806
|
async promptWithFallback(session, prompt, options) {
|
|
51326
|
-
|
|
51327
|
-
return pwf(session, prompt, options);
|
|
51807
|
+
return promptWithFallback(session, prompt, options);
|
|
51328
51808
|
}
|
|
51329
|
-
/**
|
|
51330
|
-
* Get model description from session.
|
|
51331
|
-
*/
|
|
51332
51809
|
describeModel(session) {
|
|
51333
|
-
|
|
51334
|
-
const { describeModel: describeModel2 } = (init_pi(), __toCommonJS(pi_exports));
|
|
51335
|
-
_DefaultPiRuntime.describeModelFn = describeModel2;
|
|
51336
|
-
}
|
|
51337
|
-
return _DefaultPiRuntime.describeModelFn(session);
|
|
51810
|
+
return describeModel(session);
|
|
51338
51811
|
}
|
|
51339
51812
|
};
|
|
51340
51813
|
defaultPiRuntimeInstance = null;
|
|
@@ -51378,12 +51851,10 @@ async function createResolvedAgentSession(options) {
|
|
|
51378
51851
|
};
|
|
51379
51852
|
}
|
|
51380
51853
|
async function promptWithAutoRetry(session, prompt, options) {
|
|
51381
|
-
|
|
51382
|
-
return pwf(session, prompt, options);
|
|
51854
|
+
return promptWithFallback(session, prompt, options);
|
|
51383
51855
|
}
|
|
51384
51856
|
async function describeAgentModel(session) {
|
|
51385
|
-
|
|
51386
|
-
return dm(session);
|
|
51857
|
+
return describeModel(session);
|
|
51387
51858
|
}
|
|
51388
51859
|
var sessionLog;
|
|
51389
51860
|
var init_agent_session_helpers = __esm({
|
|
@@ -51391,6 +51862,7 @@ var init_agent_session_helpers = __esm({
|
|
|
51391
51862
|
"use strict";
|
|
51392
51863
|
init_runtime_resolution();
|
|
51393
51864
|
init_logger2();
|
|
51865
|
+
init_pi();
|
|
51394
51866
|
sessionLog = createLogger2("agent-session");
|
|
51395
51867
|
}
|
|
51396
51868
|
});
|
|
@@ -54967,21 +55439,31 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
|
|
|
54967
55439
|
return false;
|
|
54968
55440
|
}
|
|
54969
55441
|
}
|
|
54970
|
-
|
|
55442
|
+
function resetMergeWithWarn(rootDir, taskId, label) {
|
|
55443
|
+
try {
|
|
55444
|
+
execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
|
|
55445
|
+
} catch (err) {
|
|
55446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55447
|
+
mergerLog.warn(`${taskId}: git reset --merge cleanup failed during ${label}: ${msg}`);
|
|
55448
|
+
}
|
|
55449
|
+
}
|
|
55450
|
+
function buildDeterministicMergeMessage(params) {
|
|
55451
|
+
const { taskId, branch, commitLog, includeTaskId } = params;
|
|
55452
|
+
const prefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
55453
|
+
const subject = `${prefix}: merge ${branch}`;
|
|
55454
|
+
const body = commitLog && commitLog.trim().length > 0 ? commitLog.trim() : `- merge ${branch}`;
|
|
55455
|
+
const escape = (s) => s.replace(/(["\\$`])/g, "\\$1");
|
|
55456
|
+
return {
|
|
55457
|
+
subjectArg: `-m "${escape(subject)}"`,
|
|
55458
|
+
bodyArg: `-m "${escape(body)}"`
|
|
55459
|
+
};
|
|
55460
|
+
}
|
|
55461
|
+
async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, includeTaskId, preAttemptHeadSha, authorArg) {
|
|
54971
55462
|
try {
|
|
54972
|
-
const { stdout: stagedFiles } = await execAsync2("git diff --cached --name-only", {
|
|
54973
|
-
cwd: rootDir,
|
|
54974
|
-
encoding: "utf-8"
|
|
54975
|
-
});
|
|
54976
55463
|
const { stdout: unstagedFiles } = await execAsync2("git diff --name-only", {
|
|
54977
55464
|
cwd: rootDir,
|
|
54978
55465
|
encoding: "utf-8"
|
|
54979
55466
|
});
|
|
54980
|
-
const hasChanges = stagedFiles.trim().length > 0 || unstagedFiles.trim().length > 0;
|
|
54981
|
-
if (!hasChanges) {
|
|
54982
|
-
mergerLog.log(`${taskId}: no changes to amend after verification fix`);
|
|
54983
|
-
return false;
|
|
54984
|
-
}
|
|
54985
55467
|
if (unstagedFiles.trim().length > 0) {
|
|
54986
55468
|
await execAsync2("git add -A", { cwd: rootDir });
|
|
54987
55469
|
}
|
|
@@ -54989,12 +55471,10 @@ async function amendMergeCommitWithFixes(rootDir, taskId, authorArg) {
|
|
|
54989
55471
|
cwd: rootDir,
|
|
54990
55472
|
encoding: "utf-8"
|
|
54991
55473
|
});
|
|
54992
|
-
const gitlinkPaths = [];
|
|
54993
55474
|
for (const line of staged.split("\n")) {
|
|
54994
55475
|
const match = line.match(/^:\d{6} 160000 [^\t]+\t(.+)$/);
|
|
54995
|
-
if (match)
|
|
54996
|
-
|
|
54997
|
-
for (const path2 of gitlinkPaths) {
|
|
55476
|
+
if (!match) continue;
|
|
55477
|
+
const path2 = match[1];
|
|
54998
55478
|
mergerLog.warn(`${taskId}: refusing to stage gitlink "${path2}" (project uses no submodules \u2014 likely a nested worktree). Unstaging.`);
|
|
54999
55479
|
try {
|
|
55000
55480
|
await execAsync2(`git reset HEAD -- "${path2}"`, { cwd: rootDir });
|
|
@@ -55007,15 +55487,43 @@ async function amendMergeCommitWithFixes(rootDir, taskId, authorArg) {
|
|
|
55007
55487
|
cwd: rootDir,
|
|
55008
55488
|
encoding: "utf-8"
|
|
55009
55489
|
});
|
|
55010
|
-
|
|
55011
|
-
|
|
55012
|
-
|
|
55490
|
+
const hasStaged = finalStaged.trim().length > 0;
|
|
55491
|
+
const { stdout: currentHeadOut } = await execAsync2("git rev-parse HEAD", {
|
|
55492
|
+
cwd: rootDir,
|
|
55493
|
+
encoding: "utf-8"
|
|
55494
|
+
});
|
|
55495
|
+
const currentHead = currentHeadOut.trim();
|
|
55496
|
+
const headMoved = currentHead !== preAttemptHeadSha;
|
|
55497
|
+
if (!hasStaged && !headMoved) {
|
|
55498
|
+
mergerLog.warn(
|
|
55499
|
+
`${taskId}: refusing to record merge \u2014 no commit was created and no changes are staged. This usually means the AI agent never ran git commit and the in-merge fix had nothing to add.`
|
|
55500
|
+
);
|
|
55501
|
+
return false;
|
|
55502
|
+
}
|
|
55503
|
+
const { subjectArg, bodyArg } = buildDeterministicMergeMessage({
|
|
55504
|
+
taskId,
|
|
55505
|
+
branch,
|
|
55506
|
+
commitLog,
|
|
55507
|
+
includeTaskId
|
|
55508
|
+
});
|
|
55509
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
55510
|
+
if (!headMoved) {
|
|
55511
|
+
await execAsync2(
|
|
55512
|
+
`git commit ${subjectArg} ${bodyArg}${trailerArg}${authorArg}`,
|
|
55513
|
+
{ cwd: rootDir }
|
|
55514
|
+
);
|
|
55515
|
+
mergerLog.log(`${taskId}: created fresh merge commit after verification fix (no prior commit to amend)`);
|
|
55013
55516
|
return true;
|
|
55014
55517
|
}
|
|
55015
|
-
|
|
55518
|
+
await execAsync2(
|
|
55519
|
+
`git commit --amend ${subjectArg} ${bodyArg}${trailerArg}${authorArg}`,
|
|
55520
|
+
{ cwd: rootDir }
|
|
55521
|
+
);
|
|
55522
|
+
mergerLog.log(`${taskId}: amended merge commit with verification fixes (deterministic message)`);
|
|
55523
|
+
return true;
|
|
55016
55524
|
} catch (err) {
|
|
55017
55525
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
55018
|
-
mergerLog.warn(`${taskId}: failed to
|
|
55526
|
+
mergerLog.warn(`${taskId}: failed to finalize merge commit: ${errorMessage}`);
|
|
55019
55527
|
return false;
|
|
55020
55528
|
}
|
|
55021
55529
|
}
|
|
@@ -55114,24 +55622,26 @@ async function resolveTaskDiffBaseRef({
|
|
|
55114
55622
|
baseBranch,
|
|
55115
55623
|
baseCommitSha
|
|
55116
55624
|
}) {
|
|
55117
|
-
const resolvedBaseBranch = baseBranch?.trim() || "main";
|
|
55625
|
+
const resolvedBaseBranch = baseBranch?.trim() || (baseCommitSha ? void 0 : "main");
|
|
55118
55626
|
const quotedHeadRef = quoteArg(headRef);
|
|
55119
55627
|
let mergeBase;
|
|
55120
|
-
|
|
55628
|
+
if (resolvedBaseBranch) {
|
|
55121
55629
|
try {
|
|
55122
|
-
|
|
55123
|
-
|
|
55124
|
-
|
|
55125
|
-
|
|
55126
|
-
|
|
55630
|
+
try {
|
|
55631
|
+
const { stdout } = await execAsync2(`git merge-base ${quotedHeadRef} ${quoteArg(resolvedBaseBranch)}`, {
|
|
55632
|
+
cwd,
|
|
55633
|
+
encoding: "utf-8"
|
|
55634
|
+
});
|
|
55635
|
+
mergeBase = stdout.trim() || void 0;
|
|
55636
|
+
} catch {
|
|
55637
|
+
const { stdout } = await execAsync2(`git merge-base ${quotedHeadRef} ${quoteArg(`origin/${resolvedBaseBranch}`)}`, {
|
|
55638
|
+
cwd,
|
|
55639
|
+
encoding: "utf-8"
|
|
55640
|
+
});
|
|
55641
|
+
mergeBase = stdout.trim() || void 0;
|
|
55642
|
+
}
|
|
55127
55643
|
} catch {
|
|
55128
|
-
const { stdout } = await execAsync2(`git merge-base ${quotedHeadRef} ${quoteArg(`origin/${resolvedBaseBranch}`)}`, {
|
|
55129
|
-
cwd,
|
|
55130
|
-
encoding: "utf-8"
|
|
55131
|
-
});
|
|
55132
|
-
mergeBase = stdout.trim() || void 0;
|
|
55133
55644
|
}
|
|
55134
|
-
} catch {
|
|
55135
55645
|
}
|
|
55136
55646
|
if (mergeBase) {
|
|
55137
55647
|
try {
|
|
@@ -55238,12 +55748,37 @@ async function resolveTrivialWhitespace(filePath, cwd) {
|
|
|
55238
55748
|
throw new Error(`Failed to auto-resolve ${filePath} trivial conflict: ${error}`);
|
|
55239
55749
|
}
|
|
55240
55750
|
}
|
|
55751
|
+
function buildTaskIdTrailerArg(taskId) {
|
|
55752
|
+
return ` -m "${FUSION_TASK_ID_TRAILER_KEY}: ${taskId}"`;
|
|
55753
|
+
}
|
|
55754
|
+
async function ensureTaskIdTrailerOnHead(rootDir, taskId) {
|
|
55755
|
+
try {
|
|
55756
|
+
const { stdout: existingMessage } = await execAsync2("git log -1 --pretty=%B", {
|
|
55757
|
+
cwd: rootDir,
|
|
55758
|
+
encoding: "utf-8"
|
|
55759
|
+
});
|
|
55760
|
+
const trailerLine = `${FUSION_TASK_ID_TRAILER_KEY}: ${taskId}`;
|
|
55761
|
+
if (existingMessage.includes(trailerLine)) return;
|
|
55762
|
+
await execAsync2(
|
|
55763
|
+
`git -c trailer.ifExists=addIfDifferent commit --amend --no-edit --trailer "${trailerLine}"`,
|
|
55764
|
+
{ cwd: rootDir }
|
|
55765
|
+
);
|
|
55766
|
+
} catch (err) {
|
|
55767
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55768
|
+
mergerLog.warn(`${taskId}: failed to add ${FUSION_TASK_ID_TRAILER_KEY} trailer to HEAD (${msg}) \u2014 relying on subject grep for recovery`);
|
|
55769
|
+
}
|
|
55770
|
+
}
|
|
55241
55771
|
function getCommitAuthorArg(settings) {
|
|
55242
55772
|
if (settings.commitAuthorEnabled === false) return "";
|
|
55243
55773
|
const name = settings.commitAuthorName || "Fusion";
|
|
55244
55774
|
const email = settings.commitAuthorEmail || "noreply@runfusion.ai";
|
|
55245
55775
|
return ` --author="${name} <${email}>"`;
|
|
55246
55776
|
}
|
|
55777
|
+
function buildSourceIssueRef(sourceIssue) {
|
|
55778
|
+
if (!sourceIssue || sourceIssue.provider !== "github") return "";
|
|
55779
|
+
if (!sourceIssue.repository || !sourceIssue.issueNumber) return "";
|
|
55780
|
+
return `${sourceIssue.repository}#${sourceIssue.issueNumber}`;
|
|
55781
|
+
}
|
|
55247
55782
|
function buildMergeSystemPrompt(includeTaskId, agentPrompts, authorArg) {
|
|
55248
55783
|
const commitFormat = includeTaskId ? `\`\`\`
|
|
55249
55784
|
git commit -m "<type>(<scope>): <summary>" -m "<body>"${authorArg || ""}
|
|
@@ -55254,6 +55789,7 @@ Message format:
|
|
|
55254
55789
|
- **Scope:** the task ID (e.g., KB-001)
|
|
55255
55790
|
- **Summary:** one line describing what the squash brings in (imperative mood)
|
|
55256
55791
|
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
|
|
55792
|
+
- **GitHub reference:** when the prompt includes a source issue reference, add \`Ref: owner/repo#N\` to the commit body
|
|
55257
55793
|
${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
|
|
55258
55794
|
|
|
55259
55795
|
Example:
|
|
@@ -55271,6 +55807,7 @@ Message format:
|
|
|
55271
55807
|
- **Type:** feat, fix, refactor, docs, test, chore
|
|
55272
55808
|
- **Summary:** one line describing what the squash brings in (imperative mood)
|
|
55273
55809
|
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
|
|
55810
|
+
- **GitHub reference:** when the prompt includes a source issue reference, add \`Ref: owner/repo#N\` to the commit body
|
|
55274
55811
|
${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
|
|
55275
55812
|
Do NOT include a scope in the commit message type.
|
|
55276
55813
|
|
|
@@ -55654,6 +56191,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55654
56191
|
throw new Error(`Cannot merge ${taskId}: ${mergeBlocker}`);
|
|
55655
56192
|
}
|
|
55656
56193
|
const branch = task.branch || `fusion/${taskId.toLowerCase()}`;
|
|
56194
|
+
const sourceIssueRef = buildSourceIssueRef(task.sourceIssue);
|
|
55657
56195
|
const worktreePath = task.worktree;
|
|
55658
56196
|
const result = {
|
|
55659
56197
|
task,
|
|
@@ -55676,6 +56214,13 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55676
56214
|
const settings = await store.getSettings();
|
|
55677
56215
|
const includeTaskId = settings.includeTaskIdInCommit !== false;
|
|
55678
56216
|
const smartConflictResolution = (settings.smartConflictResolution ?? settings.autoResolveConflicts) !== false;
|
|
56217
|
+
const mergeConflictStrategy = normalizeMergeConflictStrategy(
|
|
56218
|
+
settings.mergeConflictStrategy
|
|
56219
|
+
);
|
|
56220
|
+
if (mergeConflictStrategy === "smart-prefer-main" || mergeConflictStrategy === "smart-prefer-branch") {
|
|
56221
|
+
await tryFastForwardFromOrigin(rootDir, taskId);
|
|
56222
|
+
}
|
|
56223
|
+
let mergeWasEmpty = false;
|
|
55679
56224
|
try {
|
|
55680
56225
|
execSync(`git rev-parse --verify "${branch}"`, {
|
|
55681
56226
|
cwd: rootDir,
|
|
@@ -55735,6 +56280,57 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55735
56280
|
mergerLog.warn(`${taskId}: unable to verify/checkout main branch \u2014 proceeding on current HEAD`);
|
|
55736
56281
|
}
|
|
55737
56282
|
}
|
|
56283
|
+
let rebaseHappened = false;
|
|
56284
|
+
let preferMainRebaseFailureMessage;
|
|
56285
|
+
if (settings.worktreeRebaseBeforeMerge === false && settings.worktreeRebaseLocalBase === false && mergeConflictStrategy === "smart-prefer-main") {
|
|
56286
|
+
throw new Error(
|
|
56287
|
+
`Incompatible settings for ${taskId}: mergeConflictStrategy="smart-prefer-main" requires at least one of worktreeRebaseBeforeMerge or worktreeRebaseLocalBase to remain enabled. The strategy relies on rebasing the branch onto current main to preserve main's deletions; with both disabled it can silently re-introduce branch-only content. Re-enable a rebase stage or switch to "smart-prefer-branch" / "ai-only".`
|
|
56288
|
+
);
|
|
56289
|
+
}
|
|
56290
|
+
async function runLocalBaseRebase(label) {
|
|
56291
|
+
if (!worktreePath) return;
|
|
56292
|
+
try {
|
|
56293
|
+
const { stdout: localHeadOut } = await execAsync2("git rev-parse HEAD", {
|
|
56294
|
+
cwd: rootDir,
|
|
56295
|
+
encoding: "utf-8"
|
|
56296
|
+
});
|
|
56297
|
+
const localHead = localHeadOut.trim();
|
|
56298
|
+
if (!localHead) return;
|
|
56299
|
+
let alreadyContains = false;
|
|
56300
|
+
try {
|
|
56301
|
+
await execAsync2(`git merge-base --is-ancestor "${localHead}" HEAD`, { cwd: worktreePath });
|
|
56302
|
+
alreadyContains = true;
|
|
56303
|
+
} catch {
|
|
56304
|
+
}
|
|
56305
|
+
if (alreadyContains) {
|
|
56306
|
+
rebaseHappened = true;
|
|
56307
|
+
return;
|
|
56308
|
+
}
|
|
56309
|
+
throwIfAborted(options.signal, taskId);
|
|
56310
|
+
await execAsync2(`git rebase "${localHead}"`, { cwd: worktreePath });
|
|
56311
|
+
rebaseHappened = true;
|
|
56312
|
+
mergerLog.log(`${taskId}: rebased ${branch} onto local HEAD ${localHead.slice(0, 8)}${label ? ` (${label})` : ""}`);
|
|
56313
|
+
await store.appendAgentLog(
|
|
56314
|
+
taskId,
|
|
56315
|
+
`Pre-merge rebase: ${branch} \u2192 local HEAD ${localHead.slice(0, 8)}${label ? ` (${label})` : ""}`,
|
|
56316
|
+
"text",
|
|
56317
|
+
void 0,
|
|
56318
|
+
"merger"
|
|
56319
|
+
);
|
|
56320
|
+
} catch (localRebaseErr) {
|
|
56321
|
+
rethrowIfMergeAborted(localRebaseErr);
|
|
56322
|
+
const lmsg = localRebaseErr instanceof Error ? localRebaseErr.message : String(localRebaseErr);
|
|
56323
|
+
mergerLog.warn(`${taskId}: pre-merge rebase onto local HEAD failed (${lmsg}) \u2014 aborting and falling through`);
|
|
56324
|
+
try {
|
|
56325
|
+
await execAsync2("git rebase --abort", { cwd: worktreePath });
|
|
56326
|
+
} catch (abortError) {
|
|
56327
|
+
mergerLog.warn(`${taskId}: failed to abort local-HEAD rebase: ${getCommandErrorMessage(abortError)}`);
|
|
56328
|
+
}
|
|
56329
|
+
if (mergeConflictStrategy === "smart-prefer-main" && !preferMainRebaseFailureMessage) {
|
|
56330
|
+
preferMainRebaseFailureMessage = `Pre-merge rebase onto local HEAD aborted (${lmsg})`;
|
|
56331
|
+
}
|
|
56332
|
+
}
|
|
56333
|
+
}
|
|
55738
56334
|
if (settings.worktreeRebaseBeforeMerge !== false) {
|
|
55739
56335
|
try {
|
|
55740
56336
|
let remote = settings.worktreeRebaseRemote?.trim();
|
|
@@ -55769,7 +56365,9 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55769
56365
|
}
|
|
55770
56366
|
}
|
|
55771
56367
|
if (!remote) {
|
|
55772
|
-
mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping
|
|
56368
|
+
mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping remote rebase stage (local-base stage may still run)`);
|
|
56369
|
+
} else if (!worktreePath) {
|
|
56370
|
+
mergerLog.warn(`${taskId}: no worktreePath \u2014 skipping remote rebase stage`);
|
|
55773
56371
|
} else {
|
|
55774
56372
|
throwIfAborted(options.signal, taskId);
|
|
55775
56373
|
mergerLog.log(`${taskId}: fetching ${remote} before merge`);
|
|
@@ -55781,17 +56379,21 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55781
56379
|
);
|
|
55782
56380
|
const mainBranch = mainBranchOut.trim();
|
|
55783
56381
|
const remoteRef = `${remote}/${mainBranch}`;
|
|
55784
|
-
|
|
55785
|
-
|
|
55786
|
-
|
|
55787
|
-
|
|
55788
|
-
|
|
55789
|
-
|
|
55790
|
-
|
|
56382
|
+
throwIfAborted(options.signal, taskId);
|
|
56383
|
+
await execAsync2(`git rebase "${remoteRef}"`, { cwd: worktreePath });
|
|
56384
|
+
rebaseHappened = true;
|
|
56385
|
+
mergerLog.log(`${taskId}: rebased ${branch} onto ${remoteRef}`);
|
|
56386
|
+
await store.appendAgentLog(
|
|
56387
|
+
taskId,
|
|
56388
|
+
`Pre-merge rebase: ${branch} \u2192 ${remoteRef}`,
|
|
56389
|
+
"text",
|
|
56390
|
+
void 0,
|
|
56391
|
+
"merger"
|
|
56392
|
+
);
|
|
55791
56393
|
} catch (rebaseErr) {
|
|
55792
56394
|
rethrowIfMergeAborted(rebaseErr);
|
|
55793
56395
|
const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
|
|
55794
|
-
mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through
|
|
56396
|
+
mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through`);
|
|
55795
56397
|
if (worktreePath) {
|
|
55796
56398
|
try {
|
|
55797
56399
|
await execAsync2("git rebase --abort", { cwd: worktreePath });
|
|
@@ -55799,14 +56401,32 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55799
56401
|
mergerLog.warn(`${taskId}: failed to abort pre-merge rebase: ${getCommandErrorMessage(abortError)}`);
|
|
55800
56402
|
}
|
|
55801
56403
|
}
|
|
56404
|
+
if (mergeConflictStrategy === "smart-prefer-main") {
|
|
56405
|
+
preferMainRebaseFailureMessage = `Pre-merge rebase onto remote main aborted (${msg})`;
|
|
56406
|
+
}
|
|
55802
56407
|
}
|
|
55803
56408
|
}
|
|
55804
56409
|
} catch (err) {
|
|
55805
56410
|
rethrowIfMergeAborted(err);
|
|
55806
56411
|
const msg = err instanceof Error ? err.message : String(err);
|
|
55807
|
-
mergerLog.warn(`${taskId}: pre-merge rebase pipeline failed (${msg}) \u2014 proceeding without rebase`);
|
|
56412
|
+
mergerLog.warn(`${taskId}: pre-merge remote rebase pipeline failed (${msg}) \u2014 proceeding without remote rebase`);
|
|
55808
56413
|
}
|
|
55809
56414
|
}
|
|
56415
|
+
if (settings.worktreeRebaseLocalBase !== false && !preferMainRebaseFailureMessage) {
|
|
56416
|
+
await runLocalBaseRebase(
|
|
56417
|
+
settings.worktreeRebaseBeforeMerge === false ? "remote rebase disabled" : ""
|
|
56418
|
+
);
|
|
56419
|
+
}
|
|
56420
|
+
if (preferMainRebaseFailureMessage) {
|
|
56421
|
+
throw new Error(
|
|
56422
|
+
`${preferMainRebaseFailureMessage} for ${taskId}. Strategy "smart-prefer-main" requires a successful rebase to preserve main's deletions; falling through to a -X ours merge would silently re-introduce branch-only content. Resolve the rebase conflict manually, or switch mergeConflictStrategy to "smart-prefer-branch" / "ai-only".`
|
|
56423
|
+
);
|
|
56424
|
+
}
|
|
56425
|
+
if (mergeConflictStrategy === "smart-prefer-main" && !rebaseHappened) {
|
|
56426
|
+
mergerLog.warn(
|
|
56427
|
+
`${taskId}: smart-prefer-main ran without a successful pre-merge rebase (${worktreePath ? "no remote resolvable or rebase disabled" : "no worktreePath"}). Main's deletions may not be preserved if the branch re-introduces them.`
|
|
56428
|
+
);
|
|
56429
|
+
}
|
|
55810
56430
|
const diffBaseRef = await resolveTaskDiffBaseRef({
|
|
55811
56431
|
cwd: rootDir,
|
|
55812
56432
|
headRef: branch,
|
|
@@ -55867,6 +56487,25 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55867
56487
|
}
|
|
55868
56488
|
const mergeAttempt = async (attemptNum) => {
|
|
55869
56489
|
mergerLog.log(`${taskId}: merge attempt ${attemptNum}/3...`);
|
|
56490
|
+
const attemptLabel = attemptNum === 1 ? "Attempt 1: AI merge" : attemptNum === 2 ? "Attempt 2: auto-resolve known conflicts, then AI" : `Attempt 3: ${mergeConflictStrategy === "smart-prefer-main" ? "-X ours" : "-X theirs"} fallback`;
|
|
56491
|
+
await store.appendAgentLog(
|
|
56492
|
+
taskId,
|
|
56493
|
+
`Starting merge ${attemptLabel}`,
|
|
56494
|
+
"text",
|
|
56495
|
+
void 0,
|
|
56496
|
+
"merger"
|
|
56497
|
+
);
|
|
56498
|
+
let preAttemptHeadSha = "";
|
|
56499
|
+
try {
|
|
56500
|
+
const { stdout } = await execAsync2("git rev-parse HEAD", {
|
|
56501
|
+
cwd: rootDir,
|
|
56502
|
+
encoding: "utf-8"
|
|
56503
|
+
});
|
|
56504
|
+
preAttemptHeadSha = stdout.trim();
|
|
56505
|
+
} catch (err) {
|
|
56506
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56507
|
+
mergerLog.warn(`${taskId}: failed to capture pre-attempt HEAD (${msg}) \u2014 verification-fix finalizer will fall back to amend`);
|
|
56508
|
+
}
|
|
55870
56509
|
try {
|
|
55871
56510
|
const success = await executeMergeAttempt({
|
|
55872
56511
|
store,
|
|
@@ -55876,7 +56515,9 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55876
56515
|
commitLog,
|
|
55877
56516
|
diffStat,
|
|
55878
56517
|
includeTaskId,
|
|
56518
|
+
sourceIssueRef,
|
|
55879
56519
|
smartConflictResolution,
|
|
56520
|
+
mergeConflictStrategy,
|
|
55880
56521
|
attemptNum,
|
|
55881
56522
|
options,
|
|
55882
56523
|
result,
|
|
@@ -55888,7 +56529,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55888
56529
|
}, aiTracker);
|
|
55889
56530
|
if (success) {
|
|
55890
56531
|
result.attemptsMade = attemptNum;
|
|
55891
|
-
result.resolutionStrategy = getResolutionStrategy(attemptNum, smartConflictResolution);
|
|
56532
|
+
result.resolutionStrategy = getResolutionStrategy(attemptNum, smartConflictResolution, mergeConflictStrategy);
|
|
55892
56533
|
result.resolutionMethod = getResolutionMethod(result.resolutionStrategy, result.autoResolvedCount, aiTracker.aiWasInvoked);
|
|
55893
56534
|
result.merged = true;
|
|
55894
56535
|
return true;
|
|
@@ -55983,12 +56624,27 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55983
56624
|
}
|
|
55984
56625
|
if (fixSuccess) {
|
|
55985
56626
|
const authorArg = getCommitAuthorArg(settings);
|
|
55986
|
-
await
|
|
56627
|
+
const finalized = await commitOrAmendMergeWithFixes(
|
|
56628
|
+
rootDir,
|
|
56629
|
+
taskId,
|
|
56630
|
+
branch,
|
|
56631
|
+
commitLog,
|
|
56632
|
+
includeTaskId,
|
|
56633
|
+
preAttemptHeadSha,
|
|
56634
|
+
authorArg
|
|
56635
|
+
);
|
|
56636
|
+
if (!finalized) {
|
|
56637
|
+
resetMergeWithWarn(rootDir, taskId, "verification-fix finalize");
|
|
56638
|
+
throw new Error(
|
|
56639
|
+
`${taskId}: verification fix succeeded but no merge commit could be created \u2014 refusing to mark merge complete.`
|
|
56640
|
+
);
|
|
56641
|
+
}
|
|
55987
56642
|
return true;
|
|
55988
56643
|
}
|
|
55989
56644
|
}
|
|
55990
56645
|
}
|
|
55991
56646
|
mergerLog.error(`${taskId}: deterministic verification failed \u2014 aborting merge (in-merge fix exhausted or disabled)`);
|
|
56647
|
+
resetMergeWithWarn(rootDir, taskId, "deterministic-verification rollback");
|
|
55992
56648
|
throw error;
|
|
55993
56649
|
}
|
|
55994
56650
|
if (error.message?.includes("Build verification failed")) {
|
|
@@ -56059,7 +56715,21 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56059
56715
|
}
|
|
56060
56716
|
if (fixSuccess) {
|
|
56061
56717
|
const authorArg = getCommitAuthorArg(settings);
|
|
56062
|
-
await
|
|
56718
|
+
const finalized = await commitOrAmendMergeWithFixes(
|
|
56719
|
+
rootDir,
|
|
56720
|
+
taskId,
|
|
56721
|
+
branch,
|
|
56722
|
+
commitLog,
|
|
56723
|
+
includeTaskId,
|
|
56724
|
+
preAttemptHeadSha,
|
|
56725
|
+
authorArg
|
|
56726
|
+
);
|
|
56727
|
+
if (!finalized) {
|
|
56728
|
+
resetMergeWithWarn(rootDir, taskId, "build-verification fix finalize");
|
|
56729
|
+
throw new Error(
|
|
56730
|
+
`${taskId}: build verification fix succeeded but no merge commit could be created \u2014 refusing to mark merge complete.`
|
|
56731
|
+
);
|
|
56732
|
+
}
|
|
56063
56733
|
return true;
|
|
56064
56734
|
}
|
|
56065
56735
|
}
|
|
@@ -56073,10 +56743,18 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56073
56743
|
await audit.git({ type: "reset:hard", target: branch, metadata: { purpose: "build-retry" } });
|
|
56074
56744
|
} catch (err) {
|
|
56075
56745
|
const msg = err instanceof Error ? err.message : String(err);
|
|
56076
|
-
mergerLog.warn(`${taskId}: git reset --merge cleanup failed (build-retry): ${msg}`);
|
|
56746
|
+
mergerLog.warn(`${taskId}: git reset --merge cleanup failed during build-verification rollback (build-retry): ${msg}`);
|
|
56077
56747
|
}
|
|
56078
56748
|
return false;
|
|
56079
56749
|
}
|
|
56750
|
+
resetMergeWithWarn(rootDir, taskId, "build-verification rollback (no retries left)");
|
|
56751
|
+
throw error;
|
|
56752
|
+
}
|
|
56753
|
+
if (error.name === "MergeNonConflictError") {
|
|
56754
|
+
try {
|
|
56755
|
+
execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
|
|
56756
|
+
} catch {
|
|
56757
|
+
}
|
|
56080
56758
|
throw error;
|
|
56081
56759
|
}
|
|
56082
56760
|
if (attemptNum < 3 && smartConflictResolution) {
|
|
@@ -56096,12 +56774,15 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56096
56774
|
const aiTracker = { aiWasInvoked: false };
|
|
56097
56775
|
let merged = false;
|
|
56098
56776
|
merged = await mergeAttempt(1);
|
|
56099
|
-
if (!merged && smartConflictResolution) {
|
|
56777
|
+
if (!merged && smartConflictResolution && mergeConflictStrategy !== "abort") {
|
|
56100
56778
|
merged = await mergeAttempt(2);
|
|
56101
56779
|
}
|
|
56102
|
-
if (!merged && smartConflictResolution) {
|
|
56780
|
+
if (!merged && smartConflictResolution && mergeConflictStrategy !== "ai-only" && mergeConflictStrategy !== "abort") {
|
|
56103
56781
|
merged = await mergeAttempt(3);
|
|
56104
56782
|
}
|
|
56783
|
+
if (aiTracker.mergeWasEmpty) {
|
|
56784
|
+
mergeWasEmpty = true;
|
|
56785
|
+
}
|
|
56105
56786
|
if (!merged) {
|
|
56106
56787
|
try {
|
|
56107
56788
|
execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
|
|
@@ -56109,6 +56790,10 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56109
56790
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
56110
56791
|
mergerLog.warn(`${taskId}: git reset --merge cleanup failed: ${errorMessage}`);
|
|
56111
56792
|
}
|
|
56793
|
+
if (mergeConflictStrategy === "abort") {
|
|
56794
|
+
result.resolutionStrategy = "abort";
|
|
56795
|
+
throw new Error(`Merge conflict for ${taskId}: aborted per mergeConflictStrategy="abort" \u2014 manual resolution required`);
|
|
56796
|
+
}
|
|
56112
56797
|
throw new Error(`AI merge failed for ${taskId}: all 3 attempts exhausted`);
|
|
56113
56798
|
}
|
|
56114
56799
|
try {
|
|
@@ -56135,17 +56820,24 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56135
56820
|
} catch {
|
|
56136
56821
|
}
|
|
56137
56822
|
const isEmptyCommit = filesChanged === 0;
|
|
56138
|
-
const recordedSha = isEmptyCommit ? void 0 : commitSha;
|
|
56823
|
+
const recordedSha = isEmptyCommit || mergeWasEmpty ? void 0 : commitSha;
|
|
56139
56824
|
if (isEmptyCommit) {
|
|
56140
56825
|
mergerLog.warn(
|
|
56141
56826
|
`${taskId}: local squash produced an empty commit (${commitSha?.slice(0, 8)}) \u2014 branch likely contained dupes of main. Skipping commitSha; recovery will backfill when real commit lands.`
|
|
56142
56827
|
);
|
|
56828
|
+
} else if (mergeWasEmpty) {
|
|
56829
|
+
mergerLog.warn(
|
|
56830
|
+
`${taskId}: merge succeeded without committing (branch already on main). Skipping commitSha; nothing new landed locally.`
|
|
56831
|
+
);
|
|
56143
56832
|
}
|
|
56833
|
+
const recordedFilesChanged = mergeWasEmpty ? 0 : filesChanged;
|
|
56834
|
+
const recordedInsertions = mergeWasEmpty ? 0 : insertions;
|
|
56835
|
+
const recordedDeletions = mergeWasEmpty ? 0 : deletions;
|
|
56144
56836
|
const mergeDetails = {
|
|
56145
56837
|
commitSha: recordedSha,
|
|
56146
|
-
filesChanged,
|
|
56147
|
-
insertions,
|
|
56148
|
-
deletions,
|
|
56838
|
+
filesChanged: recordedFilesChanged,
|
|
56839
|
+
insertions: recordedInsertions,
|
|
56840
|
+
deletions: recordedDeletions,
|
|
56149
56841
|
mergeCommitMessage: commitLog,
|
|
56150
56842
|
mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56151
56843
|
mergeConfirmed: true,
|
|
@@ -56156,6 +56848,26 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56156
56848
|
};
|
|
56157
56849
|
await store.updateTask(taskId, { mergeDetails });
|
|
56158
56850
|
mergerLog.log(`${taskId}: merge details stored (commitSha: ${recordedSha?.slice(0, 8) ?? "<deferred>"})`);
|
|
56851
|
+
const summaryParts = [
|
|
56852
|
+
`Merge completed via ${result.resolutionStrategy ?? "unknown"} (attempt ${result.attemptsMade ?? "?"}/3)`
|
|
56853
|
+
];
|
|
56854
|
+
if (recordedSha) {
|
|
56855
|
+
summaryParts.push(`commit ${recordedSha.slice(0, 8)}`);
|
|
56856
|
+
} else if (mergeWasEmpty) {
|
|
56857
|
+
summaryParts.push("no commit landed (branch already on main)");
|
|
56858
|
+
} else if (isEmptyCommit) {
|
|
56859
|
+
summaryParts.push("squash collapsed to empty (sha deferred)");
|
|
56860
|
+
}
|
|
56861
|
+
if (!mergeWasEmpty && filesChanged !== void 0) {
|
|
56862
|
+
summaryParts.push(`${filesChanged} file${filesChanged === 1 ? "" : "s"} changed (+${insertions ?? 0}/-${deletions ?? 0})`);
|
|
56863
|
+
}
|
|
56864
|
+
await store.appendAgentLog(
|
|
56865
|
+
taskId,
|
|
56866
|
+
summaryParts.join(" \xB7 "),
|
|
56867
|
+
"text",
|
|
56868
|
+
void 0,
|
|
56869
|
+
"merger"
|
|
56870
|
+
);
|
|
56159
56871
|
} catch (err) {
|
|
56160
56872
|
mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
|
|
56161
56873
|
}
|
|
@@ -56250,6 +56962,27 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56250
56962
|
});
|
|
56251
56963
|
if (pushResult.pushed) {
|
|
56252
56964
|
mergerLog.log(`${taskId}: pushed merged result to remote`);
|
|
56965
|
+
try {
|
|
56966
|
+
const postPushSha = execSync("git rev-parse HEAD", {
|
|
56967
|
+
cwd: rootDir,
|
|
56968
|
+
stdio: "pipe",
|
|
56969
|
+
encoding: "utf-8"
|
|
56970
|
+
}).trim() || void 0;
|
|
56971
|
+
if (postPushSha) {
|
|
56972
|
+
const existingTask = await store.getTask(taskId).catch(() => null);
|
|
56973
|
+
const existingDetails = existingTask?.mergeDetails;
|
|
56974
|
+
if (existingDetails?.commitSha && existingDetails.commitSha !== postPushSha) {
|
|
56975
|
+
await store.updateTask(taskId, {
|
|
56976
|
+
mergeDetails: { ...existingDetails, commitSha: postPushSha }
|
|
56977
|
+
});
|
|
56978
|
+
mergerLog.log(
|
|
56979
|
+
`${taskId}: post-push HEAD changed from ${existingDetails.commitSha.slice(0, 8)} to ${postPushSha.slice(0, 8)} \u2014 refreshed mergeDetails.commitSha`
|
|
56980
|
+
);
|
|
56981
|
+
}
|
|
56982
|
+
}
|
|
56983
|
+
} catch (refreshErr) {
|
|
56984
|
+
mergerLog.warn(`${taskId}: failed to refresh mergeDetails after push: ${refreshErr.message}`);
|
|
56985
|
+
}
|
|
56253
56986
|
} else {
|
|
56254
56987
|
mergerLog.warn(`${taskId}: push to remote failed: ${pushResult.error}`);
|
|
56255
56988
|
}
|
|
@@ -56277,18 +57010,74 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56277
57010
|
await completeTask(store, taskId, result);
|
|
56278
57011
|
return result;
|
|
56279
57012
|
}
|
|
56280
|
-
function
|
|
57013
|
+
async function tryFastForwardFromOrigin(rootDir, taskId) {
|
|
57014
|
+
let currentBranch;
|
|
57015
|
+
try {
|
|
57016
|
+
currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
57017
|
+
cwd: rootDir,
|
|
57018
|
+
encoding: "utf-8",
|
|
57019
|
+
stdio: "pipe"
|
|
57020
|
+
}).trim();
|
|
57021
|
+
} catch {
|
|
57022
|
+
return;
|
|
57023
|
+
}
|
|
57024
|
+
if (!currentBranch || currentBranch === "HEAD") return;
|
|
57025
|
+
try {
|
|
57026
|
+
await execAsync2(`git fetch origin "${currentBranch}"`, { cwd: rootDir });
|
|
57027
|
+
} catch (err) {
|
|
57028
|
+
mergerLog.log(`${taskId}: pre-merge fetch failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
57029
|
+
return;
|
|
57030
|
+
}
|
|
57031
|
+
let behind = 0;
|
|
57032
|
+
let ahead = 0;
|
|
57033
|
+
try {
|
|
57034
|
+
const counts = execSync(`git rev-list --left-right --count "origin/${currentBranch}...HEAD"`, {
|
|
57035
|
+
cwd: rootDir,
|
|
57036
|
+
encoding: "utf-8",
|
|
57037
|
+
stdio: "pipe"
|
|
57038
|
+
}).trim();
|
|
57039
|
+
const [b, a] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
57040
|
+
behind = b;
|
|
57041
|
+
ahead = a;
|
|
57042
|
+
} catch {
|
|
57043
|
+
return;
|
|
57044
|
+
}
|
|
57045
|
+
if (behind === 0) return;
|
|
57046
|
+
if (ahead > 0) {
|
|
57047
|
+
mergerLog.log(`${taskId}: local ${currentBranch} has ${ahead} unpushed commit(s); skipping fast-forward`);
|
|
57048
|
+
return;
|
|
57049
|
+
}
|
|
57050
|
+
try {
|
|
57051
|
+
await execAsync2(`git merge --ff-only "origin/${currentBranch}"`, { cwd: rootDir });
|
|
57052
|
+
mergerLog.log(`${taskId}: fast-forwarded ${currentBranch} by ${behind} commit(s) from origin`);
|
|
57053
|
+
} catch (err) {
|
|
57054
|
+
mergerLog.log(`${taskId}: fast-forward failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
57055
|
+
}
|
|
57056
|
+
}
|
|
57057
|
+
function getResolutionStrategy(attemptNum, smartConflictResolution, mergeConflictStrategy = "smart-prefer-main") {
|
|
56281
57058
|
if (!smartConflictResolution || attemptNum === 1) {
|
|
56282
57059
|
return "ai";
|
|
56283
57060
|
}
|
|
56284
57061
|
if (attemptNum === 2) {
|
|
56285
57062
|
return "auto-resolve";
|
|
56286
57063
|
}
|
|
56287
|
-
|
|
57064
|
+
switch (mergeConflictStrategy) {
|
|
57065
|
+
case "ai-only":
|
|
57066
|
+
return "ai";
|
|
57067
|
+
case "smart-prefer-main":
|
|
57068
|
+
return "ours";
|
|
57069
|
+
case "abort":
|
|
57070
|
+
return "abort";
|
|
57071
|
+
case "smart-prefer-branch":
|
|
57072
|
+
default:
|
|
57073
|
+
return "theirs";
|
|
57074
|
+
}
|
|
56288
57075
|
}
|
|
56289
57076
|
function getResolutionMethod(strategy, autoResolvedCount, aiWasUsed) {
|
|
56290
57077
|
if (strategy === "ai") return "ai";
|
|
56291
57078
|
if (strategy === "theirs") return "theirs";
|
|
57079
|
+
if (strategy === "ours") return "ours";
|
|
57080
|
+
if (strategy === "abort") return "abort";
|
|
56292
57081
|
if (strategy === "auto-resolve") {
|
|
56293
57082
|
if (autoResolvedCount && autoResolvedCount > 0) {
|
|
56294
57083
|
return aiWasUsed ? "mixed" : "auto";
|
|
@@ -56306,6 +57095,7 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56306
57095
|
commitLog,
|
|
56307
57096
|
diffStat,
|
|
56308
57097
|
includeTaskId,
|
|
57098
|
+
sourceIssueRef,
|
|
56309
57099
|
smartConflictResolution,
|
|
56310
57100
|
attemptNum,
|
|
56311
57101
|
options,
|
|
@@ -56317,12 +57107,15 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56317
57107
|
buildSource
|
|
56318
57108
|
} = params;
|
|
56319
57109
|
if (attemptNum === 3) {
|
|
56320
|
-
|
|
57110
|
+
if (params.mergeConflictStrategy === "smart-prefer-main") {
|
|
57111
|
+
return attemptWithSideStrategy(params, "ours", aiTracker);
|
|
57112
|
+
}
|
|
57113
|
+
return attemptWithSideStrategy(params, "theirs", aiTracker);
|
|
56321
57114
|
}
|
|
56322
57115
|
let hasConflicts = false;
|
|
56323
57116
|
try {
|
|
56324
57117
|
if (attemptNum === 2 && smartConflictResolution) {
|
|
56325
|
-
let
|
|
57118
|
+
let mergeError;
|
|
56326
57119
|
try {
|
|
56327
57120
|
await execAsync2(`git merge --squash "${branch}"`, {
|
|
56328
57121
|
cwd: rootDir
|
|
@@ -56330,9 +57123,18 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56330
57123
|
throwIfAborted(options.signal, taskId);
|
|
56331
57124
|
} catch (error) {
|
|
56332
57125
|
rethrowIfMergeAborted(error);
|
|
56333
|
-
|
|
57126
|
+
mergeError = error;
|
|
56334
57127
|
}
|
|
56335
57128
|
const conflictedFiles = await getConflictedFiles(rootDir);
|
|
57129
|
+
if (mergeError && conflictedFiles.length === 0) {
|
|
57130
|
+
const cause = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
|
57131
|
+
const fatal = new Error(
|
|
57132
|
+
`${taskId}: git merge --squash failed without producing conflicts (${cause}) \u2014 refusing to treat as a no-op merge.`
|
|
57133
|
+
);
|
|
57134
|
+
fatal.name = "MergeNonConflictError";
|
|
57135
|
+
throw fatal;
|
|
57136
|
+
}
|
|
57137
|
+
const mergeExitedWithConflicts = mergeError !== void 0;
|
|
56336
57138
|
if (conflictedFiles.length > 0 || mergeExitedWithConflicts) {
|
|
56337
57139
|
const classified = [];
|
|
56338
57140
|
for (const file of conflictedFiles) {
|
|
@@ -56375,11 +57177,14 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56375
57177
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
|
56376
57178
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56377
57179
|
const authorArg = getCommitAuthorArg(settings);
|
|
57180
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
56378
57181
|
await execAsync2(
|
|
56379
|
-
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${authorArg}`,
|
|
57182
|
+
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${trailerArg}${authorArg}`,
|
|
56380
57183
|
{ cwd: rootDir }
|
|
56381
57184
|
);
|
|
56382
57185
|
mergerLog.log(`${taskId}: committed after auto-resolving all conflicts`);
|
|
57186
|
+
} else {
|
|
57187
|
+
aiTracker.mergeWasEmpty = true;
|
|
56383
57188
|
}
|
|
56384
57189
|
if (testCommand || buildCommand2) {
|
|
56385
57190
|
throwIfAborted(options.signal, taskId);
|
|
@@ -56404,6 +57209,7 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56404
57209
|
).trim() === "0";
|
|
56405
57210
|
if (squashIsEmpty) {
|
|
56406
57211
|
mergerLog.log(`${taskId}: squash merge staged nothing \u2014 already merged`);
|
|
57212
|
+
aiTracker.mergeWasEmpty = true;
|
|
56407
57213
|
if (testCommand || buildCommand2) {
|
|
56408
57214
|
throwIfAborted(options.signal, taskId);
|
|
56409
57215
|
await runDeterministicVerification(
|
|
@@ -56431,6 +57237,7 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56431
57237
|
).trim() === "0";
|
|
56432
57238
|
if (squashIsEmpty) {
|
|
56433
57239
|
mergerLog.log(`${taskId}: squash merge staged nothing \u2014 already merged`);
|
|
57240
|
+
aiTracker.mergeWasEmpty = true;
|
|
56434
57241
|
if (testCommand || buildCommand2) {
|
|
56435
57242
|
throwIfAborted(options.signal, taskId);
|
|
56436
57243
|
await runDeterministicVerification(
|
|
@@ -56479,19 +57286,12 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56479
57286
|
simplifiedContext: attemptNum === 2,
|
|
56480
57287
|
options,
|
|
56481
57288
|
testCommand,
|
|
56482
|
-
buildCommand: buildCommand2
|
|
57289
|
+
buildCommand: buildCommand2,
|
|
57290
|
+
sourceIssueRef
|
|
56483
57291
|
});
|
|
56484
57292
|
if (!agentResult.success) {
|
|
56485
57293
|
const errorMessage = agentResult.error || "Build verification failed";
|
|
56486
57294
|
await store.logEntry(taskId, "Build verification failed during merge", errorMessage);
|
|
56487
|
-
try {
|
|
56488
|
-
execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
|
|
56489
|
-
} catch (resetErr) {
|
|
56490
|
-
const msg = resetErr instanceof Error ? resetErr.message : String(resetErr);
|
|
56491
|
-
mergerLog.warn(
|
|
56492
|
-
`${taskId}: git reset --merge cleanup failed during build-verification rollback (build-verification reset, build-retry): ${msg}`
|
|
56493
|
-
);
|
|
56494
|
-
}
|
|
56495
57295
|
throw new Error(`Build verification failed for ${taskId}: ${errorMessage}`);
|
|
56496
57296
|
}
|
|
56497
57297
|
if (testCommand || buildCommand2) {
|
|
@@ -56507,6 +57307,24 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56507
57307
|
options.signal
|
|
56508
57308
|
);
|
|
56509
57309
|
}
|
|
57310
|
+
try {
|
|
57311
|
+
const authorArg = getCommitAuthorArg(params.settings);
|
|
57312
|
+
const { subjectArg, bodyArg } = buildDeterministicMergeMessage({
|
|
57313
|
+
taskId,
|
|
57314
|
+
branch,
|
|
57315
|
+
commitLog,
|
|
57316
|
+
includeTaskId
|
|
57317
|
+
});
|
|
57318
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
57319
|
+
await execAsync2(
|
|
57320
|
+
`git commit --amend ${subjectArg} ${bodyArg}${trailerArg}${authorArg}`,
|
|
57321
|
+
{ cwd: rootDir }
|
|
57322
|
+
);
|
|
57323
|
+
mergerLog.log(`${taskId}: rewrote AI-authored merge commit message with deterministic body`);
|
|
57324
|
+
} catch (err) {
|
|
57325
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57326
|
+
mergerLog.warn(`${taskId}: failed to canonicalize merge commit message (${msg}) \u2014 keeping AI-written message`);
|
|
57327
|
+
}
|
|
56510
57328
|
return true;
|
|
56511
57329
|
} catch (error) {
|
|
56512
57330
|
if (error instanceof Error && error.name === "MergeAbortedError") {
|
|
@@ -56528,12 +57346,12 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56528
57346
|
throw error;
|
|
56529
57347
|
}
|
|
56530
57348
|
}
|
|
56531
|
-
async function
|
|
56532
|
-
const { rootDir, branch, commitLog, includeTaskId, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
|
|
56533
|
-
mergerLog.log(`${taskId}: attempting merge with -X
|
|
57349
|
+
async function attemptWithSideStrategy(params, side = "theirs", aiTracker) {
|
|
57350
|
+
const { rootDir, branch, commitLog, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
|
|
57351
|
+
mergerLog.log(`${taskId}: attempting merge with -X ${side} strategy`);
|
|
56534
57352
|
try {
|
|
56535
57353
|
throwIfAborted(params.options.signal, taskId);
|
|
56536
|
-
await execAsync2(`git merge -X
|
|
57354
|
+
await execAsync2(`git merge -X ${side} --squash "${branch}"`, {
|
|
56537
57355
|
cwd: rootDir
|
|
56538
57356
|
});
|
|
56539
57357
|
const conflictedOutput = execSync("git diff --name-only --diff-filter=U", {
|
|
@@ -56541,7 +57359,7 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56541
57359
|
encoding: "utf-8"
|
|
56542
57360
|
}).trim();
|
|
56543
57361
|
if (conflictedOutput.length > 0) {
|
|
56544
|
-
mergerLog.warn(`${taskId}: -X
|
|
57362
|
+
mergerLog.warn(`${taskId}: -X ${side} left unresolved conflicts: ${conflictedOutput}`);
|
|
56545
57363
|
return false;
|
|
56546
57364
|
}
|
|
56547
57365
|
const staged = execSync("git diff --cached --quiet 2>&1; echo $?", {
|
|
@@ -56549,6 +57367,7 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56549
57367
|
encoding: "utf-8"
|
|
56550
57368
|
}).trim();
|
|
56551
57369
|
if (staged === "0") {
|
|
57370
|
+
if (aiTracker) aiTracker.mergeWasEmpty = true;
|
|
56552
57371
|
if (testCommand || buildCommand2) {
|
|
56553
57372
|
throwIfAborted(params.options.signal, taskId);
|
|
56554
57373
|
await runDeterministicVerification(
|
|
@@ -56568,11 +57387,13 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56568
57387
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
|
56569
57388
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56570
57389
|
const authorArg = getCommitAuthorArg(settings);
|
|
57390
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
57391
|
+
const issueRefBodyArg = sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : "";
|
|
56571
57392
|
await execAsync2(
|
|
56572
|
-
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${authorArg}`,
|
|
57393
|
+
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${issueRefBodyArg}${trailerArg}${authorArg}`,
|
|
56573
57394
|
{ cwd: rootDir }
|
|
56574
57395
|
);
|
|
56575
|
-
mergerLog.log(`${taskId}: committed with -X
|
|
57396
|
+
mergerLog.log(`${taskId}: committed with -X ${side} auto-resolution`);
|
|
56576
57397
|
if (testCommand || buildCommand2) {
|
|
56577
57398
|
throwIfAborted(params.options.signal, taskId);
|
|
56578
57399
|
await runDeterministicVerification(
|
|
@@ -56591,7 +57412,7 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56591
57412
|
if (error instanceof Error && error.name === "MergeAbortedError") {
|
|
56592
57413
|
throw error;
|
|
56593
57414
|
}
|
|
56594
|
-
mergerLog.error(`${taskId}: -X
|
|
57415
|
+
mergerLog.error(`${taskId}: -X ${side} merge failed: ${error}`);
|
|
56595
57416
|
return false;
|
|
56596
57417
|
}
|
|
56597
57418
|
}
|
|
@@ -56606,6 +57427,7 @@ async function runAiAgentForCommit(params) {
|
|
|
56606
57427
|
includeTaskId,
|
|
56607
57428
|
hasConflicts,
|
|
56608
57429
|
simplifiedContext,
|
|
57430
|
+
sourceIssueRef,
|
|
56609
57431
|
options,
|
|
56610
57432
|
testCommand,
|
|
56611
57433
|
buildCommand: buildCommand2
|
|
@@ -56704,7 +57526,8 @@ async function runAiAgentForCommit(params) {
|
|
|
56704
57526
|
simplifiedContext,
|
|
56705
57527
|
testCommand,
|
|
56706
57528
|
buildCommand: buildCommand2,
|
|
56707
|
-
authorArg
|
|
57529
|
+
authorArg,
|
|
57530
|
+
sourceIssueRef
|
|
56708
57531
|
});
|
|
56709
57532
|
mergerLog.log(`${taskId}: starting fresh merge agent session`);
|
|
56710
57533
|
try {
|
|
@@ -56736,7 +57559,8 @@ async function runAiAgentForCommit(params) {
|
|
|
56736
57559
|
// Also skip detailed context
|
|
56737
57560
|
testCommand,
|
|
56738
57561
|
buildCommand: buildCommand2,
|
|
56739
|
-
authorArg
|
|
57562
|
+
authorArg,
|
|
57563
|
+
sourceIssueRef
|
|
56740
57564
|
});
|
|
56741
57565
|
try {
|
|
56742
57566
|
await withRateLimitRetry(async () => {
|
|
@@ -56777,13 +57601,17 @@ async function runAiAgentForCommit(params) {
|
|
|
56777
57601
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
|
56778
57602
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56779
57603
|
const authorArg2 = getCommitAuthorArg(settings);
|
|
57604
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
57605
|
+
const issueRefBodyArg = sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : "";
|
|
56780
57606
|
await execAsync2(
|
|
56781
|
-
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${authorArg2}`,
|
|
57607
|
+
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${issueRefBodyArg}${trailerArg}${authorArg2}`,
|
|
56782
57608
|
{ cwd: rootDir }
|
|
56783
57609
|
);
|
|
56784
57610
|
} else {
|
|
56785
57611
|
throw new Error(`Agent did not commit and did not report build failure for ${taskId}`);
|
|
56786
57612
|
}
|
|
57613
|
+
} else {
|
|
57614
|
+
await ensureTaskIdTrailerOnHead(rootDir, taskId);
|
|
56787
57615
|
}
|
|
56788
57616
|
return { success: true };
|
|
56789
57617
|
} catch (err) {
|
|
@@ -56799,7 +57627,7 @@ async function runAiAgentForCommit(params) {
|
|
|
56799
57627
|
}
|
|
56800
57628
|
}
|
|
56801
57629
|
function buildMergePrompt(params) {
|
|
56802
|
-
const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, testCommand, buildCommand: buildCommand2, authorArg } = params;
|
|
57630
|
+
const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, sourceIssueRef, testCommand, buildCommand: buildCommand2, authorArg } = params;
|
|
56803
57631
|
const truncatedCommitLog = truncateWithEllipsis(commitLog, MERGE_COMMIT_LOG_MAX_CHARS);
|
|
56804
57632
|
const truncatedDiffStat = truncateWithEllipsis(diffStat, MERGE_DIFF_STAT_MAX_CHARS);
|
|
56805
57633
|
const parts = [
|
|
@@ -56835,6 +57663,13 @@ function buildMergePrompt(params) {
|
|
|
56835
57663
|
`Write and run the \`git commit\` command with a good message summarizing the work.${authorArg ? ` Be sure to include \`${authorArg.trim()}\` in the commit command.` : ""}`
|
|
56836
57664
|
);
|
|
56837
57665
|
}
|
|
57666
|
+
if (sourceIssueRef) {
|
|
57667
|
+
parts.push(
|
|
57668
|
+
"",
|
|
57669
|
+
"Include this in the commit message body:",
|
|
57670
|
+
`- Ref: ${sourceIssueRef}`
|
|
57671
|
+
);
|
|
57672
|
+
}
|
|
56838
57673
|
if (testCommand) {
|
|
56839
57674
|
parts.push(
|
|
56840
57675
|
"",
|
|
@@ -57011,8 +57846,9 @@ If issues are found that need attention, describe them clearly.`;
|
|
|
57011
57846
|
agent: "merger"
|
|
57012
57847
|
});
|
|
57013
57848
|
try {
|
|
57014
|
-
const
|
|
57015
|
-
const
|
|
57849
|
+
const defaultModel = resolveProjectDefaultModel(settings);
|
|
57850
|
+
const stepProvider = workflowStep.modelProvider || defaultModel.provider;
|
|
57851
|
+
const stepModelId = workflowStep.modelId || defaultModel.modelId;
|
|
57016
57852
|
const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId);
|
|
57017
57853
|
let postMergeInstructions = "";
|
|
57018
57854
|
if (mergeOptions.agentStore) {
|
|
@@ -57095,12 +57931,11 @@ async function completeTask(store, taskId, result) {
|
|
|
57095
57931
|
result.task = task;
|
|
57096
57932
|
store.emit("task:merged", result);
|
|
57097
57933
|
}
|
|
57098
|
-
var execAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError;
|
|
57934
|
+
var execAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, VERIFICATION_COMMAND_MAX_BUFFER, VERIFICATION_COMMAND_TIMEOUT_MS, VERIFICATION_LOG_MAX_CHARS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, FUSION_TASK_ID_TRAILER_KEY;
|
|
57099
57935
|
var init_merger = __esm({
|
|
57100
57936
|
"../engine/src/merger.ts"() {
|
|
57101
57937
|
"use strict";
|
|
57102
57938
|
init_src();
|
|
57103
|
-
init_src();
|
|
57104
57939
|
init_pi();
|
|
57105
57940
|
init_session_token_usage();
|
|
57106
57941
|
init_agent_session_helpers();
|
|
@@ -57170,6 +58005,7 @@ var init_merger = __esm({
|
|
|
57170
58005
|
this.name = "MergeAbortedError";
|
|
57171
58006
|
}
|
|
57172
58007
|
};
|
|
58008
|
+
FUSION_TASK_ID_TRAILER_KEY = "Fusion-Task-Id";
|
|
57173
58009
|
}
|
|
57174
58010
|
});
|
|
57175
58011
|
|
|
@@ -58757,6 +59593,7 @@ function buildExecutionPrompt(task, rootDir, settings, worktreePath) {
|
|
|
58757
59593
|
const reviewMatch = prompt.match(/##\s*Review Level[:\s]*(\d)/);
|
|
58758
59594
|
const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0;
|
|
58759
59595
|
const authorArg = settings?.commitAuthorEnabled !== false ? ` --author="${settings?.commitAuthorName || "Fusion"} <${settings?.commitAuthorEmail || "noreply@runfusion.ai"}>"` : "";
|
|
59596
|
+
const sourceIssueRef = task.sourceIssue?.provider === "github" && task.sourceIssue.repository && task.sourceIssue.issueNumber ? `${task.sourceIssue.repository}#${task.sourceIssue.issueNumber}` : "";
|
|
58760
59597
|
const hasProgress = task.steps.length > 0 && task.steps.some((s) => s.status !== "pending");
|
|
58761
59598
|
let progressSection = "";
|
|
58762
59599
|
if (hasProgress) {
|
|
@@ -58859,7 +59696,7 @@ ${hasProgress ? `Resume from Step ${task.currentStep}. Do NOT redo completed ste
|
|
|
58859
59696
|
Use \`fn_task_update\` to report progress on every step transition.
|
|
58860
59697
|
Use \`fn_task_log\` for important actions and decisions.
|
|
58861
59698
|
Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
|
|
58862
|
-
Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N \u2014 description"${authorArg}\`
|
|
59699
|
+
Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N \u2014 description"${sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : ""}${authorArg}\`
|
|
58863
59700
|
When all steps are complete: call \`fn_task_done()\`
|
|
58864
59701
|
|
|
58865
59702
|
If a build command is configured, run that exact command in this worktree before calling \`fn_task_done()\`.
|
|
@@ -59045,6 +59882,7 @@ If the task's PROMPT.md includes a "Documentation Requirements" section listing
|
|
|
59045
59882
|
## Git discipline
|
|
59046
59883
|
- Commit after completing each step (not after every file change)
|
|
59047
59884
|
- Use conventional commit messages prefixed with the task ID
|
|
59885
|
+
- When the task has a GitHub issue reference, include \`Ref: owner/repo#N\` in the commit body
|
|
59048
59886
|
- Do NOT commit broken or half-implemented code
|
|
59049
59887
|
|
|
59050
59888
|
## Worktree Boundaries
|
|
@@ -59138,7 +59976,10 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59138
59976
|
executorLog.log(`[event:task:moved] ${task.id}: ${from} \u2192 ${to}`);
|
|
59139
59977
|
if (to === "in-progress") {
|
|
59140
59978
|
executorLog.log(`[event:task:moved] Initiating execute() for ${task.id}`);
|
|
59141
|
-
|
|
59979
|
+
void (async () => {
|
|
59980
|
+
const taskForExecution = await this.resetMergeStateIfNeeded(task, from);
|
|
59981
|
+
await this.execute(taskForExecution);
|
|
59982
|
+
})().catch(
|
|
59142
59983
|
(err) => executorLog.error(`Failed to start ${task.id}:`, err)
|
|
59143
59984
|
);
|
|
59144
59985
|
} else if (from === "in-progress") {
|
|
@@ -59416,6 +60257,59 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59416
60257
|
if (task.steps.length === 0) return false;
|
|
59417
60258
|
return task.steps.every((s) => s.status === "done" || s.status === "skipped");
|
|
59418
60259
|
}
|
|
60260
|
+
async resetMergeStateIfNeeded(task, from) {
|
|
60261
|
+
if (from !== "in-review" && from !== "done") {
|
|
60262
|
+
return task;
|
|
60263
|
+
}
|
|
60264
|
+
const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr";
|
|
60265
|
+
if (!hasMergeEvidence) {
|
|
60266
|
+
return task;
|
|
60267
|
+
}
|
|
60268
|
+
return this.cleanupMergeStateForReverification(
|
|
60269
|
+
task,
|
|
60270
|
+
`Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`
|
|
60271
|
+
);
|
|
60272
|
+
}
|
|
60273
|
+
async cleanupMergeStateForReverification(task, logMessage) {
|
|
60274
|
+
await this.store.updateTask(task.id, {
|
|
60275
|
+
mergeDetails: null,
|
|
60276
|
+
mergeRetries: 0,
|
|
60277
|
+
verificationFailureCount: 0,
|
|
60278
|
+
workflowStepResults: []
|
|
60279
|
+
});
|
|
60280
|
+
const refreshedTask = await this.store.getTask(task.id);
|
|
60281
|
+
const steps = refreshedTask.steps ?? [];
|
|
60282
|
+
if (steps.length > 0) {
|
|
60283
|
+
const allStepsComplete = this.isTaskWorkComplete(refreshedTask);
|
|
60284
|
+
if (allStepsComplete) {
|
|
60285
|
+
await this.reopenLastStepForRevision(task.id, refreshedTask);
|
|
60286
|
+
} else {
|
|
60287
|
+
const resetIndexes = /* @__PURE__ */ new Set();
|
|
60288
|
+
for (let i = 0; i < steps.length; i++) {
|
|
60289
|
+
const name = steps[i].name.toLowerCase();
|
|
60290
|
+
if (/testing|verification/.test(name) || /documentation|delivery/.test(name)) {
|
|
60291
|
+
resetIndexes.add(i);
|
|
60292
|
+
}
|
|
60293
|
+
}
|
|
60294
|
+
if (resetIndexes.size === 0) {
|
|
60295
|
+
const reopened = await this.reopenLastStepForRevision(task.id, refreshedTask);
|
|
60296
|
+
if (reopened) {
|
|
60297
|
+
resetIndexes.add(reopened.index);
|
|
60298
|
+
}
|
|
60299
|
+
} else {
|
|
60300
|
+
for (const index of resetIndexes) {
|
|
60301
|
+
if (steps[index].status !== "pending") {
|
|
60302
|
+
await this.store.updateStep(task.id, index, "pending");
|
|
60303
|
+
}
|
|
60304
|
+
}
|
|
60305
|
+
const earliestIndex = Math.min(...Array.from(resetIndexes));
|
|
60306
|
+
await this.store.updateTask(task.id, { currentStep: earliestIndex });
|
|
60307
|
+
}
|
|
60308
|
+
}
|
|
60309
|
+
}
|
|
60310
|
+
await this.store.logEntry(task.id, logMessage, void 0, this.currentRunContext);
|
|
60311
|
+
return this.store.getTask(task.id);
|
|
60312
|
+
}
|
|
59419
60313
|
isNoProgressNoTaskDoneFailure(task) {
|
|
59420
60314
|
return task.status === "failed" && task.error?.includes("without calling fn_task_done") === true && task.steps.every((step) => step.status === "pending");
|
|
59421
60315
|
}
|
|
@@ -59637,7 +60531,7 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59637
60531
|
if (inProgress.length === 0) return;
|
|
59638
60532
|
executorLog.log(`Found ${inProgress.length} orphaned in-progress task(s)`);
|
|
59639
60533
|
for (const task of inProgress) {
|
|
59640
|
-
if (this.isTaskWorkComplete(task)) {
|
|
60534
|
+
if (this.isTaskWorkComplete(task) && !task.mergeDetails) {
|
|
59641
60535
|
if (this.recoveringCompleted.has(task.id)) {
|
|
59642
60536
|
executorLog.log(`${task.id} completed-task recovery already running - skipping duplicate startup recovery`);
|
|
59643
60537
|
continue;
|
|
@@ -59772,6 +60666,13 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59772
60666
|
return;
|
|
59773
60667
|
}
|
|
59774
60668
|
}
|
|
60669
|
+
if (task.column === "in-progress" && task.mergeDetails) {
|
|
60670
|
+
executorLog.warn(`${task.id}: stale mergeDetails found while executing in-progress task \u2014 resetting merge state before continuing`);
|
|
60671
|
+
task = await this.cleanupMergeStateForReverification(
|
|
60672
|
+
task,
|
|
60673
|
+
"Executor detected stale merge state while task was in-progress \u2014 reset verification steps and merge metadata before resuming"
|
|
60674
|
+
);
|
|
60675
|
+
}
|
|
59775
60676
|
if (task.column === "in-progress" && !task.worktree) {
|
|
59776
60677
|
executorLog.error(
|
|
59777
60678
|
`${task.id}: drift detected \u2014 task is in-progress with no worktree. Recovering by creating a fresh worktree. This usually indicates a partial updateTask/moveTask sequence failed somewhere upstream.`
|
|
@@ -61776,10 +62677,19 @@ and show an appropriate message to the user.\`
|
|
|
61776
62677
|
this.options.onAgentTool?.(taskId, toolName);
|
|
61777
62678
|
}
|
|
61778
62679
|
});
|
|
61779
|
-
|
|
61780
|
-
|
|
61781
|
-
|
|
61782
|
-
|
|
62680
|
+
const defaultModel = resolveProjectDefaultModel(settings);
|
|
62681
|
+
const primaryProvider = workflowStep.modelProvider || defaultModel.provider;
|
|
62682
|
+
const primaryModelId = workflowStep.modelId || defaultModel.modelId;
|
|
62683
|
+
const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId);
|
|
62684
|
+
const fallbackCandidates = [
|
|
62685
|
+
{ provider: settings.validatorFallbackProvider, modelId: settings.validatorFallbackModelId, label: "validatorFallback" },
|
|
62686
|
+
{ provider: settings.fallbackProvider, modelId: settings.fallbackModelId, label: "globalFallback" }
|
|
62687
|
+
];
|
|
62688
|
+
const fallback = fallbackCandidates.find(
|
|
62689
|
+
(c) => c.provider && c.modelId && (c.provider !== primaryProvider || c.modelId !== primaryModelId)
|
|
62690
|
+
);
|
|
62691
|
+
const timeoutMs = Math.max(6e4, settings.workflowStepTimeoutMs ?? 36e4);
|
|
62692
|
+
const runOnce = async (provider, modelId, attemptLabel) => {
|
|
61783
62693
|
const stepInstructions = await this.resolveInstructionsForRole("executor");
|
|
61784
62694
|
const stepSystemPrompt = buildSystemPromptWithInstructions(systemPrompt, stepInstructions);
|
|
61785
62695
|
const skillContext = await buildSessionSkillContext({
|
|
@@ -61797,16 +62707,19 @@ and show an appropriate message to the user.\`
|
|
|
61797
62707
|
cwd: worktreePath,
|
|
61798
62708
|
systemPrompt: stepSystemPrompt,
|
|
61799
62709
|
tools: toolMode,
|
|
61800
|
-
defaultProvider:
|
|
61801
|
-
defaultModelId:
|
|
62710
|
+
defaultProvider: provider,
|
|
62711
|
+
defaultModelId: modelId,
|
|
61802
62712
|
fallbackProvider: settings.fallbackProvider,
|
|
61803
62713
|
fallbackModelId: settings.fallbackModelId,
|
|
61804
62714
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
|
61805
62715
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
|
61806
62716
|
...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
|
|
61807
62717
|
});
|
|
61808
|
-
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
|
|
61809
|
-
await this.store.logEntry(
|
|
62718
|
+
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`);
|
|
62719
|
+
await this.store.logEntry(
|
|
62720
|
+
task.id,
|
|
62721
|
+
`Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`
|
|
62722
|
+
);
|
|
61810
62723
|
let output = "";
|
|
61811
62724
|
session.subscribe((event) => {
|
|
61812
62725
|
if (event.type === "message_update") {
|
|
@@ -61825,33 +62738,75 @@ and show an appropriate message to the user.\`
|
|
|
61825
62738
|
agentLogger.onToolEnd(event.toolName, event.isError, event.result);
|
|
61826
62739
|
}
|
|
61827
62740
|
});
|
|
61828
|
-
|
|
61829
|
-
|
|
61830
|
-
|
|
62741
|
+
let timedOut = false;
|
|
62742
|
+
let timeoutHandle;
|
|
62743
|
+
const timeoutPromise = new Promise((resolveTimeout) => {
|
|
62744
|
+
timeoutHandle = setTimeout(() => {
|
|
62745
|
+
timedOut = true;
|
|
62746
|
+
resolveTimeout("timeout");
|
|
62747
|
+
}, timeoutMs);
|
|
62748
|
+
});
|
|
62749
|
+
try {
|
|
62750
|
+
const promptPromise = promptWithFallback(
|
|
62751
|
+
session,
|
|
62752
|
+
`Execute the workflow step "${workflowStep.name}" for task ${task.id}.
|
|
61831
62753
|
|
|
61832
62754
|
Review the work done in this worktree and evaluate it against the criteria in your instructions.`
|
|
61833
|
-
|
|
61834
|
-
|
|
61835
|
-
|
|
61836
|
-
|
|
61837
|
-
|
|
61838
|
-
|
|
61839
|
-
|
|
61840
|
-
|
|
61841
|
-
|
|
61842
|
-
|
|
61843
|
-
|
|
61844
|
-
|
|
61845
|
-
|
|
61846
|
-
|
|
61847
|
-
|
|
62755
|
+
);
|
|
62756
|
+
const outcome = await Promise.race([
|
|
62757
|
+
promptPromise.then(() => "completed"),
|
|
62758
|
+
timeoutPromise
|
|
62759
|
+
]);
|
|
62760
|
+
if (outcome === "timeout") {
|
|
62761
|
+
executorLog.warn(`${task.id}: workflow step '${workflowStep.name}' (${attemptLabel}) timed out after ${timeoutMs}ms \u2014 disposing session`);
|
|
62762
|
+
await this.store.logEntry(
|
|
62763
|
+
task.id,
|
|
62764
|
+
`Workflow step '${workflowStep.name}' ${attemptLabel === "primary" ? "primary" : "fallback"} model timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 aborting session`
|
|
62765
|
+
);
|
|
62766
|
+
try {
|
|
62767
|
+
session.dispose();
|
|
62768
|
+
} catch {
|
|
62769
|
+
}
|
|
62770
|
+
await agentLogger.flush();
|
|
62771
|
+
return { success: false, error: `workflow step timed out after ${timeoutMs}ms`, timedOut: true };
|
|
62772
|
+
}
|
|
62773
|
+
checkSessionError(session);
|
|
62774
|
+
await accumulateSessionTokenUsage(this.store, task.id, session);
|
|
62775
|
+
session.dispose();
|
|
62776
|
+
await agentLogger.flush();
|
|
62777
|
+
const trimmedOutput = output.trim();
|
|
62778
|
+
const revisionMatch = trimmedOutput.match(/^REQUEST REVISION\s*\n*/i);
|
|
62779
|
+
if (revisionMatch) {
|
|
62780
|
+
const feedbackStart = revisionMatch[0].length;
|
|
62781
|
+
const feedback = trimmedOutput.slice(feedbackStart).trim();
|
|
62782
|
+
return { success: false, revisionRequested: true, output: feedback };
|
|
62783
|
+
}
|
|
62784
|
+
return { success: true, output };
|
|
62785
|
+
} catch (err) {
|
|
62786
|
+
await agentLogger.flush();
|
|
62787
|
+
try {
|
|
62788
|
+
session.dispose();
|
|
62789
|
+
} catch {
|
|
62790
|
+
}
|
|
62791
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
62792
|
+
return { success: false, error: errorMessage };
|
|
62793
|
+
} finally {
|
|
62794
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
62795
|
+
void timedOut;
|
|
61848
62796
|
}
|
|
61849
|
-
|
|
61850
|
-
|
|
61851
|
-
|
|
61852
|
-
|
|
61853
|
-
|
|
62797
|
+
};
|
|
62798
|
+
const primaryOutcome = await runOnce(primaryProvider, primaryModelId, "primary");
|
|
62799
|
+
if (!primaryOutcome.timedOut) return primaryOutcome;
|
|
62800
|
+
if (!fallback) {
|
|
62801
|
+
executorLog.warn(`${task.id}: workflow step '${workflowStep.name}' timed out and no fallback model is configured`);
|
|
62802
|
+
await this.store.logEntry(
|
|
62803
|
+
task.id,
|
|
62804
|
+
`Workflow step '${workflowStep.name}' timed out \u2014 no fallback model configured (set settings.validatorFallbackProvider/Id or fallbackProvider/Id)`
|
|
62805
|
+
);
|
|
62806
|
+
return primaryOutcome;
|
|
61854
62807
|
}
|
|
62808
|
+
executorLog.log(`${task.id}: retrying workflow step '${workflowStep.name}' with fallback ${fallback.provider}/${fallback.modelId} (label=${fallback.label})`);
|
|
62809
|
+
return runOnce(fallback.provider, fallback.modelId, "fallback");
|
|
61855
62810
|
}
|
|
61856
62811
|
MAX_WORKTREE_RETRIES = 3;
|
|
61857
62812
|
WORKTREE_RETRY_DELAYS = [100, 500, 1e3];
|
|
@@ -61879,7 +62834,13 @@ Review the work done in this worktree and evaluate it against the criteria in yo
|
|
|
61879
62834
|
}
|
|
61880
62835
|
for (let attempt = 0; attempt < this.MAX_WORKTREE_RETRIES; attempt++) {
|
|
61881
62836
|
try {
|
|
61882
|
-
|
|
62837
|
+
const result = await this.tryCreateWorktree(branch, currentPath, taskId, resolvedStartPoint, attempt);
|
|
62838
|
+
await this.rebaseNewWorktreeOntoRemote(result.path, result.branch, taskId).catch((err) => {
|
|
62839
|
+
executorLog.warn(
|
|
62840
|
+
`Post-create worktree rebase failed for ${taskId} (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
62841
|
+
);
|
|
62842
|
+
});
|
|
62843
|
+
return result;
|
|
61883
62844
|
} catch (error) {
|
|
61884
62845
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
61885
62846
|
const isLastAttempt = attempt === this.MAX_WORKTREE_RETRIES - 1;
|
|
@@ -61900,6 +62861,84 @@ Review the work done in this worktree and evaluate it against the criteria in yo
|
|
|
61900
62861
|
}
|
|
61901
62862
|
throw new Error("Unexpected exit from worktree creation retry loop");
|
|
61902
62863
|
}
|
|
62864
|
+
quoteShellArg(value) {
|
|
62865
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
62866
|
+
}
|
|
62867
|
+
/**
|
|
62868
|
+
* After creating a fresh task worktree, fetch the configured remote and
|
|
62869
|
+
* rebase the task branch onto `<remote>/<defaultBranch>`. The result is a
|
|
62870
|
+
* branch that contains origin's tip plus any local main commits, so the
|
|
62871
|
+
* eventual merge has fewer surprises and the executor sees the freshest
|
|
62872
|
+
* code its peers/CI may have published.
|
|
62873
|
+
*
|
|
62874
|
+
* No-op when `worktreeRebaseBeforeMerge` is disabled, no remote is
|
|
62875
|
+
* configured/resolvable, or the rebase produces conflicts (we abort and
|
|
62876
|
+
* leave the worktree as-is so the executor can still run).
|
|
62877
|
+
*/
|
|
62878
|
+
async rebaseNewWorktreeOntoRemote(worktreePath, branch, taskId) {
|
|
62879
|
+
let settings;
|
|
62880
|
+
try {
|
|
62881
|
+
settings = await this.store.getSettings();
|
|
62882
|
+
} catch {
|
|
62883
|
+
return;
|
|
62884
|
+
}
|
|
62885
|
+
if (settings.worktreeRebaseBeforeMerge === false) return;
|
|
62886
|
+
let remote = settings.worktreeRebaseRemote?.trim() || "";
|
|
62887
|
+
if (!remote) {
|
|
62888
|
+
try {
|
|
62889
|
+
const { stdout } = await execAsync5("git remote", { cwd: this.rootDir });
|
|
62890
|
+
const remotes = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
62891
|
+
if (remotes.includes("origin")) remote = "origin";
|
|
62892
|
+
else if (remotes.length === 1) remote = remotes[0];
|
|
62893
|
+
} catch {
|
|
62894
|
+
}
|
|
62895
|
+
}
|
|
62896
|
+
if (!remote) return;
|
|
62897
|
+
let defaultBranch = "";
|
|
62898
|
+
try {
|
|
62899
|
+
const { stdout } = await execAsync5(`git rev-parse --abbrev-ref ${remote}/HEAD`, { cwd: this.rootDir });
|
|
62900
|
+
defaultBranch = stdout.trim().replace(new RegExp(`^${remote}/`), "");
|
|
62901
|
+
} catch {
|
|
62902
|
+
}
|
|
62903
|
+
if (!defaultBranch) {
|
|
62904
|
+
try {
|
|
62905
|
+
const { stdout } = await execAsync5("git rev-parse --abbrev-ref HEAD", { cwd: this.rootDir });
|
|
62906
|
+
defaultBranch = stdout.trim();
|
|
62907
|
+
} catch {
|
|
62908
|
+
return;
|
|
62909
|
+
}
|
|
62910
|
+
}
|
|
62911
|
+
if (!defaultBranch || defaultBranch === "HEAD") return;
|
|
62912
|
+
const remoteRef = `${remote}/${defaultBranch}`;
|
|
62913
|
+
try {
|
|
62914
|
+
await execAsync5(`git fetch ${this.quoteShellArg(remote)} ${this.quoteShellArg(defaultBranch)}`, { cwd: this.rootDir });
|
|
62915
|
+
} catch (err) {
|
|
62916
|
+
executorLog.warn(
|
|
62917
|
+
`Worktree rebase: fetch ${remote} ${defaultBranch} failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`
|
|
62918
|
+
);
|
|
62919
|
+
return;
|
|
62920
|
+
}
|
|
62921
|
+
try {
|
|
62922
|
+
await execAsync5(`git rebase ${this.quoteShellArg(remoteRef)}`, { cwd: worktreePath });
|
|
62923
|
+
await this.store.logEntry(
|
|
62924
|
+
taskId,
|
|
62925
|
+
`Rebased new worktree branch ${branch} onto ${remoteRef}`
|
|
62926
|
+
);
|
|
62927
|
+
} catch (rebaseErr) {
|
|
62928
|
+
const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
|
|
62929
|
+
executorLog.warn(
|
|
62930
|
+
`Worktree rebase: rebase onto ${remoteRef} failed for ${taskId} \u2014 aborting and leaving local base intact: ${msg}`
|
|
62931
|
+
);
|
|
62932
|
+
try {
|
|
62933
|
+
await execAsync5("git rebase --abort", { cwd: worktreePath });
|
|
62934
|
+
} catch {
|
|
62935
|
+
}
|
|
62936
|
+
await this.store.logEntry(
|
|
62937
|
+
taskId,
|
|
62938
|
+
`Could not rebase new worktree onto ${remoteRef} \u2014 kept local base. The merge-time rebase will retry with conflict resolution.`
|
|
62939
|
+
);
|
|
62940
|
+
}
|
|
62941
|
+
}
|
|
61903
62942
|
/**
|
|
61904
62943
|
* Resolve a stored baseBranch to a concrete commit SHA.
|
|
61905
62944
|
*
|
|
@@ -62631,6 +63670,7 @@ Child agent: ${agent.id} (${name})`;
|
|
|
62631
63670
|
});
|
|
62632
63671
|
const parentAgent = childTask.assignedAgentId ? await this.options.agentStore.getAgent(childTask.assignedAgentId).catch(() => null) : null;
|
|
62633
63672
|
const childRuntimeHint = extractRuntimeHint(agent.runtimeConfig) ?? extractRuntimeHint(parentAgent?.runtimeConfig);
|
|
63673
|
+
const { provider: childExecutorProvider, modelId: childExecutorModelId } = resolveExecutorModelPair2(void 0, void 0, settings);
|
|
62634
63674
|
const { session: childSession } = await createResolvedAgentSession({
|
|
62635
63675
|
sessionPurpose: "executor",
|
|
62636
63676
|
runtimeHint: childRuntimeHint,
|
|
@@ -62638,8 +63678,8 @@ Child agent: ${agent.id} (${name})`;
|
|
|
62638
63678
|
cwd: childWorktreePath,
|
|
62639
63679
|
systemPrompt: childSystemPrompt,
|
|
62640
63680
|
tools: "coding",
|
|
62641
|
-
defaultProvider:
|
|
62642
|
-
defaultModelId:
|
|
63681
|
+
defaultProvider: childExecutorProvider,
|
|
63682
|
+
defaultModelId: childExecutorModelId,
|
|
62643
63683
|
fallbackProvider: settings.fallbackProvider,
|
|
62644
63684
|
fallbackModelId: settings.fallbackModelId,
|
|
62645
63685
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
|
@@ -62735,6 +63775,25 @@ var init_mission_feature_sync = __esm({
|
|
|
62735
63775
|
}
|
|
62736
63776
|
});
|
|
62737
63777
|
|
|
63778
|
+
// ../engine/src/effective-node.ts
|
|
63779
|
+
function isSetNodeId(nodeId) {
|
|
63780
|
+
return typeof nodeId === "string" && nodeId.trim().length > 0;
|
|
63781
|
+
}
|
|
63782
|
+
function resolveEffectiveNode(task, settings) {
|
|
63783
|
+
if (isSetNodeId(task.nodeId)) {
|
|
63784
|
+
return { nodeId: task.nodeId, source: "task-override" };
|
|
63785
|
+
}
|
|
63786
|
+
if (isSetNodeId(settings.defaultNodeId)) {
|
|
63787
|
+
return { nodeId: settings.defaultNodeId, source: "project-default" };
|
|
63788
|
+
}
|
|
63789
|
+
return { nodeId: void 0, source: "local" };
|
|
63790
|
+
}
|
|
63791
|
+
var init_effective_node = __esm({
|
|
63792
|
+
"../engine/src/effective-node.ts"() {
|
|
63793
|
+
"use strict";
|
|
63794
|
+
}
|
|
63795
|
+
});
|
|
63796
|
+
|
|
62738
63797
|
// ../engine/src/scheduler.ts
|
|
62739
63798
|
import { existsSync as existsSync26 } from "node:fs";
|
|
62740
63799
|
import { readFile as readFile15 } from "node:fs/promises";
|
|
@@ -62793,6 +63852,7 @@ var init_scheduler = __esm({
|
|
|
62793
63852
|
init_logger2();
|
|
62794
63853
|
init_mission_feature_sync();
|
|
62795
63854
|
init_spec_staleness();
|
|
63855
|
+
init_effective_node();
|
|
62796
63856
|
Scheduler = class {
|
|
62797
63857
|
/**
|
|
62798
63858
|
* Async listener guard convention:
|
|
@@ -63216,14 +64276,19 @@ var init_scheduler = __esm({
|
|
|
63216
64276
|
schedulerLog.log(`Task ${task.id} is paused \u2014 skipping dispatch`);
|
|
63217
64277
|
continue;
|
|
63218
64278
|
}
|
|
64279
|
+
const effectiveNode = resolveEffectiveNode(freshTask, settings);
|
|
64280
|
+
schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
|
|
63219
64281
|
schedulerLog.log(`Starting ${task.id}: ${task.title || task.id} (deps satisfied)`);
|
|
63220
64282
|
await this.store.updateTask(task.id, {
|
|
63221
64283
|
status: null,
|
|
63222
64284
|
blockedBy: null,
|
|
63223
64285
|
baseBranch: baseBranch ?? void 0,
|
|
63224
|
-
worktree: plannedWorktree
|
|
64286
|
+
worktree: plannedWorktree,
|
|
64287
|
+
effectiveNodeId: effectiveNode.nodeId ?? null,
|
|
64288
|
+
effectiveNodeSource: effectiveNode.source
|
|
63225
64289
|
});
|
|
63226
64290
|
await this.store.moveTask(task.id, "in-progress");
|
|
64291
|
+
await this.store.logEntry(task.id, `Node routing resolved: ${effectiveNode.nodeId ?? "local"} (source: ${effectiveNode.source})`);
|
|
63227
64292
|
this.options.onSchedule?.(task);
|
|
63228
64293
|
started++;
|
|
63229
64294
|
if (settings.groupOverlappingFiles) {
|
|
@@ -65679,6 +66744,453 @@ Please review the PR comments and address any remaining issues.`;
|
|
|
65679
66744
|
}
|
|
65680
66745
|
});
|
|
65681
66746
|
|
|
66747
|
+
// ../engine/src/notification/ntfy-provider.ts
|
|
66748
|
+
var SUPPORTED_EVENTS, NtfyNotificationProvider;
|
|
66749
|
+
var init_ntfy_provider = __esm({
|
|
66750
|
+
"../engine/src/notification/ntfy-provider.ts"() {
|
|
66751
|
+
"use strict";
|
|
66752
|
+
init_notifier();
|
|
66753
|
+
SUPPORTED_EVENTS = /* @__PURE__ */ new Set([
|
|
66754
|
+
"in-review",
|
|
66755
|
+
"merged",
|
|
66756
|
+
"failed",
|
|
66757
|
+
"awaiting-approval",
|
|
66758
|
+
"awaiting-user-review",
|
|
66759
|
+
"planning-awaiting-input"
|
|
66760
|
+
]);
|
|
66761
|
+
NtfyNotificationProvider = class {
|
|
66762
|
+
config;
|
|
66763
|
+
abortController = null;
|
|
66764
|
+
getProviderId() {
|
|
66765
|
+
return "ntfy";
|
|
66766
|
+
}
|
|
66767
|
+
async initialize(config) {
|
|
66768
|
+
if (typeof config.topic !== "string" || config.topic.trim() === "") {
|
|
66769
|
+
return;
|
|
66770
|
+
}
|
|
66771
|
+
this.config = config;
|
|
66772
|
+
this.config.events = resolveNtfyEvents(this.config.events);
|
|
66773
|
+
this.abortController = new AbortController();
|
|
66774
|
+
}
|
|
66775
|
+
async shutdown() {
|
|
66776
|
+
this.abortController?.abort();
|
|
66777
|
+
this.abortController = null;
|
|
66778
|
+
}
|
|
66779
|
+
isEventSupported(event) {
|
|
66780
|
+
if (!SUPPORTED_EVENTS.has(event)) {
|
|
66781
|
+
return false;
|
|
66782
|
+
}
|
|
66783
|
+
const enabledEvents = this.config?.events ?? [...DEFAULT_NTFY_EVENTS];
|
|
66784
|
+
return enabledEvents.includes(event);
|
|
66785
|
+
}
|
|
66786
|
+
async sendNotification(event, payload) {
|
|
66787
|
+
if (!this.config?.topic) {
|
|
66788
|
+
return { success: false, providerId: this.getProviderId(), error: "ntfy topic not configured" };
|
|
66789
|
+
}
|
|
66790
|
+
if (!this.isEventSupported(event)) {
|
|
66791
|
+
return {
|
|
66792
|
+
success: false,
|
|
66793
|
+
providerId: this.getProviderId(),
|
|
66794
|
+
error: `unsupported event: ${event}`
|
|
66795
|
+
};
|
|
66796
|
+
}
|
|
66797
|
+
const taskLike = {
|
|
66798
|
+
id: payload.taskId,
|
|
66799
|
+
title: payload.taskTitle,
|
|
66800
|
+
description: payload.taskDescription ?? ""
|
|
66801
|
+
};
|
|
66802
|
+
const identifier = formatTaskIdentifier(taskLike);
|
|
66803
|
+
const clickUrl = buildNtfyClickUrl({
|
|
66804
|
+
dashboardHost: this.config.dashboardHost,
|
|
66805
|
+
projectId: this.config.projectId,
|
|
66806
|
+
taskId: payload.taskId
|
|
66807
|
+
});
|
|
66808
|
+
const contentByEvent = {
|
|
66809
|
+
"in-review": {
|
|
66810
|
+
title: `Task ${payload.taskId} completed`,
|
|
66811
|
+
message: `Task "${identifier}" is ready for review`,
|
|
66812
|
+
priority: "default"
|
|
66813
|
+
},
|
|
66814
|
+
merged: {
|
|
66815
|
+
title: `Task ${payload.taskId} merged`,
|
|
66816
|
+
message: `Task "${identifier}" has been merged to main`,
|
|
66817
|
+
priority: "default"
|
|
66818
|
+
},
|
|
66819
|
+
failed: {
|
|
66820
|
+
title: `Task ${payload.taskId} failed`,
|
|
66821
|
+
message: `Task "${identifier}" has failed and needs attention`,
|
|
66822
|
+
priority: "high"
|
|
66823
|
+
},
|
|
66824
|
+
"awaiting-approval": {
|
|
66825
|
+
title: `Plan needs approval for ${payload.taskId}`,
|
|
66826
|
+
message: `Task "${identifier}" needs your approval before it can proceed`,
|
|
66827
|
+
priority: "high"
|
|
66828
|
+
},
|
|
66829
|
+
"awaiting-user-review": {
|
|
66830
|
+
title: `User review needed for ${payload.taskId}`,
|
|
66831
|
+
message: `Task "${identifier}" needs human review before it can proceed`,
|
|
66832
|
+
priority: "high"
|
|
66833
|
+
},
|
|
66834
|
+
"planning-awaiting-input": {
|
|
66835
|
+
title: `Planning input needed for ${payload.taskId}`,
|
|
66836
|
+
message: `Task "${identifier}" is awaiting your input during planning`,
|
|
66837
|
+
priority: "high"
|
|
66838
|
+
}
|
|
66839
|
+
};
|
|
66840
|
+
const content = contentByEvent[event];
|
|
66841
|
+
await sendNtfyNotification({
|
|
66842
|
+
ntfyBaseUrl: this.config.ntfyBaseUrl,
|
|
66843
|
+
topic: this.config.topic,
|
|
66844
|
+
title: content.title,
|
|
66845
|
+
message: content.message,
|
|
66846
|
+
priority: content.priority,
|
|
66847
|
+
clickUrl,
|
|
66848
|
+
signal: this.abortController?.signal
|
|
66849
|
+
});
|
|
66850
|
+
return { success: true, providerId: this.getProviderId() };
|
|
66851
|
+
}
|
|
66852
|
+
};
|
|
66853
|
+
}
|
|
66854
|
+
});
|
|
66855
|
+
|
|
66856
|
+
// ../engine/src/notification/webhook-provider.ts
|
|
66857
|
+
var WebhookNotificationProvider;
|
|
66858
|
+
var init_webhook_provider = __esm({
|
|
66859
|
+
"../engine/src/notification/webhook-provider.ts"() {
|
|
66860
|
+
"use strict";
|
|
66861
|
+
init_logger2();
|
|
66862
|
+
WebhookNotificationProvider = class {
|
|
66863
|
+
config = null;
|
|
66864
|
+
abortController = null;
|
|
66865
|
+
getProviderId() {
|
|
66866
|
+
return "webhook";
|
|
66867
|
+
}
|
|
66868
|
+
async initialize(config) {
|
|
66869
|
+
const webhookUrl = typeof config.webhookUrl === "string" ? config.webhookUrl.trim() : "";
|
|
66870
|
+
if (!webhookUrl) {
|
|
66871
|
+
throw new Error("webhookUrl is required");
|
|
66872
|
+
}
|
|
66873
|
+
let parsedUrl;
|
|
66874
|
+
try {
|
|
66875
|
+
parsedUrl = new URL(webhookUrl);
|
|
66876
|
+
} catch {
|
|
66877
|
+
throw new Error("webhookUrl must be a valid URL");
|
|
66878
|
+
}
|
|
66879
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
66880
|
+
throw new Error("webhookUrl must use http:// or https://");
|
|
66881
|
+
}
|
|
66882
|
+
const webhookFormat = config.webhookFormat === "slack" || config.webhookFormat === "discord" || config.webhookFormat === "generic" ? config.webhookFormat : "generic";
|
|
66883
|
+
this.config = {
|
|
66884
|
+
webhookUrl,
|
|
66885
|
+
webhookFormat,
|
|
66886
|
+
events: Array.isArray(config.events) ? config.events.filter((event) => typeof event === "string") : []
|
|
66887
|
+
};
|
|
66888
|
+
this.abortController?.abort();
|
|
66889
|
+
this.abortController = new AbortController();
|
|
66890
|
+
}
|
|
66891
|
+
async shutdown() {
|
|
66892
|
+
this.abortController?.abort();
|
|
66893
|
+
this.abortController = null;
|
|
66894
|
+
this.config = null;
|
|
66895
|
+
}
|
|
66896
|
+
isEventSupported(event) {
|
|
66897
|
+
if (!this.config?.events || this.config.events.length === 0) {
|
|
66898
|
+
return true;
|
|
66899
|
+
}
|
|
66900
|
+
return this.config.events.includes(event);
|
|
66901
|
+
}
|
|
66902
|
+
async sendNotification(event, payload) {
|
|
66903
|
+
if (!this.config) {
|
|
66904
|
+
return { success: false, providerId: this.getProviderId(), error: "Not initialized" };
|
|
66905
|
+
}
|
|
66906
|
+
if (!this.isEventSupported(event)) {
|
|
66907
|
+
return {
|
|
66908
|
+
success: false,
|
|
66909
|
+
providerId: this.getProviderId(),
|
|
66910
|
+
error: `unsupported event: ${event}`
|
|
66911
|
+
};
|
|
66912
|
+
}
|
|
66913
|
+
try {
|
|
66914
|
+
const message = this.formatMessage(event, payload);
|
|
66915
|
+
const body = this.formatPayload(payload, message);
|
|
66916
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
66917
|
+
method: "POST",
|
|
66918
|
+
headers: {
|
|
66919
|
+
"Content-Type": "application/json"
|
|
66920
|
+
},
|
|
66921
|
+
body: JSON.stringify(body),
|
|
66922
|
+
signal: this.abortController?.signal
|
|
66923
|
+
});
|
|
66924
|
+
if (!response.ok) {
|
|
66925
|
+
const error = `Webhook notification failed: ${response.status} ${response.statusText}`;
|
|
66926
|
+
schedulerLog.log(error);
|
|
66927
|
+
return {
|
|
66928
|
+
success: false,
|
|
66929
|
+
providerId: this.getProviderId(),
|
|
66930
|
+
error
|
|
66931
|
+
};
|
|
66932
|
+
}
|
|
66933
|
+
return { success: true, providerId: this.getProviderId() };
|
|
66934
|
+
} catch (error) {
|
|
66935
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66936
|
+
schedulerLog.log(`Failed to send webhook notification: ${message}`);
|
|
66937
|
+
return {
|
|
66938
|
+
success: false,
|
|
66939
|
+
providerId: this.getProviderId(),
|
|
66940
|
+
error: message
|
|
66941
|
+
};
|
|
66942
|
+
}
|
|
66943
|
+
}
|
|
66944
|
+
formatMessage(event, payload) {
|
|
66945
|
+
const identifier = this.formatTaskIdentifier(payload);
|
|
66946
|
+
switch (event) {
|
|
66947
|
+
case "in-review":
|
|
66948
|
+
return `Task "${identifier}" is ready for review`;
|
|
66949
|
+
case "merged":
|
|
66950
|
+
return `Task "${identifier}" has been merged to main`;
|
|
66951
|
+
case "failed":
|
|
66952
|
+
return `Task "${identifier}" has failed and needs attention`;
|
|
66953
|
+
case "awaiting-approval":
|
|
66954
|
+
return `Task "${identifier}" needs your approval before it can proceed`;
|
|
66955
|
+
case "awaiting-user-review":
|
|
66956
|
+
return `Task "${identifier}" needs human review before it can proceed`;
|
|
66957
|
+
case "planning-awaiting-input":
|
|
66958
|
+
return `Task "${identifier}" is awaiting your input during planning`;
|
|
66959
|
+
case "gridlock":
|
|
66960
|
+
return "Pipeline gridlocked";
|
|
66961
|
+
default:
|
|
66962
|
+
return `Event "${event}" for task ${identifier}`;
|
|
66963
|
+
}
|
|
66964
|
+
}
|
|
66965
|
+
formatTaskIdentifier(payload) {
|
|
66966
|
+
if (payload.taskTitle?.trim()) {
|
|
66967
|
+
return payload.taskTitle;
|
|
66968
|
+
}
|
|
66969
|
+
const description = payload.taskDescription ?? "";
|
|
66970
|
+
const snippet = description.length > 200 ? `${description.slice(0, 200)}...` : description;
|
|
66971
|
+
return `${payload.taskId}: ${snippet}`;
|
|
66972
|
+
}
|
|
66973
|
+
formatPayload(payload, message) {
|
|
66974
|
+
if (!this.config) {
|
|
66975
|
+
return {};
|
|
66976
|
+
}
|
|
66977
|
+
if (this.config.webhookFormat === "slack") {
|
|
66978
|
+
return { text: message };
|
|
66979
|
+
}
|
|
66980
|
+
if (this.config.webhookFormat === "discord") {
|
|
66981
|
+
return { content: message };
|
|
66982
|
+
}
|
|
66983
|
+
return {
|
|
66984
|
+
event: payload.event,
|
|
66985
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
66986
|
+
task: {
|
|
66987
|
+
id: payload.taskId,
|
|
66988
|
+
title: payload.taskTitle
|
|
66989
|
+
},
|
|
66990
|
+
metadata: payload.metadata
|
|
66991
|
+
};
|
|
66992
|
+
}
|
|
66993
|
+
};
|
|
66994
|
+
}
|
|
66995
|
+
});
|
|
66996
|
+
|
|
66997
|
+
// ../engine/src/notification/notification-service.ts
|
|
66998
|
+
var NotificationService;
|
|
66999
|
+
var init_notification_service = __esm({
|
|
67000
|
+
"../engine/src/notification/notification-service.ts"() {
|
|
67001
|
+
"use strict";
|
|
67002
|
+
init_src();
|
|
67003
|
+
init_notifier();
|
|
67004
|
+
init_logger2();
|
|
67005
|
+
init_ntfy_provider();
|
|
67006
|
+
init_webhook_provider();
|
|
67007
|
+
NotificationService = class {
|
|
67008
|
+
constructor(store, options = {}) {
|
|
67009
|
+
this.store = store;
|
|
67010
|
+
this.options = options;
|
|
67011
|
+
}
|
|
67012
|
+
dispatcher = new NotificationDispatcher();
|
|
67013
|
+
notifiedEvents = /* @__PURE__ */ new Set();
|
|
67014
|
+
started = false;
|
|
67015
|
+
notificationsEnabled = false;
|
|
67016
|
+
ntfyProvider;
|
|
67017
|
+
webhookProvider;
|
|
67018
|
+
registerProvider(provider) {
|
|
67019
|
+
this.dispatcher.registerProvider(provider);
|
|
67020
|
+
}
|
|
67021
|
+
async start() {
|
|
67022
|
+
if (this.started) {
|
|
67023
|
+
return;
|
|
67024
|
+
}
|
|
67025
|
+
const settings = await this.store.getSettings();
|
|
67026
|
+
this.setNotificationsEnabledFromSettings(settings);
|
|
67027
|
+
await this.syncNtfyProvider(settings);
|
|
67028
|
+
await this.syncWebhookProvider(settings);
|
|
67029
|
+
await this.dispatcher.initializeAll();
|
|
67030
|
+
this.store.on("task:moved", this.handleTaskMoved);
|
|
67031
|
+
this.store.on("task:updated", this.handleTaskUpdated);
|
|
67032
|
+
this.store.on("task:merged", this.handleTaskMerged);
|
|
67033
|
+
this.store.on("settings:updated", this.handleSettingsUpdated);
|
|
67034
|
+
this.started = true;
|
|
67035
|
+
schedulerLog.log("NotificationService started");
|
|
67036
|
+
}
|
|
67037
|
+
async stop() {
|
|
67038
|
+
if (!this.started) {
|
|
67039
|
+
return;
|
|
67040
|
+
}
|
|
67041
|
+
if (typeof this.store.off === "function") {
|
|
67042
|
+
this.store.off("task:moved", this.handleTaskMoved);
|
|
67043
|
+
this.store.off("task:updated", this.handleTaskUpdated);
|
|
67044
|
+
this.store.off("task:merged", this.handleTaskMerged);
|
|
67045
|
+
this.store.off("settings:updated", this.handleSettingsUpdated);
|
|
67046
|
+
}
|
|
67047
|
+
await this.dispatcher.shutdownAll();
|
|
67048
|
+
this.started = false;
|
|
67049
|
+
schedulerLog.log("NotificationService stopped");
|
|
67050
|
+
}
|
|
67051
|
+
handleTaskMoved = (data) => {
|
|
67052
|
+
if (!this.notificationsEnabled || data.to !== "in-review") {
|
|
67053
|
+
return;
|
|
67054
|
+
}
|
|
67055
|
+
const payload = this.createTaskPayload(data.task, "in-review");
|
|
67056
|
+
this.maybeNotify(data.task.id, "in-review", payload);
|
|
67057
|
+
};
|
|
67058
|
+
handleTaskUpdated = (task) => {
|
|
67059
|
+
if (!this.notificationsEnabled) {
|
|
67060
|
+
return;
|
|
67061
|
+
}
|
|
67062
|
+
if (task.status === "failed") {
|
|
67063
|
+
this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
|
|
67064
|
+
}
|
|
67065
|
+
if (task.status === "awaiting-approval") {
|
|
67066
|
+
this.maybeNotify(
|
|
67067
|
+
task.id,
|
|
67068
|
+
"awaiting-approval",
|
|
67069
|
+
this.createTaskPayload(task, "awaiting-approval")
|
|
67070
|
+
);
|
|
67071
|
+
}
|
|
67072
|
+
if (task.status === "awaiting-user-review") {
|
|
67073
|
+
this.maybeNotify(
|
|
67074
|
+
task.id,
|
|
67075
|
+
"awaiting-user-review",
|
|
67076
|
+
this.createTaskPayload(task, "awaiting-user-review")
|
|
67077
|
+
);
|
|
67078
|
+
}
|
|
67079
|
+
};
|
|
67080
|
+
handleTaskMerged = (result) => {
|
|
67081
|
+
if (!this.notificationsEnabled || !result.merged) {
|
|
67082
|
+
return;
|
|
67083
|
+
}
|
|
67084
|
+
this.maybeNotify(
|
|
67085
|
+
result.task.id,
|
|
67086
|
+
"merged",
|
|
67087
|
+
this.createTaskPayload(result.task, "merged")
|
|
67088
|
+
);
|
|
67089
|
+
};
|
|
67090
|
+
handleSettingsUpdated = async (data) => {
|
|
67091
|
+
const { settings, previous } = data;
|
|
67092
|
+
this.setNotificationsEnabledFromSettings(settings);
|
|
67093
|
+
if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
|
|
67094
|
+
const wasEnabled = Boolean(previous.ntfyEnabled && previous.ntfyTopic);
|
|
67095
|
+
const isEnabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
|
|
67096
|
+
await this.syncNtfyProvider(settings);
|
|
67097
|
+
if (isEnabled && !wasEnabled) {
|
|
67098
|
+
schedulerLog.log("NotificationService ntfy enabled");
|
|
67099
|
+
} else if (!isEnabled && wasEnabled) {
|
|
67100
|
+
schedulerLog.log("NotificationService ntfy disabled");
|
|
67101
|
+
} else if (settings.ntfyTopic !== previous.ntfyTopic) {
|
|
67102
|
+
schedulerLog.log("NotificationService ntfy topic updated");
|
|
67103
|
+
} else if (settings.ntfyBaseUrl !== previous.ntfyBaseUrl) {
|
|
67104
|
+
schedulerLog.log("NotificationService ntfy base URL updated");
|
|
67105
|
+
} else if (settings.ntfyDashboardHost !== previous.ntfyDashboardHost) {
|
|
67106
|
+
schedulerLog.log("NotificationService ntfy dashboard host updated");
|
|
67107
|
+
} else if (JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
|
|
67108
|
+
schedulerLog.log("NotificationService ntfy events updated");
|
|
67109
|
+
}
|
|
67110
|
+
}
|
|
67111
|
+
if (settings.webhookEnabled !== previous.webhookEnabled || settings.webhookUrl !== previous.webhookUrl || settings.webhookFormat !== previous.webhookFormat || JSON.stringify(settings.webhookEvents) !== JSON.stringify(previous.webhookEvents)) {
|
|
67112
|
+
await this.syncWebhookProvider(settings);
|
|
67113
|
+
schedulerLog.log("WebhookNotificationProvider config updated");
|
|
67114
|
+
}
|
|
67115
|
+
};
|
|
67116
|
+
async syncNtfyProvider(settings) {
|
|
67117
|
+
const enabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
|
|
67118
|
+
if (!enabled) {
|
|
67119
|
+
if (this.ntfyProvider) {
|
|
67120
|
+
await this.ntfyProvider.shutdown?.();
|
|
67121
|
+
this.dispatcher.unregisterProvider(this.ntfyProvider.getProviderId());
|
|
67122
|
+
this.ntfyProvider = void 0;
|
|
67123
|
+
}
|
|
67124
|
+
return;
|
|
67125
|
+
}
|
|
67126
|
+
if (!this.ntfyProvider) {
|
|
67127
|
+
this.ntfyProvider = new NtfyNotificationProvider();
|
|
67128
|
+
this.registerProvider(this.ntfyProvider);
|
|
67129
|
+
}
|
|
67130
|
+
await this.ntfyProvider.initialize?.({
|
|
67131
|
+
topic: settings.ntfyTopic,
|
|
67132
|
+
ntfyBaseUrl: settings.ntfyBaseUrl ?? this.options.ntfyBaseUrl,
|
|
67133
|
+
dashboardHost: settings.ntfyDashboardHost,
|
|
67134
|
+
events: settings.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS],
|
|
67135
|
+
projectId: this.options.projectId
|
|
67136
|
+
});
|
|
67137
|
+
}
|
|
67138
|
+
async syncWebhookProvider(settings) {
|
|
67139
|
+
const enabled = Boolean(settings.webhookEnabled && settings.webhookUrl);
|
|
67140
|
+
if (!enabled) {
|
|
67141
|
+
if (this.webhookProvider) {
|
|
67142
|
+
await this.webhookProvider.shutdown?.();
|
|
67143
|
+
this.dispatcher.unregisterProvider(this.webhookProvider.getProviderId());
|
|
67144
|
+
this.webhookProvider = void 0;
|
|
67145
|
+
}
|
|
67146
|
+
return;
|
|
67147
|
+
}
|
|
67148
|
+
if (!this.webhookProvider) {
|
|
67149
|
+
this.webhookProvider = new WebhookNotificationProvider();
|
|
67150
|
+
this.registerProvider(this.webhookProvider);
|
|
67151
|
+
}
|
|
67152
|
+
await this.webhookProvider.initialize?.({
|
|
67153
|
+
webhookUrl: settings.webhookUrl,
|
|
67154
|
+
webhookFormat: settings.webhookFormat ?? "generic",
|
|
67155
|
+
events: settings.webhookEvents ?? []
|
|
67156
|
+
});
|
|
67157
|
+
}
|
|
67158
|
+
setNotificationsEnabledFromSettings(settings) {
|
|
67159
|
+
this.notificationsEnabled = Boolean(
|
|
67160
|
+
settings.ntfyEnabled && settings.ntfyTopic || settings.webhookEnabled && settings.webhookUrl
|
|
67161
|
+
);
|
|
67162
|
+
}
|
|
67163
|
+
createTaskPayload(task, event) {
|
|
67164
|
+
return {
|
|
67165
|
+
taskId: task.id,
|
|
67166
|
+
taskTitle: task.title,
|
|
67167
|
+
taskDescription: task.description,
|
|
67168
|
+
event
|
|
67169
|
+
};
|
|
67170
|
+
}
|
|
67171
|
+
maybeNotify(taskId, eventType, payload) {
|
|
67172
|
+
const key = `${taskId}:${eventType}`;
|
|
67173
|
+
if (this.notifiedEvents.has(key)) {
|
|
67174
|
+
return;
|
|
67175
|
+
}
|
|
67176
|
+
this.notifiedEvents.add(key);
|
|
67177
|
+
this.dispatcher.dispatch(eventType, payload).catch(() => {
|
|
67178
|
+
});
|
|
67179
|
+
}
|
|
67180
|
+
};
|
|
67181
|
+
}
|
|
67182
|
+
});
|
|
67183
|
+
|
|
67184
|
+
// ../engine/src/notification/index.ts
|
|
67185
|
+
var init_notification = __esm({
|
|
67186
|
+
"../engine/src/notification/index.ts"() {
|
|
67187
|
+
"use strict";
|
|
67188
|
+
init_ntfy_provider();
|
|
67189
|
+
init_webhook_provider();
|
|
67190
|
+
init_notification_service();
|
|
67191
|
+
}
|
|
67192
|
+
});
|
|
67193
|
+
|
|
65682
67194
|
// ../engine/src/notifier.ts
|
|
65683
67195
|
function formatTaskIdentifier(task) {
|
|
65684
67196
|
if (task.title) {
|
|
@@ -65757,6 +67269,7 @@ var init_notifier = __esm({
|
|
|
65757
67269
|
"../engine/src/notifier.ts"() {
|
|
65758
67270
|
"use strict";
|
|
65759
67271
|
init_logger2();
|
|
67272
|
+
init_notification();
|
|
65760
67273
|
DEFAULT_NTFY_BASE_URL = "https://ntfy.sh";
|
|
65761
67274
|
DEFAULT_NTFY_EVENTS = [
|
|
65762
67275
|
"in-review",
|
|
@@ -65764,7 +67277,8 @@ var init_notifier = __esm({
|
|
|
65764
67277
|
"failed",
|
|
65765
67278
|
"awaiting-approval",
|
|
65766
67279
|
"awaiting-user-review",
|
|
65767
|
-
"planning-awaiting-input"
|
|
67280
|
+
"planning-awaiting-input",
|
|
67281
|
+
"gridlock"
|
|
65768
67282
|
];
|
|
65769
67283
|
NtfyNotifier = class {
|
|
65770
67284
|
constructor(store, options = {}) {
|
|
@@ -65772,6 +67286,10 @@ var init_notifier = __esm({
|
|
|
65772
67286
|
this.defaultNtfyBaseUrl = resolveNtfyBaseUrl(options.ntfyBaseUrl);
|
|
65773
67287
|
this.ntfyBaseUrl = this.defaultNtfyBaseUrl;
|
|
65774
67288
|
this.projectId = options.projectId;
|
|
67289
|
+
this.notificationService = new NotificationService(store, {
|
|
67290
|
+
projectId: this.projectId,
|
|
67291
|
+
ntfyBaseUrl: options.ntfyBaseUrl
|
|
67292
|
+
});
|
|
65775
67293
|
}
|
|
65776
67294
|
config = {
|
|
65777
67295
|
enabled: false,
|
|
@@ -65779,6 +67297,7 @@ var init_notifier = __esm({
|
|
|
65779
67297
|
dashboardHost: void 0,
|
|
65780
67298
|
events: [...DEFAULT_NTFY_EVENTS]
|
|
65781
67299
|
};
|
|
67300
|
+
notificationService;
|
|
65782
67301
|
ntfyBaseUrl;
|
|
65783
67302
|
defaultNtfyBaseUrl;
|
|
65784
67303
|
projectId;
|
|
@@ -65788,154 +67307,23 @@ var init_notifier = __esm({
|
|
|
65788
67307
|
this.abortController = new AbortController();
|
|
65789
67308
|
const settings = await this.store.getSettings();
|
|
65790
67309
|
this.loadConfig(settings);
|
|
65791
|
-
this.store.on("task:moved", this.handleTaskMoved);
|
|
65792
|
-
this.store.on("task:updated", this.handleTaskUpdated);
|
|
65793
|
-
this.store.on("task:merged", this.handleTaskMerged);
|
|
65794
67310
|
this.store.on("settings:updated", this.handleSettingsUpdated);
|
|
67311
|
+
await this.notificationService.start();
|
|
65795
67312
|
schedulerLog.log("NtfyNotifier started");
|
|
65796
67313
|
}
|
|
65797
67314
|
stop() {
|
|
65798
67315
|
if (typeof this.store.off === "function") {
|
|
65799
|
-
this.store.off("task:moved", this.handleTaskMoved);
|
|
65800
|
-
this.store.off("task:updated", this.handleTaskUpdated);
|
|
65801
|
-
this.store.off("task:merged", this.handleTaskMerged);
|
|
65802
67316
|
this.store.off("settings:updated", this.handleSettingsUpdated);
|
|
65803
67317
|
}
|
|
65804
67318
|
if (this.abortController) {
|
|
65805
67319
|
this.abortController.abort();
|
|
65806
67320
|
this.abortController = null;
|
|
65807
67321
|
}
|
|
67322
|
+
void this.notificationService.stop();
|
|
65808
67323
|
schedulerLog.log("NtfyNotifier stopped");
|
|
65809
67324
|
}
|
|
65810
|
-
handleTaskMoved = (data) => {
|
|
65811
|
-
if (!this.config.enabled || !this.config.topic) return;
|
|
65812
|
-
const { task, to } = data;
|
|
65813
|
-
if (to === "in-review" && this.isEventEnabled("in-review")) {
|
|
65814
|
-
const clickUrl = buildNtfyClickUrl({
|
|
65815
|
-
dashboardHost: this.config.dashboardHost,
|
|
65816
|
-
projectId: this.projectId,
|
|
65817
|
-
taskId: task.id
|
|
65818
|
-
});
|
|
65819
|
-
this.maybeNotify(
|
|
65820
|
-
task.id,
|
|
65821
|
-
"in-review",
|
|
65822
|
-
() => sendNtfyNotification({
|
|
65823
|
-
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
65824
|
-
topic: this.config.topic,
|
|
65825
|
-
title: `Task ${task.id} completed`,
|
|
65826
|
-
message: `Task "${formatTaskIdentifier(task)}" is ready for review`,
|
|
65827
|
-
priority: "default",
|
|
65828
|
-
clickUrl,
|
|
65829
|
-
signal: this.abortController?.signal
|
|
65830
|
-
})
|
|
65831
|
-
);
|
|
65832
|
-
}
|
|
65833
|
-
};
|
|
65834
|
-
handleTaskUpdated = (task) => {
|
|
65835
|
-
if (!this.config.enabled || !this.config.topic) return;
|
|
65836
|
-
if (task.status === "failed" && this.isEventEnabled("failed")) {
|
|
65837
|
-
const clickUrl = buildNtfyClickUrl({
|
|
65838
|
-
dashboardHost: this.config.dashboardHost,
|
|
65839
|
-
projectId: this.projectId,
|
|
65840
|
-
taskId: task.id
|
|
65841
|
-
});
|
|
65842
|
-
this.maybeNotify(
|
|
65843
|
-
task.id,
|
|
65844
|
-
"failed",
|
|
65845
|
-
() => sendNtfyNotification({
|
|
65846
|
-
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
65847
|
-
topic: this.config.topic,
|
|
65848
|
-
title: `Task ${task.id} failed`,
|
|
65849
|
-
message: `Task "${formatTaskIdentifier(task)}" has failed and needs attention`,
|
|
65850
|
-
priority: "high",
|
|
65851
|
-
clickUrl,
|
|
65852
|
-
signal: this.abortController?.signal
|
|
65853
|
-
})
|
|
65854
|
-
);
|
|
65855
|
-
}
|
|
65856
|
-
if (task.status === "awaiting-approval" && this.isEventEnabled("awaiting-approval")) {
|
|
65857
|
-
const clickUrl = buildNtfyClickUrl({
|
|
65858
|
-
dashboardHost: this.config.dashboardHost,
|
|
65859
|
-
projectId: this.projectId,
|
|
65860
|
-
taskId: task.id
|
|
65861
|
-
});
|
|
65862
|
-
this.maybeNotify(
|
|
65863
|
-
task.id,
|
|
65864
|
-
"awaiting-approval",
|
|
65865
|
-
() => sendNtfyNotification({
|
|
65866
|
-
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
65867
|
-
topic: this.config.topic,
|
|
65868
|
-
title: `Plan needs approval for ${task.id}`,
|
|
65869
|
-
message: `Task "${formatTaskIdentifier(task)}" needs your approval before it can proceed`,
|
|
65870
|
-
priority: "high",
|
|
65871
|
-
clickUrl,
|
|
65872
|
-
signal: this.abortController?.signal
|
|
65873
|
-
})
|
|
65874
|
-
);
|
|
65875
|
-
}
|
|
65876
|
-
if (task.status === "awaiting-user-review" && this.isEventEnabled("awaiting-user-review")) {
|
|
65877
|
-
const clickUrl = buildNtfyClickUrl({
|
|
65878
|
-
dashboardHost: this.config.dashboardHost,
|
|
65879
|
-
projectId: this.projectId,
|
|
65880
|
-
taskId: task.id
|
|
65881
|
-
});
|
|
65882
|
-
this.maybeNotify(
|
|
65883
|
-
task.id,
|
|
65884
|
-
"awaiting-user-review",
|
|
65885
|
-
() => sendNtfyNotification({
|
|
65886
|
-
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
65887
|
-
topic: this.config.topic,
|
|
65888
|
-
title: `User review needed for ${task.id}`,
|
|
65889
|
-
message: `Task "${formatTaskIdentifier(task)}" needs human review before it can proceed`,
|
|
65890
|
-
priority: "high",
|
|
65891
|
-
clickUrl,
|
|
65892
|
-
signal: this.abortController?.signal
|
|
65893
|
-
})
|
|
65894
|
-
);
|
|
65895
|
-
}
|
|
65896
|
-
};
|
|
65897
|
-
handleTaskMerged = (result) => {
|
|
65898
|
-
if (!this.config.enabled || !this.config.topic) return;
|
|
65899
|
-
if (result.merged && this.isEventEnabled("merged")) {
|
|
65900
|
-
const clickUrl = buildNtfyClickUrl({
|
|
65901
|
-
dashboardHost: this.config.dashboardHost,
|
|
65902
|
-
projectId: this.projectId,
|
|
65903
|
-
taskId: result.task.id
|
|
65904
|
-
});
|
|
65905
|
-
this.maybeNotify(
|
|
65906
|
-
result.task.id,
|
|
65907
|
-
"merged",
|
|
65908
|
-
() => sendNtfyNotification({
|
|
65909
|
-
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
65910
|
-
topic: this.config.topic,
|
|
65911
|
-
title: `Task ${result.task.id} merged`,
|
|
65912
|
-
message: `Task "${formatTaskIdentifier(result.task)}" has been merged to main`,
|
|
65913
|
-
priority: "default",
|
|
65914
|
-
clickUrl,
|
|
65915
|
-
signal: this.abortController?.signal
|
|
65916
|
-
})
|
|
65917
|
-
);
|
|
65918
|
-
}
|
|
65919
|
-
};
|
|
65920
67325
|
handleSettingsUpdated = (data) => {
|
|
65921
|
-
|
|
65922
|
-
if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
|
|
65923
|
-
const wasEnabled = this.config.enabled;
|
|
65924
|
-
this.loadConfig(settings);
|
|
65925
|
-
if (this.config.enabled && !wasEnabled) {
|
|
65926
|
-
schedulerLog.log("NtfyNotifier enabled");
|
|
65927
|
-
} else if (!this.config.enabled && wasEnabled) {
|
|
65928
|
-
schedulerLog.log("NtfyNotifier disabled");
|
|
65929
|
-
} else if (this.config.topic !== previous.ntfyTopic) {
|
|
65930
|
-
schedulerLog.log("NtfyNotifier topic updated");
|
|
65931
|
-
} else if (this.ntfyBaseUrl !== resolveNtfyBaseUrl(previous.ntfyBaseUrl)) {
|
|
65932
|
-
schedulerLog.log("NtfyNotifier base URL updated");
|
|
65933
|
-
} else if (this.config.dashboardHost !== previous.ntfyDashboardHost) {
|
|
65934
|
-
schedulerLog.log("NtfyNotifier dashboard host updated");
|
|
65935
|
-
} else if (JSON.stringify(this.config.events) !== JSON.stringify(previous.ntfyEvents)) {
|
|
65936
|
-
schedulerLog.log("NtfyNotifier events updated");
|
|
65937
|
-
}
|
|
65938
|
-
}
|
|
67326
|
+
this.loadConfig(data.settings);
|
|
65939
67327
|
};
|
|
65940
67328
|
loadConfig(settings) {
|
|
65941
67329
|
this.config = {
|
|
@@ -65946,11 +67334,38 @@ var init_notifier = __esm({
|
|
|
65946
67334
|
};
|
|
65947
67335
|
this.ntfyBaseUrl = resolveNtfyBaseUrl(settings.ntfyBaseUrl, this.defaultNtfyBaseUrl);
|
|
65948
67336
|
}
|
|
67337
|
+
notifyGridlock(event) {
|
|
67338
|
+
if (!this.config.enabled || !this.config.topic || !this.isEventEnabled("gridlock")) return;
|
|
67339
|
+
const blockedTasks = event.blockedTaskIds.sort();
|
|
67340
|
+
const reasonSummary = Object.values(event.reasons).reduce((acc, reason) => {
|
|
67341
|
+
acc[reason] = (acc[reason] ?? 0) + 1;
|
|
67342
|
+
return acc;
|
|
67343
|
+
}, {});
|
|
67344
|
+
const reasons = [];
|
|
67345
|
+
if (reasonSummary.dependency) reasons.push(`${reasonSummary.dependency} dependency`);
|
|
67346
|
+
if (reasonSummary.overlap) reasons.push(`${reasonSummary.overlap} overlap`);
|
|
67347
|
+
const clickUrl = buildNtfyClickUrl({
|
|
67348
|
+
dashboardHost: this.config.dashboardHost,
|
|
67349
|
+
projectId: this.projectId
|
|
67350
|
+
});
|
|
67351
|
+
const dedupKey = `gridlock:${blockedTasks.join(",")}`;
|
|
67352
|
+
this.maybeNotifyByKey(
|
|
67353
|
+
dedupKey,
|
|
67354
|
+
() => sendNtfyNotification({
|
|
67355
|
+
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
67356
|
+
topic: this.config.topic,
|
|
67357
|
+
title: "Pipeline gridlocked",
|
|
67358
|
+
message: `${event.blockedTaskCount} todo tasks are blocked (${reasons.join(", ")}). Blocked: ${blockedTasks.join(", ")}. Blocking: ${event.blockingTaskIds.join(", ") || "none"}.`,
|
|
67359
|
+
priority: "high",
|
|
67360
|
+
clickUrl,
|
|
67361
|
+
signal: this.abortController?.signal
|
|
67362
|
+
})
|
|
67363
|
+
);
|
|
67364
|
+
}
|
|
65949
67365
|
isEventEnabled(event) {
|
|
65950
67366
|
return isNtfyEventEnabled(this.config.events, event);
|
|
65951
67367
|
}
|
|
65952
|
-
|
|
65953
|
-
const key = `${taskId}:${eventType}`;
|
|
67368
|
+
maybeNotifyByKey(key, notifyFn) {
|
|
65954
67369
|
if (this.notifiedEvents.has(key)) {
|
|
65955
67370
|
return;
|
|
65956
67371
|
}
|
|
@@ -65978,11 +67393,10 @@ var init_shell_utils = __esm({
|
|
|
65978
67393
|
import { exec as exec6 } from "node:child_process";
|
|
65979
67394
|
import { promisify as promisify7 } from "node:util";
|
|
65980
67395
|
async function createAiPromptExecutor(cwd) {
|
|
65981
|
-
const { createFnAgent: createFnAgent5, promptWithFallback: promptWithFallback2 } = await Promise.resolve().then(() => (init_pi(), pi_exports));
|
|
65982
67396
|
const disposeLog = createLogger2("cron-runner");
|
|
65983
67397
|
return async (prompt, modelProvider, modelId) => {
|
|
65984
67398
|
let responseText = "";
|
|
65985
|
-
const { session } = await
|
|
67399
|
+
const { session } = await createFnAgent2({
|
|
65986
67400
|
cwd,
|
|
65987
67401
|
systemPrompt: AI_AUTOMATION_SYSTEM_PROMPT,
|
|
65988
67402
|
tools: "readonly",
|
|
@@ -65993,7 +67407,7 @@ async function createAiPromptExecutor(cwd) {
|
|
|
65993
67407
|
}
|
|
65994
67408
|
});
|
|
65995
67409
|
try {
|
|
65996
|
-
await
|
|
67410
|
+
await promptWithFallback(session, prompt);
|
|
65997
67411
|
return responseText;
|
|
65998
67412
|
} finally {
|
|
65999
67413
|
try {
|
|
@@ -66019,8 +67433,10 @@ var execAsync6, log6, DEFAULT_TIMEOUT_MS, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT
|
|
|
66019
67433
|
var init_cron_runner = __esm({
|
|
66020
67434
|
"../engine/src/cron-runner.ts"() {
|
|
66021
67435
|
"use strict";
|
|
67436
|
+
init_src();
|
|
66022
67437
|
init_logger2();
|
|
66023
67438
|
init_shell_utils();
|
|
67439
|
+
init_pi();
|
|
66024
67440
|
execAsync6 = promisify7(exec6);
|
|
66025
67441
|
log6 = createLogger2("cron-runner");
|
|
66026
67442
|
DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -66343,8 +67759,9 @@ var init_cron_runner = __esm({
|
|
|
66343
67759
|
};
|
|
66344
67760
|
}
|
|
66345
67761
|
const settings = await this.store.getSettings();
|
|
66346
|
-
const
|
|
66347
|
-
const
|
|
67762
|
+
const defaultModel = resolveProjectDefaultModel(settings);
|
|
67763
|
+
const modelProvider = step.modelProvider?.trim() || defaultModel.provider;
|
|
67764
|
+
const modelId = step.modelId?.trim() || defaultModel.modelId;
|
|
66348
67765
|
const model = modelProvider && modelId ? `${modelProvider}/${modelId}` : "default";
|
|
66349
67766
|
log6.log(` AI prompt step "${step.name}" using model: ${model}`);
|
|
66350
67767
|
log6.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
|
|
@@ -67341,6 +68758,7 @@ var init_agent_heartbeat = __esm({
|
|
|
67341
68758
|
init_agent_instructions();
|
|
67342
68759
|
init_logger2();
|
|
67343
68760
|
init_run_audit();
|
|
68761
|
+
init_pi();
|
|
67344
68762
|
HEARTBEAT_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window.
|
|
67345
68763
|
|
|
67346
68764
|
Your job:
|
|
@@ -68056,7 +69474,6 @@ When sending messages:
|
|
|
68056
69474
|
};
|
|
68057
69475
|
const previousBlockedState = await this.store.getLastBlockedState(agentId);
|
|
68058
69476
|
if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
|
|
68059
|
-
heartbeatLog.log(`Task ${resolvedTaskId2} is still blocked by ${blockedBy} (duplicate state) \u2014 skipping comment`);
|
|
68060
69477
|
await this.completeRun(agentId, run.id, {
|
|
68061
69478
|
status: "completed",
|
|
68062
69479
|
resultJson: { reason: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
|
|
@@ -68108,7 +69525,6 @@ When sending messages:
|
|
|
68108
69525
|
};
|
|
68109
69526
|
}
|
|
68110
69527
|
};
|
|
68111
|
-
const { promptWithFallback: promptWithFallback2 } = await Promise.resolve().then(() => (init_pi(), pi_exports));
|
|
68112
69528
|
const { createResolvedAgentSession: createResolvedAgentSession2, extractRuntimeHint: extractRuntimeHint2 } = await Promise.resolve().then(() => (init_agent_session_helpers(), agent_session_helpers_exports));
|
|
68113
69529
|
const { buildSessionSkillContextSync: buildSessionSkillContextSync2 } = await Promise.resolve().then(() => (init_session_skill_context(), session_skill_context_exports));
|
|
68114
69530
|
let heartbeatTools;
|
|
@@ -68302,7 +69718,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
68302
69718
|
"Review the task status and take appropriate action. Call fn_heartbeat_done when finished."
|
|
68303
69719
|
].join("\n");
|
|
68304
69720
|
}
|
|
68305
|
-
await
|
|
69721
|
+
await promptWithFallback(session, executionPrompt);
|
|
68306
69722
|
let usageInput = 0;
|
|
68307
69723
|
let usageOutput = Math.ceil(outputLength / 4);
|
|
68308
69724
|
let usageCached = 0;
|
|
@@ -69155,13 +70571,40 @@ var init_self_healing = __esm({
|
|
|
69155
70571
|
return Date.now() - updatedAt >= timeoutMs;
|
|
69156
70572
|
}
|
|
69157
70573
|
async findLandedTaskCommit(task) {
|
|
69158
|
-
const
|
|
70574
|
+
const storedSha = task.mergeDetails?.commitSha;
|
|
70575
|
+
if (storedSha) {
|
|
70576
|
+
try {
|
|
70577
|
+
await execAsync8(
|
|
70578
|
+
`git merge-base --is-ancestor ${shellQuote(storedSha)} HEAD`,
|
|
70579
|
+
{ cwd: this.options.rootDir }
|
|
70580
|
+
);
|
|
70581
|
+
const { stdout: stdout2 } = await execAsync8(
|
|
70582
|
+
`git log -1 --format=%H%x1f%s ${shellQuote(storedSha)}`,
|
|
70583
|
+
{ cwd: this.options.rootDir, maxBuffer: 1024 * 1024 }
|
|
70584
|
+
);
|
|
70585
|
+
const [sha2, subject2] = stdout2.trim().split("");
|
|
70586
|
+
if (sha2) {
|
|
70587
|
+
const commit2 = { sha: sha2, subject: subject2 };
|
|
70588
|
+
try {
|
|
70589
|
+
const stats = await execAsync8(`git show --shortstat --format= ${shellQuote(sha2)}`, {
|
|
70590
|
+
cwd: this.options.rootDir,
|
|
70591
|
+
maxBuffer: 1024 * 1024
|
|
70592
|
+
});
|
|
70593
|
+
Object.assign(commit2, parseShortstat(stats.stdout));
|
|
70594
|
+
} catch {
|
|
70595
|
+
}
|
|
70596
|
+
return commit2;
|
|
70597
|
+
}
|
|
70598
|
+
} catch {
|
|
70599
|
+
}
|
|
70600
|
+
}
|
|
70601
|
+
const readLog = async (range, grepArg, fixedStrings) => {
|
|
69159
70602
|
const command = [
|
|
69160
70603
|
"git log",
|
|
69161
70604
|
"--format=%H%x1f%s",
|
|
69162
70605
|
"--max-count=20",
|
|
69163
|
-
"--fixed-strings",
|
|
69164
|
-
`--grep=${
|
|
70606
|
+
...fixedStrings ? ["--fixed-strings"] : ["-E"],
|
|
70607
|
+
`--grep=${grepArg}`,
|
|
69165
70608
|
shellQuote(range)
|
|
69166
70609
|
].join(" ");
|
|
69167
70610
|
return execAsync8(command, {
|
|
@@ -69169,22 +70612,34 @@ var init_self_healing = __esm({
|
|
|
69169
70612
|
maxBuffer: 1024 * 1024
|
|
69170
70613
|
});
|
|
69171
70614
|
};
|
|
69172
|
-
|
|
69173
|
-
|
|
69174
|
-
|
|
69175
|
-
|
|
69176
|
-
|
|
69177
|
-
|
|
69178
|
-
|
|
69179
|
-
|
|
69180
|
-
|
|
69181
|
-
|
|
69182
|
-
|
|
69183
|
-
|
|
69184
|
-
|
|
69185
|
-
|
|
69186
|
-
|
|
69187
|
-
|
|
70615
|
+
const search = async (grepArg, fixedStrings) => {
|
|
70616
|
+
let out;
|
|
70617
|
+
try {
|
|
70618
|
+
const r = await readLog(
|
|
70619
|
+
task.baseCommitSha ? `${task.baseCommitSha}..HEAD` : "HEAD",
|
|
70620
|
+
grepArg,
|
|
70621
|
+
fixedStrings
|
|
70622
|
+
);
|
|
70623
|
+
out = r.stdout;
|
|
70624
|
+
} catch (err) {
|
|
70625
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
70626
|
+
log8.warn(
|
|
70627
|
+
`Failed to read git log for landed commit lookup (${task.id}): ${errorMessage} \u2014 retrying with HEAD range`
|
|
70628
|
+
);
|
|
70629
|
+
if (!task.baseCommitSha) return "";
|
|
70630
|
+
const r = await readLog("HEAD", grepArg, fixedStrings);
|
|
70631
|
+
out = r.stdout;
|
|
70632
|
+
}
|
|
70633
|
+
if (!out.trim() && task.baseCommitSha) {
|
|
70634
|
+
const r = await readLog("HEAD", grepArg, fixedStrings);
|
|
70635
|
+
out = r.stdout;
|
|
70636
|
+
}
|
|
70637
|
+
return out;
|
|
70638
|
+
};
|
|
70639
|
+
const trailerPattern = `^Fusion-Task-Id: ${task.id}$`;
|
|
70640
|
+
let stdout = await search(shellQuote(trailerPattern), false);
|
|
70641
|
+
if (!stdout.trim()) {
|
|
70642
|
+
stdout = await search(shellQuote(task.id), true);
|
|
69188
70643
|
}
|
|
69189
70644
|
const firstLine = stdout.trim().split("\n").find(Boolean);
|
|
69190
70645
|
if (!firstLine) return null;
|
|
@@ -69564,7 +71019,7 @@ var init_self_healing = __esm({
|
|
|
69564
71019
|
if (!timeoutMs || timeoutMs <= 0) return 0;
|
|
69565
71020
|
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
|
69566
71021
|
const candidates = tasks.filter(
|
|
69567
|
-
(task) => task.column === "in-review" && Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) && this.isPastInterruptedMergeGrace(task, timeoutMs)
|
|
71022
|
+
(task) => task.column === "in-review" && !task.paused && Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) && this.isPastInterruptedMergeGrace(task, timeoutMs)
|
|
69568
71023
|
);
|
|
69569
71024
|
if (candidates.length === 0) return 0;
|
|
69570
71025
|
log8.warn(`Found ${candidates.length} stale merging task(s) in in-review`);
|
|
@@ -69605,6 +71060,13 @@ var init_self_healing = __esm({
|
|
|
69605
71060
|
"Auto-recovered: stale merge status cleared; merge will be retried"
|
|
69606
71061
|
);
|
|
69607
71062
|
log8.log(`Recovered interrupted merge ${task.id}: cleared stale status for retry`);
|
|
71063
|
+
try {
|
|
71064
|
+
this.options.enqueueMerge?.(task.id);
|
|
71065
|
+
} catch (enqueueErr) {
|
|
71066
|
+
log8.warn(
|
|
71067
|
+
`Failed to re-enqueue ${task.id} after stale-merge recovery (will rely on polling sweep): ${enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr)}`
|
|
71068
|
+
);
|
|
71069
|
+
}
|
|
69608
71070
|
recovered++;
|
|
69609
71071
|
} catch (err) {
|
|
69610
71072
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -69635,7 +71097,7 @@ var init_self_healing = __esm({
|
|
|
69635
71097
|
try {
|
|
69636
71098
|
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
|
69637
71099
|
const mergedButNotDone = tasks.filter(
|
|
69638
|
-
(t) => t.column === "in-review" && t.mergeDetails?.mergeConfirmed === true
|
|
71100
|
+
(t) => t.column === "in-review" && !t.paused && t.mergeDetails?.mergeConfirmed === true
|
|
69639
71101
|
);
|
|
69640
71102
|
if (mergedButNotDone.length === 0) return 0;
|
|
69641
71103
|
log8.warn(`Found ${mergedButNotDone.length} merged task(s) stuck in in-review`);
|
|
@@ -69683,7 +71145,7 @@ var init_self_healing = __esm({
|
|
|
69683
71145
|
try {
|
|
69684
71146
|
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
|
69685
71147
|
const misclassified = tasks.filter(
|
|
69686
|
-
(t) => t.column === "in-review" && t.status === "failed" && t.error?.includes("without calling task_done") && t.steps.length > 0 && t.steps.every((s) => s.status === "done" || s.status === "skipped")
|
|
71148
|
+
(t) => t.column === "in-review" && !t.paused && t.status === "failed" && t.error?.includes("without calling task_done") && t.steps.length > 0 && t.steps.every((s) => s.status === "done" || s.status === "skipped")
|
|
69687
71149
|
);
|
|
69688
71150
|
if (misclassified.length === 0) return 0;
|
|
69689
71151
|
log8.warn(`Found ${misclassified.length} misclassified failure(s) with all steps done`);
|
|
@@ -70803,6 +72265,12 @@ var init_in_process_runtime = __esm({
|
|
|
70803
72265
|
ephemeralCleanupTimers = /* @__PURE__ */ new Map();
|
|
70804
72266
|
/** Listener for agent:stateChanged events to clean up terminated ephemeral agents */
|
|
70805
72267
|
ephemeralTerminationListener;
|
|
72268
|
+
/**
|
|
72269
|
+
* Optional callback the runtime forwards to SelfHealingManager so that
|
|
72270
|
+
* stale-merge recovery can re-enqueue tasks immediately. Set by ProjectEngine
|
|
72271
|
+
* before `start()` via `setMergeEnqueuer`.
|
|
72272
|
+
*/
|
|
72273
|
+
mergeEnqueuer;
|
|
70806
72274
|
/**
|
|
70807
72275
|
* Start the runtime and initialize all subsystems.
|
|
70808
72276
|
*
|
|
@@ -70923,8 +72391,7 @@ var init_in_process_runtime = __esm({
|
|
|
70923
72391
|
this.recordActivity();
|
|
70924
72392
|
runtimeLog.log(`Scheduled task ${task.id}`);
|
|
70925
72393
|
},
|
|
70926
|
-
onBlocked: (
|
|
70927
|
-
runtimeLog.log(`Task ${task.id} blocked by: ${blockedBy.join(", ")}`);
|
|
72394
|
+
onBlocked: () => {
|
|
70928
72395
|
}
|
|
70929
72396
|
});
|
|
70930
72397
|
this.stuckTaskDetector = new StuckTaskDetector(this.taskStore, {
|
|
@@ -71244,7 +72711,8 @@ var init_in_process_runtime = __esm({
|
|
|
71244
72711
|
getExecutingTaskIds: () => this.executor.getExecutingTaskIds(),
|
|
71245
72712
|
recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
|
|
71246
72713
|
getPlanningTaskIds: () => this.triageProcessor?.getProcessingTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
71247
|
-
evictStaleTriageProcessing: () => this.triageProcessor?.evictStaleProcessing() ?? /* @__PURE__ */ new Set()
|
|
72714
|
+
evictStaleTriageProcessing: () => this.triageProcessor?.evictStaleProcessing() ?? /* @__PURE__ */ new Set(),
|
|
72715
|
+
enqueueMerge: this.mergeEnqueuer ? (taskId) => this.mergeEnqueuer?.(taskId) : void 0
|
|
71248
72716
|
});
|
|
71249
72717
|
this.selfHealingManager.start();
|
|
71250
72718
|
this.stuckTaskDetector.start();
|
|
@@ -71421,6 +72889,14 @@ var init_in_process_runtime = __esm({
|
|
|
71421
72889
|
getStatus() {
|
|
71422
72890
|
return this.status;
|
|
71423
72891
|
}
|
|
72892
|
+
/**
|
|
72893
|
+
* Register a callback used by SelfHealingManager to re-enqueue tasks for
|
|
72894
|
+
* auto-merge after clearing a stale `merging` status. Must be called before
|
|
72895
|
+
* `start()` because SelfHealingManager is constructed during startup.
|
|
72896
|
+
*/
|
|
72897
|
+
setMergeEnqueuer(enqueueMerge) {
|
|
72898
|
+
this.mergeEnqueuer = enqueueMerge;
|
|
72899
|
+
}
|
|
71424
72900
|
/**
|
|
71425
72901
|
* Get the project's TaskStore instance.
|
|
71426
72902
|
* @throws Error if runtime has not been started
|
|
@@ -73190,6 +74666,132 @@ var init_project_manager = __esm({
|
|
|
73190
74666
|
}
|
|
73191
74667
|
});
|
|
73192
74668
|
|
|
74669
|
+
// ../engine/src/gridlock-detector.ts
|
|
74670
|
+
var gridlockLog, GridlockDetector;
|
|
74671
|
+
var init_gridlock_detector = __esm({
|
|
74672
|
+
"../engine/src/gridlock-detector.ts"() {
|
|
74673
|
+
"use strict";
|
|
74674
|
+
init_logger2();
|
|
74675
|
+
init_scheduler();
|
|
74676
|
+
gridlockLog = createLogger2("gridlock-detector");
|
|
74677
|
+
GridlockDetector = class {
|
|
74678
|
+
constructor(store, options = {}) {
|
|
74679
|
+
this.store = store;
|
|
74680
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 3e4;
|
|
74681
|
+
this.missionStore = options.missionStore;
|
|
74682
|
+
this.onGridlock = options.onGridlock;
|
|
74683
|
+
}
|
|
74684
|
+
interval = null;
|
|
74685
|
+
pollIntervalMs;
|
|
74686
|
+
missionStore;
|
|
74687
|
+
onGridlock;
|
|
74688
|
+
lastGridlockKey = null;
|
|
74689
|
+
start() {
|
|
74690
|
+
if (this.interval) return;
|
|
74691
|
+
this.interval = setInterval(() => {
|
|
74692
|
+
this.detectGridlock().catch((error) => {
|
|
74693
|
+
gridlockLog.error("Failed gridlock detection cycle:", error);
|
|
74694
|
+
});
|
|
74695
|
+
}, this.pollIntervalMs);
|
|
74696
|
+
gridlockLog.log(`Started (poll interval: ${this.pollIntervalMs}ms)`);
|
|
74697
|
+
}
|
|
74698
|
+
stop() {
|
|
74699
|
+
if (!this.interval) return;
|
|
74700
|
+
clearInterval(this.interval);
|
|
74701
|
+
this.interval = null;
|
|
74702
|
+
gridlockLog.log("Stopped");
|
|
74703
|
+
}
|
|
74704
|
+
async detectGridlock() {
|
|
74705
|
+
const [tasks, settings] = await Promise.all([
|
|
74706
|
+
this.store.listTasks({ slim: true, includeArchived: false }),
|
|
74707
|
+
this.store.getSettings()
|
|
74708
|
+
]);
|
|
74709
|
+
const now = Date.now();
|
|
74710
|
+
const schedulable = tasks.filter((task) => {
|
|
74711
|
+
if (task.column !== "todo" || task.paused) return false;
|
|
74712
|
+
if (task.nextRecoveryAt && new Date(task.nextRecoveryAt).getTime() > now) return false;
|
|
74713
|
+
if (this.isMissionBlocked(task)) return false;
|
|
74714
|
+
return true;
|
|
74715
|
+
});
|
|
74716
|
+
if (schedulable.length === 0) {
|
|
74717
|
+
this.lastGridlockKey = null;
|
|
74718
|
+
return null;
|
|
74719
|
+
}
|
|
74720
|
+
const active = tasks.filter((task) => task.column === "in-progress" || task.column === "in-review" && Boolean(task.worktree));
|
|
74721
|
+
if (active.length === 0) {
|
|
74722
|
+
this.lastGridlockKey = null;
|
|
74723
|
+
return null;
|
|
74724
|
+
}
|
|
74725
|
+
const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
|
|
74726
|
+
const activeScopes = /* @__PURE__ */ new Map();
|
|
74727
|
+
if (settings.groupOverlappingFiles) {
|
|
74728
|
+
for (const task of active) {
|
|
74729
|
+
const scope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths);
|
|
74730
|
+
if (scope.length > 0) {
|
|
74731
|
+
activeScopes.set(task.id, scope);
|
|
74732
|
+
}
|
|
74733
|
+
}
|
|
74734
|
+
}
|
|
74735
|
+
const reasons = {};
|
|
74736
|
+
const blockingTaskIds = /* @__PURE__ */ new Set();
|
|
74737
|
+
for (const task of schedulable) {
|
|
74738
|
+
const unmetDeps = task.dependencies.filter((depId) => {
|
|
74739
|
+
const dep = tasks.find((candidate) => candidate.id === depId);
|
|
74740
|
+
return dep && dep.column !== "done" && dep.column !== "in-review" && dep.column !== "archived";
|
|
74741
|
+
});
|
|
74742
|
+
if (unmetDeps.length > 0) {
|
|
74743
|
+
reasons[task.id] = "dependency";
|
|
74744
|
+
for (const depId of unmetDeps) blockingTaskIds.add(depId);
|
|
74745
|
+
continue;
|
|
74746
|
+
}
|
|
74747
|
+
if (!settings.groupOverlappingFiles) continue;
|
|
74748
|
+
const taskScope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths);
|
|
74749
|
+
if (taskScope.length === 0) continue;
|
|
74750
|
+
for (const [activeId, activeScope] of activeScopes) {
|
|
74751
|
+
if (pathsOverlap2(taskScope, activeScope)) {
|
|
74752
|
+
reasons[task.id] = "overlap";
|
|
74753
|
+
blockingTaskIds.add(activeId);
|
|
74754
|
+
break;
|
|
74755
|
+
}
|
|
74756
|
+
}
|
|
74757
|
+
}
|
|
74758
|
+
const blockedTaskIds = Object.keys(reasons).sort();
|
|
74759
|
+
if (blockedTaskIds.length !== schedulable.length) {
|
|
74760
|
+
this.lastGridlockKey = null;
|
|
74761
|
+
return null;
|
|
74762
|
+
}
|
|
74763
|
+
const gridlockKey = blockedTaskIds.join(",");
|
|
74764
|
+
const event = {
|
|
74765
|
+
blockedTaskCount: blockedTaskIds.length,
|
|
74766
|
+
reasons,
|
|
74767
|
+
blockedTaskIds,
|
|
74768
|
+
blockingTaskIds: Array.from(blockingTaskIds).sort()
|
|
74769
|
+
};
|
|
74770
|
+
if (this.lastGridlockKey !== gridlockKey) {
|
|
74771
|
+
this.lastGridlockKey = gridlockKey;
|
|
74772
|
+
gridlockLog.warn(`Gridlock detected: blocked=${event.blockedTaskIds.join(",")}; blocking=${event.blockingTaskIds.join(",")}`);
|
|
74773
|
+
this.onGridlock?.(event);
|
|
74774
|
+
}
|
|
74775
|
+
return event;
|
|
74776
|
+
}
|
|
74777
|
+
isMissionBlocked(task) {
|
|
74778
|
+
if (!this.missionStore || !task.sliceId) return false;
|
|
74779
|
+
try {
|
|
74780
|
+
const slice = this.missionStore.getSlice(task.sliceId);
|
|
74781
|
+
if (!slice) return false;
|
|
74782
|
+
const milestone = this.missionStore.getMilestone(slice.milestoneId);
|
|
74783
|
+
if (!milestone) return false;
|
|
74784
|
+
const mission = this.missionStore.getMission(milestone.missionId);
|
|
74785
|
+
return mission?.status === "blocked";
|
|
74786
|
+
} catch (error) {
|
|
74787
|
+
gridlockLog.warn(`Mission lookup failed for ${task.id}:`, error);
|
|
74788
|
+
return false;
|
|
74789
|
+
}
|
|
74790
|
+
}
|
|
74791
|
+
};
|
|
74792
|
+
}
|
|
74793
|
+
});
|
|
74794
|
+
|
|
73193
74795
|
// ../engine/src/remote-access/provider-adapters.ts
|
|
73194
74796
|
import { accessSync, constants as fsConstants } from "node:fs";
|
|
73195
74797
|
function isAbsoluteOrPathLike(input) {
|
|
@@ -73772,6 +75374,8 @@ var init_project_engine = __esm({
|
|
|
73772
75374
|
init_pr_monitor();
|
|
73773
75375
|
init_pr_comment_handler();
|
|
73774
75376
|
init_notifier();
|
|
75377
|
+
init_notification();
|
|
75378
|
+
init_gridlock_detector();
|
|
73775
75379
|
init_cron_runner();
|
|
73776
75380
|
init_merger();
|
|
73777
75381
|
init_concurrency();
|
|
@@ -73785,11 +75389,24 @@ var init_project_engine = __esm({
|
|
|
73785
75389
|
this.options = options;
|
|
73786
75390
|
const runtimeConfig = options.externalTaskStore ? { ...config, externalTaskStore: options.externalTaskStore } : config;
|
|
73787
75391
|
this.runtime = new InProcessRuntime(runtimeConfig, centralCore);
|
|
75392
|
+
this.runtime.setMergeEnqueuer?.((taskId) => {
|
|
75393
|
+
if (this.activeMergeTaskId === taskId) {
|
|
75394
|
+
this.mergeAbortController?.abort();
|
|
75395
|
+
this.mergeAbortController = null;
|
|
75396
|
+
this.activeMergeSession?.dispose();
|
|
75397
|
+
this.activeMergeSession = null;
|
|
75398
|
+
this.activeMergeTaskId = null;
|
|
75399
|
+
}
|
|
75400
|
+
this.mergeActive.delete(taskId);
|
|
75401
|
+
this.internalEnqueueMerge(taskId);
|
|
75402
|
+
});
|
|
73788
75403
|
}
|
|
73789
75404
|
runtime;
|
|
73790
75405
|
prMonitor;
|
|
73791
75406
|
prCommentHandler;
|
|
73792
75407
|
notifier;
|
|
75408
|
+
notificationService;
|
|
75409
|
+
gridlockDetector;
|
|
73793
75410
|
cronRunner;
|
|
73794
75411
|
automationStore;
|
|
73795
75412
|
remoteTunnelManager;
|
|
@@ -73854,12 +75471,21 @@ var init_project_engine = __esm({
|
|
|
73854
75471
|
(taskId, prInfo, comments) => this.prCommentHandler.handleNewComments(taskId, prInfo, comments)
|
|
73855
75472
|
);
|
|
73856
75473
|
if (!this.options.skipNotifier) {
|
|
75474
|
+
this.notificationService = new NotificationService(store, {
|
|
75475
|
+
projectId: this.options.projectId,
|
|
75476
|
+
ntfyBaseUrl: this.options.ntfyBaseUrl
|
|
75477
|
+
});
|
|
75478
|
+
await this.notificationService.start();
|
|
73857
75479
|
this.notifier = new NtfyNotifier(store, {
|
|
73858
75480
|
projectId: this.options.projectId,
|
|
73859
75481
|
ntfyBaseUrl: this.options.ntfyBaseUrl
|
|
73860
75482
|
});
|
|
73861
75483
|
await this.notifier.start();
|
|
73862
75484
|
}
|
|
75485
|
+
this.gridlockDetector = new GridlockDetector(store, {
|
|
75486
|
+
onGridlock: (event) => this.notifier?.notifyGridlock(event)
|
|
75487
|
+
});
|
|
75488
|
+
this.gridlockDetector.start();
|
|
73863
75489
|
this.setAutomationSubsystemHealth(
|
|
73864
75490
|
"initializing",
|
|
73865
75491
|
"Initializing AutomationStore and CronRunner"
|
|
@@ -73990,7 +75616,9 @@ ${detail}`
|
|
|
73990
75616
|
}
|
|
73991
75617
|
} catch {
|
|
73992
75618
|
}
|
|
75619
|
+
this.notificationService?.stop();
|
|
73993
75620
|
this.notifier?.stop();
|
|
75621
|
+
this.gridlockDetector?.stop();
|
|
73994
75622
|
this.cronRunner?.stop();
|
|
73995
75623
|
this.setAutomationSubsystemHealth("not-initialized", "Automation subsystem stopped");
|
|
73996
75624
|
const tunnelManager = this.remoteTunnelManager;
|
|
@@ -75809,6 +77437,8 @@ __export(src_exports2, {
|
|
|
75809
77437
|
MissionAutopilot: () => MissionAutopilot,
|
|
75810
77438
|
MissionExecutionLoop: () => MissionExecutionLoop,
|
|
75811
77439
|
NodeHealthMonitor: () => NodeHealthMonitor,
|
|
77440
|
+
NotificationService: () => NotificationService,
|
|
77441
|
+
NtfyNotificationProvider: () => NtfyNotificationProvider,
|
|
75812
77442
|
NtfyNotifier: () => NtfyNotifier,
|
|
75813
77443
|
PRIORITY_EXECUTE: () => PRIORITY_EXECUTE,
|
|
75814
77444
|
PRIORITY_MERGE: () => PRIORITY_MERGE,
|
|
@@ -75833,6 +77463,7 @@ __export(src_exports2, {
|
|
|
75833
77463
|
TriageProcessor: () => TriageProcessor,
|
|
75834
77464
|
TunnelProcessManager: () => TunnelProcessManager,
|
|
75835
77465
|
UsageLimitPauser: () => UsageLimitPauser,
|
|
77466
|
+
WebhookNotificationProvider: () => WebhookNotificationProvider,
|
|
75836
77467
|
WorktreePool: () => WorktreePool,
|
|
75837
77468
|
aiMergeTask: () => aiMergeTask,
|
|
75838
77469
|
buildAgentChatPrompt: () => buildAgentChatPrompt,
|
|
@@ -75851,6 +77482,7 @@ __export(src_exports2, {
|
|
|
75851
77482
|
createTaskLogTool: () => createTaskLogTool,
|
|
75852
77483
|
describeAgentModel: () => describeAgentModel,
|
|
75853
77484
|
describeModel: () => describeModel,
|
|
77485
|
+
formatTaskIdentifier: () => formatTaskIdentifier,
|
|
75854
77486
|
getDefaultPiRuntime: () => getDefaultPiRuntime,
|
|
75855
77487
|
getHostExtensionPaths: () => getHostExtensionPaths,
|
|
75856
77488
|
getTunnelProviderAdapter: () => getTunnelProviderAdapter,
|
|
@@ -75902,6 +77534,7 @@ var init_src2 = __esm({
|
|
|
75902
77534
|
init_pr_monitor();
|
|
75903
77535
|
init_pr_comment_handler();
|
|
75904
77536
|
init_notifier();
|
|
77537
|
+
init_notification();
|
|
75905
77538
|
init_cron_runner();
|
|
75906
77539
|
init_routine_runner();
|
|
75907
77540
|
init_routine_scheduler();
|
|
@@ -76087,6 +77720,7 @@ async function ensureNtfyHelpersReady() {
|
|
|
76087
77720
|
}
|
|
76088
77721
|
try {
|
|
76089
77722
|
const engine = await Promise.resolve().then(() => (init_src2(), src_exports2));
|
|
77723
|
+
const hasNotificationService = "NotificationService" in engine && typeof engine.NotificationService === "function";
|
|
76090
77724
|
const hasAllHelpers = "isNtfyEventEnabled" in engine && "buildNtfyClickUrl" in engine && "sendNtfyNotification" in engine && typeof engine.isNtfyEventEnabled === "function" && typeof engine.buildNtfyClickUrl === "function" && typeof engine.sendNtfyNotification === "function";
|
|
76091
77725
|
if (!hasAllHelpers) {
|
|
76092
77726
|
return;
|
|
@@ -76096,6 +77730,12 @@ async function ensureNtfyHelpersReady() {
|
|
|
76096
77730
|
buildNtfyClickUrl: engine.buildNtfyClickUrl,
|
|
76097
77731
|
sendNtfyNotification: engine.sendNtfyNotification
|
|
76098
77732
|
};
|
|
77733
|
+
if (hasNotificationService) {
|
|
77734
|
+
diagnostics.info(
|
|
77735
|
+
"NotificationService abstraction detected in engine",
|
|
77736
|
+
{ operation: "notification-service-detection" }
|
|
77737
|
+
);
|
|
77738
|
+
}
|
|
76099
77739
|
} catch {
|
|
76100
77740
|
}
|
|
76101
77741
|
}
|
|
@@ -76118,6 +77758,11 @@ function cleanupInMemorySession(sessionId) {
|
|
|
76118
77758
|
if (!session) {
|
|
76119
77759
|
return false;
|
|
76120
77760
|
}
|
|
77761
|
+
const activeGeneration = activeGenerations.get(sessionId);
|
|
77762
|
+
if (activeGeneration) {
|
|
77763
|
+
clearTimeout(activeGeneration.timer);
|
|
77764
|
+
activeGenerations.delete(sessionId);
|
|
77765
|
+
}
|
|
76121
77766
|
if (session.agent) {
|
|
76122
77767
|
try {
|
|
76123
77768
|
session.agent.session.dispose?.();
|
|
@@ -76451,109 +78096,157 @@ async function maybeNotifyPlanningAwaitingInput(session, question) {
|
|
|
76451
78096
|
});
|
|
76452
78097
|
}
|
|
76453
78098
|
}
|
|
78099
|
+
function setSessionError(session, message) {
|
|
78100
|
+
session.error = message;
|
|
78101
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78102
|
+
persistSession(session, "error", message);
|
|
78103
|
+
planningStreamManager.broadcast(session.id, {
|
|
78104
|
+
type: "error",
|
|
78105
|
+
data: message
|
|
78106
|
+
});
|
|
78107
|
+
}
|
|
78108
|
+
function createAbortError() {
|
|
78109
|
+
const error = new Error("Generation aborted");
|
|
78110
|
+
error.name = "AbortError";
|
|
78111
|
+
return error;
|
|
78112
|
+
}
|
|
78113
|
+
async function runGenerationWithTimeout(session, operation) {
|
|
78114
|
+
const existing = activeGenerations.get(session.id);
|
|
78115
|
+
if (existing) {
|
|
78116
|
+
clearTimeout(existing.timer);
|
|
78117
|
+
existing.abortController.abort();
|
|
78118
|
+
}
|
|
78119
|
+
const abortController = new AbortController();
|
|
78120
|
+
let timeoutTriggered = false;
|
|
78121
|
+
const timer = setTimeout(() => {
|
|
78122
|
+
timeoutTriggered = true;
|
|
78123
|
+
setSessionError(session, "AI generation timed out. You can retry or start a new session.");
|
|
78124
|
+
abortController.abort();
|
|
78125
|
+
}, GENERATION_TIMEOUT_MS);
|
|
78126
|
+
activeGenerations.set(session.id, { abortController, timer });
|
|
78127
|
+
const abortPromise = new Promise((_, reject) => {
|
|
78128
|
+
abortController.signal.addEventListener(
|
|
78129
|
+
"abort",
|
|
78130
|
+
() => reject(createAbortError()),
|
|
78131
|
+
{ once: true }
|
|
78132
|
+
);
|
|
78133
|
+
});
|
|
78134
|
+
try {
|
|
78135
|
+
return await Promise.race([operation(), abortPromise]);
|
|
78136
|
+
} catch (error) {
|
|
78137
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
78138
|
+
if (!timeoutTriggered && !session.error) {
|
|
78139
|
+
setSessionError(session, "Generation stopped by user. You can retry or start a new session.");
|
|
78140
|
+
}
|
|
78141
|
+
}
|
|
78142
|
+
throw error;
|
|
78143
|
+
} finally {
|
|
78144
|
+
clearTimeout(timer);
|
|
78145
|
+
activeGenerations.delete(session.id);
|
|
78146
|
+
}
|
|
78147
|
+
}
|
|
76454
78148
|
async function continueAgentConversation(session, message) {
|
|
76455
78149
|
if (!session.agent) {
|
|
76456
78150
|
throw new InvalidSessionStateError("AI agent not initialized");
|
|
76457
78151
|
}
|
|
76458
78152
|
try {
|
|
76459
|
-
session
|
|
76460
|
-
|
|
76461
|
-
|
|
76462
|
-
|
|
76463
|
-
|
|
76464
|
-
if (
|
|
76465
|
-
|
|
76466
|
-
|
|
76467
|
-
|
|
78153
|
+
await runGenerationWithTimeout(session, async () => {
|
|
78154
|
+
session.thinkingOutput = "";
|
|
78155
|
+
await session.agent.session.prompt(message);
|
|
78156
|
+
const lastMessage = session.agent.session.state.messages.filter((m) => m.role === "assistant").pop();
|
|
78157
|
+
let responseText = session.thinkingOutput;
|
|
78158
|
+
if (lastMessage?.content) {
|
|
78159
|
+
if (typeof lastMessage.content === "string") {
|
|
78160
|
+
responseText = lastMessage.content;
|
|
78161
|
+
} else if (Array.isArray(lastMessage.content)) {
|
|
78162
|
+
responseText = lastMessage.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
78163
|
+
}
|
|
76468
78164
|
}
|
|
76469
|
-
|
|
76470
|
-
|
|
76471
|
-
|
|
76472
|
-
|
|
76473
|
-
|
|
76474
|
-
|
|
76475
|
-
|
|
76476
|
-
|
|
76477
|
-
|
|
76478
|
-
|
|
76479
|
-
|
|
76480
|
-
|
|
76481
|
-
{ sessionId: session.id, attempt: attempt + 1, operation: "parse-retry" }
|
|
76482
|
-
);
|
|
76483
|
-
try {
|
|
76484
|
-
session.thinkingOutput = "";
|
|
76485
|
-
await session.agent.session.prompt(
|
|
76486
|
-
'Your previous response could not be parsed as JSON. Please respond with ONLY a valid JSON object: either {"type":"question","data":{...}} or {"type":"complete","data":{...}}. No markdown, no explanation, just the JSON.'
|
|
78165
|
+
let parsed;
|
|
78166
|
+
let lastError;
|
|
78167
|
+
for (let attempt = 0; attempt <= MAX_PARSE_RETRIES; attempt++) {
|
|
78168
|
+
try {
|
|
78169
|
+
parsed = parseAgentResponse(responseText);
|
|
78170
|
+
break;
|
|
78171
|
+
} catch (err) {
|
|
78172
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
78173
|
+
if (attempt < MAX_PARSE_RETRIES) {
|
|
78174
|
+
diagnostics.warn(
|
|
78175
|
+
"Parse attempt failed, requesting reformat",
|
|
78176
|
+
{ sessionId: session.id, attempt: attempt + 1, operation: "parse-retry" }
|
|
76487
78177
|
);
|
|
76488
|
-
|
|
76489
|
-
|
|
76490
|
-
|
|
76491
|
-
|
|
76492
|
-
|
|
76493
|
-
|
|
76494
|
-
|
|
78178
|
+
try {
|
|
78179
|
+
session.thinkingOutput = "";
|
|
78180
|
+
await session.agent.session.prompt(
|
|
78181
|
+
'Your previous response could not be parsed as JSON. Please respond with ONLY a valid JSON object: either {"type":"question","data":{...}} or {"type":"complete","data":{...}}. No markdown, no explanation, just the JSON.'
|
|
78182
|
+
);
|
|
78183
|
+
const retryMessage = session.agent.session.state.messages.filter((m) => m.role === "assistant").pop();
|
|
78184
|
+
let retryText = session.thinkingOutput;
|
|
78185
|
+
if (retryMessage?.content) {
|
|
78186
|
+
if (typeof retryMessage.content === "string") {
|
|
78187
|
+
retryText = retryMessage.content;
|
|
78188
|
+
} else if (Array.isArray(retryMessage.content)) {
|
|
78189
|
+
retryText = retryMessage.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
78190
|
+
}
|
|
76495
78191
|
}
|
|
78192
|
+
responseText = retryText;
|
|
78193
|
+
} catch (retryErr) {
|
|
78194
|
+
diagnostics.errorFromException(
|
|
78195
|
+
"Retry prompt failed for session",
|
|
78196
|
+
retryErr,
|
|
78197
|
+
{ sessionId: session.id, operation: "retry-prompt" }
|
|
78198
|
+
);
|
|
78199
|
+
break;
|
|
76496
78200
|
}
|
|
76497
|
-
responseText = retryText;
|
|
76498
|
-
} catch (retryErr) {
|
|
76499
|
-
diagnostics.errorFromException(
|
|
76500
|
-
"Retry prompt failed for session",
|
|
76501
|
-
retryErr,
|
|
76502
|
-
{ sessionId: session.id, operation: "retry-prompt" }
|
|
76503
|
-
);
|
|
76504
|
-
break;
|
|
76505
78201
|
}
|
|
76506
78202
|
}
|
|
76507
78203
|
}
|
|
76508
|
-
|
|
76509
|
-
|
|
76510
|
-
|
|
76511
|
-
|
|
76512
|
-
|
|
76513
|
-
|
|
76514
|
-
|
|
76515
|
-
|
|
76516
|
-
|
|
76517
|
-
|
|
76518
|
-
|
|
76519
|
-
|
|
76520
|
-
|
|
76521
|
-
|
|
78204
|
+
if (!parsed) {
|
|
78205
|
+
const errorMsg = `${lastError?.message || "Failed to parse AI response"} You can try responding again or start a new planning session.`;
|
|
78206
|
+
diagnostics.error(
|
|
78207
|
+
"All parse attempts exhausted for session",
|
|
78208
|
+
{ sessionId: session.id, message: errorMsg, operation: "parse-exhausted" }
|
|
78209
|
+
);
|
|
78210
|
+
session.error = errorMsg;
|
|
78211
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78212
|
+
persistSession(session, "error", errorMsg);
|
|
78213
|
+
planningStreamManager.broadcast(session.id, {
|
|
78214
|
+
type: "error",
|
|
78215
|
+
data: errorMsg
|
|
78216
|
+
});
|
|
78217
|
+
return;
|
|
78218
|
+
}
|
|
78219
|
+
if (parsed.type === "question") {
|
|
78220
|
+
session.currentQuestion = parsed.data;
|
|
78221
|
+
session.error = void 0;
|
|
78222
|
+
session.lastGeneratedThinking = session.thinkingOutput;
|
|
78223
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78224
|
+
persistSession(session, "awaiting_input");
|
|
78225
|
+
void maybeNotifyPlanningAwaitingInput(session, parsed.data);
|
|
78226
|
+
planningStreamManager.broadcast(session.id, {
|
|
78227
|
+
type: "question",
|
|
78228
|
+
data: parsed.data
|
|
78229
|
+
});
|
|
78230
|
+
} else if (parsed.type === "complete") {
|
|
78231
|
+
session.summary = parsed.data;
|
|
78232
|
+
session.currentQuestion = void 0;
|
|
78233
|
+
session.error = void 0;
|
|
78234
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78235
|
+
persistSession(session, "complete");
|
|
78236
|
+
planningStreamManager.broadcast(session.id, {
|
|
78237
|
+
type: "summary",
|
|
78238
|
+
data: parsed.data
|
|
78239
|
+
});
|
|
78240
|
+
planningStreamManager.broadcast(session.id, { type: "complete" });
|
|
78241
|
+
}
|
|
78242
|
+
});
|
|
78243
|
+
} catch (err) {
|
|
78244
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
76522
78245
|
return;
|
|
76523
78246
|
}
|
|
76524
|
-
if (parsed.type === "question") {
|
|
76525
|
-
session.currentQuestion = parsed.data;
|
|
76526
|
-
session.error = void 0;
|
|
76527
|
-
session.lastGeneratedThinking = session.thinkingOutput;
|
|
76528
|
-
session.updatedAt = /* @__PURE__ */ new Date();
|
|
76529
|
-
persistSession(session, "awaiting_input");
|
|
76530
|
-
void maybeNotifyPlanningAwaitingInput(session, parsed.data);
|
|
76531
|
-
planningStreamManager.broadcast(session.id, {
|
|
76532
|
-
type: "question",
|
|
76533
|
-
data: parsed.data
|
|
76534
|
-
});
|
|
76535
|
-
} else if (parsed.type === "complete") {
|
|
76536
|
-
session.summary = parsed.data;
|
|
76537
|
-
session.currentQuestion = void 0;
|
|
76538
|
-
session.error = void 0;
|
|
76539
|
-
session.updatedAt = /* @__PURE__ */ new Date();
|
|
76540
|
-
persistSession(session, "complete");
|
|
76541
|
-
planningStreamManager.broadcast(session.id, {
|
|
76542
|
-
type: "summary",
|
|
76543
|
-
data: parsed.data
|
|
76544
|
-
});
|
|
76545
|
-
planningStreamManager.broadcast(session.id, { type: "complete" });
|
|
76546
|
-
}
|
|
76547
|
-
} catch (err) {
|
|
76548
78247
|
const errorMessage = err instanceof Error ? err.message : "AI processing failed";
|
|
76549
78248
|
diagnostics.errorFromException("Agent conversation error for session", err, { sessionId: session.id, operation: "conversation" });
|
|
76550
|
-
session
|
|
76551
|
-
session.updatedAt = /* @__PURE__ */ new Date();
|
|
76552
|
-
persistSession(session, "error", errorMessage);
|
|
76553
|
-
planningStreamManager.broadcast(session.id, {
|
|
76554
|
-
type: "error",
|
|
76555
|
-
data: errorMessage
|
|
76556
|
-
});
|
|
78249
|
+
setSessionError(session, errorMessage);
|
|
76557
78250
|
}
|
|
76558
78251
|
}
|
|
76559
78252
|
function extractJsonCandidate(text) {
|
|
@@ -76825,7 +78518,7 @@ function getSession(sessionId) {
|
|
|
76825
78518
|
return void 0;
|
|
76826
78519
|
}
|
|
76827
78520
|
}
|
|
76828
|
-
var createFnAgent4, planningNtfyHelpers, diagnostics, PLANNING_SYSTEM_PROMPT, SESSION_TTL_MS, CLEANUP_INTERVAL_MS2, MAX_SESSIONS_PER_IP_PER_HOUR, RATE_LIMIT_WINDOW_MS2, sessions, rateLimits2, _aiSessionStore, cleanupInterval2, PlanningStreamManager, planningStreamManager, MAX_PARSE_RETRIES, RateLimitError2, SessionNotFoundError, InvalidSessionStateError;
|
|
78521
|
+
var createFnAgent4, planningNtfyHelpers, diagnostics, PLANNING_SYSTEM_PROMPT, SESSION_TTL_MS, CLEANUP_INTERVAL_MS2, MAX_SESSIONS_PER_IP_PER_HOUR, RATE_LIMIT_WINDOW_MS2, GENERATION_TIMEOUT_MS, sessions, rateLimits2, activeGenerations, _aiSessionStore, cleanupInterval2, PlanningStreamManager, planningStreamManager, MAX_PARSE_RETRIES, RateLimitError2, SessionNotFoundError, InvalidSessionStateError;
|
|
76829
78522
|
var init_planning = __esm({
|
|
76830
78523
|
"../dashboard/src/planning.ts"() {
|
|
76831
78524
|
"use strict";
|
|
@@ -76896,8 +78589,10 @@ For completion:
|
|
|
76896
78589
|
CLEANUP_INTERVAL_MS2 = 5 * 60 * 1e3;
|
|
76897
78590
|
MAX_SESSIONS_PER_IP_PER_HOUR = 5;
|
|
76898
78591
|
RATE_LIMIT_WINDOW_MS2 = 60 * 60 * 1e3;
|
|
78592
|
+
GENERATION_TIMEOUT_MS = 12e4;
|
|
76899
78593
|
sessions = /* @__PURE__ */ new Map();
|
|
76900
78594
|
rateLimits2 = /* @__PURE__ */ new Map();
|
|
78595
|
+
activeGenerations = /* @__PURE__ */ new Map();
|
|
76901
78596
|
cleanupInterval2 = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS2);
|
|
76902
78597
|
cleanupInterval2.unref?.();
|
|
76903
78598
|
process.on("beforeExit", () => {
|
|
@@ -77035,6 +78730,108 @@ var init_ai_session_store = __esm({
|
|
|
77035
78730
|
}
|
|
77036
78731
|
});
|
|
77037
78732
|
|
|
78733
|
+
// ../dashboard/src/ai-session-timeout.ts
|
|
78734
|
+
function createAbortError2() {
|
|
78735
|
+
const error = new Error("Generation aborted");
|
|
78736
|
+
error.name = "AbortError";
|
|
78737
|
+
return error;
|
|
78738
|
+
}
|
|
78739
|
+
function isAbortError(err) {
|
|
78740
|
+
return err instanceof Error && err.name === "AbortError";
|
|
78741
|
+
}
|
|
78742
|
+
var GenerationGuard;
|
|
78743
|
+
var init_ai_session_timeout = __esm({
|
|
78744
|
+
"../dashboard/src/ai-session-timeout.ts"() {
|
|
78745
|
+
"use strict";
|
|
78746
|
+
GenerationGuard = class {
|
|
78747
|
+
active = /* @__PURE__ */ new Map();
|
|
78748
|
+
/**
|
|
78749
|
+
* Tracks the cause of a pending abort so the original `run()` can
|
|
78750
|
+
* distinguish a user-initiated stop from re-entrant displacement. The flag
|
|
78751
|
+
* is consumed (deleted) by the catch block of the displaced `run()`.
|
|
78752
|
+
*/
|
|
78753
|
+
abortCause = /* @__PURE__ */ new Map();
|
|
78754
|
+
/**
|
|
78755
|
+
* Wrap `op` with a timeout + abort. If a previous generation is still
|
|
78756
|
+
* registered for the same id, it is aborted first (the prior `run()`
|
|
78757
|
+
* rejects with `AbortError`, marked as `displaced` so its `onUserStop`
|
|
78758
|
+
* handler does NOT fire).
|
|
78759
|
+
*/
|
|
78760
|
+
async run(sessionId, timeoutMs, handlers, op) {
|
|
78761
|
+
this.cancelInternal(sessionId, "displaced");
|
|
78762
|
+
const abort = new AbortController();
|
|
78763
|
+
const timer = setTimeout(() => {
|
|
78764
|
+
this.abortCause.set(abort, "timeout");
|
|
78765
|
+
try {
|
|
78766
|
+
handlers.onTimeout();
|
|
78767
|
+
} catch {
|
|
78768
|
+
}
|
|
78769
|
+
abort.abort();
|
|
78770
|
+
}, timeoutMs);
|
|
78771
|
+
const entry = { abort, timer };
|
|
78772
|
+
this.active.set(sessionId, entry);
|
|
78773
|
+
const abortPromise = new Promise((_, reject) => {
|
|
78774
|
+
abort.signal.addEventListener(
|
|
78775
|
+
"abort",
|
|
78776
|
+
() => reject(createAbortError2()),
|
|
78777
|
+
{ once: true }
|
|
78778
|
+
);
|
|
78779
|
+
});
|
|
78780
|
+
try {
|
|
78781
|
+
return await Promise.race([op(), abortPromise]);
|
|
78782
|
+
} catch (err) {
|
|
78783
|
+
if (isAbortError(err)) {
|
|
78784
|
+
const cause = this.abortCause.get(abort) ?? "user-stop";
|
|
78785
|
+
if (cause === "user-stop") {
|
|
78786
|
+
try {
|
|
78787
|
+
handlers.onUserStop?.();
|
|
78788
|
+
} catch {
|
|
78789
|
+
}
|
|
78790
|
+
}
|
|
78791
|
+
}
|
|
78792
|
+
throw err;
|
|
78793
|
+
} finally {
|
|
78794
|
+
clearTimeout(timer);
|
|
78795
|
+
this.abortCause.delete(abort);
|
|
78796
|
+
if (this.active.get(sessionId) === entry) {
|
|
78797
|
+
this.active.delete(sessionId);
|
|
78798
|
+
}
|
|
78799
|
+
}
|
|
78800
|
+
}
|
|
78801
|
+
/** AbortSignal of the in-flight generation, if any — for tools that honor it. */
|
|
78802
|
+
signal(sessionId) {
|
|
78803
|
+
return this.active.get(sessionId)?.abort.signal;
|
|
78804
|
+
}
|
|
78805
|
+
has(sessionId) {
|
|
78806
|
+
return this.active.has(sessionId);
|
|
78807
|
+
}
|
|
78808
|
+
/**
|
|
78809
|
+
* Manually abort the active generation. Returns true if there was one.
|
|
78810
|
+
* The wrapped operation will reject with `AbortError`, and the `onUserStop`
|
|
78811
|
+
* handler from the original `run()` call will fire.
|
|
78812
|
+
*/
|
|
78813
|
+
stop(sessionId) {
|
|
78814
|
+
return this.cancelInternal(sessionId, "user-stop");
|
|
78815
|
+
}
|
|
78816
|
+
/** Reset all in-flight generations (test/shutdown only). */
|
|
78817
|
+
reset() {
|
|
78818
|
+
for (const sessionId of [...this.active.keys()]) {
|
|
78819
|
+
this.cancelInternal(sessionId, "user-stop");
|
|
78820
|
+
}
|
|
78821
|
+
}
|
|
78822
|
+
cancelInternal(sessionId, cause) {
|
|
78823
|
+
const entry = this.active.get(sessionId);
|
|
78824
|
+
if (!entry) return false;
|
|
78825
|
+
clearTimeout(entry.timer);
|
|
78826
|
+
this.abortCause.set(entry.abort, cause);
|
|
78827
|
+
entry.abort.abort();
|
|
78828
|
+
this.active.delete(sessionId);
|
|
78829
|
+
return true;
|
|
78830
|
+
}
|
|
78831
|
+
};
|
|
78832
|
+
}
|
|
78833
|
+
});
|
|
78834
|
+
|
|
77038
78835
|
// ../dashboard/src/subtask-breakdown.ts
|
|
77039
78836
|
import { EventEmitter as EventEmitter24 } from "node:events";
|
|
77040
78837
|
function cleanupInMemorySubtaskSession(sessionId) {
|
|
@@ -77042,6 +78839,7 @@ function cleanupInMemorySubtaskSession(sessionId) {
|
|
|
77042
78839
|
if (!session) {
|
|
77043
78840
|
return false;
|
|
77044
78841
|
}
|
|
78842
|
+
generationGuard.stop(sessionId);
|
|
77045
78843
|
try {
|
|
77046
78844
|
session.agent?.session?.dispose?.();
|
|
77047
78845
|
} catch {
|
|
@@ -77058,17 +78856,19 @@ function cleanupExpiredSessions2() {
|
|
|
77058
78856
|
}
|
|
77059
78857
|
}
|
|
77060
78858
|
}
|
|
77061
|
-
var diagnostics3, SESSION_TTL_MS2, CLEANUP_INTERVAL_MS3, sessions2, cleanupInterval3, SubtaskStreamManager, subtaskStreamManager;
|
|
78859
|
+
var diagnostics3, SESSION_TTL_MS2, CLEANUP_INTERVAL_MS3, generationGuard, sessions2, cleanupInterval3, SubtaskStreamManager, subtaskStreamManager;
|
|
77062
78860
|
var init_subtask_breakdown = __esm({
|
|
77063
78861
|
"../dashboard/src/subtask-breakdown.ts"() {
|
|
77064
78862
|
"use strict";
|
|
77065
78863
|
init_src();
|
|
77066
78864
|
init_sse_buffer();
|
|
77067
78865
|
init_ai_session_diagnostics();
|
|
78866
|
+
init_ai_session_timeout();
|
|
77068
78867
|
init_src2();
|
|
77069
78868
|
diagnostics3 = createSessionDiagnostics("subtask-breakdown");
|
|
77070
78869
|
SESSION_TTL_MS2 = 7 * 24 * 60 * 60 * 1e3;
|
|
77071
78870
|
CLEANUP_INTERVAL_MS3 = 5 * 60 * 1e3;
|
|
78871
|
+
generationGuard = new GenerationGuard();
|
|
77072
78872
|
sessions2 = /* @__PURE__ */ new Map();
|
|
77073
78873
|
cleanupInterval3 = setInterval(cleanupExpiredSessions2, CLEANUP_INTERVAL_MS3);
|
|
77074
78874
|
cleanupInterval3.unref?.();
|
|
@@ -77143,6 +78943,7 @@ function cleanupInMemoryMissionSession(sessionId) {
|
|
|
77143
78943
|
if (!session) {
|
|
77144
78944
|
return false;
|
|
77145
78945
|
}
|
|
78946
|
+
generationGuard2.stop(sessionId);
|
|
77146
78947
|
if (session.agent) {
|
|
77147
78948
|
try {
|
|
77148
78949
|
session.agent.session.dispose?.();
|
|
@@ -77167,18 +78968,20 @@ function cleanupExpiredSessions3() {
|
|
|
77167
78968
|
}
|
|
77168
78969
|
}
|
|
77169
78970
|
}
|
|
77170
|
-
var diagnostics4, SESSION_TTL_MS3, CLEANUP_INTERVAL_MS4, RATE_LIMIT_WINDOW_MS3, sessions3, rateLimits3, cleanupInterval4, MissionInterviewStreamManager, missionInterviewStreamManager;
|
|
78971
|
+
var diagnostics4, SESSION_TTL_MS3, CLEANUP_INTERVAL_MS4, RATE_LIMIT_WINDOW_MS3, generationGuard2, sessions3, rateLimits3, cleanupInterval4, MissionInterviewStreamManager, missionInterviewStreamManager;
|
|
77171
78972
|
var init_mission_interview = __esm({
|
|
77172
78973
|
"../dashboard/src/mission-interview.ts"() {
|
|
77173
78974
|
"use strict";
|
|
77174
78975
|
init_src();
|
|
77175
78976
|
init_sse_buffer();
|
|
77176
78977
|
init_ai_session_diagnostics();
|
|
78978
|
+
init_ai_session_timeout();
|
|
77177
78979
|
init_src2();
|
|
77178
78980
|
diagnostics4 = createSessionDiagnostics("mission-interview");
|
|
77179
78981
|
SESSION_TTL_MS3 = 7 * 24 * 60 * 60 * 1e3;
|
|
77180
78982
|
CLEANUP_INTERVAL_MS4 = 5 * 60 * 1e3;
|
|
77181
78983
|
RATE_LIMIT_WINDOW_MS3 = 60 * 60 * 1e3;
|
|
78984
|
+
generationGuard2 = new GenerationGuard();
|
|
77182
78985
|
sessions3 = /* @__PURE__ */ new Map();
|
|
77183
78986
|
rateLimits3 = /* @__PURE__ */ new Map();
|
|
77184
78987
|
cleanupInterval4 = setInterval(cleanupExpiredSessions3, CLEANUP_INTERVAL_MS4);
|
|
@@ -77258,6 +79061,7 @@ function cleanupInMemorySession2(sessionId) {
|
|
|
77258
79061
|
if (!session) {
|
|
77259
79062
|
return false;
|
|
77260
79063
|
}
|
|
79064
|
+
generationGuard3.stop(sessionId);
|
|
77261
79065
|
if (session.agent) {
|
|
77262
79066
|
try {
|
|
77263
79067
|
session.agent.session.dispose?.();
|
|
@@ -77282,19 +79086,21 @@ function cleanupExpiredSessions4() {
|
|
|
77282
79086
|
}
|
|
77283
79087
|
}
|
|
77284
79088
|
}
|
|
77285
|
-
var diagnostics5, SESSION_TTL_MS4, CLEANUP_INTERVAL_MS5, RATE_LIMIT_WINDOW_MS4, sessions4, rateLimits4, cleanupInterval5, MilestoneSliceInterviewStreamManager, milestoneSliceInterviewStreamManager;
|
|
79089
|
+
var diagnostics5, SESSION_TTL_MS4, CLEANUP_INTERVAL_MS5, RATE_LIMIT_WINDOW_MS4, generationGuard3, sessions4, rateLimits4, cleanupInterval5, MilestoneSliceInterviewStreamManager, milestoneSliceInterviewStreamManager;
|
|
77286
79090
|
var init_milestone_slice_interview = __esm({
|
|
77287
79091
|
"../dashboard/src/milestone-slice-interview.ts"() {
|
|
77288
79092
|
"use strict";
|
|
77289
79093
|
init_sse_buffer();
|
|
77290
79094
|
init_mission_interview();
|
|
77291
79095
|
init_ai_session_diagnostics();
|
|
79096
|
+
init_ai_session_timeout();
|
|
77292
79097
|
init_mission_interview();
|
|
77293
79098
|
init_src2();
|
|
77294
79099
|
diagnostics5 = createSessionDiagnostics("milestone-slice-interview");
|
|
77295
79100
|
SESSION_TTL_MS4 = 7 * 24 * 60 * 60 * 1e3;
|
|
77296
79101
|
CLEANUP_INTERVAL_MS5 = 5 * 60 * 1e3;
|
|
77297
79102
|
RATE_LIMIT_WINDOW_MS4 = 60 * 60 * 1e3;
|
|
79103
|
+
generationGuard3 = new GenerationGuard();
|
|
77298
79104
|
sessions4 = /* @__PURE__ */ new Map();
|
|
77299
79105
|
rateLimits4 = /* @__PURE__ */ new Map();
|
|
77300
79106
|
cleanupInterval5 = setInterval(cleanupExpiredSessions4, CLEANUP_INTERVAL_MS5);
|
|
@@ -78139,6 +79945,7 @@ var init_register_task_workflow_routes = __esm({
|
|
|
78139
79945
|
var init_register_planning_subtask_routes = __esm({
|
|
78140
79946
|
"../dashboard/src/routes/register-planning-subtask-routes.ts"() {
|
|
78141
79947
|
"use strict";
|
|
79948
|
+
init_src();
|
|
78142
79949
|
init_api_error();
|
|
78143
79950
|
init_sse_buffer();
|
|
78144
79951
|
}
|
|
@@ -78153,12 +79960,14 @@ var init_rate_limit = __esm({
|
|
|
78153
79960
|
});
|
|
78154
79961
|
|
|
78155
79962
|
// ../dashboard/src/routes/register-chat-routes.ts
|
|
79963
|
+
var CHAT_MAX_ATTACHMENT_SIZE;
|
|
78156
79964
|
var init_register_chat_routes = __esm({
|
|
78157
79965
|
"../dashboard/src/routes/register-chat-routes.ts"() {
|
|
78158
79966
|
"use strict";
|
|
78159
79967
|
init_api_error();
|
|
78160
79968
|
init_rate_limit();
|
|
78161
79969
|
init_sse_buffer();
|
|
79970
|
+
CHAT_MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
|
|
78162
79971
|
}
|
|
78163
79972
|
});
|
|
78164
79973
|
|
|
@@ -80381,16 +82190,26 @@ var init_register_agents_projects_nodes = __esm({
|
|
|
80381
82190
|
}
|
|
80382
82191
|
});
|
|
80383
82192
|
|
|
80384
|
-
// ../dashboard/src/
|
|
82193
|
+
// ../dashboard/src/exec-file.ts
|
|
80385
82194
|
import { execFile as execFile5 } from "node:child_process";
|
|
80386
|
-
import * as fsPromises from "node:fs/promises";
|
|
80387
82195
|
import { promisify as promisify12 } from "node:util";
|
|
80388
|
-
var
|
|
82196
|
+
var execFileAsync3;
|
|
82197
|
+
var init_exec_file = __esm({
|
|
82198
|
+
"../dashboard/src/exec-file.ts"() {
|
|
82199
|
+
"use strict";
|
|
82200
|
+
execFileAsync3 = promisify12(execFile5);
|
|
82201
|
+
}
|
|
82202
|
+
});
|
|
82203
|
+
|
|
82204
|
+
// ../dashboard/src/routes/register-project-routes.ts
|
|
82205
|
+
import * as fsPromises from "node:fs/promises";
|
|
82206
|
+
var access3, stat6, mkdir12, readdir8, rm2;
|
|
80389
82207
|
var init_register_project_routes = __esm({
|
|
80390
82208
|
"../dashboard/src/routes/register-project-routes.ts"() {
|
|
80391
82209
|
"use strict";
|
|
80392
82210
|
init_src();
|
|
80393
82211
|
init_api_error();
|
|
82212
|
+
init_exec_file();
|
|
80394
82213
|
init_project_store_resolver();
|
|
80395
82214
|
({
|
|
80396
82215
|
access: access3,
|
|
@@ -80399,7 +82218,6 @@ var init_register_project_routes = __esm({
|
|
|
80399
82218
|
readdir: readdir8,
|
|
80400
82219
|
rm: rm2
|
|
80401
82220
|
} = fsPromises);
|
|
80402
|
-
execFileAsync3 = promisify12(execFile5);
|
|
80403
82221
|
}
|
|
80404
82222
|
});
|
|
80405
82223
|
|
|
@@ -85738,12 +87556,29 @@ var init_project_context = __esm({
|
|
|
85738
87556
|
}
|
|
85739
87557
|
});
|
|
85740
87558
|
|
|
87559
|
+
// src/commands/node.ts
|
|
87560
|
+
import { createInterface } from "node:readline/promises";
|
|
87561
|
+
async function findNodeByNameOrId(central, nameOrId) {
|
|
87562
|
+
const byId = await central.getNode(nameOrId);
|
|
87563
|
+
if (byId) {
|
|
87564
|
+
return byId;
|
|
87565
|
+
}
|
|
87566
|
+
return central.getNodeByName(nameOrId);
|
|
87567
|
+
}
|
|
87568
|
+
var init_node = __esm({
|
|
87569
|
+
"src/commands/node.ts"() {
|
|
87570
|
+
"use strict";
|
|
87571
|
+
init_src();
|
|
87572
|
+
}
|
|
87573
|
+
});
|
|
87574
|
+
|
|
85741
87575
|
// src/commands/task.ts
|
|
85742
87576
|
var task_exports = {};
|
|
85743
87577
|
__export(task_exports, {
|
|
85744
87578
|
fetchGitHubIssues: () => fetchGitHubIssues,
|
|
85745
87579
|
runTaskArchive: () => runTaskArchive,
|
|
85746
87580
|
runTaskAttach: () => runTaskAttach,
|
|
87581
|
+
runTaskClearNode: () => runTaskClearNode,
|
|
85747
87582
|
runTaskComment: () => runTaskComment,
|
|
85748
87583
|
runTaskComments: () => runTaskComments,
|
|
85749
87584
|
runTaskCreate: () => runTaskCreate,
|
|
@@ -85761,13 +87596,14 @@ __export(task_exports, {
|
|
|
85761
87596
|
runTaskPrCreate: () => runTaskPrCreate,
|
|
85762
87597
|
runTaskRefine: () => runTaskRefine,
|
|
85763
87598
|
runTaskRetry: () => runTaskRetry,
|
|
87599
|
+
runTaskSetNode: () => runTaskSetNode,
|
|
85764
87600
|
runTaskShow: () => runTaskShow,
|
|
85765
87601
|
runTaskSteer: () => runTaskSteer,
|
|
85766
87602
|
runTaskUnarchive: () => runTaskUnarchive,
|
|
85767
87603
|
runTaskUnpause: () => runTaskUnpause,
|
|
85768
87604
|
runTaskUpdate: () => runTaskUpdate
|
|
85769
87605
|
});
|
|
85770
|
-
import { createInterface } from "node:readline/promises";
|
|
87606
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
85771
87607
|
import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync30, readFileSync as readFileSync9 } from "node:fs";
|
|
85772
87608
|
import { join as join37 } from "node:path";
|
|
85773
87609
|
function asLocalProjectContext(store) {
|
|
@@ -85832,11 +87668,28 @@ async function getProjectContext(projectName) {
|
|
|
85832
87668
|
async function getProjectPath(projectName) {
|
|
85833
87669
|
return (await getCommandContext(projectName)).projectPath;
|
|
85834
87670
|
}
|
|
85835
|
-
async function
|
|
87671
|
+
async function resolveNodeByNameOrId(nodeNameOrId) {
|
|
87672
|
+
const central = new CentralCore();
|
|
87673
|
+
await central.init();
|
|
87674
|
+
try {
|
|
87675
|
+
const looksLikeNodeId = nodeNameOrId.includes("-") && nodeNameOrId.length > 20;
|
|
87676
|
+
let node = looksLikeNodeId ? await central.getNode(nodeNameOrId) : await central.getNodeByName(nodeNameOrId);
|
|
87677
|
+
if (!node) {
|
|
87678
|
+
node = await findNodeByNameOrId(central, nodeNameOrId);
|
|
87679
|
+
}
|
|
87680
|
+
if (!node) {
|
|
87681
|
+
throw new Error(`Node not found: ${nodeNameOrId}`);
|
|
87682
|
+
}
|
|
87683
|
+
return { id: node.id, name: node.name };
|
|
87684
|
+
} finally {
|
|
87685
|
+
await central.close();
|
|
87686
|
+
}
|
|
87687
|
+
}
|
|
87688
|
+
async function runTaskCreate(descriptionArg, attachFiles, depends, projectName, nodeName) {
|
|
85836
87689
|
let description = descriptionArg;
|
|
85837
87690
|
const projectContext = await getProjectContext(projectName);
|
|
85838
87691
|
if (!description) {
|
|
85839
|
-
const rl =
|
|
87692
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
85840
87693
|
description = await rl.question("Task description: ");
|
|
85841
87694
|
rl.close();
|
|
85842
87695
|
}
|
|
@@ -85846,6 +87699,16 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
|
|
|
85846
87699
|
}
|
|
85847
87700
|
const store = projectContext?.store ?? await getStore(projectName);
|
|
85848
87701
|
const task = await store.createTask({ description: description.trim(), dependencies: depends });
|
|
87702
|
+
let resolvedNode;
|
|
87703
|
+
if (nodeName) {
|
|
87704
|
+
try {
|
|
87705
|
+
resolvedNode = await resolveNodeByNameOrId(nodeName);
|
|
87706
|
+
await store.updateTask(task.id, { nodeId: resolvedNode.id });
|
|
87707
|
+
} catch (error) {
|
|
87708
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
87709
|
+
process.exit(1);
|
|
87710
|
+
}
|
|
87711
|
+
}
|
|
85849
87712
|
const label = task.description.length > 60 ? task.description.slice(0, 60) + "\u2026" : task.description;
|
|
85850
87713
|
console.log();
|
|
85851
87714
|
if (projectContext) {
|
|
@@ -85856,6 +87719,9 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
|
|
|
85856
87719
|
if (task.dependencies.length > 0) {
|
|
85857
87720
|
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
|
85858
87721
|
}
|
|
87722
|
+
if (resolvedNode) {
|
|
87723
|
+
console.log(` Node: ${resolvedNode.name || resolvedNode.id}`);
|
|
87724
|
+
}
|
|
85859
87725
|
console.log(` Path: .fusion/tasks/${task.id}/`);
|
|
85860
87726
|
if (attachFiles && attachFiles.length > 0) {
|
|
85861
87727
|
const { readFile: readFile19 } = await import("node:fs/promises");
|
|
@@ -86047,15 +87913,62 @@ async function runTaskLogs(id, options = {}, projectName) {
|
|
|
86047
87913
|
});
|
|
86048
87914
|
}
|
|
86049
87915
|
}
|
|
87916
|
+
async function runTaskSetNode(id, nodeNameOrId, projectName) {
|
|
87917
|
+
const store = await getStore(projectName);
|
|
87918
|
+
const task = await store.getTask(id);
|
|
87919
|
+
if (task.column === "in-progress") {
|
|
87920
|
+
console.error(`Cannot change node override: task ${id} is in progress`);
|
|
87921
|
+
process.exit(1);
|
|
87922
|
+
}
|
|
87923
|
+
let resolvedNode;
|
|
87924
|
+
try {
|
|
87925
|
+
resolvedNode = await resolveNodeByNameOrId(nodeNameOrId);
|
|
87926
|
+
} catch (error) {
|
|
87927
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
87928
|
+
process.exit(1);
|
|
87929
|
+
return;
|
|
87930
|
+
}
|
|
87931
|
+
await store.updateTask(id, { nodeId: resolvedNode.id });
|
|
87932
|
+
console.log(`\u2713 Set node override for ${id}: ${resolvedNode.name || resolvedNode.id}`);
|
|
87933
|
+
}
|
|
87934
|
+
async function runTaskClearNode(id, projectName) {
|
|
87935
|
+
const store = await getStore(projectName);
|
|
87936
|
+
const task = await store.getTask(id);
|
|
87937
|
+
if (task.column === "in-progress") {
|
|
87938
|
+
console.error(`Cannot change node override: task ${id} is in progress`);
|
|
87939
|
+
process.exit(1);
|
|
87940
|
+
}
|
|
87941
|
+
await store.updateTask(id, { nodeId: null });
|
|
87942
|
+
console.log(`\u2713 Cleared node override for ${id}`);
|
|
87943
|
+
}
|
|
86050
87944
|
async function runTaskShow(id, projectName) {
|
|
86051
87945
|
const store = await getStore(projectName);
|
|
86052
87946
|
const task = await store.getTask(id);
|
|
87947
|
+
const settings = "getSettings" in store ? await store.getSettings() : {};
|
|
87948
|
+
let nodeSummary = "(default local)";
|
|
87949
|
+
if (task.nodeId) {
|
|
87950
|
+
let nodeName;
|
|
87951
|
+
const central = new CentralCore();
|
|
87952
|
+
await central.init();
|
|
87953
|
+
try {
|
|
87954
|
+
nodeName = (await central.getNode(task.nodeId))?.name;
|
|
87955
|
+
} finally {
|
|
87956
|
+
await central.close();
|
|
87957
|
+
}
|
|
87958
|
+
nodeSummary = nodeName ? `${nodeName} (${task.nodeId})` : task.nodeId;
|
|
87959
|
+
} else if (settings.defaultNodeId) {
|
|
87960
|
+
nodeSummary = `project default: ${settings.defaultNodeId}`;
|
|
87961
|
+
}
|
|
86053
87962
|
console.log();
|
|
86054
87963
|
console.log(` ${task.id}: ${task.title || task.description}`);
|
|
86055
87964
|
console.log(` Column: ${COLUMN_LABELS[task.column]}${task.size ? ` \xB7 Size: ${task.size}` : ""}${task.reviewLevel !== void 0 ? ` \xB7 Review: ${task.reviewLevel}` : ""}`);
|
|
86056
87965
|
if (task.dependencies.length) {
|
|
86057
87966
|
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
|
86058
87967
|
}
|
|
87968
|
+
console.log(` Node: ${nodeSummary}`);
|
|
87969
|
+
if (settings.unavailableNodePolicy) {
|
|
87970
|
+
console.log(` Unavailable Node Policy: ${settings.unavailableNodePolicy}`);
|
|
87971
|
+
}
|
|
86059
87972
|
console.log();
|
|
86060
87973
|
if (task.steps.length > 0) {
|
|
86061
87974
|
console.log(` Steps (${task.steps.filter((s) => s.status === "done").length}/${task.steps.length}):`);
|
|
@@ -86172,7 +88085,7 @@ async function runTaskRefine(id, feedbackArg, projectName) {
|
|
|
86172
88085
|
const store = await getStore(projectName);
|
|
86173
88086
|
let feedback = feedbackArg;
|
|
86174
88087
|
if (feedback === void 0) {
|
|
86175
|
-
const rl =
|
|
88088
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86176
88089
|
feedback = await rl.question("What needs to be refined? ");
|
|
86177
88090
|
rl.close();
|
|
86178
88091
|
}
|
|
@@ -86243,7 +88156,7 @@ async function runTaskDelete(id, force, projectName) {
|
|
|
86243
88156
|
return;
|
|
86244
88157
|
}
|
|
86245
88158
|
if (!force) {
|
|
86246
|
-
const rl =
|
|
88159
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86247
88160
|
const answer = await rl.question(`Are you sure you want to delete ${id}? [y/N] `);
|
|
86248
88161
|
rl.close();
|
|
86249
88162
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -86307,7 +88220,7 @@ async function runTaskImportGitHubInteractive(ownerRepo, options = {}, projectNa
|
|
|
86307
88220
|
console.log(` ${i + 1}. #${issue.number} ${issue.title.slice(0, 80)}${issue.title.length > 80 ? "\u2026" : ""}${status}`);
|
|
86308
88221
|
}
|
|
86309
88222
|
console.log();
|
|
86310
|
-
const rl =
|
|
88223
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86311
88224
|
let selectedIndices = [];
|
|
86312
88225
|
let validInput = false;
|
|
86313
88226
|
while (!validInput) {
|
|
@@ -86464,7 +88377,7 @@ async function runTaskComment(id, message, author = "user", projectName) {
|
|
|
86464
88377
|
const store = await getStore(projectName);
|
|
86465
88378
|
let text = message;
|
|
86466
88379
|
if (text === void 0) {
|
|
86467
|
-
const rl =
|
|
88380
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86468
88381
|
text = await rl.question("Comment: ");
|
|
86469
88382
|
rl.close();
|
|
86470
88383
|
}
|
|
@@ -86507,7 +88420,7 @@ async function runTaskSteer(id, message, projectName) {
|
|
|
86507
88420
|
const store = await getStore(projectName);
|
|
86508
88421
|
let text = message;
|
|
86509
88422
|
if (text === void 0) {
|
|
86510
|
-
const rl =
|
|
88423
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86511
88424
|
text = await rl.question("Message: ");
|
|
86512
88425
|
rl.close();
|
|
86513
88426
|
}
|
|
@@ -86638,7 +88551,7 @@ async function promptText(question) {
|
|
|
86638
88551
|
console.log(` ${question.description}`);
|
|
86639
88552
|
}
|
|
86640
88553
|
console.log(" (Enter your response. Type DONE on its own line when finished):\n");
|
|
86641
|
-
const rl =
|
|
88554
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86642
88555
|
const lines = [];
|
|
86643
88556
|
return new Promise((resolve17) => {
|
|
86644
88557
|
const askLine = () => {
|
|
@@ -86672,7 +88585,7 @@ async function promptSingleSelect(question) {
|
|
|
86672
88585
|
console.log(` ${opt.description}`);
|
|
86673
88586
|
}
|
|
86674
88587
|
}
|
|
86675
|
-
const rl =
|
|
88588
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86676
88589
|
while (true) {
|
|
86677
88590
|
const answer = await rl.question("\n Select (1-" + question.options.length + "): ");
|
|
86678
88591
|
const num = parseInt(answer.trim(), 10);
|
|
@@ -86700,7 +88613,7 @@ async function promptMultiSelect(question) {
|
|
|
86700
88613
|
console.log(` ${opt.description}`);
|
|
86701
88614
|
}
|
|
86702
88615
|
}
|
|
86703
|
-
const rl =
|
|
88616
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86704
88617
|
while (true) {
|
|
86705
88618
|
const answer = await rl.question("\n Select (comma-separated): ");
|
|
86706
88619
|
const nums = answer.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
@@ -86723,7 +88636,7 @@ async function promptConfirm(question) {
|
|
|
86723
88636
|
if (question.description) {
|
|
86724
88637
|
console.log(` ${question.description}`);
|
|
86725
88638
|
}
|
|
86726
|
-
const rl =
|
|
88639
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86727
88640
|
const answer = await rl.question("\n [Y/n]: ");
|
|
86728
88641
|
rl.close();
|
|
86729
88642
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -86788,7 +88701,7 @@ function wrapText(text, width) {
|
|
|
86788
88701
|
async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
|
|
86789
88702
|
let initialPlan = initialPlanArg;
|
|
86790
88703
|
if (!initialPlan) {
|
|
86791
|
-
const rl =
|
|
88704
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86792
88705
|
console.log("\n Let's plan your task. What would you like to accomplish?\n");
|
|
86793
88706
|
initialPlan = await rl.question(" Describe your idea: ");
|
|
86794
88707
|
rl.close();
|
|
@@ -86889,7 +88802,7 @@ async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
|
|
|
86889
88802
|
displaySummary(result.data);
|
|
86890
88803
|
let confirmed = yesFlag;
|
|
86891
88804
|
if (!yesFlag) {
|
|
86892
|
-
const rl =
|
|
88805
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86893
88806
|
const answer = await rl.question(" Create this task? [Y/n]: ");
|
|
86894
88807
|
rl.close();
|
|
86895
88808
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -86931,6 +88844,7 @@ var init_task = __esm({
|
|
|
86931
88844
|
init_src4();
|
|
86932
88845
|
init_gh_cli();
|
|
86933
88846
|
init_project_context();
|
|
88847
|
+
init_node();
|
|
86934
88848
|
STEP_STATUSES2 = ["pending", "in-progress", "done", "skipped"];
|
|
86935
88849
|
ANSI = {
|
|
86936
88850
|
reset: "\x1B[0m",
|
|
@@ -87143,17 +89057,17 @@ async function fetchGitHubIssuesViaGh(owner, repo, options = {}) {
|
|
|
87143
89057
|
}
|
|
87144
89058
|
const path2 = `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?${queryParams.toString()}`;
|
|
87145
89059
|
try {
|
|
87146
|
-
const issues = await runGhJsonAsync(["api", path2]);
|
|
89060
|
+
const issues = await runGhJsonAsync(["api", path2], { signal: options.signal });
|
|
87147
89061
|
return issues.filter((issue) => !issue.pull_request);
|
|
87148
89062
|
} catch (error) {
|
|
87149
89063
|
throw new Error(getGhErrorMessage(error));
|
|
87150
89064
|
}
|
|
87151
89065
|
}
|
|
87152
|
-
async function fetchGitHubIssueViaGh(owner, repo, issueNumber) {
|
|
89066
|
+
async function fetchGitHubIssueViaGh(owner, repo, issueNumber, options = {}) {
|
|
87153
89067
|
ensureGhCliAuth();
|
|
87154
89068
|
const path2 = `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
|
|
87155
89069
|
try {
|
|
87156
|
-
return await runGhJsonAsync(["api", path2]);
|
|
89070
|
+
return await runGhJsonAsync(["api", path2], { signal: options.signal });
|
|
87157
89071
|
} catch (error) {
|
|
87158
89072
|
throw new Error(getGhErrorMessage(error));
|
|
87159
89073
|
}
|
|
@@ -87234,12 +89148,18 @@ Column: triage
|
|
|
87234
89148
|
], {
|
|
87235
89149
|
description: "Agent ID to assign this task to, or null to clear (e.g. 'agent-abc123')"
|
|
87236
89150
|
})
|
|
89151
|
+
),
|
|
89152
|
+
nodeId: Type7.Optional(
|
|
89153
|
+
Type7.Union([Type7.String(), Type7.Null()], {
|
|
89154
|
+
description: "Node ID override for this task, or null to clear"
|
|
89155
|
+
})
|
|
87237
89156
|
)
|
|
87238
89157
|
}),
|
|
87239
89158
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
87240
89159
|
const store = await getStore2(ctx.cwd);
|
|
89160
|
+
let task;
|
|
87241
89161
|
try {
|
|
87242
|
-
await store.getTask(params.id);
|
|
89162
|
+
task = await store.getTask(params.id);
|
|
87243
89163
|
} catch {
|
|
87244
89164
|
return {
|
|
87245
89165
|
content: [{ type: "text", text: `Task ${params.id} not found` }],
|
|
@@ -87265,9 +89185,21 @@ Column: triage
|
|
|
87265
89185
|
updates.assignedAgentId = params.agentId;
|
|
87266
89186
|
updatedFields.push("agentId");
|
|
87267
89187
|
}
|
|
89188
|
+
if (params.nodeId !== void 0) {
|
|
89189
|
+
const validation = validateNodeOverrideChange(task, params.nodeId ?? null);
|
|
89190
|
+
if (!validation.allowed) {
|
|
89191
|
+
return {
|
|
89192
|
+
content: [{ type: "text", text: validation.message ?? "Node override change blocked" }],
|
|
89193
|
+
isError: true,
|
|
89194
|
+
details: { error: validation.reason }
|
|
89195
|
+
};
|
|
89196
|
+
}
|
|
89197
|
+
updates.nodeId = params.nodeId;
|
|
89198
|
+
updatedFields.push("nodeId");
|
|
89199
|
+
}
|
|
87268
89200
|
if (updatedFields.length === 0) {
|
|
87269
89201
|
return {
|
|
87270
|
-
content: [{ type: "text", text: "No fields to update. Provide at least one of: title, description, depends, agentId." }],
|
|
89202
|
+
content: [{ type: "text", text: "No fields to update. Provide at least one of: title, description, depends, agentId, nodeId." }],
|
|
87271
89203
|
isError: true,
|
|
87272
89204
|
details: { error: "No fields provided" }
|
|
87273
89205
|
};
|
|
@@ -87648,11 +89580,11 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
|
|
|
87648
89580
|
})
|
|
87649
89581
|
)
|
|
87650
89582
|
}),
|
|
87651
|
-
async execute(_toolCallId, params,
|
|
89583
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
87652
89584
|
const [owner, repo] = params.ownerRepo.split("/");
|
|
87653
89585
|
const limit = params.limit ?? 30;
|
|
87654
89586
|
const labels = params.labels;
|
|
87655
|
-
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels });
|
|
89587
|
+
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels, signal });
|
|
87656
89588
|
if (issues.length === 0) {
|
|
87657
89589
|
return {
|
|
87658
89590
|
content: [{ type: "text", text: `No open issues found in ${owner}/${repo}.` }],
|
|
@@ -87727,9 +89659,9 @@ ${createdTasks.map((task) => ` ${task.id}: ${task.title}`).join("\n") || " Non
|
|
|
87727
89659
|
minimum: 1
|
|
87728
89660
|
})
|
|
87729
89661
|
}),
|
|
87730
|
-
async execute(_toolCallId, params,
|
|
89662
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
87731
89663
|
const { owner, repo, issueNumber } = params;
|
|
87732
|
-
const issue = await fetchGitHubIssueViaGh(owner, repo, issueNumber);
|
|
89664
|
+
const issue = await fetchGitHubIssueViaGh(owner, repo, issueNumber, { signal });
|
|
87733
89665
|
if (issue.pull_request) {
|
|
87734
89666
|
throw new Error(`#${issueNumber} is a pull request, not an issue`);
|
|
87735
89667
|
}
|
|
@@ -87813,9 +89745,9 @@ ${sourceUrl}`
|
|
|
87813
89745
|
})
|
|
87814
89746
|
)
|
|
87815
89747
|
}),
|
|
87816
|
-
async execute(_toolCallId, params,
|
|
89748
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
87817
89749
|
const { owner, repo, limit = 30, labels } = params;
|
|
87818
|
-
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels });
|
|
89750
|
+
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels, signal });
|
|
87819
89751
|
if (issues.length === 0) {
|
|
87820
89752
|
return {
|
|
87821
89753
|
content: [{ type: "text", text: `No open issues found in ${owner}/${repo}.` }],
|