@runfusion/fusion 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +2932 -844
- package/dist/client/assets/{AgentDetailView-DyzuiJas.js → AgentDetailView-C3Xcrxnp.js} +3 -3
- package/dist/client/assets/{AgentDetailView-C1_lTTET.css → AgentDetailView-DIBOY8V-.css} +1 -1
- package/dist/client/assets/{AgentsView-CgweOTe6.js → AgentsView-EjE4y4rM.js} +3 -3
- package/dist/client/assets/{ChatView-DrY8FMIt.js → ChatView-DQLvKCYj.js} +1 -1
- package/dist/client/assets/DevServerView-CX7paFRQ.js +1 -0
- package/dist/client/assets/{DirectoryPicker-D5KQ-im_.js → DirectoryPicker-_cBPx6Nx.js} +1 -1
- package/dist/client/assets/{DocumentsView-D2wK7FYJ.js → DocumentsView-Wz33aYqp.js} +1 -1
- package/dist/client/assets/{InsightsView-DfY3sa1j.js → InsightsView-C7YPnS92.js} +1 -1
- package/dist/client/assets/MemoryView-DKQtFzFQ.js +2 -0
- package/dist/client/assets/{NodesView-g26-j7rg.js → NodesView-CI4rUQC4.js} +1 -1
- package/dist/client/assets/{NodesView-BYVG2yY-.css → NodesView-DCoS6iYh.css} +1 -1
- package/dist/client/assets/{PiExtensionsManager-DfMr3Gls.js → PiExtensionsManager-BFmdKgHZ.js} +3 -3
- package/dist/client/assets/{PluginManager-DiMOD-Kj.js → PluginManager-BGQU1IIw.js} +1 -1
- package/dist/client/assets/{RoadmapsView-DJC4F4CD.js → RoadmapsView-Cts3hoIS.js} +1 -1
- package/dist/client/assets/SettingsModal-D5hLoLXp.css +1 -0
- package/dist/client/assets/{SettingsModal-Cx3iMWDs.js → SettingsModal-DXvBGZHf.js} +1 -1
- package/dist/client/assets/SettingsModal-DvRd0ZOE.js +31 -0
- package/dist/client/assets/SetupWizardModal-DRF5fOoR.css +1 -0
- package/dist/client/assets/{SetupWizardModal-Cow6woq6.js → SetupWizardModal-Y2ewEE8Y.js} +1 -1
- package/dist/client/assets/{SkillsView-DTB2cmXQ.js → SkillsView-BXvrHzEZ.js} +1 -1
- package/dist/client/assets/{TodoView-CyxdHUdz.js → TodoView-NZHkv9YQ.js} +2 -2
- package/dist/client/assets/{folder-open-C3zB1vmh.js → folder-open-Kh0ScTc5.js} +1 -1
- package/dist/client/assets/index-CWz44REw.css +1 -0
- package/dist/client/assets/index-D1gavMG-.js +656 -0
- package/dist/client/assets/{list-checks-CK3_6p5e.js → list-checks-CvoT0bwU.js} +1 -1
- package/dist/client/assets/{star-BQhDgM9V.js → star-BdfwSLBU.js} +1 -1
- package/dist/client/assets/{upload-DDdZveEJ.js → upload-Bx8Yk_7Q.js} +1 -1
- package/dist/client/assets/{users-DWWgd19M.js → users-DgVaFEsz.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/extension.js +2219 -433
- package/dist/pi-claude-cli/index.ts +27 -0
- package/dist/pi-claude-cli/package.json +1 -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/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 = 50;
|
|
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,12 @@ 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
|
+
}
|
|
3751
3968
|
}
|
|
3752
3969
|
/**
|
|
3753
3970
|
* Run a single migration step inside a transaction and bump the version.
|
|
@@ -3763,6 +3980,14 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
|
|
|
3763
3980
|
const row = this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
3764
3981
|
return Boolean(row);
|
|
3765
3982
|
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Check whether an error appears to be an FTS5 corruption/integrity failure.
|
|
3985
|
+
*/
|
|
3986
|
+
isFts5CorruptionError(error) {
|
|
3987
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
3988
|
+
const lower = message.toLowerCase();
|
|
3989
|
+
return lower.includes("corruption found reading blob") || lower.includes("database disk image is malformed") || lower.includes("fts5") && lower.includes("corrupt");
|
|
3990
|
+
}
|
|
3766
3991
|
/**
|
|
3767
3992
|
* Check whether a table has a given column.
|
|
3768
3993
|
*/
|
|
@@ -18238,10 +18463,31 @@ var init_task_merge = __esm({
|
|
|
18238
18463
|
"use strict";
|
|
18239
18464
|
BLOCKING_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
18240
18465
|
"failed",
|
|
18466
|
+
// ── User-attention / awaiting-handoff states ─────────────────────────
|
|
18241
18467
|
"awaiting-inspection",
|
|
18242
18468
|
"awaiting-user-review",
|
|
18469
|
+
"awaiting-approval",
|
|
18470
|
+
// triage spec awaiting user approval
|
|
18471
|
+
// ── Active merge in-flight ───────────────────────────────────────────
|
|
18243
18472
|
"merging",
|
|
18244
|
-
"merging-pr"
|
|
18473
|
+
"merging-pr",
|
|
18474
|
+
// ── Re-planning / triage states (scope not finalized) ────────────────
|
|
18475
|
+
// A task in planning/triage hasn't finalized its scope yet — letting it
|
|
18476
|
+
// merge skips the work the user moved it back to plan. Same for the legacy
|
|
18477
|
+
// "specifying" alias migrated to "planning" in db.ts.
|
|
18478
|
+
"planning",
|
|
18479
|
+
"specifying",
|
|
18480
|
+
"needs-replan",
|
|
18481
|
+
// scheduler/executor/triage signaled re-plan
|
|
18482
|
+
// ── Mission-level validation in flight ───────────────────────────────
|
|
18483
|
+
"mission-validation",
|
|
18484
|
+
// ── Scheduler-side transient state ───────────────────────────────────
|
|
18485
|
+
"queued",
|
|
18486
|
+
// scheduler placed the task in line; not finalized
|
|
18487
|
+
// ── Abnormal termination — defensive guard ───────────────────────────
|
|
18488
|
+
// Task was killed by the stuck detector. If it surfaces in in-review,
|
|
18489
|
+
// it needs investigation, not auto-merge.
|
|
18490
|
+
"stuck-killed"
|
|
18245
18491
|
]);
|
|
18246
18492
|
NON_TERMINAL_STEP_STATUSES = /* @__PURE__ */ new Set([
|
|
18247
18493
|
"pending",
|
|
@@ -29462,6 +29708,26 @@ var init_logger = __esm({
|
|
|
29462
29708
|
}
|
|
29463
29709
|
});
|
|
29464
29710
|
|
|
29711
|
+
// ../core/src/node-override-guard.ts
|
|
29712
|
+
function validateNodeOverrideChange(task, newNodeId) {
|
|
29713
|
+
if (newNodeId === void 0) {
|
|
29714
|
+
return { allowed: true };
|
|
29715
|
+
}
|
|
29716
|
+
if (task.column === "in-progress") {
|
|
29717
|
+
return {
|
|
29718
|
+
allowed: false,
|
|
29719
|
+
reason: "task-in-progress",
|
|
29720
|
+
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.`
|
|
29721
|
+
};
|
|
29722
|
+
}
|
|
29723
|
+
return { allowed: true };
|
|
29724
|
+
}
|
|
29725
|
+
var init_node_override_guard = __esm({
|
|
29726
|
+
"../core/src/node-override-guard.ts"() {
|
|
29727
|
+
"use strict";
|
|
29728
|
+
}
|
|
29729
|
+
});
|
|
29730
|
+
|
|
29465
29731
|
// ../core/src/store.ts
|
|
29466
29732
|
import { EventEmitter as EventEmitter12 } from "node:events";
|
|
29467
29733
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
@@ -29549,6 +29815,7 @@ var init_store = __esm({
|
|
|
29549
29815
|
init_project_memory();
|
|
29550
29816
|
init_run_command();
|
|
29551
29817
|
init_logger();
|
|
29818
|
+
init_node_override_guard();
|
|
29552
29819
|
TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1e3;
|
|
29553
29820
|
TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4e3;
|
|
29554
29821
|
ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
|
|
@@ -29835,6 +30102,8 @@ var init_store = __esm({
|
|
|
29835
30102
|
assignedAgentId: row.assignedAgentId || void 0,
|
|
29836
30103
|
assigneeUserId: row.assigneeUserId || void 0,
|
|
29837
30104
|
nodeId: row.nodeId || void 0,
|
|
30105
|
+
effectiveNodeId: row.effectiveNodeId || void 0,
|
|
30106
|
+
effectiveNodeSource: row.effectiveNodeSource || void 0,
|
|
29838
30107
|
checkedOutBy: row.checkedOutBy || void 0,
|
|
29839
30108
|
checkedOutAt: row.checkedOutAt || void 0
|
|
29840
30109
|
};
|
|
@@ -30083,6 +30352,8 @@ ${recentText}` : void 0
|
|
|
30083
30352
|
"assignedAgentId",
|
|
30084
30353
|
"assigneeUserId",
|
|
30085
30354
|
"nodeId",
|
|
30355
|
+
"effectiveNodeId",
|
|
30356
|
+
"effectiveNodeSource",
|
|
30086
30357
|
"checkedOutBy",
|
|
30087
30358
|
"checkedOutAt",
|
|
30088
30359
|
// `log` is fetched in slim mode so the server can aggregate
|
|
@@ -30183,6 +30454,8 @@ ${outcome}`;
|
|
|
30183
30454
|
"assignedAgentId",
|
|
30184
30455
|
"assigneeUserId",
|
|
30185
30456
|
"nodeId",
|
|
30457
|
+
"effectiveNodeId",
|
|
30458
|
+
"effectiveNodeSource",
|
|
30186
30459
|
"checkedOutBy",
|
|
30187
30460
|
"checkedOutAt"
|
|
30188
30461
|
];
|
|
@@ -30221,9 +30494,9 @@ ${outcome}`;
|
|
|
30221
30494
|
dependencies, steps, log, attachments, steeringComments,
|
|
30222
30495
|
comments, workflowStepResults, prInfo, issueInfo,
|
|
30223
30496
|
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
|
|
30224
|
-
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, checkedOutBy, checkedOutAt
|
|
30497
|
+
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, checkedOutBy, checkedOutAt
|
|
30225
30498
|
) VALUES (
|
|
30226
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
30499
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
30227
30500
|
)
|
|
30228
30501
|
ON CONFLICT(id) DO UPDATE SET
|
|
30229
30502
|
title = excluded.title,
|
|
@@ -30291,6 +30564,8 @@ ${outcome}`;
|
|
|
30291
30564
|
assignedAgentId = excluded.assignedAgentId,
|
|
30292
30565
|
assigneeUserId = excluded.assigneeUserId,
|
|
30293
30566
|
nodeId = excluded.nodeId,
|
|
30567
|
+
effectiveNodeId = excluded.effectiveNodeId,
|
|
30568
|
+
effectiveNodeSource = excluded.effectiveNodeSource,
|
|
30294
30569
|
checkedOutBy = excluded.checkedOutBy,
|
|
30295
30570
|
checkedOutAt = excluded.checkedOutAt
|
|
30296
30571
|
`).run(
|
|
@@ -30360,11 +30635,36 @@ ${outcome}`;
|
|
|
30360
30635
|
task.assignedAgentId ?? null,
|
|
30361
30636
|
task.assigneeUserId ?? null,
|
|
30362
30637
|
task.nodeId ?? null,
|
|
30638
|
+
task.effectiveNodeId ?? null,
|
|
30639
|
+
task.effectiveNodeSource ?? null,
|
|
30363
30640
|
task.checkedOutBy ?? null,
|
|
30364
30641
|
task.checkedOutAt ?? null
|
|
30365
30642
|
);
|
|
30366
30643
|
this.db.bumpLastModified();
|
|
30367
30644
|
}
|
|
30645
|
+
upsertTaskWithFtsRecovery(task) {
|
|
30646
|
+
try {
|
|
30647
|
+
this.upsertTask(task);
|
|
30648
|
+
return;
|
|
30649
|
+
} catch (error) {
|
|
30650
|
+
if (!this.db.isFts5CorruptionError(error)) {
|
|
30651
|
+
throw error;
|
|
30652
|
+
}
|
|
30653
|
+
console.warn(`[fusion:store] FTS5 corruption detected during upsert for task ${task.id}; rebuilding index and retrying once`);
|
|
30654
|
+
try {
|
|
30655
|
+
this.db.rebuildFts5Index();
|
|
30656
|
+
} catch (rebuildError) {
|
|
30657
|
+
console.warn("[fusion:store] FTS5 rebuild failed; propagating original upsert error", rebuildError);
|
|
30658
|
+
throw error;
|
|
30659
|
+
}
|
|
30660
|
+
try {
|
|
30661
|
+
this.upsertTask(task);
|
|
30662
|
+
} catch (retryError) {
|
|
30663
|
+
console.warn("[fusion:store] Upsert retry after FTS5 rebuild failed; propagating original upsert error", retryError);
|
|
30664
|
+
throw error;
|
|
30665
|
+
}
|
|
30666
|
+
}
|
|
30667
|
+
}
|
|
30368
30668
|
/**
|
|
30369
30669
|
* Read a task from SQLite by ID.
|
|
30370
30670
|
*/
|
|
@@ -30583,7 +30883,7 @@ ${outcome}`;
|
|
|
30583
30883
|
* for backward compatibility and debugging.
|
|
30584
30884
|
*/
|
|
30585
30885
|
async atomicWriteTaskJson(dir, task) {
|
|
30586
|
-
this.
|
|
30886
|
+
this.upsertTaskWithFtsRecovery(task);
|
|
30587
30887
|
await this.writeTaskJsonFile(dir, task);
|
|
30588
30888
|
}
|
|
30589
30889
|
/**
|
|
@@ -30596,7 +30896,7 @@ ${outcome}`;
|
|
|
30596
30896
|
*/
|
|
30597
30897
|
async atomicWriteTaskJsonWithAudit(dir, task, auditInput) {
|
|
30598
30898
|
this.db.transaction(() => {
|
|
30599
|
-
this.
|
|
30899
|
+
this.upsertTaskWithFtsRecovery(task);
|
|
30600
30900
|
if (auditInput) {
|
|
30601
30901
|
const eventId = randomUUID6();
|
|
30602
30902
|
const timestamp = auditInput.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -31544,6 +31844,14 @@ ${newTask.description}
|
|
|
31544
31844
|
if (fromColumn === "in-review" && (toColumn === "todo" || toColumn === "in-progress") || fromColumn === "done" && (toColumn === "todo" || toColumn === "triage")) {
|
|
31545
31845
|
task.workflowStepResults = void 0;
|
|
31546
31846
|
}
|
|
31847
|
+
if (fromColumn === "in-review" && toColumn === "todo") {
|
|
31848
|
+
task.branch = void 0;
|
|
31849
|
+
task.baseBranch = void 0;
|
|
31850
|
+
task.baseCommitSha = void 0;
|
|
31851
|
+
task.summary = void 0;
|
|
31852
|
+
task.recoveryRetryCount = void 0;
|
|
31853
|
+
task.nextRecoveryAt = void 0;
|
|
31854
|
+
}
|
|
31547
31855
|
await this.atomicWriteTaskJson(dir, task);
|
|
31548
31856
|
if (this.isWatching) this.taskCache.set(id, { ...task });
|
|
31549
31857
|
this.emit("task:moved", { task, from: fromColumn, to: toColumn });
|
|
@@ -31557,6 +31865,12 @@ ${newTask.description}
|
|
|
31557
31865
|
}
|
|
31558
31866
|
const dir = this.taskDir(id);
|
|
31559
31867
|
const task = await this.readTaskJson(dir);
|
|
31868
|
+
if (updates.nodeId !== void 0) {
|
|
31869
|
+
const validation = validateNodeOverrideChange(task, updates.nodeId ?? null);
|
|
31870
|
+
if (!validation.allowed) {
|
|
31871
|
+
throw new Error(validation.message);
|
|
31872
|
+
}
|
|
31873
|
+
}
|
|
31560
31874
|
if (!task.log) {
|
|
31561
31875
|
task.log = [];
|
|
31562
31876
|
}
|
|
@@ -31619,6 +31933,16 @@ ${newTask.description}
|
|
|
31619
31933
|
} else if (updates.nodeId !== void 0) {
|
|
31620
31934
|
task.nodeId = updates.nodeId;
|
|
31621
31935
|
}
|
|
31936
|
+
if (updates.effectiveNodeId === null) {
|
|
31937
|
+
task.effectiveNodeId = void 0;
|
|
31938
|
+
} else if (updates.effectiveNodeId !== void 0) {
|
|
31939
|
+
task.effectiveNodeId = updates.effectiveNodeId;
|
|
31940
|
+
}
|
|
31941
|
+
if (updates.effectiveNodeSource === null) {
|
|
31942
|
+
task.effectiveNodeSource = void 0;
|
|
31943
|
+
} else if (updates.effectiveNodeSource !== void 0) {
|
|
31944
|
+
task.effectiveNodeSource = updates.effectiveNodeSource;
|
|
31945
|
+
}
|
|
31622
31946
|
if (updates.checkedOutBy === null) {
|
|
31623
31947
|
task.checkedOutBy = void 0;
|
|
31624
31948
|
task.checkedOutAt = void 0;
|
|
@@ -33852,7 +34176,7 @@ ${stepsSection}`;
|
|
|
33852
34176
|
healthCheck() {
|
|
33853
34177
|
try {
|
|
33854
34178
|
this.db.prepare("SELECT 1").get();
|
|
33855
|
-
return
|
|
34179
|
+
return this.db.checkFts5Integrity();
|
|
33856
34180
|
} catch {
|
|
33857
34181
|
return false;
|
|
33858
34182
|
}
|
|
@@ -34474,6 +34798,10 @@ var init_board = __esm({
|
|
|
34474
34798
|
|
|
34475
34799
|
// ../core/src/gh-cli.ts
|
|
34476
34800
|
import { execFileSync, execFile as execFile2 } from "node:child_process";
|
|
34801
|
+
function normalizeRunGhOptions(opts) {
|
|
34802
|
+
if (typeof opts === "string") return { cwd: opts };
|
|
34803
|
+
return opts ?? {};
|
|
34804
|
+
}
|
|
34477
34805
|
function isGhAvailable() {
|
|
34478
34806
|
try {
|
|
34479
34807
|
execFileSync("gh", ["--version"], {
|
|
@@ -34513,19 +34841,55 @@ function runGh(args, cwd) {
|
|
|
34513
34841
|
throw error;
|
|
34514
34842
|
}
|
|
34515
34843
|
}
|
|
34516
|
-
function runGhAsync(args,
|
|
34844
|
+
function runGhAsync(args, cwdOrOptions) {
|
|
34845
|
+
const { cwd, signal: externalSignal, timeoutMs = DEFAULT_GH_TIMEOUT_MS } = normalizeRunGhOptions(cwdOrOptions);
|
|
34517
34846
|
return new Promise((resolve17, reject) => {
|
|
34847
|
+
if (externalSignal?.aborted) {
|
|
34848
|
+
reject(makeGhError(`gh command aborted: ${describeAbortReason(externalSignal.reason)}`, "ABORT_ERR"));
|
|
34849
|
+
return;
|
|
34850
|
+
}
|
|
34851
|
+
const controller = new AbortController();
|
|
34852
|
+
let timedOut = false;
|
|
34853
|
+
let externalAborted = false;
|
|
34854
|
+
const onExternalAbort = () => {
|
|
34855
|
+
externalAborted = true;
|
|
34856
|
+
controller.abort();
|
|
34857
|
+
};
|
|
34858
|
+
if (externalSignal) {
|
|
34859
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
34860
|
+
}
|
|
34861
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
34862
|
+
timedOut = true;
|
|
34863
|
+
controller.abort();
|
|
34864
|
+
}, timeoutMs) : void 0;
|
|
34865
|
+
const cleanup = () => {
|
|
34866
|
+
if (timer) clearTimeout(timer);
|
|
34867
|
+
if (externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
|
|
34868
|
+
};
|
|
34518
34869
|
execFile2(
|
|
34519
34870
|
"gh",
|
|
34520
34871
|
args,
|
|
34521
34872
|
{
|
|
34522
34873
|
encoding: "utf-8",
|
|
34523
|
-
cwd
|
|
34874
|
+
cwd,
|
|
34875
|
+
signal: controller.signal
|
|
34524
34876
|
},
|
|
34525
34877
|
(error, stdout, stderr) => {
|
|
34878
|
+
cleanup();
|
|
34526
34879
|
if (error) {
|
|
34527
|
-
const
|
|
34528
|
-
|
|
34880
|
+
const isAbort = error.code === "ABORT_ERR" || error.name === "AbortError";
|
|
34881
|
+
let message;
|
|
34882
|
+
if (timedOut) {
|
|
34883
|
+
message = `gh command timed out after ${timeoutMs}ms`;
|
|
34884
|
+
} else if (isAbort && externalAborted) {
|
|
34885
|
+
message = `gh command aborted: ${describeAbortReason(externalSignal?.reason)}`;
|
|
34886
|
+
} else if (isAbort) {
|
|
34887
|
+
message = "gh command aborted";
|
|
34888
|
+
} else {
|
|
34889
|
+
message = `gh command failed: ${error.message}`;
|
|
34890
|
+
}
|
|
34891
|
+
const ghError = new Error(message);
|
|
34892
|
+
ghError.code = error.code ?? (isAbort ? "ABORT_ERR" : null);
|
|
34529
34893
|
ghError.stdout = stdout ?? "";
|
|
34530
34894
|
ghError.stderr = stderr ?? "";
|
|
34531
34895
|
reject(ghError);
|
|
@@ -34536,6 +34900,18 @@ function runGhAsync(args, cwd) {
|
|
|
34536
34900
|
);
|
|
34537
34901
|
});
|
|
34538
34902
|
}
|
|
34903
|
+
function makeGhError(message, code) {
|
|
34904
|
+
const err = new Error(message);
|
|
34905
|
+
err.code = code;
|
|
34906
|
+
err.stdout = "";
|
|
34907
|
+
err.stderr = "";
|
|
34908
|
+
return err;
|
|
34909
|
+
}
|
|
34910
|
+
function describeAbortReason(reason) {
|
|
34911
|
+
if (reason instanceof Error) return reason.message;
|
|
34912
|
+
if (typeof reason === "string") return reason;
|
|
34913
|
+
return "aborted";
|
|
34914
|
+
}
|
|
34539
34915
|
function runGhJson(args, cwd) {
|
|
34540
34916
|
const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
|
|
34541
34917
|
const output = runGh(jsonArgs, cwd);
|
|
@@ -34545,9 +34921,9 @@ function runGhJson(args, cwd) {
|
|
|
34545
34921
|
throw new Error(`Failed to parse gh JSON output: ${err instanceof Error ? err.message : String(err)}`);
|
|
34546
34922
|
}
|
|
34547
34923
|
}
|
|
34548
|
-
async function runGhJsonAsync(args,
|
|
34924
|
+
async function runGhJsonAsync(args, cwdOrOptions) {
|
|
34549
34925
|
const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
|
|
34550
|
-
const output = await runGhAsync(jsonArgs,
|
|
34926
|
+
const output = await runGhAsync(jsonArgs, cwdOrOptions);
|
|
34551
34927
|
try {
|
|
34552
34928
|
return JSON.parse(output);
|
|
34553
34929
|
} catch (err) {
|
|
@@ -34605,9 +34981,11 @@ function getCurrentRepo(cwd) {
|
|
|
34605
34981
|
return null;
|
|
34606
34982
|
}
|
|
34607
34983
|
}
|
|
34984
|
+
var DEFAULT_GH_TIMEOUT_MS;
|
|
34608
34985
|
var init_gh_cli = __esm({
|
|
34609
34986
|
"../core/src/gh-cli.ts"() {
|
|
34610
34987
|
"use strict";
|
|
34988
|
+
DEFAULT_GH_TIMEOUT_MS = 3e4;
|
|
34611
34989
|
}
|
|
34612
34990
|
});
|
|
34613
34991
|
|
|
@@ -35071,6 +35449,83 @@ var init_routine_store = __esm({
|
|
|
35071
35449
|
}
|
|
35072
35450
|
});
|
|
35073
35451
|
|
|
35452
|
+
// ../core/src/notification/dispatcher.ts
|
|
35453
|
+
var NotificationDispatcher;
|
|
35454
|
+
var init_dispatcher = __esm({
|
|
35455
|
+
"../core/src/notification/dispatcher.ts"() {
|
|
35456
|
+
"use strict";
|
|
35457
|
+
NotificationDispatcher = class {
|
|
35458
|
+
constructor(config = {}) {
|
|
35459
|
+
this.config = config;
|
|
35460
|
+
}
|
|
35461
|
+
providers = /* @__PURE__ */ new Map();
|
|
35462
|
+
registerProvider(provider) {
|
|
35463
|
+
this.providers.set(provider.getProviderId(), provider);
|
|
35464
|
+
}
|
|
35465
|
+
unregisterProvider(providerId) {
|
|
35466
|
+
this.providers.delete(providerId);
|
|
35467
|
+
}
|
|
35468
|
+
getProviders() {
|
|
35469
|
+
return [...this.providers.values()];
|
|
35470
|
+
}
|
|
35471
|
+
async dispatch(event, payload) {
|
|
35472
|
+
const providers = this.getProviders().filter(
|
|
35473
|
+
(provider) => provider.isEventSupported(event)
|
|
35474
|
+
);
|
|
35475
|
+
const results = await Promise.all(
|
|
35476
|
+
providers.map(async (provider) => {
|
|
35477
|
+
const providerId = provider.getProviderId();
|
|
35478
|
+
try {
|
|
35479
|
+
return await provider.sendNotification(event, payload);
|
|
35480
|
+
} catch (error) {
|
|
35481
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35482
|
+
console.warn(
|
|
35483
|
+
`[notification-dispatcher] Provider ${providerId} failed for event ${event}: ${message}`
|
|
35484
|
+
);
|
|
35485
|
+
return { success: false, providerId, error: message };
|
|
35486
|
+
}
|
|
35487
|
+
})
|
|
35488
|
+
);
|
|
35489
|
+
return results;
|
|
35490
|
+
}
|
|
35491
|
+
async initializeAll() {
|
|
35492
|
+
await Promise.all(
|
|
35493
|
+
this.getProviders().map(async (provider) => {
|
|
35494
|
+
if (!provider.initialize) {
|
|
35495
|
+
return;
|
|
35496
|
+
}
|
|
35497
|
+
try {
|
|
35498
|
+
await provider.initialize(this.config);
|
|
35499
|
+
} catch (error) {
|
|
35500
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35501
|
+
console.warn(
|
|
35502
|
+
`[notification-dispatcher] Provider ${provider.getProviderId()} initialization failed: ${message}`
|
|
35503
|
+
);
|
|
35504
|
+
}
|
|
35505
|
+
})
|
|
35506
|
+
);
|
|
35507
|
+
}
|
|
35508
|
+
async shutdownAll() {
|
|
35509
|
+
await Promise.all(
|
|
35510
|
+
this.getProviders().map(async (provider) => {
|
|
35511
|
+
if (!provider.shutdown) {
|
|
35512
|
+
return;
|
|
35513
|
+
}
|
|
35514
|
+
try {
|
|
35515
|
+
await provider.shutdown();
|
|
35516
|
+
} catch (error) {
|
|
35517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35518
|
+
console.warn(
|
|
35519
|
+
`[notification-dispatcher] Provider ${provider.getProviderId()} shutdown failed: ${message}`
|
|
35520
|
+
);
|
|
35521
|
+
}
|
|
35522
|
+
})
|
|
35523
|
+
);
|
|
35524
|
+
}
|
|
35525
|
+
};
|
|
35526
|
+
}
|
|
35527
|
+
});
|
|
35528
|
+
|
|
35074
35529
|
// ../core/src/plugin-loader.ts
|
|
35075
35530
|
import { basename as basename6, dirname as dirname5, extname, isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
|
|
35076
35531
|
import { copyFile, rm } from "node:fs/promises";
|
|
@@ -48447,8 +48902,10 @@ __export(src_exports, {
|
|
|
48447
48902
|
MessageStore: () => MessageStore,
|
|
48448
48903
|
MigrationCoordinator: () => MigrationCoordinator,
|
|
48449
48904
|
MissionStore: () => MissionStore,
|
|
48905
|
+
NOTIFICATION_EVENTS: () => NOTIFICATION_EVENTS,
|
|
48450
48906
|
NodeConnection: () => NodeConnection,
|
|
48451
48907
|
NodeDiscovery: () => NodeDiscovery,
|
|
48908
|
+
NotificationDispatcher: () => NotificationDispatcher,
|
|
48452
48909
|
PROJECT_SETTINGS_KEYS: () => PROJECT_SETTINGS_KEYS,
|
|
48453
48910
|
PROMPT_KEY_CATALOG: () => PROMPT_KEY_CATALOG,
|
|
48454
48911
|
PluginLoader: () => PluginLoader,
|
|
@@ -48596,6 +49053,7 @@ __export(src_exports, {
|
|
|
48596
49053
|
mergeInsights: () => mergeInsights,
|
|
48597
49054
|
migrateFromLegacy: () => migrateFromLegacy,
|
|
48598
49055
|
moveRoadmapFeature: () => moveRoadmapFeature,
|
|
49056
|
+
normalizeMergeConflictStrategy: () => normalizeMergeConflictStrategy,
|
|
48599
49057
|
normalizePermissions: () => normalizePermissions,
|
|
48600
49058
|
normalizeRoadmapFeatureOrder: () => normalizeRoadmapFeatureOrder,
|
|
48601
49059
|
normalizeRoadmapMilestoneOrder: () => normalizeRoadmapMilestoneOrder,
|
|
@@ -48635,12 +49093,20 @@ __export(src_exports, {
|
|
|
48635
49093
|
renderMemoryAuditMarkdown: () => renderMemoryAuditMarkdown,
|
|
48636
49094
|
resolveAgentPrompt: () => resolveAgentPrompt,
|
|
48637
49095
|
resolveDependencyOrder: () => resolveDependencyOrder,
|
|
49096
|
+
resolveExecutionSettingsModel: () => resolveExecutionSettingsModel,
|
|
48638
49097
|
resolveGlobalDir: () => resolveGlobalDir,
|
|
48639
49098
|
resolveMemoryBackend: () => resolveMemoryBackend,
|
|
48640
49099
|
resolveMemoryInstructionContext: () => resolveMemoryInstructionContext,
|
|
48641
49100
|
resolvePiExtensionProjectRoot: () => resolvePiExtensionProjectRoot,
|
|
49101
|
+
resolvePlanningSettingsModel: () => resolvePlanningSettingsModel,
|
|
49102
|
+
resolveProjectDefaultModel: () => resolveProjectDefaultModel,
|
|
48642
49103
|
resolvePrompt: () => resolvePrompt,
|
|
48643
49104
|
resolveRolePrompts: () => resolveRolePrompts,
|
|
49105
|
+
resolveTaskExecutionModel: () => resolveTaskExecutionModel,
|
|
49106
|
+
resolveTaskPlanningModel: () => resolveTaskPlanningModel,
|
|
49107
|
+
resolveTaskValidatorModel: () => resolveTaskValidatorModel,
|
|
49108
|
+
resolveTitleSummarizerSettingsModel: () => resolveTitleSummarizerSettingsModel,
|
|
49109
|
+
resolveValidatorSettingsModel: () => resolveValidatorSettingsModel,
|
|
48644
49110
|
runBackupCommand: () => runBackupCommand,
|
|
48645
49111
|
runCommandAsync: () => runCommandAsync,
|
|
48646
49112
|
runGh: () => runGh,
|
|
@@ -48670,6 +49136,7 @@ __export(src_exports, {
|
|
|
48670
49136
|
validateDescription: () => validateDescription,
|
|
48671
49137
|
validateImportData: () => validateImportData,
|
|
48672
49138
|
validateMessageMetadata: () => validateMessageMetadata,
|
|
49139
|
+
validateNodeOverrideChange: () => validateNodeOverrideChange,
|
|
48673
49140
|
validatePluginManifest: () => validatePluginManifest,
|
|
48674
49141
|
validateUnavailableNodePolicy: () => validateUnavailableNodePolicy,
|
|
48675
49142
|
writeAgentMemoryFile: () => writeAgentMemoryFile,
|
|
@@ -48704,15 +49171,19 @@ var init_src = __esm({
|
|
|
48704
49171
|
init_automation();
|
|
48705
49172
|
init_automation_store();
|
|
48706
49173
|
init_run_command();
|
|
49174
|
+
init_node_override_guard();
|
|
48707
49175
|
init_settings_validation();
|
|
48708
49176
|
init_routine();
|
|
48709
49177
|
init_routine_store();
|
|
49178
|
+
init_dispatcher();
|
|
49179
|
+
init_types();
|
|
48710
49180
|
init_plugin_types();
|
|
48711
49181
|
init_plugin_store();
|
|
48712
49182
|
init_plugin_loader();
|
|
48713
49183
|
init_backup();
|
|
48714
49184
|
init_settings_export();
|
|
48715
49185
|
init_ai_summarize();
|
|
49186
|
+
init_model_resolution();
|
|
48716
49187
|
init_memory_compaction();
|
|
48717
49188
|
init_roadmap_ordering();
|
|
48718
49189
|
init_task_priority();
|
|
@@ -50291,17 +50762,6 @@ var init_auth_storage = __esm({
|
|
|
50291
50762
|
});
|
|
50292
50763
|
|
|
50293
50764
|
// ../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
50765
|
import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
|
|
50306
50766
|
import { exec } from "node:child_process";
|
|
50307
50767
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -51225,16 +51685,14 @@ function wrapPluginRuntime(instance, runtimeId, runtimeName) {
|
|
|
51225
51685
|
if (typeof adapter.promptWithFallback === "function") {
|
|
51226
51686
|
return adapter.promptWithFallback(session, prompt, options);
|
|
51227
51687
|
}
|
|
51228
|
-
|
|
51229
|
-
return pwf(session, prompt, options);
|
|
51688
|
+
return promptWithFallback(session, prompt, options);
|
|
51230
51689
|
},
|
|
51231
51690
|
describeModel: (session) => {
|
|
51232
51691
|
const adapter = instance;
|
|
51233
51692
|
if (typeof adapter.describeModel === "function") {
|
|
51234
51693
|
return adapter.describeModel(session);
|
|
51235
51694
|
}
|
|
51236
|
-
|
|
51237
|
-
return dm(session);
|
|
51695
|
+
return describeModel(session);
|
|
51238
51696
|
}
|
|
51239
51697
|
};
|
|
51240
51698
|
}
|
|
@@ -51305,36 +51763,19 @@ var init_runtime_resolution = __esm({
|
|
|
51305
51763
|
"../engine/src/runtime-resolution.ts"() {
|
|
51306
51764
|
"use strict";
|
|
51307
51765
|
init_logger2();
|
|
51766
|
+
init_pi();
|
|
51308
51767
|
runtimeLog2 = createLogger2("runtime-resolver");
|
|
51309
|
-
DefaultPiRuntime = class
|
|
51768
|
+
DefaultPiRuntime = class {
|
|
51310
51769
|
id = "pi";
|
|
51311
51770
|
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
51771
|
async createSession(options) {
|
|
51318
|
-
|
|
51319
|
-
return createFnAgent5(options);
|
|
51772
|
+
return createFnAgent2(options);
|
|
51320
51773
|
}
|
|
51321
|
-
/**
|
|
51322
|
-
* Prompt with automatic retry and compaction.
|
|
51323
|
-
* Delegates to the existing promptWithFallback implementation.
|
|
51324
|
-
*/
|
|
51325
51774
|
async promptWithFallback(session, prompt, options) {
|
|
51326
|
-
|
|
51327
|
-
return pwf(session, prompt, options);
|
|
51775
|
+
return promptWithFallback(session, prompt, options);
|
|
51328
51776
|
}
|
|
51329
|
-
/**
|
|
51330
|
-
* Get model description from session.
|
|
51331
|
-
*/
|
|
51332
51777
|
describeModel(session) {
|
|
51333
|
-
|
|
51334
|
-
const { describeModel: describeModel2 } = (init_pi(), __toCommonJS(pi_exports));
|
|
51335
|
-
_DefaultPiRuntime.describeModelFn = describeModel2;
|
|
51336
|
-
}
|
|
51337
|
-
return _DefaultPiRuntime.describeModelFn(session);
|
|
51778
|
+
return describeModel(session);
|
|
51338
51779
|
}
|
|
51339
51780
|
};
|
|
51340
51781
|
defaultPiRuntimeInstance = null;
|
|
@@ -51378,12 +51819,10 @@ async function createResolvedAgentSession(options) {
|
|
|
51378
51819
|
};
|
|
51379
51820
|
}
|
|
51380
51821
|
async function promptWithAutoRetry(session, prompt, options) {
|
|
51381
|
-
|
|
51382
|
-
return pwf(session, prompt, options);
|
|
51822
|
+
return promptWithFallback(session, prompt, options);
|
|
51383
51823
|
}
|
|
51384
51824
|
async function describeAgentModel(session) {
|
|
51385
|
-
|
|
51386
|
-
return dm(session);
|
|
51825
|
+
return describeModel(session);
|
|
51387
51826
|
}
|
|
51388
51827
|
var sessionLog;
|
|
51389
51828
|
var init_agent_session_helpers = __esm({
|
|
@@ -51391,6 +51830,7 @@ var init_agent_session_helpers = __esm({
|
|
|
51391
51830
|
"use strict";
|
|
51392
51831
|
init_runtime_resolution();
|
|
51393
51832
|
init_logger2();
|
|
51833
|
+
init_pi();
|
|
51394
51834
|
sessionLog = createLogger2("agent-session");
|
|
51395
51835
|
}
|
|
51396
51836
|
});
|
|
@@ -55114,24 +55554,26 @@ async function resolveTaskDiffBaseRef({
|
|
|
55114
55554
|
baseBranch,
|
|
55115
55555
|
baseCommitSha
|
|
55116
55556
|
}) {
|
|
55117
|
-
const resolvedBaseBranch = baseBranch?.trim() || "main";
|
|
55557
|
+
const resolvedBaseBranch = baseBranch?.trim() || (baseCommitSha ? void 0 : "main");
|
|
55118
55558
|
const quotedHeadRef = quoteArg(headRef);
|
|
55119
55559
|
let mergeBase;
|
|
55120
|
-
|
|
55560
|
+
if (resolvedBaseBranch) {
|
|
55121
55561
|
try {
|
|
55122
|
-
|
|
55123
|
-
|
|
55124
|
-
|
|
55125
|
-
|
|
55126
|
-
|
|
55562
|
+
try {
|
|
55563
|
+
const { stdout } = await execAsync2(`git merge-base ${quotedHeadRef} ${quoteArg(resolvedBaseBranch)}`, {
|
|
55564
|
+
cwd,
|
|
55565
|
+
encoding: "utf-8"
|
|
55566
|
+
});
|
|
55567
|
+
mergeBase = stdout.trim() || void 0;
|
|
55568
|
+
} catch {
|
|
55569
|
+
const { stdout } = await execAsync2(`git merge-base ${quotedHeadRef} ${quoteArg(`origin/${resolvedBaseBranch}`)}`, {
|
|
55570
|
+
cwd,
|
|
55571
|
+
encoding: "utf-8"
|
|
55572
|
+
});
|
|
55573
|
+
mergeBase = stdout.trim() || void 0;
|
|
55574
|
+
}
|
|
55127
55575
|
} 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
55576
|
}
|
|
55134
|
-
} catch {
|
|
55135
55577
|
}
|
|
55136
55578
|
if (mergeBase) {
|
|
55137
55579
|
try {
|
|
@@ -55238,6 +55680,26 @@ async function resolveTrivialWhitespace(filePath, cwd) {
|
|
|
55238
55680
|
throw new Error(`Failed to auto-resolve ${filePath} trivial conflict: ${error}`);
|
|
55239
55681
|
}
|
|
55240
55682
|
}
|
|
55683
|
+
function buildTaskIdTrailerArg(taskId) {
|
|
55684
|
+
return ` -m "${FUSION_TASK_ID_TRAILER_KEY}: ${taskId}"`;
|
|
55685
|
+
}
|
|
55686
|
+
async function ensureTaskIdTrailerOnHead(rootDir, taskId) {
|
|
55687
|
+
try {
|
|
55688
|
+
const { stdout: existingMessage } = await execAsync2("git log -1 --pretty=%B", {
|
|
55689
|
+
cwd: rootDir,
|
|
55690
|
+
encoding: "utf-8"
|
|
55691
|
+
});
|
|
55692
|
+
const trailerLine = `${FUSION_TASK_ID_TRAILER_KEY}: ${taskId}`;
|
|
55693
|
+
if (existingMessage.includes(trailerLine)) return;
|
|
55694
|
+
await execAsync2(
|
|
55695
|
+
`git -c trailer.ifExists=addIfDifferent commit --amend --no-edit --trailer "${trailerLine}"`,
|
|
55696
|
+
{ cwd: rootDir }
|
|
55697
|
+
);
|
|
55698
|
+
} catch (err) {
|
|
55699
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55700
|
+
mergerLog.warn(`${taskId}: failed to add ${FUSION_TASK_ID_TRAILER_KEY} trailer to HEAD (${msg}) \u2014 relying on subject grep for recovery`);
|
|
55701
|
+
}
|
|
55702
|
+
}
|
|
55241
55703
|
function getCommitAuthorArg(settings) {
|
|
55242
55704
|
if (settings.commitAuthorEnabled === false) return "";
|
|
55243
55705
|
const name = settings.commitAuthorName || "Fusion";
|
|
@@ -55676,6 +56138,13 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55676
56138
|
const settings = await store.getSettings();
|
|
55677
56139
|
const includeTaskId = settings.includeTaskIdInCommit !== false;
|
|
55678
56140
|
const smartConflictResolution = (settings.smartConflictResolution ?? settings.autoResolveConflicts) !== false;
|
|
56141
|
+
const mergeConflictStrategy = normalizeMergeConflictStrategy(
|
|
56142
|
+
settings.mergeConflictStrategy
|
|
56143
|
+
);
|
|
56144
|
+
if (mergeConflictStrategy === "smart-prefer-main" || mergeConflictStrategy === "smart-prefer-branch") {
|
|
56145
|
+
await tryFastForwardFromOrigin(rootDir, taskId);
|
|
56146
|
+
}
|
|
56147
|
+
let mergeWasEmpty = false;
|
|
55679
56148
|
try {
|
|
55680
56149
|
execSync(`git rev-parse --verify "${branch}"`, {
|
|
55681
56150
|
cwd: rootDir,
|
|
@@ -55735,6 +56204,57 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55735
56204
|
mergerLog.warn(`${taskId}: unable to verify/checkout main branch \u2014 proceeding on current HEAD`);
|
|
55736
56205
|
}
|
|
55737
56206
|
}
|
|
56207
|
+
let rebaseHappened = false;
|
|
56208
|
+
let preferMainRebaseFailureMessage;
|
|
56209
|
+
if (settings.worktreeRebaseBeforeMerge === false && settings.worktreeRebaseLocalBase === false && mergeConflictStrategy === "smart-prefer-main") {
|
|
56210
|
+
throw new Error(
|
|
56211
|
+
`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".`
|
|
56212
|
+
);
|
|
56213
|
+
}
|
|
56214
|
+
async function runLocalBaseRebase(label) {
|
|
56215
|
+
if (!worktreePath) return;
|
|
56216
|
+
try {
|
|
56217
|
+
const { stdout: localHeadOut } = await execAsync2("git rev-parse HEAD", {
|
|
56218
|
+
cwd: rootDir,
|
|
56219
|
+
encoding: "utf-8"
|
|
56220
|
+
});
|
|
56221
|
+
const localHead = localHeadOut.trim();
|
|
56222
|
+
if (!localHead) return;
|
|
56223
|
+
let alreadyContains = false;
|
|
56224
|
+
try {
|
|
56225
|
+
await execAsync2(`git merge-base --is-ancestor "${localHead}" HEAD`, { cwd: worktreePath });
|
|
56226
|
+
alreadyContains = true;
|
|
56227
|
+
} catch {
|
|
56228
|
+
}
|
|
56229
|
+
if (alreadyContains) {
|
|
56230
|
+
rebaseHappened = true;
|
|
56231
|
+
return;
|
|
56232
|
+
}
|
|
56233
|
+
throwIfAborted(options.signal, taskId);
|
|
56234
|
+
await execAsync2(`git rebase "${localHead}"`, { cwd: worktreePath });
|
|
56235
|
+
rebaseHappened = true;
|
|
56236
|
+
mergerLog.log(`${taskId}: rebased ${branch} onto local HEAD ${localHead.slice(0, 8)}${label ? ` (${label})` : ""}`);
|
|
56237
|
+
await store.appendAgentLog(
|
|
56238
|
+
taskId,
|
|
56239
|
+
`Pre-merge rebase: ${branch} \u2192 local HEAD ${localHead.slice(0, 8)}${label ? ` (${label})` : ""}`,
|
|
56240
|
+
"text",
|
|
56241
|
+
void 0,
|
|
56242
|
+
"merger"
|
|
56243
|
+
);
|
|
56244
|
+
} catch (localRebaseErr) {
|
|
56245
|
+
rethrowIfMergeAborted(localRebaseErr);
|
|
56246
|
+
const lmsg = localRebaseErr instanceof Error ? localRebaseErr.message : String(localRebaseErr);
|
|
56247
|
+
mergerLog.warn(`${taskId}: pre-merge rebase onto local HEAD failed (${lmsg}) \u2014 aborting and falling through`);
|
|
56248
|
+
try {
|
|
56249
|
+
await execAsync2("git rebase --abort", { cwd: worktreePath });
|
|
56250
|
+
} catch (abortError) {
|
|
56251
|
+
mergerLog.warn(`${taskId}: failed to abort local-HEAD rebase: ${getCommandErrorMessage(abortError)}`);
|
|
56252
|
+
}
|
|
56253
|
+
if (mergeConflictStrategy === "smart-prefer-main" && !preferMainRebaseFailureMessage) {
|
|
56254
|
+
preferMainRebaseFailureMessage = `Pre-merge rebase onto local HEAD aborted (${lmsg})`;
|
|
56255
|
+
}
|
|
56256
|
+
}
|
|
56257
|
+
}
|
|
55738
56258
|
if (settings.worktreeRebaseBeforeMerge !== false) {
|
|
55739
56259
|
try {
|
|
55740
56260
|
let remote = settings.worktreeRebaseRemote?.trim();
|
|
@@ -55769,7 +56289,9 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55769
56289
|
}
|
|
55770
56290
|
}
|
|
55771
56291
|
if (!remote) {
|
|
55772
|
-
mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping
|
|
56292
|
+
mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping remote rebase stage (local-base stage may still run)`);
|
|
56293
|
+
} else if (!worktreePath) {
|
|
56294
|
+
mergerLog.warn(`${taskId}: no worktreePath \u2014 skipping remote rebase stage`);
|
|
55773
56295
|
} else {
|
|
55774
56296
|
throwIfAborted(options.signal, taskId);
|
|
55775
56297
|
mergerLog.log(`${taskId}: fetching ${remote} before merge`);
|
|
@@ -55781,17 +56303,21 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55781
56303
|
);
|
|
55782
56304
|
const mainBranch = mainBranchOut.trim();
|
|
55783
56305
|
const remoteRef = `${remote}/${mainBranch}`;
|
|
55784
|
-
|
|
55785
|
-
|
|
55786
|
-
|
|
55787
|
-
|
|
55788
|
-
|
|
55789
|
-
|
|
55790
|
-
|
|
56306
|
+
throwIfAborted(options.signal, taskId);
|
|
56307
|
+
await execAsync2(`git rebase "${remoteRef}"`, { cwd: worktreePath });
|
|
56308
|
+
rebaseHappened = true;
|
|
56309
|
+
mergerLog.log(`${taskId}: rebased ${branch} onto ${remoteRef}`);
|
|
56310
|
+
await store.appendAgentLog(
|
|
56311
|
+
taskId,
|
|
56312
|
+
`Pre-merge rebase: ${branch} \u2192 ${remoteRef}`,
|
|
56313
|
+
"text",
|
|
56314
|
+
void 0,
|
|
56315
|
+
"merger"
|
|
56316
|
+
);
|
|
55791
56317
|
} catch (rebaseErr) {
|
|
55792
56318
|
rethrowIfMergeAborted(rebaseErr);
|
|
55793
56319
|
const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
|
|
55794
|
-
mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through
|
|
56320
|
+
mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through`);
|
|
55795
56321
|
if (worktreePath) {
|
|
55796
56322
|
try {
|
|
55797
56323
|
await execAsync2("git rebase --abort", { cwd: worktreePath });
|
|
@@ -55799,14 +56325,32 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55799
56325
|
mergerLog.warn(`${taskId}: failed to abort pre-merge rebase: ${getCommandErrorMessage(abortError)}`);
|
|
55800
56326
|
}
|
|
55801
56327
|
}
|
|
56328
|
+
if (mergeConflictStrategy === "smart-prefer-main") {
|
|
56329
|
+
preferMainRebaseFailureMessage = `Pre-merge rebase onto remote main aborted (${msg})`;
|
|
56330
|
+
}
|
|
55802
56331
|
}
|
|
55803
56332
|
}
|
|
55804
56333
|
} catch (err) {
|
|
55805
56334
|
rethrowIfMergeAborted(err);
|
|
55806
56335
|
const msg = err instanceof Error ? err.message : String(err);
|
|
55807
|
-
mergerLog.warn(`${taskId}: pre-merge rebase pipeline failed (${msg}) \u2014 proceeding without rebase`);
|
|
56336
|
+
mergerLog.warn(`${taskId}: pre-merge remote rebase pipeline failed (${msg}) \u2014 proceeding without remote rebase`);
|
|
55808
56337
|
}
|
|
55809
56338
|
}
|
|
56339
|
+
if (settings.worktreeRebaseLocalBase !== false && !preferMainRebaseFailureMessage) {
|
|
56340
|
+
await runLocalBaseRebase(
|
|
56341
|
+
settings.worktreeRebaseBeforeMerge === false ? "remote rebase disabled" : ""
|
|
56342
|
+
);
|
|
56343
|
+
}
|
|
56344
|
+
if (preferMainRebaseFailureMessage) {
|
|
56345
|
+
throw new Error(
|
|
56346
|
+
`${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".`
|
|
56347
|
+
);
|
|
56348
|
+
}
|
|
56349
|
+
if (mergeConflictStrategy === "smart-prefer-main" && !rebaseHappened) {
|
|
56350
|
+
mergerLog.warn(
|
|
56351
|
+
`${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.`
|
|
56352
|
+
);
|
|
56353
|
+
}
|
|
55810
56354
|
const diffBaseRef = await resolveTaskDiffBaseRef({
|
|
55811
56355
|
cwd: rootDir,
|
|
55812
56356
|
headRef: branch,
|
|
@@ -55867,6 +56411,14 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55867
56411
|
}
|
|
55868
56412
|
const mergeAttempt = async (attemptNum) => {
|
|
55869
56413
|
mergerLog.log(`${taskId}: merge attempt ${attemptNum}/3...`);
|
|
56414
|
+
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`;
|
|
56415
|
+
await store.appendAgentLog(
|
|
56416
|
+
taskId,
|
|
56417
|
+
`Starting merge ${attemptLabel}`,
|
|
56418
|
+
"text",
|
|
56419
|
+
void 0,
|
|
56420
|
+
"merger"
|
|
56421
|
+
);
|
|
55870
56422
|
try {
|
|
55871
56423
|
const success = await executeMergeAttempt({
|
|
55872
56424
|
store,
|
|
@@ -55877,6 +56429,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55877
56429
|
diffStat,
|
|
55878
56430
|
includeTaskId,
|
|
55879
56431
|
smartConflictResolution,
|
|
56432
|
+
mergeConflictStrategy,
|
|
55880
56433
|
attemptNum,
|
|
55881
56434
|
options,
|
|
55882
56435
|
result,
|
|
@@ -55888,7 +56441,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
55888
56441
|
}, aiTracker);
|
|
55889
56442
|
if (success) {
|
|
55890
56443
|
result.attemptsMade = attemptNum;
|
|
55891
|
-
result.resolutionStrategy = getResolutionStrategy(attemptNum, smartConflictResolution);
|
|
56444
|
+
result.resolutionStrategy = getResolutionStrategy(attemptNum, smartConflictResolution, mergeConflictStrategy);
|
|
55892
56445
|
result.resolutionMethod = getResolutionMethod(result.resolutionStrategy, result.autoResolvedCount, aiTracker.aiWasInvoked);
|
|
55893
56446
|
result.merged = true;
|
|
55894
56447
|
return true;
|
|
@@ -56079,6 +56632,13 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56079
56632
|
}
|
|
56080
56633
|
throw error;
|
|
56081
56634
|
}
|
|
56635
|
+
if (error.name === "MergeNonConflictError") {
|
|
56636
|
+
try {
|
|
56637
|
+
execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
|
|
56638
|
+
} catch {
|
|
56639
|
+
}
|
|
56640
|
+
throw error;
|
|
56641
|
+
}
|
|
56082
56642
|
if (attemptNum < 3 && smartConflictResolution) {
|
|
56083
56643
|
mergerLog.log(`${taskId}: attempt ${attemptNum} error, cleaning up for retry...`);
|
|
56084
56644
|
try {
|
|
@@ -56096,12 +56656,15 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56096
56656
|
const aiTracker = { aiWasInvoked: false };
|
|
56097
56657
|
let merged = false;
|
|
56098
56658
|
merged = await mergeAttempt(1);
|
|
56099
|
-
if (!merged && smartConflictResolution) {
|
|
56659
|
+
if (!merged && smartConflictResolution && mergeConflictStrategy !== "abort") {
|
|
56100
56660
|
merged = await mergeAttempt(2);
|
|
56101
56661
|
}
|
|
56102
|
-
if (!merged && smartConflictResolution) {
|
|
56662
|
+
if (!merged && smartConflictResolution && mergeConflictStrategy !== "ai-only" && mergeConflictStrategy !== "abort") {
|
|
56103
56663
|
merged = await mergeAttempt(3);
|
|
56104
56664
|
}
|
|
56665
|
+
if (aiTracker.mergeWasEmpty) {
|
|
56666
|
+
mergeWasEmpty = true;
|
|
56667
|
+
}
|
|
56105
56668
|
if (!merged) {
|
|
56106
56669
|
try {
|
|
56107
56670
|
execSync("git reset --merge", { cwd: rootDir, stdio: "pipe" });
|
|
@@ -56109,6 +56672,10 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56109
56672
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
56110
56673
|
mergerLog.warn(`${taskId}: git reset --merge cleanup failed: ${errorMessage}`);
|
|
56111
56674
|
}
|
|
56675
|
+
if (mergeConflictStrategy === "abort") {
|
|
56676
|
+
result.resolutionStrategy = "abort";
|
|
56677
|
+
throw new Error(`Merge conflict for ${taskId}: aborted per mergeConflictStrategy="abort" \u2014 manual resolution required`);
|
|
56678
|
+
}
|
|
56112
56679
|
throw new Error(`AI merge failed for ${taskId}: all 3 attempts exhausted`);
|
|
56113
56680
|
}
|
|
56114
56681
|
try {
|
|
@@ -56135,17 +56702,24 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56135
56702
|
} catch {
|
|
56136
56703
|
}
|
|
56137
56704
|
const isEmptyCommit = filesChanged === 0;
|
|
56138
|
-
const recordedSha = isEmptyCommit ? void 0 : commitSha;
|
|
56705
|
+
const recordedSha = isEmptyCommit || mergeWasEmpty ? void 0 : commitSha;
|
|
56139
56706
|
if (isEmptyCommit) {
|
|
56140
56707
|
mergerLog.warn(
|
|
56141
56708
|
`${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
56709
|
);
|
|
56710
|
+
} else if (mergeWasEmpty) {
|
|
56711
|
+
mergerLog.warn(
|
|
56712
|
+
`${taskId}: merge succeeded without committing (branch already on main). Skipping commitSha; nothing new landed locally.`
|
|
56713
|
+
);
|
|
56143
56714
|
}
|
|
56715
|
+
const recordedFilesChanged = mergeWasEmpty ? 0 : filesChanged;
|
|
56716
|
+
const recordedInsertions = mergeWasEmpty ? 0 : insertions;
|
|
56717
|
+
const recordedDeletions = mergeWasEmpty ? 0 : deletions;
|
|
56144
56718
|
const mergeDetails = {
|
|
56145
56719
|
commitSha: recordedSha,
|
|
56146
|
-
filesChanged,
|
|
56147
|
-
insertions,
|
|
56148
|
-
deletions,
|
|
56720
|
+
filesChanged: recordedFilesChanged,
|
|
56721
|
+
insertions: recordedInsertions,
|
|
56722
|
+
deletions: recordedDeletions,
|
|
56149
56723
|
mergeCommitMessage: commitLog,
|
|
56150
56724
|
mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56151
56725
|
mergeConfirmed: true,
|
|
@@ -56156,6 +56730,26 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56156
56730
|
};
|
|
56157
56731
|
await store.updateTask(taskId, { mergeDetails });
|
|
56158
56732
|
mergerLog.log(`${taskId}: merge details stored (commitSha: ${recordedSha?.slice(0, 8) ?? "<deferred>"})`);
|
|
56733
|
+
const summaryParts = [
|
|
56734
|
+
`Merge completed via ${result.resolutionStrategy ?? "unknown"} (attempt ${result.attemptsMade ?? "?"}/3)`
|
|
56735
|
+
];
|
|
56736
|
+
if (recordedSha) {
|
|
56737
|
+
summaryParts.push(`commit ${recordedSha.slice(0, 8)}`);
|
|
56738
|
+
} else if (mergeWasEmpty) {
|
|
56739
|
+
summaryParts.push("no commit landed (branch already on main)");
|
|
56740
|
+
} else if (isEmptyCommit) {
|
|
56741
|
+
summaryParts.push("squash collapsed to empty (sha deferred)");
|
|
56742
|
+
}
|
|
56743
|
+
if (!mergeWasEmpty && filesChanged !== void 0) {
|
|
56744
|
+
summaryParts.push(`${filesChanged} file${filesChanged === 1 ? "" : "s"} changed (+${insertions ?? 0}/-${deletions ?? 0})`);
|
|
56745
|
+
}
|
|
56746
|
+
await store.appendAgentLog(
|
|
56747
|
+
taskId,
|
|
56748
|
+
summaryParts.join(" \xB7 "),
|
|
56749
|
+
"text",
|
|
56750
|
+
void 0,
|
|
56751
|
+
"merger"
|
|
56752
|
+
);
|
|
56159
56753
|
} catch (err) {
|
|
56160
56754
|
mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
|
|
56161
56755
|
}
|
|
@@ -56250,6 +56844,27 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56250
56844
|
});
|
|
56251
56845
|
if (pushResult.pushed) {
|
|
56252
56846
|
mergerLog.log(`${taskId}: pushed merged result to remote`);
|
|
56847
|
+
try {
|
|
56848
|
+
const postPushSha = execSync("git rev-parse HEAD", {
|
|
56849
|
+
cwd: rootDir,
|
|
56850
|
+
stdio: "pipe",
|
|
56851
|
+
encoding: "utf-8"
|
|
56852
|
+
}).trim() || void 0;
|
|
56853
|
+
if (postPushSha) {
|
|
56854
|
+
const existingTask = await store.getTask(taskId).catch(() => null);
|
|
56855
|
+
const existingDetails = existingTask?.mergeDetails;
|
|
56856
|
+
if (existingDetails?.commitSha && existingDetails.commitSha !== postPushSha) {
|
|
56857
|
+
await store.updateTask(taskId, {
|
|
56858
|
+
mergeDetails: { ...existingDetails, commitSha: postPushSha }
|
|
56859
|
+
});
|
|
56860
|
+
mergerLog.log(
|
|
56861
|
+
`${taskId}: post-push HEAD changed from ${existingDetails.commitSha.slice(0, 8)} to ${postPushSha.slice(0, 8)} \u2014 refreshed mergeDetails.commitSha`
|
|
56862
|
+
);
|
|
56863
|
+
}
|
|
56864
|
+
}
|
|
56865
|
+
} catch (refreshErr) {
|
|
56866
|
+
mergerLog.warn(`${taskId}: failed to refresh mergeDetails after push: ${refreshErr.message}`);
|
|
56867
|
+
}
|
|
56253
56868
|
} else {
|
|
56254
56869
|
mergerLog.warn(`${taskId}: push to remote failed: ${pushResult.error}`);
|
|
56255
56870
|
}
|
|
@@ -56277,18 +56892,74 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
56277
56892
|
await completeTask(store, taskId, result);
|
|
56278
56893
|
return result;
|
|
56279
56894
|
}
|
|
56280
|
-
function
|
|
56895
|
+
async function tryFastForwardFromOrigin(rootDir, taskId) {
|
|
56896
|
+
let currentBranch;
|
|
56897
|
+
try {
|
|
56898
|
+
currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
56899
|
+
cwd: rootDir,
|
|
56900
|
+
encoding: "utf-8",
|
|
56901
|
+
stdio: "pipe"
|
|
56902
|
+
}).trim();
|
|
56903
|
+
} catch {
|
|
56904
|
+
return;
|
|
56905
|
+
}
|
|
56906
|
+
if (!currentBranch || currentBranch === "HEAD") return;
|
|
56907
|
+
try {
|
|
56908
|
+
await execAsync2(`git fetch origin "${currentBranch}"`, { cwd: rootDir });
|
|
56909
|
+
} catch (err) {
|
|
56910
|
+
mergerLog.log(`${taskId}: pre-merge fetch failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
56911
|
+
return;
|
|
56912
|
+
}
|
|
56913
|
+
let behind = 0;
|
|
56914
|
+
let ahead = 0;
|
|
56915
|
+
try {
|
|
56916
|
+
const counts = execSync(`git rev-list --left-right --count "origin/${currentBranch}...HEAD"`, {
|
|
56917
|
+
cwd: rootDir,
|
|
56918
|
+
encoding: "utf-8",
|
|
56919
|
+
stdio: "pipe"
|
|
56920
|
+
}).trim();
|
|
56921
|
+
const [b, a] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
56922
|
+
behind = b;
|
|
56923
|
+
ahead = a;
|
|
56924
|
+
} catch {
|
|
56925
|
+
return;
|
|
56926
|
+
}
|
|
56927
|
+
if (behind === 0) return;
|
|
56928
|
+
if (ahead > 0) {
|
|
56929
|
+
mergerLog.log(`${taskId}: local ${currentBranch} has ${ahead} unpushed commit(s); skipping fast-forward`);
|
|
56930
|
+
return;
|
|
56931
|
+
}
|
|
56932
|
+
try {
|
|
56933
|
+
await execAsync2(`git merge --ff-only "origin/${currentBranch}"`, { cwd: rootDir });
|
|
56934
|
+
mergerLog.log(`${taskId}: fast-forwarded ${currentBranch} by ${behind} commit(s) from origin`);
|
|
56935
|
+
} catch (err) {
|
|
56936
|
+
mergerLog.log(`${taskId}: fast-forward failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
56937
|
+
}
|
|
56938
|
+
}
|
|
56939
|
+
function getResolutionStrategy(attemptNum, smartConflictResolution, mergeConflictStrategy = "smart-prefer-main") {
|
|
56281
56940
|
if (!smartConflictResolution || attemptNum === 1) {
|
|
56282
56941
|
return "ai";
|
|
56283
56942
|
}
|
|
56284
56943
|
if (attemptNum === 2) {
|
|
56285
56944
|
return "auto-resolve";
|
|
56286
56945
|
}
|
|
56287
|
-
|
|
56946
|
+
switch (mergeConflictStrategy) {
|
|
56947
|
+
case "ai-only":
|
|
56948
|
+
return "ai";
|
|
56949
|
+
case "smart-prefer-main":
|
|
56950
|
+
return "ours";
|
|
56951
|
+
case "abort":
|
|
56952
|
+
return "abort";
|
|
56953
|
+
case "smart-prefer-branch":
|
|
56954
|
+
default:
|
|
56955
|
+
return "theirs";
|
|
56956
|
+
}
|
|
56288
56957
|
}
|
|
56289
56958
|
function getResolutionMethod(strategy, autoResolvedCount, aiWasUsed) {
|
|
56290
56959
|
if (strategy === "ai") return "ai";
|
|
56291
56960
|
if (strategy === "theirs") return "theirs";
|
|
56961
|
+
if (strategy === "ours") return "ours";
|
|
56962
|
+
if (strategy === "abort") return "abort";
|
|
56292
56963
|
if (strategy === "auto-resolve") {
|
|
56293
56964
|
if (autoResolvedCount && autoResolvedCount > 0) {
|
|
56294
56965
|
return aiWasUsed ? "mixed" : "auto";
|
|
@@ -56317,12 +56988,15 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56317
56988
|
buildSource
|
|
56318
56989
|
} = params;
|
|
56319
56990
|
if (attemptNum === 3) {
|
|
56320
|
-
|
|
56991
|
+
if (params.mergeConflictStrategy === "smart-prefer-main") {
|
|
56992
|
+
return attemptWithSideStrategy(params, "ours", aiTracker);
|
|
56993
|
+
}
|
|
56994
|
+
return attemptWithSideStrategy(params, "theirs", aiTracker);
|
|
56321
56995
|
}
|
|
56322
56996
|
let hasConflicts = false;
|
|
56323
56997
|
try {
|
|
56324
56998
|
if (attemptNum === 2 && smartConflictResolution) {
|
|
56325
|
-
let
|
|
56999
|
+
let mergeError;
|
|
56326
57000
|
try {
|
|
56327
57001
|
await execAsync2(`git merge --squash "${branch}"`, {
|
|
56328
57002
|
cwd: rootDir
|
|
@@ -56330,9 +57004,18 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56330
57004
|
throwIfAborted(options.signal, taskId);
|
|
56331
57005
|
} catch (error) {
|
|
56332
57006
|
rethrowIfMergeAborted(error);
|
|
56333
|
-
|
|
57007
|
+
mergeError = error;
|
|
56334
57008
|
}
|
|
56335
57009
|
const conflictedFiles = await getConflictedFiles(rootDir);
|
|
57010
|
+
if (mergeError && conflictedFiles.length === 0) {
|
|
57011
|
+
const cause = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
|
57012
|
+
const fatal = new Error(
|
|
57013
|
+
`${taskId}: git merge --squash failed without producing conflicts (${cause}) \u2014 refusing to treat as a no-op merge.`
|
|
57014
|
+
);
|
|
57015
|
+
fatal.name = "MergeNonConflictError";
|
|
57016
|
+
throw fatal;
|
|
57017
|
+
}
|
|
57018
|
+
const mergeExitedWithConflicts = mergeError !== void 0;
|
|
56336
57019
|
if (conflictedFiles.length > 0 || mergeExitedWithConflicts) {
|
|
56337
57020
|
const classified = [];
|
|
56338
57021
|
for (const file of conflictedFiles) {
|
|
@@ -56375,11 +57058,14 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56375
57058
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
|
56376
57059
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56377
57060
|
const authorArg = getCommitAuthorArg(settings);
|
|
57061
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
56378
57062
|
await execAsync2(
|
|
56379
|
-
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${authorArg}`,
|
|
57063
|
+
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${trailerArg}${authorArg}`,
|
|
56380
57064
|
{ cwd: rootDir }
|
|
56381
57065
|
);
|
|
56382
57066
|
mergerLog.log(`${taskId}: committed after auto-resolving all conflicts`);
|
|
57067
|
+
} else {
|
|
57068
|
+
aiTracker.mergeWasEmpty = true;
|
|
56383
57069
|
}
|
|
56384
57070
|
if (testCommand || buildCommand2) {
|
|
56385
57071
|
throwIfAborted(options.signal, taskId);
|
|
@@ -56404,6 +57090,7 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56404
57090
|
).trim() === "0";
|
|
56405
57091
|
if (squashIsEmpty) {
|
|
56406
57092
|
mergerLog.log(`${taskId}: squash merge staged nothing \u2014 already merged`);
|
|
57093
|
+
aiTracker.mergeWasEmpty = true;
|
|
56407
57094
|
if (testCommand || buildCommand2) {
|
|
56408
57095
|
throwIfAborted(options.signal, taskId);
|
|
56409
57096
|
await runDeterministicVerification(
|
|
@@ -56431,6 +57118,7 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56431
57118
|
).trim() === "0";
|
|
56432
57119
|
if (squashIsEmpty) {
|
|
56433
57120
|
mergerLog.log(`${taskId}: squash merge staged nothing \u2014 already merged`);
|
|
57121
|
+
aiTracker.mergeWasEmpty = true;
|
|
56434
57122
|
if (testCommand || buildCommand2) {
|
|
56435
57123
|
throwIfAborted(options.signal, taskId);
|
|
56436
57124
|
await runDeterministicVerification(
|
|
@@ -56528,12 +57216,12 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
56528
57216
|
throw error;
|
|
56529
57217
|
}
|
|
56530
57218
|
}
|
|
56531
|
-
async function
|
|
57219
|
+
async function attemptWithSideStrategy(params, side = "theirs", aiTracker) {
|
|
56532
57220
|
const { rootDir, branch, commitLog, includeTaskId, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
|
|
56533
|
-
mergerLog.log(`${taskId}: attempting merge with -X
|
|
57221
|
+
mergerLog.log(`${taskId}: attempting merge with -X ${side} strategy`);
|
|
56534
57222
|
try {
|
|
56535
57223
|
throwIfAborted(params.options.signal, taskId);
|
|
56536
|
-
await execAsync2(`git merge -X
|
|
57224
|
+
await execAsync2(`git merge -X ${side} --squash "${branch}"`, {
|
|
56537
57225
|
cwd: rootDir
|
|
56538
57226
|
});
|
|
56539
57227
|
const conflictedOutput = execSync("git diff --name-only --diff-filter=U", {
|
|
@@ -56541,7 +57229,7 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56541
57229
|
encoding: "utf-8"
|
|
56542
57230
|
}).trim();
|
|
56543
57231
|
if (conflictedOutput.length > 0) {
|
|
56544
|
-
mergerLog.warn(`${taskId}: -X
|
|
57232
|
+
mergerLog.warn(`${taskId}: -X ${side} left unresolved conflicts: ${conflictedOutput}`);
|
|
56545
57233
|
return false;
|
|
56546
57234
|
}
|
|
56547
57235
|
const staged = execSync("git diff --cached --quiet 2>&1; echo $?", {
|
|
@@ -56549,6 +57237,7 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56549
57237
|
encoding: "utf-8"
|
|
56550
57238
|
}).trim();
|
|
56551
57239
|
if (staged === "0") {
|
|
57240
|
+
if (aiTracker) aiTracker.mergeWasEmpty = true;
|
|
56552
57241
|
if (testCommand || buildCommand2) {
|
|
56553
57242
|
throwIfAborted(params.options.signal, taskId);
|
|
56554
57243
|
await runDeterministicVerification(
|
|
@@ -56568,11 +57257,12 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56568
57257
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
|
56569
57258
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56570
57259
|
const authorArg = getCommitAuthorArg(settings);
|
|
57260
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
56571
57261
|
await execAsync2(
|
|
56572
|
-
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${authorArg}`,
|
|
57262
|
+
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${trailerArg}${authorArg}`,
|
|
56573
57263
|
{ cwd: rootDir }
|
|
56574
57264
|
);
|
|
56575
|
-
mergerLog.log(`${taskId}: committed with -X
|
|
57265
|
+
mergerLog.log(`${taskId}: committed with -X ${side} auto-resolution`);
|
|
56576
57266
|
if (testCommand || buildCommand2) {
|
|
56577
57267
|
throwIfAborted(params.options.signal, taskId);
|
|
56578
57268
|
await runDeterministicVerification(
|
|
@@ -56591,7 +57281,7 @@ async function attemptWithTheirsStrategy(params) {
|
|
|
56591
57281
|
if (error instanceof Error && error.name === "MergeAbortedError") {
|
|
56592
57282
|
throw error;
|
|
56593
57283
|
}
|
|
56594
|
-
mergerLog.error(`${taskId}: -X
|
|
57284
|
+
mergerLog.error(`${taskId}: -X ${side} merge failed: ${error}`);
|
|
56595
57285
|
return false;
|
|
56596
57286
|
}
|
|
56597
57287
|
}
|
|
@@ -56777,13 +57467,16 @@ async function runAiAgentForCommit(params) {
|
|
|
56777
57467
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
|
56778
57468
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56779
57469
|
const authorArg2 = getCommitAuthorArg(settings);
|
|
57470
|
+
const trailerArg = buildTaskIdTrailerArg(taskId);
|
|
56780
57471
|
await execAsync2(
|
|
56781
|
-
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${authorArg2}`,
|
|
57472
|
+
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${trailerArg}${authorArg2}`,
|
|
56782
57473
|
{ cwd: rootDir }
|
|
56783
57474
|
);
|
|
56784
57475
|
} else {
|
|
56785
57476
|
throw new Error(`Agent did not commit and did not report build failure for ${taskId}`);
|
|
56786
57477
|
}
|
|
57478
|
+
} else {
|
|
57479
|
+
await ensureTaskIdTrailerOnHead(rootDir, taskId);
|
|
56787
57480
|
}
|
|
56788
57481
|
return { success: true };
|
|
56789
57482
|
} catch (err) {
|
|
@@ -57011,8 +57704,9 @@ If issues are found that need attention, describe them clearly.`;
|
|
|
57011
57704
|
agent: "merger"
|
|
57012
57705
|
});
|
|
57013
57706
|
try {
|
|
57014
|
-
const
|
|
57015
|
-
const
|
|
57707
|
+
const defaultModel = resolveProjectDefaultModel(settings);
|
|
57708
|
+
const stepProvider = workflowStep.modelProvider || defaultModel.provider;
|
|
57709
|
+
const stepModelId = workflowStep.modelId || defaultModel.modelId;
|
|
57016
57710
|
const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId);
|
|
57017
57711
|
let postMergeInstructions = "";
|
|
57018
57712
|
if (mergeOptions.agentStore) {
|
|
@@ -57095,12 +57789,11 @@ async function completeTask(store, taskId, result) {
|
|
|
57095
57789
|
result.task = task;
|
|
57096
57790
|
store.emit("task:merged", result);
|
|
57097
57791
|
}
|
|
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;
|
|
57792
|
+
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
57793
|
var init_merger = __esm({
|
|
57100
57794
|
"../engine/src/merger.ts"() {
|
|
57101
57795
|
"use strict";
|
|
57102
57796
|
init_src();
|
|
57103
|
-
init_src();
|
|
57104
57797
|
init_pi();
|
|
57105
57798
|
init_session_token_usage();
|
|
57106
57799
|
init_agent_session_helpers();
|
|
@@ -57170,6 +57863,7 @@ var init_merger = __esm({
|
|
|
57170
57863
|
this.name = "MergeAbortedError";
|
|
57171
57864
|
}
|
|
57172
57865
|
};
|
|
57866
|
+
FUSION_TASK_ID_TRAILER_KEY = "Fusion-Task-Id";
|
|
57173
57867
|
}
|
|
57174
57868
|
});
|
|
57175
57869
|
|
|
@@ -59138,7 +59832,10 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59138
59832
|
executorLog.log(`[event:task:moved] ${task.id}: ${from} \u2192 ${to}`);
|
|
59139
59833
|
if (to === "in-progress") {
|
|
59140
59834
|
executorLog.log(`[event:task:moved] Initiating execute() for ${task.id}`);
|
|
59141
|
-
|
|
59835
|
+
void (async () => {
|
|
59836
|
+
const taskForExecution = await this.resetMergeStateIfNeeded(task, from);
|
|
59837
|
+
await this.execute(taskForExecution);
|
|
59838
|
+
})().catch(
|
|
59142
59839
|
(err) => executorLog.error(`Failed to start ${task.id}:`, err)
|
|
59143
59840
|
);
|
|
59144
59841
|
} else if (from === "in-progress") {
|
|
@@ -59416,6 +60113,59 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59416
60113
|
if (task.steps.length === 0) return false;
|
|
59417
60114
|
return task.steps.every((s) => s.status === "done" || s.status === "skipped");
|
|
59418
60115
|
}
|
|
60116
|
+
async resetMergeStateIfNeeded(task, from) {
|
|
60117
|
+
if (from !== "in-review" && from !== "done") {
|
|
60118
|
+
return task;
|
|
60119
|
+
}
|
|
60120
|
+
const hasMergeEvidence = Boolean(task.mergeDetails) || (task.mergeRetries ?? 0) > 0 || (task.verificationFailureCount ?? 0) > 0 || task.status === "merging" || task.status === "merging-pr";
|
|
60121
|
+
if (!hasMergeEvidence) {
|
|
60122
|
+
return task;
|
|
60123
|
+
}
|
|
60124
|
+
return this.cleanupMergeStateForReverification(
|
|
60125
|
+
task,
|
|
60126
|
+
`Task returned to in-progress from ${from} column \u2014 resetting verification steps and merge state for re-verification`
|
|
60127
|
+
);
|
|
60128
|
+
}
|
|
60129
|
+
async cleanupMergeStateForReverification(task, logMessage) {
|
|
60130
|
+
await this.store.updateTask(task.id, {
|
|
60131
|
+
mergeDetails: null,
|
|
60132
|
+
mergeRetries: 0,
|
|
60133
|
+
verificationFailureCount: 0,
|
|
60134
|
+
workflowStepResults: []
|
|
60135
|
+
});
|
|
60136
|
+
const refreshedTask = await this.store.getTask(task.id);
|
|
60137
|
+
const steps = refreshedTask.steps ?? [];
|
|
60138
|
+
if (steps.length > 0) {
|
|
60139
|
+
const allStepsComplete = this.isTaskWorkComplete(refreshedTask);
|
|
60140
|
+
if (allStepsComplete) {
|
|
60141
|
+
await this.reopenLastStepForRevision(task.id, refreshedTask);
|
|
60142
|
+
} else {
|
|
60143
|
+
const resetIndexes = /* @__PURE__ */ new Set();
|
|
60144
|
+
for (let i = 0; i < steps.length; i++) {
|
|
60145
|
+
const name = steps[i].name.toLowerCase();
|
|
60146
|
+
if (/testing|verification/.test(name) || /documentation|delivery/.test(name)) {
|
|
60147
|
+
resetIndexes.add(i);
|
|
60148
|
+
}
|
|
60149
|
+
}
|
|
60150
|
+
if (resetIndexes.size === 0) {
|
|
60151
|
+
const reopened = await this.reopenLastStepForRevision(task.id, refreshedTask);
|
|
60152
|
+
if (reopened) {
|
|
60153
|
+
resetIndexes.add(reopened.index);
|
|
60154
|
+
}
|
|
60155
|
+
} else {
|
|
60156
|
+
for (const index of resetIndexes) {
|
|
60157
|
+
if (steps[index].status !== "pending") {
|
|
60158
|
+
await this.store.updateStep(task.id, index, "pending");
|
|
60159
|
+
}
|
|
60160
|
+
}
|
|
60161
|
+
const earliestIndex = Math.min(...Array.from(resetIndexes));
|
|
60162
|
+
await this.store.updateTask(task.id, { currentStep: earliestIndex });
|
|
60163
|
+
}
|
|
60164
|
+
}
|
|
60165
|
+
}
|
|
60166
|
+
await this.store.logEntry(task.id, logMessage, void 0, this.currentRunContext);
|
|
60167
|
+
return this.store.getTask(task.id);
|
|
60168
|
+
}
|
|
59419
60169
|
isNoProgressNoTaskDoneFailure(task) {
|
|
59420
60170
|
return task.status === "failed" && task.error?.includes("without calling fn_task_done") === true && task.steps.every((step) => step.status === "pending");
|
|
59421
60171
|
}
|
|
@@ -59637,7 +60387,7 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59637
60387
|
if (inProgress.length === 0) return;
|
|
59638
60388
|
executorLog.log(`Found ${inProgress.length} orphaned in-progress task(s)`);
|
|
59639
60389
|
for (const task of inProgress) {
|
|
59640
|
-
if (this.isTaskWorkComplete(task)) {
|
|
60390
|
+
if (this.isTaskWorkComplete(task) && !task.mergeDetails) {
|
|
59641
60391
|
if (this.recoveringCompleted.has(task.id)) {
|
|
59642
60392
|
executorLog.log(`${task.id} completed-task recovery already running - skipping duplicate startup recovery`);
|
|
59643
60393
|
continue;
|
|
@@ -59772,6 +60522,13 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
59772
60522
|
return;
|
|
59773
60523
|
}
|
|
59774
60524
|
}
|
|
60525
|
+
if (task.column === "in-progress" && task.mergeDetails) {
|
|
60526
|
+
executorLog.warn(`${task.id}: stale mergeDetails found while executing in-progress task \u2014 resetting merge state before continuing`);
|
|
60527
|
+
task = await this.cleanupMergeStateForReverification(
|
|
60528
|
+
task,
|
|
60529
|
+
"Executor detected stale merge state while task was in-progress \u2014 reset verification steps and merge metadata before resuming"
|
|
60530
|
+
);
|
|
60531
|
+
}
|
|
59775
60532
|
if (task.column === "in-progress" && !task.worktree) {
|
|
59776
60533
|
executorLog.error(
|
|
59777
60534
|
`${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 +62533,19 @@ and show an appropriate message to the user.\`
|
|
|
61776
62533
|
this.options.onAgentTool?.(taskId, toolName);
|
|
61777
62534
|
}
|
|
61778
62535
|
});
|
|
61779
|
-
|
|
61780
|
-
|
|
61781
|
-
|
|
61782
|
-
|
|
62536
|
+
const defaultModel = resolveProjectDefaultModel(settings);
|
|
62537
|
+
const primaryProvider = workflowStep.modelProvider || defaultModel.provider;
|
|
62538
|
+
const primaryModelId = workflowStep.modelId || defaultModel.modelId;
|
|
62539
|
+
const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId);
|
|
62540
|
+
const fallbackCandidates = [
|
|
62541
|
+
{ provider: settings.validatorFallbackProvider, modelId: settings.validatorFallbackModelId, label: "validatorFallback" },
|
|
62542
|
+
{ provider: settings.fallbackProvider, modelId: settings.fallbackModelId, label: "globalFallback" }
|
|
62543
|
+
];
|
|
62544
|
+
const fallback = fallbackCandidates.find(
|
|
62545
|
+
(c) => c.provider && c.modelId && (c.provider !== primaryProvider || c.modelId !== primaryModelId)
|
|
62546
|
+
);
|
|
62547
|
+
const timeoutMs = Math.max(6e4, settings.workflowStepTimeoutMs ?? 36e4);
|
|
62548
|
+
const runOnce = async (provider, modelId, attemptLabel) => {
|
|
61783
62549
|
const stepInstructions = await this.resolveInstructionsForRole("executor");
|
|
61784
62550
|
const stepSystemPrompt = buildSystemPromptWithInstructions(systemPrompt, stepInstructions);
|
|
61785
62551
|
const skillContext = await buildSessionSkillContext({
|
|
@@ -61797,16 +62563,19 @@ and show an appropriate message to the user.\`
|
|
|
61797
62563
|
cwd: worktreePath,
|
|
61798
62564
|
systemPrompt: stepSystemPrompt,
|
|
61799
62565
|
tools: toolMode,
|
|
61800
|
-
defaultProvider:
|
|
61801
|
-
defaultModelId:
|
|
62566
|
+
defaultProvider: provider,
|
|
62567
|
+
defaultModelId: modelId,
|
|
61802
62568
|
fallbackProvider: settings.fallbackProvider,
|
|
61803
62569
|
fallbackModelId: settings.fallbackModelId,
|
|
61804
62570
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
|
61805
62571
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
|
61806
62572
|
...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
|
|
61807
62573
|
});
|
|
61808
|
-
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride ? " (workflow step override)" : ""}`);
|
|
61809
|
-
await this.store.logEntry(
|
|
62574
|
+
executorLog.log(`${task.id}: workflow step '${workflowStep.name}' using model ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`);
|
|
62575
|
+
await this.store.logEntry(
|
|
62576
|
+
task.id,
|
|
62577
|
+
`Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`
|
|
62578
|
+
);
|
|
61810
62579
|
let output = "";
|
|
61811
62580
|
session.subscribe((event) => {
|
|
61812
62581
|
if (event.type === "message_update") {
|
|
@@ -61825,33 +62594,75 @@ and show an appropriate message to the user.\`
|
|
|
61825
62594
|
agentLogger.onToolEnd(event.toolName, event.isError, event.result);
|
|
61826
62595
|
}
|
|
61827
62596
|
});
|
|
61828
|
-
|
|
61829
|
-
|
|
61830
|
-
|
|
62597
|
+
let timedOut = false;
|
|
62598
|
+
let timeoutHandle;
|
|
62599
|
+
const timeoutPromise = new Promise((resolveTimeout) => {
|
|
62600
|
+
timeoutHandle = setTimeout(() => {
|
|
62601
|
+
timedOut = true;
|
|
62602
|
+
resolveTimeout("timeout");
|
|
62603
|
+
}, timeoutMs);
|
|
62604
|
+
});
|
|
62605
|
+
try {
|
|
62606
|
+
const promptPromise = promptWithFallback(
|
|
62607
|
+
session,
|
|
62608
|
+
`Execute the workflow step "${workflowStep.name}" for task ${task.id}.
|
|
61831
62609
|
|
|
61832
62610
|
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
|
-
|
|
62611
|
+
);
|
|
62612
|
+
const outcome = await Promise.race([
|
|
62613
|
+
promptPromise.then(() => "completed"),
|
|
62614
|
+
timeoutPromise
|
|
62615
|
+
]);
|
|
62616
|
+
if (outcome === "timeout") {
|
|
62617
|
+
executorLog.warn(`${task.id}: workflow step '${workflowStep.name}' (${attemptLabel}) timed out after ${timeoutMs}ms \u2014 disposing session`);
|
|
62618
|
+
await this.store.logEntry(
|
|
62619
|
+
task.id,
|
|
62620
|
+
`Workflow step '${workflowStep.name}' ${attemptLabel === "primary" ? "primary" : "fallback"} model timed out after ${Math.round(timeoutMs / 1e3)}s \u2014 aborting session`
|
|
62621
|
+
);
|
|
62622
|
+
try {
|
|
62623
|
+
session.dispose();
|
|
62624
|
+
} catch {
|
|
62625
|
+
}
|
|
62626
|
+
await agentLogger.flush();
|
|
62627
|
+
return { success: false, error: `workflow step timed out after ${timeoutMs}ms`, timedOut: true };
|
|
62628
|
+
}
|
|
62629
|
+
checkSessionError(session);
|
|
62630
|
+
await accumulateSessionTokenUsage(this.store, task.id, session);
|
|
62631
|
+
session.dispose();
|
|
62632
|
+
await agentLogger.flush();
|
|
62633
|
+
const trimmedOutput = output.trim();
|
|
62634
|
+
const revisionMatch = trimmedOutput.match(/^REQUEST REVISION\s*\n*/i);
|
|
62635
|
+
if (revisionMatch) {
|
|
62636
|
+
const feedbackStart = revisionMatch[0].length;
|
|
62637
|
+
const feedback = trimmedOutput.slice(feedbackStart).trim();
|
|
62638
|
+
return { success: false, revisionRequested: true, output: feedback };
|
|
62639
|
+
}
|
|
62640
|
+
return { success: true, output };
|
|
62641
|
+
} catch (err) {
|
|
62642
|
+
await agentLogger.flush();
|
|
62643
|
+
try {
|
|
62644
|
+
session.dispose();
|
|
62645
|
+
} catch {
|
|
62646
|
+
}
|
|
62647
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
62648
|
+
return { success: false, error: errorMessage };
|
|
62649
|
+
} finally {
|
|
62650
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
62651
|
+
void timedOut;
|
|
61848
62652
|
}
|
|
61849
|
-
|
|
61850
|
-
|
|
61851
|
-
|
|
61852
|
-
|
|
61853
|
-
|
|
62653
|
+
};
|
|
62654
|
+
const primaryOutcome = await runOnce(primaryProvider, primaryModelId, "primary");
|
|
62655
|
+
if (!primaryOutcome.timedOut) return primaryOutcome;
|
|
62656
|
+
if (!fallback) {
|
|
62657
|
+
executorLog.warn(`${task.id}: workflow step '${workflowStep.name}' timed out and no fallback model is configured`);
|
|
62658
|
+
await this.store.logEntry(
|
|
62659
|
+
task.id,
|
|
62660
|
+
`Workflow step '${workflowStep.name}' timed out \u2014 no fallback model configured (set settings.validatorFallbackProvider/Id or fallbackProvider/Id)`
|
|
62661
|
+
);
|
|
62662
|
+
return primaryOutcome;
|
|
61854
62663
|
}
|
|
62664
|
+
executorLog.log(`${task.id}: retrying workflow step '${workflowStep.name}' with fallback ${fallback.provider}/${fallback.modelId} (label=${fallback.label})`);
|
|
62665
|
+
return runOnce(fallback.provider, fallback.modelId, "fallback");
|
|
61855
62666
|
}
|
|
61856
62667
|
MAX_WORKTREE_RETRIES = 3;
|
|
61857
62668
|
WORKTREE_RETRY_DELAYS = [100, 500, 1e3];
|
|
@@ -61879,7 +62690,13 @@ Review the work done in this worktree and evaluate it against the criteria in yo
|
|
|
61879
62690
|
}
|
|
61880
62691
|
for (let attempt = 0; attempt < this.MAX_WORKTREE_RETRIES; attempt++) {
|
|
61881
62692
|
try {
|
|
61882
|
-
|
|
62693
|
+
const result = await this.tryCreateWorktree(branch, currentPath, taskId, resolvedStartPoint, attempt);
|
|
62694
|
+
await this.rebaseNewWorktreeOntoRemote(result.path, result.branch, taskId).catch((err) => {
|
|
62695
|
+
executorLog.warn(
|
|
62696
|
+
`Post-create worktree rebase failed for ${taskId} (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
62697
|
+
);
|
|
62698
|
+
});
|
|
62699
|
+
return result;
|
|
61883
62700
|
} catch (error) {
|
|
61884
62701
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
61885
62702
|
const isLastAttempt = attempt === this.MAX_WORKTREE_RETRIES - 1;
|
|
@@ -61900,6 +62717,84 @@ Review the work done in this worktree and evaluate it against the criteria in yo
|
|
|
61900
62717
|
}
|
|
61901
62718
|
throw new Error("Unexpected exit from worktree creation retry loop");
|
|
61902
62719
|
}
|
|
62720
|
+
quoteShellArg(value) {
|
|
62721
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
62722
|
+
}
|
|
62723
|
+
/**
|
|
62724
|
+
* After creating a fresh task worktree, fetch the configured remote and
|
|
62725
|
+
* rebase the task branch onto `<remote>/<defaultBranch>`. The result is a
|
|
62726
|
+
* branch that contains origin's tip plus any local main commits, so the
|
|
62727
|
+
* eventual merge has fewer surprises and the executor sees the freshest
|
|
62728
|
+
* code its peers/CI may have published.
|
|
62729
|
+
*
|
|
62730
|
+
* No-op when `worktreeRebaseBeforeMerge` is disabled, no remote is
|
|
62731
|
+
* configured/resolvable, or the rebase produces conflicts (we abort and
|
|
62732
|
+
* leave the worktree as-is so the executor can still run).
|
|
62733
|
+
*/
|
|
62734
|
+
async rebaseNewWorktreeOntoRemote(worktreePath, branch, taskId) {
|
|
62735
|
+
let settings;
|
|
62736
|
+
try {
|
|
62737
|
+
settings = await this.store.getSettings();
|
|
62738
|
+
} catch {
|
|
62739
|
+
return;
|
|
62740
|
+
}
|
|
62741
|
+
if (settings.worktreeRebaseBeforeMerge === false) return;
|
|
62742
|
+
let remote = settings.worktreeRebaseRemote?.trim() || "";
|
|
62743
|
+
if (!remote) {
|
|
62744
|
+
try {
|
|
62745
|
+
const { stdout } = await execAsync5("git remote", { cwd: this.rootDir });
|
|
62746
|
+
const remotes = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
62747
|
+
if (remotes.includes("origin")) remote = "origin";
|
|
62748
|
+
else if (remotes.length === 1) remote = remotes[0];
|
|
62749
|
+
} catch {
|
|
62750
|
+
}
|
|
62751
|
+
}
|
|
62752
|
+
if (!remote) return;
|
|
62753
|
+
let defaultBranch = "";
|
|
62754
|
+
try {
|
|
62755
|
+
const { stdout } = await execAsync5(`git rev-parse --abbrev-ref ${remote}/HEAD`, { cwd: this.rootDir });
|
|
62756
|
+
defaultBranch = stdout.trim().replace(new RegExp(`^${remote}/`), "");
|
|
62757
|
+
} catch {
|
|
62758
|
+
}
|
|
62759
|
+
if (!defaultBranch) {
|
|
62760
|
+
try {
|
|
62761
|
+
const { stdout } = await execAsync5("git rev-parse --abbrev-ref HEAD", { cwd: this.rootDir });
|
|
62762
|
+
defaultBranch = stdout.trim();
|
|
62763
|
+
} catch {
|
|
62764
|
+
return;
|
|
62765
|
+
}
|
|
62766
|
+
}
|
|
62767
|
+
if (!defaultBranch || defaultBranch === "HEAD") return;
|
|
62768
|
+
const remoteRef = `${remote}/${defaultBranch}`;
|
|
62769
|
+
try {
|
|
62770
|
+
await execAsync5(`git fetch ${this.quoteShellArg(remote)} ${this.quoteShellArg(defaultBranch)}`, { cwd: this.rootDir });
|
|
62771
|
+
} catch (err) {
|
|
62772
|
+
executorLog.warn(
|
|
62773
|
+
`Worktree rebase: fetch ${remote} ${defaultBranch} failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`
|
|
62774
|
+
);
|
|
62775
|
+
return;
|
|
62776
|
+
}
|
|
62777
|
+
try {
|
|
62778
|
+
await execAsync5(`git rebase ${this.quoteShellArg(remoteRef)}`, { cwd: worktreePath });
|
|
62779
|
+
await this.store.logEntry(
|
|
62780
|
+
taskId,
|
|
62781
|
+
`Rebased new worktree branch ${branch} onto ${remoteRef}`
|
|
62782
|
+
);
|
|
62783
|
+
} catch (rebaseErr) {
|
|
62784
|
+
const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
|
|
62785
|
+
executorLog.warn(
|
|
62786
|
+
`Worktree rebase: rebase onto ${remoteRef} failed for ${taskId} \u2014 aborting and leaving local base intact: ${msg}`
|
|
62787
|
+
);
|
|
62788
|
+
try {
|
|
62789
|
+
await execAsync5("git rebase --abort", { cwd: worktreePath });
|
|
62790
|
+
} catch {
|
|
62791
|
+
}
|
|
62792
|
+
await this.store.logEntry(
|
|
62793
|
+
taskId,
|
|
62794
|
+
`Could not rebase new worktree onto ${remoteRef} \u2014 kept local base. The merge-time rebase will retry with conflict resolution.`
|
|
62795
|
+
);
|
|
62796
|
+
}
|
|
62797
|
+
}
|
|
61903
62798
|
/**
|
|
61904
62799
|
* Resolve a stored baseBranch to a concrete commit SHA.
|
|
61905
62800
|
*
|
|
@@ -62631,6 +63526,7 @@ Child agent: ${agent.id} (${name})`;
|
|
|
62631
63526
|
});
|
|
62632
63527
|
const parentAgent = childTask.assignedAgentId ? await this.options.agentStore.getAgent(childTask.assignedAgentId).catch(() => null) : null;
|
|
62633
63528
|
const childRuntimeHint = extractRuntimeHint(agent.runtimeConfig) ?? extractRuntimeHint(parentAgent?.runtimeConfig);
|
|
63529
|
+
const { provider: childExecutorProvider, modelId: childExecutorModelId } = resolveExecutorModelPair2(void 0, void 0, settings);
|
|
62634
63530
|
const { session: childSession } = await createResolvedAgentSession({
|
|
62635
63531
|
sessionPurpose: "executor",
|
|
62636
63532
|
runtimeHint: childRuntimeHint,
|
|
@@ -62638,8 +63534,8 @@ Child agent: ${agent.id} (${name})`;
|
|
|
62638
63534
|
cwd: childWorktreePath,
|
|
62639
63535
|
systemPrompt: childSystemPrompt,
|
|
62640
63536
|
tools: "coding",
|
|
62641
|
-
defaultProvider:
|
|
62642
|
-
defaultModelId:
|
|
63537
|
+
defaultProvider: childExecutorProvider,
|
|
63538
|
+
defaultModelId: childExecutorModelId,
|
|
62643
63539
|
fallbackProvider: settings.fallbackProvider,
|
|
62644
63540
|
fallbackModelId: settings.fallbackModelId,
|
|
62645
63541
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
|
@@ -62735,6 +63631,25 @@ var init_mission_feature_sync = __esm({
|
|
|
62735
63631
|
}
|
|
62736
63632
|
});
|
|
62737
63633
|
|
|
63634
|
+
// ../engine/src/effective-node.ts
|
|
63635
|
+
function isSetNodeId(nodeId) {
|
|
63636
|
+
return typeof nodeId === "string" && nodeId.trim().length > 0;
|
|
63637
|
+
}
|
|
63638
|
+
function resolveEffectiveNode(task, settings) {
|
|
63639
|
+
if (isSetNodeId(task.nodeId)) {
|
|
63640
|
+
return { nodeId: task.nodeId, source: "task-override" };
|
|
63641
|
+
}
|
|
63642
|
+
if (isSetNodeId(settings.defaultNodeId)) {
|
|
63643
|
+
return { nodeId: settings.defaultNodeId, source: "project-default" };
|
|
63644
|
+
}
|
|
63645
|
+
return { nodeId: void 0, source: "local" };
|
|
63646
|
+
}
|
|
63647
|
+
var init_effective_node = __esm({
|
|
63648
|
+
"../engine/src/effective-node.ts"() {
|
|
63649
|
+
"use strict";
|
|
63650
|
+
}
|
|
63651
|
+
});
|
|
63652
|
+
|
|
62738
63653
|
// ../engine/src/scheduler.ts
|
|
62739
63654
|
import { existsSync as existsSync26 } from "node:fs";
|
|
62740
63655
|
import { readFile as readFile15 } from "node:fs/promises";
|
|
@@ -62793,6 +63708,7 @@ var init_scheduler = __esm({
|
|
|
62793
63708
|
init_logger2();
|
|
62794
63709
|
init_mission_feature_sync();
|
|
62795
63710
|
init_spec_staleness();
|
|
63711
|
+
init_effective_node();
|
|
62796
63712
|
Scheduler = class {
|
|
62797
63713
|
/**
|
|
62798
63714
|
* Async listener guard convention:
|
|
@@ -63216,14 +64132,19 @@ var init_scheduler = __esm({
|
|
|
63216
64132
|
schedulerLog.log(`Task ${task.id} is paused \u2014 skipping dispatch`);
|
|
63217
64133
|
continue;
|
|
63218
64134
|
}
|
|
64135
|
+
const effectiveNode = resolveEffectiveNode(freshTask, settings);
|
|
64136
|
+
schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
|
|
63219
64137
|
schedulerLog.log(`Starting ${task.id}: ${task.title || task.id} (deps satisfied)`);
|
|
63220
64138
|
await this.store.updateTask(task.id, {
|
|
63221
64139
|
status: null,
|
|
63222
64140
|
blockedBy: null,
|
|
63223
64141
|
baseBranch: baseBranch ?? void 0,
|
|
63224
|
-
worktree: plannedWorktree
|
|
64142
|
+
worktree: plannedWorktree,
|
|
64143
|
+
effectiveNodeId: effectiveNode.nodeId ?? null,
|
|
64144
|
+
effectiveNodeSource: effectiveNode.source
|
|
63225
64145
|
});
|
|
63226
64146
|
await this.store.moveTask(task.id, "in-progress");
|
|
64147
|
+
await this.store.logEntry(task.id, `Node routing resolved: ${effectiveNode.nodeId ?? "local"} (source: ${effectiveNode.source})`);
|
|
63227
64148
|
this.options.onSchedule?.(task);
|
|
63228
64149
|
started++;
|
|
63229
64150
|
if (settings.groupOverlappingFiles) {
|
|
@@ -65679,6 +66600,453 @@ Please review the PR comments and address any remaining issues.`;
|
|
|
65679
66600
|
}
|
|
65680
66601
|
});
|
|
65681
66602
|
|
|
66603
|
+
// ../engine/src/notification/ntfy-provider.ts
|
|
66604
|
+
var SUPPORTED_EVENTS, NtfyNotificationProvider;
|
|
66605
|
+
var init_ntfy_provider = __esm({
|
|
66606
|
+
"../engine/src/notification/ntfy-provider.ts"() {
|
|
66607
|
+
"use strict";
|
|
66608
|
+
init_notifier();
|
|
66609
|
+
SUPPORTED_EVENTS = /* @__PURE__ */ new Set([
|
|
66610
|
+
"in-review",
|
|
66611
|
+
"merged",
|
|
66612
|
+
"failed",
|
|
66613
|
+
"awaiting-approval",
|
|
66614
|
+
"awaiting-user-review",
|
|
66615
|
+
"planning-awaiting-input"
|
|
66616
|
+
]);
|
|
66617
|
+
NtfyNotificationProvider = class {
|
|
66618
|
+
config;
|
|
66619
|
+
abortController = null;
|
|
66620
|
+
getProviderId() {
|
|
66621
|
+
return "ntfy";
|
|
66622
|
+
}
|
|
66623
|
+
async initialize(config) {
|
|
66624
|
+
if (typeof config.topic !== "string" || config.topic.trim() === "") {
|
|
66625
|
+
return;
|
|
66626
|
+
}
|
|
66627
|
+
this.config = config;
|
|
66628
|
+
this.config.events = resolveNtfyEvents(this.config.events);
|
|
66629
|
+
this.abortController = new AbortController();
|
|
66630
|
+
}
|
|
66631
|
+
async shutdown() {
|
|
66632
|
+
this.abortController?.abort();
|
|
66633
|
+
this.abortController = null;
|
|
66634
|
+
}
|
|
66635
|
+
isEventSupported(event) {
|
|
66636
|
+
if (!SUPPORTED_EVENTS.has(event)) {
|
|
66637
|
+
return false;
|
|
66638
|
+
}
|
|
66639
|
+
const enabledEvents = this.config?.events ?? [...DEFAULT_NTFY_EVENTS];
|
|
66640
|
+
return enabledEvents.includes(event);
|
|
66641
|
+
}
|
|
66642
|
+
async sendNotification(event, payload) {
|
|
66643
|
+
if (!this.config?.topic) {
|
|
66644
|
+
return { success: false, providerId: this.getProviderId(), error: "ntfy topic not configured" };
|
|
66645
|
+
}
|
|
66646
|
+
if (!this.isEventSupported(event)) {
|
|
66647
|
+
return {
|
|
66648
|
+
success: false,
|
|
66649
|
+
providerId: this.getProviderId(),
|
|
66650
|
+
error: `unsupported event: ${event}`
|
|
66651
|
+
};
|
|
66652
|
+
}
|
|
66653
|
+
const taskLike = {
|
|
66654
|
+
id: payload.taskId,
|
|
66655
|
+
title: payload.taskTitle,
|
|
66656
|
+
description: payload.taskDescription ?? ""
|
|
66657
|
+
};
|
|
66658
|
+
const identifier = formatTaskIdentifier(taskLike);
|
|
66659
|
+
const clickUrl = buildNtfyClickUrl({
|
|
66660
|
+
dashboardHost: this.config.dashboardHost,
|
|
66661
|
+
projectId: this.config.projectId,
|
|
66662
|
+
taskId: payload.taskId
|
|
66663
|
+
});
|
|
66664
|
+
const contentByEvent = {
|
|
66665
|
+
"in-review": {
|
|
66666
|
+
title: `Task ${payload.taskId} completed`,
|
|
66667
|
+
message: `Task "${identifier}" is ready for review`,
|
|
66668
|
+
priority: "default"
|
|
66669
|
+
},
|
|
66670
|
+
merged: {
|
|
66671
|
+
title: `Task ${payload.taskId} merged`,
|
|
66672
|
+
message: `Task "${identifier}" has been merged to main`,
|
|
66673
|
+
priority: "default"
|
|
66674
|
+
},
|
|
66675
|
+
failed: {
|
|
66676
|
+
title: `Task ${payload.taskId} failed`,
|
|
66677
|
+
message: `Task "${identifier}" has failed and needs attention`,
|
|
66678
|
+
priority: "high"
|
|
66679
|
+
},
|
|
66680
|
+
"awaiting-approval": {
|
|
66681
|
+
title: `Plan needs approval for ${payload.taskId}`,
|
|
66682
|
+
message: `Task "${identifier}" needs your approval before it can proceed`,
|
|
66683
|
+
priority: "high"
|
|
66684
|
+
},
|
|
66685
|
+
"awaiting-user-review": {
|
|
66686
|
+
title: `User review needed for ${payload.taskId}`,
|
|
66687
|
+
message: `Task "${identifier}" needs human review before it can proceed`,
|
|
66688
|
+
priority: "high"
|
|
66689
|
+
},
|
|
66690
|
+
"planning-awaiting-input": {
|
|
66691
|
+
title: `Planning input needed for ${payload.taskId}`,
|
|
66692
|
+
message: `Task "${identifier}" is awaiting your input during planning`,
|
|
66693
|
+
priority: "high"
|
|
66694
|
+
}
|
|
66695
|
+
};
|
|
66696
|
+
const content = contentByEvent[event];
|
|
66697
|
+
await sendNtfyNotification({
|
|
66698
|
+
ntfyBaseUrl: this.config.ntfyBaseUrl,
|
|
66699
|
+
topic: this.config.topic,
|
|
66700
|
+
title: content.title,
|
|
66701
|
+
message: content.message,
|
|
66702
|
+
priority: content.priority,
|
|
66703
|
+
clickUrl,
|
|
66704
|
+
signal: this.abortController?.signal
|
|
66705
|
+
});
|
|
66706
|
+
return { success: true, providerId: this.getProviderId() };
|
|
66707
|
+
}
|
|
66708
|
+
};
|
|
66709
|
+
}
|
|
66710
|
+
});
|
|
66711
|
+
|
|
66712
|
+
// ../engine/src/notification/webhook-provider.ts
|
|
66713
|
+
var WebhookNotificationProvider;
|
|
66714
|
+
var init_webhook_provider = __esm({
|
|
66715
|
+
"../engine/src/notification/webhook-provider.ts"() {
|
|
66716
|
+
"use strict";
|
|
66717
|
+
init_logger2();
|
|
66718
|
+
WebhookNotificationProvider = class {
|
|
66719
|
+
config = null;
|
|
66720
|
+
abortController = null;
|
|
66721
|
+
getProviderId() {
|
|
66722
|
+
return "webhook";
|
|
66723
|
+
}
|
|
66724
|
+
async initialize(config) {
|
|
66725
|
+
const webhookUrl = typeof config.webhookUrl === "string" ? config.webhookUrl.trim() : "";
|
|
66726
|
+
if (!webhookUrl) {
|
|
66727
|
+
throw new Error("webhookUrl is required");
|
|
66728
|
+
}
|
|
66729
|
+
let parsedUrl;
|
|
66730
|
+
try {
|
|
66731
|
+
parsedUrl = new URL(webhookUrl);
|
|
66732
|
+
} catch {
|
|
66733
|
+
throw new Error("webhookUrl must be a valid URL");
|
|
66734
|
+
}
|
|
66735
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
66736
|
+
throw new Error("webhookUrl must use http:// or https://");
|
|
66737
|
+
}
|
|
66738
|
+
const webhookFormat = config.webhookFormat === "slack" || config.webhookFormat === "discord" || config.webhookFormat === "generic" ? config.webhookFormat : "generic";
|
|
66739
|
+
this.config = {
|
|
66740
|
+
webhookUrl,
|
|
66741
|
+
webhookFormat,
|
|
66742
|
+
events: Array.isArray(config.events) ? config.events.filter((event) => typeof event === "string") : []
|
|
66743
|
+
};
|
|
66744
|
+
this.abortController?.abort();
|
|
66745
|
+
this.abortController = new AbortController();
|
|
66746
|
+
}
|
|
66747
|
+
async shutdown() {
|
|
66748
|
+
this.abortController?.abort();
|
|
66749
|
+
this.abortController = null;
|
|
66750
|
+
this.config = null;
|
|
66751
|
+
}
|
|
66752
|
+
isEventSupported(event) {
|
|
66753
|
+
if (!this.config?.events || this.config.events.length === 0) {
|
|
66754
|
+
return true;
|
|
66755
|
+
}
|
|
66756
|
+
return this.config.events.includes(event);
|
|
66757
|
+
}
|
|
66758
|
+
async sendNotification(event, payload) {
|
|
66759
|
+
if (!this.config) {
|
|
66760
|
+
return { success: false, providerId: this.getProviderId(), error: "Not initialized" };
|
|
66761
|
+
}
|
|
66762
|
+
if (!this.isEventSupported(event)) {
|
|
66763
|
+
return {
|
|
66764
|
+
success: false,
|
|
66765
|
+
providerId: this.getProviderId(),
|
|
66766
|
+
error: `unsupported event: ${event}`
|
|
66767
|
+
};
|
|
66768
|
+
}
|
|
66769
|
+
try {
|
|
66770
|
+
const message = this.formatMessage(event, payload);
|
|
66771
|
+
const body = this.formatPayload(payload, message);
|
|
66772
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
66773
|
+
method: "POST",
|
|
66774
|
+
headers: {
|
|
66775
|
+
"Content-Type": "application/json"
|
|
66776
|
+
},
|
|
66777
|
+
body: JSON.stringify(body),
|
|
66778
|
+
signal: this.abortController?.signal
|
|
66779
|
+
});
|
|
66780
|
+
if (!response.ok) {
|
|
66781
|
+
const error = `Webhook notification failed: ${response.status} ${response.statusText}`;
|
|
66782
|
+
schedulerLog.log(error);
|
|
66783
|
+
return {
|
|
66784
|
+
success: false,
|
|
66785
|
+
providerId: this.getProviderId(),
|
|
66786
|
+
error
|
|
66787
|
+
};
|
|
66788
|
+
}
|
|
66789
|
+
return { success: true, providerId: this.getProviderId() };
|
|
66790
|
+
} catch (error) {
|
|
66791
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66792
|
+
schedulerLog.log(`Failed to send webhook notification: ${message}`);
|
|
66793
|
+
return {
|
|
66794
|
+
success: false,
|
|
66795
|
+
providerId: this.getProviderId(),
|
|
66796
|
+
error: message
|
|
66797
|
+
};
|
|
66798
|
+
}
|
|
66799
|
+
}
|
|
66800
|
+
formatMessage(event, payload) {
|
|
66801
|
+
const identifier = this.formatTaskIdentifier(payload);
|
|
66802
|
+
switch (event) {
|
|
66803
|
+
case "in-review":
|
|
66804
|
+
return `Task "${identifier}" is ready for review`;
|
|
66805
|
+
case "merged":
|
|
66806
|
+
return `Task "${identifier}" has been merged to main`;
|
|
66807
|
+
case "failed":
|
|
66808
|
+
return `Task "${identifier}" has failed and needs attention`;
|
|
66809
|
+
case "awaiting-approval":
|
|
66810
|
+
return `Task "${identifier}" needs your approval before it can proceed`;
|
|
66811
|
+
case "awaiting-user-review":
|
|
66812
|
+
return `Task "${identifier}" needs human review before it can proceed`;
|
|
66813
|
+
case "planning-awaiting-input":
|
|
66814
|
+
return `Task "${identifier}" is awaiting your input during planning`;
|
|
66815
|
+
case "gridlock":
|
|
66816
|
+
return "Pipeline gridlocked";
|
|
66817
|
+
default:
|
|
66818
|
+
return `Event "${event}" for task ${identifier}`;
|
|
66819
|
+
}
|
|
66820
|
+
}
|
|
66821
|
+
formatTaskIdentifier(payload) {
|
|
66822
|
+
if (payload.taskTitle?.trim()) {
|
|
66823
|
+
return payload.taskTitle;
|
|
66824
|
+
}
|
|
66825
|
+
const description = payload.taskDescription ?? "";
|
|
66826
|
+
const snippet = description.length > 200 ? `${description.slice(0, 200)}...` : description;
|
|
66827
|
+
return `${payload.taskId}: ${snippet}`;
|
|
66828
|
+
}
|
|
66829
|
+
formatPayload(payload, message) {
|
|
66830
|
+
if (!this.config) {
|
|
66831
|
+
return {};
|
|
66832
|
+
}
|
|
66833
|
+
if (this.config.webhookFormat === "slack") {
|
|
66834
|
+
return { text: message };
|
|
66835
|
+
}
|
|
66836
|
+
if (this.config.webhookFormat === "discord") {
|
|
66837
|
+
return { content: message };
|
|
66838
|
+
}
|
|
66839
|
+
return {
|
|
66840
|
+
event: payload.event,
|
|
66841
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
66842
|
+
task: {
|
|
66843
|
+
id: payload.taskId,
|
|
66844
|
+
title: payload.taskTitle
|
|
66845
|
+
},
|
|
66846
|
+
metadata: payload.metadata
|
|
66847
|
+
};
|
|
66848
|
+
}
|
|
66849
|
+
};
|
|
66850
|
+
}
|
|
66851
|
+
});
|
|
66852
|
+
|
|
66853
|
+
// ../engine/src/notification/notification-service.ts
|
|
66854
|
+
var NotificationService;
|
|
66855
|
+
var init_notification_service = __esm({
|
|
66856
|
+
"../engine/src/notification/notification-service.ts"() {
|
|
66857
|
+
"use strict";
|
|
66858
|
+
init_src();
|
|
66859
|
+
init_notifier();
|
|
66860
|
+
init_logger2();
|
|
66861
|
+
init_ntfy_provider();
|
|
66862
|
+
init_webhook_provider();
|
|
66863
|
+
NotificationService = class {
|
|
66864
|
+
constructor(store, options = {}) {
|
|
66865
|
+
this.store = store;
|
|
66866
|
+
this.options = options;
|
|
66867
|
+
}
|
|
66868
|
+
dispatcher = new NotificationDispatcher();
|
|
66869
|
+
notifiedEvents = /* @__PURE__ */ new Set();
|
|
66870
|
+
started = false;
|
|
66871
|
+
notificationsEnabled = false;
|
|
66872
|
+
ntfyProvider;
|
|
66873
|
+
webhookProvider;
|
|
66874
|
+
registerProvider(provider) {
|
|
66875
|
+
this.dispatcher.registerProvider(provider);
|
|
66876
|
+
}
|
|
66877
|
+
async start() {
|
|
66878
|
+
if (this.started) {
|
|
66879
|
+
return;
|
|
66880
|
+
}
|
|
66881
|
+
const settings = await this.store.getSettings();
|
|
66882
|
+
this.setNotificationsEnabledFromSettings(settings);
|
|
66883
|
+
await this.syncNtfyProvider(settings);
|
|
66884
|
+
await this.syncWebhookProvider(settings);
|
|
66885
|
+
await this.dispatcher.initializeAll();
|
|
66886
|
+
this.store.on("task:moved", this.handleTaskMoved);
|
|
66887
|
+
this.store.on("task:updated", this.handleTaskUpdated);
|
|
66888
|
+
this.store.on("task:merged", this.handleTaskMerged);
|
|
66889
|
+
this.store.on("settings:updated", this.handleSettingsUpdated);
|
|
66890
|
+
this.started = true;
|
|
66891
|
+
schedulerLog.log("NotificationService started");
|
|
66892
|
+
}
|
|
66893
|
+
async stop() {
|
|
66894
|
+
if (!this.started) {
|
|
66895
|
+
return;
|
|
66896
|
+
}
|
|
66897
|
+
if (typeof this.store.off === "function") {
|
|
66898
|
+
this.store.off("task:moved", this.handleTaskMoved);
|
|
66899
|
+
this.store.off("task:updated", this.handleTaskUpdated);
|
|
66900
|
+
this.store.off("task:merged", this.handleTaskMerged);
|
|
66901
|
+
this.store.off("settings:updated", this.handleSettingsUpdated);
|
|
66902
|
+
}
|
|
66903
|
+
await this.dispatcher.shutdownAll();
|
|
66904
|
+
this.started = false;
|
|
66905
|
+
schedulerLog.log("NotificationService stopped");
|
|
66906
|
+
}
|
|
66907
|
+
handleTaskMoved = (data) => {
|
|
66908
|
+
if (!this.notificationsEnabled || data.to !== "in-review") {
|
|
66909
|
+
return;
|
|
66910
|
+
}
|
|
66911
|
+
const payload = this.createTaskPayload(data.task, "in-review");
|
|
66912
|
+
this.maybeNotify(data.task.id, "in-review", payload);
|
|
66913
|
+
};
|
|
66914
|
+
handleTaskUpdated = (task) => {
|
|
66915
|
+
if (!this.notificationsEnabled) {
|
|
66916
|
+
return;
|
|
66917
|
+
}
|
|
66918
|
+
if (task.status === "failed") {
|
|
66919
|
+
this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
|
|
66920
|
+
}
|
|
66921
|
+
if (task.status === "awaiting-approval") {
|
|
66922
|
+
this.maybeNotify(
|
|
66923
|
+
task.id,
|
|
66924
|
+
"awaiting-approval",
|
|
66925
|
+
this.createTaskPayload(task, "awaiting-approval")
|
|
66926
|
+
);
|
|
66927
|
+
}
|
|
66928
|
+
if (task.status === "awaiting-user-review") {
|
|
66929
|
+
this.maybeNotify(
|
|
66930
|
+
task.id,
|
|
66931
|
+
"awaiting-user-review",
|
|
66932
|
+
this.createTaskPayload(task, "awaiting-user-review")
|
|
66933
|
+
);
|
|
66934
|
+
}
|
|
66935
|
+
};
|
|
66936
|
+
handleTaskMerged = (result) => {
|
|
66937
|
+
if (!this.notificationsEnabled || !result.merged) {
|
|
66938
|
+
return;
|
|
66939
|
+
}
|
|
66940
|
+
this.maybeNotify(
|
|
66941
|
+
result.task.id,
|
|
66942
|
+
"merged",
|
|
66943
|
+
this.createTaskPayload(result.task, "merged")
|
|
66944
|
+
);
|
|
66945
|
+
};
|
|
66946
|
+
handleSettingsUpdated = async (data) => {
|
|
66947
|
+
const { settings, previous } = data;
|
|
66948
|
+
this.setNotificationsEnabledFromSettings(settings);
|
|
66949
|
+
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)) {
|
|
66950
|
+
const wasEnabled = Boolean(previous.ntfyEnabled && previous.ntfyTopic);
|
|
66951
|
+
const isEnabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
|
|
66952
|
+
await this.syncNtfyProvider(settings);
|
|
66953
|
+
if (isEnabled && !wasEnabled) {
|
|
66954
|
+
schedulerLog.log("NotificationService ntfy enabled");
|
|
66955
|
+
} else if (!isEnabled && wasEnabled) {
|
|
66956
|
+
schedulerLog.log("NotificationService ntfy disabled");
|
|
66957
|
+
} else if (settings.ntfyTopic !== previous.ntfyTopic) {
|
|
66958
|
+
schedulerLog.log("NotificationService ntfy topic updated");
|
|
66959
|
+
} else if (settings.ntfyBaseUrl !== previous.ntfyBaseUrl) {
|
|
66960
|
+
schedulerLog.log("NotificationService ntfy base URL updated");
|
|
66961
|
+
} else if (settings.ntfyDashboardHost !== previous.ntfyDashboardHost) {
|
|
66962
|
+
schedulerLog.log("NotificationService ntfy dashboard host updated");
|
|
66963
|
+
} else if (JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
|
|
66964
|
+
schedulerLog.log("NotificationService ntfy events updated");
|
|
66965
|
+
}
|
|
66966
|
+
}
|
|
66967
|
+
if (settings.webhookEnabled !== previous.webhookEnabled || settings.webhookUrl !== previous.webhookUrl || settings.webhookFormat !== previous.webhookFormat || JSON.stringify(settings.webhookEvents) !== JSON.stringify(previous.webhookEvents)) {
|
|
66968
|
+
await this.syncWebhookProvider(settings);
|
|
66969
|
+
schedulerLog.log("WebhookNotificationProvider config updated");
|
|
66970
|
+
}
|
|
66971
|
+
};
|
|
66972
|
+
async syncNtfyProvider(settings) {
|
|
66973
|
+
const enabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
|
|
66974
|
+
if (!enabled) {
|
|
66975
|
+
if (this.ntfyProvider) {
|
|
66976
|
+
await this.ntfyProvider.shutdown?.();
|
|
66977
|
+
this.dispatcher.unregisterProvider(this.ntfyProvider.getProviderId());
|
|
66978
|
+
this.ntfyProvider = void 0;
|
|
66979
|
+
}
|
|
66980
|
+
return;
|
|
66981
|
+
}
|
|
66982
|
+
if (!this.ntfyProvider) {
|
|
66983
|
+
this.ntfyProvider = new NtfyNotificationProvider();
|
|
66984
|
+
this.registerProvider(this.ntfyProvider);
|
|
66985
|
+
}
|
|
66986
|
+
await this.ntfyProvider.initialize?.({
|
|
66987
|
+
topic: settings.ntfyTopic,
|
|
66988
|
+
ntfyBaseUrl: settings.ntfyBaseUrl ?? this.options.ntfyBaseUrl,
|
|
66989
|
+
dashboardHost: settings.ntfyDashboardHost,
|
|
66990
|
+
events: settings.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS],
|
|
66991
|
+
projectId: this.options.projectId
|
|
66992
|
+
});
|
|
66993
|
+
}
|
|
66994
|
+
async syncWebhookProvider(settings) {
|
|
66995
|
+
const enabled = Boolean(settings.webhookEnabled && settings.webhookUrl);
|
|
66996
|
+
if (!enabled) {
|
|
66997
|
+
if (this.webhookProvider) {
|
|
66998
|
+
await this.webhookProvider.shutdown?.();
|
|
66999
|
+
this.dispatcher.unregisterProvider(this.webhookProvider.getProviderId());
|
|
67000
|
+
this.webhookProvider = void 0;
|
|
67001
|
+
}
|
|
67002
|
+
return;
|
|
67003
|
+
}
|
|
67004
|
+
if (!this.webhookProvider) {
|
|
67005
|
+
this.webhookProvider = new WebhookNotificationProvider();
|
|
67006
|
+
this.registerProvider(this.webhookProvider);
|
|
67007
|
+
}
|
|
67008
|
+
await this.webhookProvider.initialize?.({
|
|
67009
|
+
webhookUrl: settings.webhookUrl,
|
|
67010
|
+
webhookFormat: settings.webhookFormat ?? "generic",
|
|
67011
|
+
events: settings.webhookEvents ?? []
|
|
67012
|
+
});
|
|
67013
|
+
}
|
|
67014
|
+
setNotificationsEnabledFromSettings(settings) {
|
|
67015
|
+
this.notificationsEnabled = Boolean(
|
|
67016
|
+
settings.ntfyEnabled && settings.ntfyTopic || settings.webhookEnabled && settings.webhookUrl
|
|
67017
|
+
);
|
|
67018
|
+
}
|
|
67019
|
+
createTaskPayload(task, event) {
|
|
67020
|
+
return {
|
|
67021
|
+
taskId: task.id,
|
|
67022
|
+
taskTitle: task.title,
|
|
67023
|
+
taskDescription: task.description,
|
|
67024
|
+
event
|
|
67025
|
+
};
|
|
67026
|
+
}
|
|
67027
|
+
maybeNotify(taskId, eventType, payload) {
|
|
67028
|
+
const key = `${taskId}:${eventType}`;
|
|
67029
|
+
if (this.notifiedEvents.has(key)) {
|
|
67030
|
+
return;
|
|
67031
|
+
}
|
|
67032
|
+
this.notifiedEvents.add(key);
|
|
67033
|
+
this.dispatcher.dispatch(eventType, payload).catch(() => {
|
|
67034
|
+
});
|
|
67035
|
+
}
|
|
67036
|
+
};
|
|
67037
|
+
}
|
|
67038
|
+
});
|
|
67039
|
+
|
|
67040
|
+
// ../engine/src/notification/index.ts
|
|
67041
|
+
var init_notification = __esm({
|
|
67042
|
+
"../engine/src/notification/index.ts"() {
|
|
67043
|
+
"use strict";
|
|
67044
|
+
init_ntfy_provider();
|
|
67045
|
+
init_webhook_provider();
|
|
67046
|
+
init_notification_service();
|
|
67047
|
+
}
|
|
67048
|
+
});
|
|
67049
|
+
|
|
65682
67050
|
// ../engine/src/notifier.ts
|
|
65683
67051
|
function formatTaskIdentifier(task) {
|
|
65684
67052
|
if (task.title) {
|
|
@@ -65757,6 +67125,7 @@ var init_notifier = __esm({
|
|
|
65757
67125
|
"../engine/src/notifier.ts"() {
|
|
65758
67126
|
"use strict";
|
|
65759
67127
|
init_logger2();
|
|
67128
|
+
init_notification();
|
|
65760
67129
|
DEFAULT_NTFY_BASE_URL = "https://ntfy.sh";
|
|
65761
67130
|
DEFAULT_NTFY_EVENTS = [
|
|
65762
67131
|
"in-review",
|
|
@@ -65764,7 +67133,8 @@ var init_notifier = __esm({
|
|
|
65764
67133
|
"failed",
|
|
65765
67134
|
"awaiting-approval",
|
|
65766
67135
|
"awaiting-user-review",
|
|
65767
|
-
"planning-awaiting-input"
|
|
67136
|
+
"planning-awaiting-input",
|
|
67137
|
+
"gridlock"
|
|
65768
67138
|
];
|
|
65769
67139
|
NtfyNotifier = class {
|
|
65770
67140
|
constructor(store, options = {}) {
|
|
@@ -65772,6 +67142,10 @@ var init_notifier = __esm({
|
|
|
65772
67142
|
this.defaultNtfyBaseUrl = resolveNtfyBaseUrl(options.ntfyBaseUrl);
|
|
65773
67143
|
this.ntfyBaseUrl = this.defaultNtfyBaseUrl;
|
|
65774
67144
|
this.projectId = options.projectId;
|
|
67145
|
+
this.notificationService = new NotificationService(store, {
|
|
67146
|
+
projectId: this.projectId,
|
|
67147
|
+
ntfyBaseUrl: options.ntfyBaseUrl
|
|
67148
|
+
});
|
|
65775
67149
|
}
|
|
65776
67150
|
config = {
|
|
65777
67151
|
enabled: false,
|
|
@@ -65779,6 +67153,7 @@ var init_notifier = __esm({
|
|
|
65779
67153
|
dashboardHost: void 0,
|
|
65780
67154
|
events: [...DEFAULT_NTFY_EVENTS]
|
|
65781
67155
|
};
|
|
67156
|
+
notificationService;
|
|
65782
67157
|
ntfyBaseUrl;
|
|
65783
67158
|
defaultNtfyBaseUrl;
|
|
65784
67159
|
projectId;
|
|
@@ -65788,154 +67163,23 @@ var init_notifier = __esm({
|
|
|
65788
67163
|
this.abortController = new AbortController();
|
|
65789
67164
|
const settings = await this.store.getSettings();
|
|
65790
67165
|
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
67166
|
this.store.on("settings:updated", this.handleSettingsUpdated);
|
|
67167
|
+
await this.notificationService.start();
|
|
65795
67168
|
schedulerLog.log("NtfyNotifier started");
|
|
65796
67169
|
}
|
|
65797
67170
|
stop() {
|
|
65798
67171
|
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
67172
|
this.store.off("settings:updated", this.handleSettingsUpdated);
|
|
65803
67173
|
}
|
|
65804
67174
|
if (this.abortController) {
|
|
65805
67175
|
this.abortController.abort();
|
|
65806
67176
|
this.abortController = null;
|
|
65807
67177
|
}
|
|
67178
|
+
void this.notificationService.stop();
|
|
65808
67179
|
schedulerLog.log("NtfyNotifier stopped");
|
|
65809
67180
|
}
|
|
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
67181
|
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
|
-
}
|
|
67182
|
+
this.loadConfig(data.settings);
|
|
65939
67183
|
};
|
|
65940
67184
|
loadConfig(settings) {
|
|
65941
67185
|
this.config = {
|
|
@@ -65946,11 +67190,38 @@ var init_notifier = __esm({
|
|
|
65946
67190
|
};
|
|
65947
67191
|
this.ntfyBaseUrl = resolveNtfyBaseUrl(settings.ntfyBaseUrl, this.defaultNtfyBaseUrl);
|
|
65948
67192
|
}
|
|
67193
|
+
notifyGridlock(event) {
|
|
67194
|
+
if (!this.config.enabled || !this.config.topic || !this.isEventEnabled("gridlock")) return;
|
|
67195
|
+
const blockedTasks = event.blockedTaskIds.sort();
|
|
67196
|
+
const reasonSummary = Object.values(event.reasons).reduce((acc, reason) => {
|
|
67197
|
+
acc[reason] = (acc[reason] ?? 0) + 1;
|
|
67198
|
+
return acc;
|
|
67199
|
+
}, {});
|
|
67200
|
+
const reasons = [];
|
|
67201
|
+
if (reasonSummary.dependency) reasons.push(`${reasonSummary.dependency} dependency`);
|
|
67202
|
+
if (reasonSummary.overlap) reasons.push(`${reasonSummary.overlap} overlap`);
|
|
67203
|
+
const clickUrl = buildNtfyClickUrl({
|
|
67204
|
+
dashboardHost: this.config.dashboardHost,
|
|
67205
|
+
projectId: this.projectId
|
|
67206
|
+
});
|
|
67207
|
+
const dedupKey = `gridlock:${blockedTasks.join(",")}`;
|
|
67208
|
+
this.maybeNotifyByKey(
|
|
67209
|
+
dedupKey,
|
|
67210
|
+
() => sendNtfyNotification({
|
|
67211
|
+
ntfyBaseUrl: this.ntfyBaseUrl,
|
|
67212
|
+
topic: this.config.topic,
|
|
67213
|
+
title: "Pipeline gridlocked",
|
|
67214
|
+
message: `${event.blockedTaskCount} todo tasks are blocked (${reasons.join(", ")}). Blocked: ${blockedTasks.join(", ")}. Blocking: ${event.blockingTaskIds.join(", ") || "none"}.`,
|
|
67215
|
+
priority: "high",
|
|
67216
|
+
clickUrl,
|
|
67217
|
+
signal: this.abortController?.signal
|
|
67218
|
+
})
|
|
67219
|
+
);
|
|
67220
|
+
}
|
|
65949
67221
|
isEventEnabled(event) {
|
|
65950
67222
|
return isNtfyEventEnabled(this.config.events, event);
|
|
65951
67223
|
}
|
|
65952
|
-
|
|
65953
|
-
const key = `${taskId}:${eventType}`;
|
|
67224
|
+
maybeNotifyByKey(key, notifyFn) {
|
|
65954
67225
|
if (this.notifiedEvents.has(key)) {
|
|
65955
67226
|
return;
|
|
65956
67227
|
}
|
|
@@ -65978,11 +67249,10 @@ var init_shell_utils = __esm({
|
|
|
65978
67249
|
import { exec as exec6 } from "node:child_process";
|
|
65979
67250
|
import { promisify as promisify7 } from "node:util";
|
|
65980
67251
|
async function createAiPromptExecutor(cwd) {
|
|
65981
|
-
const { createFnAgent: createFnAgent5, promptWithFallback: promptWithFallback2 } = await Promise.resolve().then(() => (init_pi(), pi_exports));
|
|
65982
67252
|
const disposeLog = createLogger2("cron-runner");
|
|
65983
67253
|
return async (prompt, modelProvider, modelId) => {
|
|
65984
67254
|
let responseText = "";
|
|
65985
|
-
const { session } = await
|
|
67255
|
+
const { session } = await createFnAgent2({
|
|
65986
67256
|
cwd,
|
|
65987
67257
|
systemPrompt: AI_AUTOMATION_SYSTEM_PROMPT,
|
|
65988
67258
|
tools: "readonly",
|
|
@@ -65993,7 +67263,7 @@ async function createAiPromptExecutor(cwd) {
|
|
|
65993
67263
|
}
|
|
65994
67264
|
});
|
|
65995
67265
|
try {
|
|
65996
|
-
await
|
|
67266
|
+
await promptWithFallback(session, prompt);
|
|
65997
67267
|
return responseText;
|
|
65998
67268
|
} finally {
|
|
65999
67269
|
try {
|
|
@@ -66019,8 +67289,10 @@ var execAsync6, log6, DEFAULT_TIMEOUT_MS, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT
|
|
|
66019
67289
|
var init_cron_runner = __esm({
|
|
66020
67290
|
"../engine/src/cron-runner.ts"() {
|
|
66021
67291
|
"use strict";
|
|
67292
|
+
init_src();
|
|
66022
67293
|
init_logger2();
|
|
66023
67294
|
init_shell_utils();
|
|
67295
|
+
init_pi();
|
|
66024
67296
|
execAsync6 = promisify7(exec6);
|
|
66025
67297
|
log6 = createLogger2("cron-runner");
|
|
66026
67298
|
DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -66343,8 +67615,9 @@ var init_cron_runner = __esm({
|
|
|
66343
67615
|
};
|
|
66344
67616
|
}
|
|
66345
67617
|
const settings = await this.store.getSettings();
|
|
66346
|
-
const
|
|
66347
|
-
const
|
|
67618
|
+
const defaultModel = resolveProjectDefaultModel(settings);
|
|
67619
|
+
const modelProvider = step.modelProvider?.trim() || defaultModel.provider;
|
|
67620
|
+
const modelId = step.modelId?.trim() || defaultModel.modelId;
|
|
66348
67621
|
const model = modelProvider && modelId ? `${modelProvider}/${modelId}` : "default";
|
|
66349
67622
|
log6.log(` AI prompt step "${step.name}" using model: ${model}`);
|
|
66350
67623
|
log6.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
|
|
@@ -67341,6 +68614,7 @@ var init_agent_heartbeat = __esm({
|
|
|
67341
68614
|
init_agent_instructions();
|
|
67342
68615
|
init_logger2();
|
|
67343
68616
|
init_run_audit();
|
|
68617
|
+
init_pi();
|
|
67344
68618
|
HEARTBEAT_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window.
|
|
67345
68619
|
|
|
67346
68620
|
Your job:
|
|
@@ -68056,7 +69330,6 @@ When sending messages:
|
|
|
68056
69330
|
};
|
|
68057
69331
|
const previousBlockedState = await this.store.getLastBlockedState(agentId);
|
|
68058
69332
|
if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
|
|
68059
|
-
heartbeatLog.log(`Task ${resolvedTaskId2} is still blocked by ${blockedBy} (duplicate state) \u2014 skipping comment`);
|
|
68060
69333
|
await this.completeRun(agentId, run.id, {
|
|
68061
69334
|
status: "completed",
|
|
68062
69335
|
resultJson: { reason: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
|
|
@@ -68108,7 +69381,6 @@ When sending messages:
|
|
|
68108
69381
|
};
|
|
68109
69382
|
}
|
|
68110
69383
|
};
|
|
68111
|
-
const { promptWithFallback: promptWithFallback2 } = await Promise.resolve().then(() => (init_pi(), pi_exports));
|
|
68112
69384
|
const { createResolvedAgentSession: createResolvedAgentSession2, extractRuntimeHint: extractRuntimeHint2 } = await Promise.resolve().then(() => (init_agent_session_helpers(), agent_session_helpers_exports));
|
|
68113
69385
|
const { buildSessionSkillContextSync: buildSessionSkillContextSync2 } = await Promise.resolve().then(() => (init_session_skill_context(), session_skill_context_exports));
|
|
68114
69386
|
let heartbeatTools;
|
|
@@ -68302,7 +69574,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
68302
69574
|
"Review the task status and take appropriate action. Call fn_heartbeat_done when finished."
|
|
68303
69575
|
].join("\n");
|
|
68304
69576
|
}
|
|
68305
|
-
await
|
|
69577
|
+
await promptWithFallback(session, executionPrompt);
|
|
68306
69578
|
let usageInput = 0;
|
|
68307
69579
|
let usageOutput = Math.ceil(outputLength / 4);
|
|
68308
69580
|
let usageCached = 0;
|
|
@@ -69155,13 +70427,40 @@ var init_self_healing = __esm({
|
|
|
69155
70427
|
return Date.now() - updatedAt >= timeoutMs;
|
|
69156
70428
|
}
|
|
69157
70429
|
async findLandedTaskCommit(task) {
|
|
69158
|
-
const
|
|
70430
|
+
const storedSha = task.mergeDetails?.commitSha;
|
|
70431
|
+
if (storedSha) {
|
|
70432
|
+
try {
|
|
70433
|
+
await execAsync8(
|
|
70434
|
+
`git merge-base --is-ancestor ${shellQuote(storedSha)} HEAD`,
|
|
70435
|
+
{ cwd: this.options.rootDir }
|
|
70436
|
+
);
|
|
70437
|
+
const { stdout: stdout2 } = await execAsync8(
|
|
70438
|
+
`git log -1 --format=%H%x1f%s ${shellQuote(storedSha)}`,
|
|
70439
|
+
{ cwd: this.options.rootDir, maxBuffer: 1024 * 1024 }
|
|
70440
|
+
);
|
|
70441
|
+
const [sha2, subject2] = stdout2.trim().split("");
|
|
70442
|
+
if (sha2) {
|
|
70443
|
+
const commit2 = { sha: sha2, subject: subject2 };
|
|
70444
|
+
try {
|
|
70445
|
+
const stats = await execAsync8(`git show --shortstat --format= ${shellQuote(sha2)}`, {
|
|
70446
|
+
cwd: this.options.rootDir,
|
|
70447
|
+
maxBuffer: 1024 * 1024
|
|
70448
|
+
});
|
|
70449
|
+
Object.assign(commit2, parseShortstat(stats.stdout));
|
|
70450
|
+
} catch {
|
|
70451
|
+
}
|
|
70452
|
+
return commit2;
|
|
70453
|
+
}
|
|
70454
|
+
} catch {
|
|
70455
|
+
}
|
|
70456
|
+
}
|
|
70457
|
+
const readLog = async (range, grepArg, fixedStrings) => {
|
|
69159
70458
|
const command = [
|
|
69160
70459
|
"git log",
|
|
69161
70460
|
"--format=%H%x1f%s",
|
|
69162
70461
|
"--max-count=20",
|
|
69163
|
-
"--fixed-strings",
|
|
69164
|
-
`--grep=${
|
|
70462
|
+
...fixedStrings ? ["--fixed-strings"] : ["-E"],
|
|
70463
|
+
`--grep=${grepArg}`,
|
|
69165
70464
|
shellQuote(range)
|
|
69166
70465
|
].join(" ");
|
|
69167
70466
|
return execAsync8(command, {
|
|
@@ -69169,22 +70468,34 @@ var init_self_healing = __esm({
|
|
|
69169
70468
|
maxBuffer: 1024 * 1024
|
|
69170
70469
|
});
|
|
69171
70470
|
};
|
|
69172
|
-
|
|
69173
|
-
|
|
69174
|
-
|
|
69175
|
-
|
|
69176
|
-
|
|
69177
|
-
|
|
69178
|
-
|
|
69179
|
-
|
|
69180
|
-
|
|
69181
|
-
|
|
69182
|
-
|
|
69183
|
-
|
|
69184
|
-
|
|
69185
|
-
|
|
69186
|
-
|
|
69187
|
-
|
|
70471
|
+
const search = async (grepArg, fixedStrings) => {
|
|
70472
|
+
let out;
|
|
70473
|
+
try {
|
|
70474
|
+
const r = await readLog(
|
|
70475
|
+
task.baseCommitSha ? `${task.baseCommitSha}..HEAD` : "HEAD",
|
|
70476
|
+
grepArg,
|
|
70477
|
+
fixedStrings
|
|
70478
|
+
);
|
|
70479
|
+
out = r.stdout;
|
|
70480
|
+
} catch (err) {
|
|
70481
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
70482
|
+
log8.warn(
|
|
70483
|
+
`Failed to read git log for landed commit lookup (${task.id}): ${errorMessage} \u2014 retrying with HEAD range`
|
|
70484
|
+
);
|
|
70485
|
+
if (!task.baseCommitSha) return "";
|
|
70486
|
+
const r = await readLog("HEAD", grepArg, fixedStrings);
|
|
70487
|
+
out = r.stdout;
|
|
70488
|
+
}
|
|
70489
|
+
if (!out.trim() && task.baseCommitSha) {
|
|
70490
|
+
const r = await readLog("HEAD", grepArg, fixedStrings);
|
|
70491
|
+
out = r.stdout;
|
|
70492
|
+
}
|
|
70493
|
+
return out;
|
|
70494
|
+
};
|
|
70495
|
+
const trailerPattern = `^Fusion-Task-Id: ${task.id}$`;
|
|
70496
|
+
let stdout = await search(shellQuote(trailerPattern), false);
|
|
70497
|
+
if (!stdout.trim()) {
|
|
70498
|
+
stdout = await search(shellQuote(task.id), true);
|
|
69188
70499
|
}
|
|
69189
70500
|
const firstLine = stdout.trim().split("\n").find(Boolean);
|
|
69190
70501
|
if (!firstLine) return null;
|
|
@@ -69564,7 +70875,7 @@ var init_self_healing = __esm({
|
|
|
69564
70875
|
if (!timeoutMs || timeoutMs <= 0) return 0;
|
|
69565
70876
|
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
|
69566
70877
|
const candidates = tasks.filter(
|
|
69567
|
-
(task) => task.column === "in-review" && Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) && this.isPastInterruptedMergeGrace(task, timeoutMs)
|
|
70878
|
+
(task) => task.column === "in-review" && !task.paused && Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) && this.isPastInterruptedMergeGrace(task, timeoutMs)
|
|
69568
70879
|
);
|
|
69569
70880
|
if (candidates.length === 0) return 0;
|
|
69570
70881
|
log8.warn(`Found ${candidates.length} stale merging task(s) in in-review`);
|
|
@@ -69605,6 +70916,13 @@ var init_self_healing = __esm({
|
|
|
69605
70916
|
"Auto-recovered: stale merge status cleared; merge will be retried"
|
|
69606
70917
|
);
|
|
69607
70918
|
log8.log(`Recovered interrupted merge ${task.id}: cleared stale status for retry`);
|
|
70919
|
+
try {
|
|
70920
|
+
this.options.enqueueMerge?.(task.id);
|
|
70921
|
+
} catch (enqueueErr) {
|
|
70922
|
+
log8.warn(
|
|
70923
|
+
`Failed to re-enqueue ${task.id} after stale-merge recovery (will rely on polling sweep): ${enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr)}`
|
|
70924
|
+
);
|
|
70925
|
+
}
|
|
69608
70926
|
recovered++;
|
|
69609
70927
|
} catch (err) {
|
|
69610
70928
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -69635,7 +70953,7 @@ var init_self_healing = __esm({
|
|
|
69635
70953
|
try {
|
|
69636
70954
|
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
|
69637
70955
|
const mergedButNotDone = tasks.filter(
|
|
69638
|
-
(t) => t.column === "in-review" && t.mergeDetails?.mergeConfirmed === true
|
|
70956
|
+
(t) => t.column === "in-review" && !t.paused && t.mergeDetails?.mergeConfirmed === true
|
|
69639
70957
|
);
|
|
69640
70958
|
if (mergedButNotDone.length === 0) return 0;
|
|
69641
70959
|
log8.warn(`Found ${mergedButNotDone.length} merged task(s) stuck in in-review`);
|
|
@@ -69683,7 +71001,7 @@ var init_self_healing = __esm({
|
|
|
69683
71001
|
try {
|
|
69684
71002
|
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
|
69685
71003
|
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")
|
|
71004
|
+
(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
71005
|
);
|
|
69688
71006
|
if (misclassified.length === 0) return 0;
|
|
69689
71007
|
log8.warn(`Found ${misclassified.length} misclassified failure(s) with all steps done`);
|
|
@@ -70803,6 +72121,12 @@ var init_in_process_runtime = __esm({
|
|
|
70803
72121
|
ephemeralCleanupTimers = /* @__PURE__ */ new Map();
|
|
70804
72122
|
/** Listener for agent:stateChanged events to clean up terminated ephemeral agents */
|
|
70805
72123
|
ephemeralTerminationListener;
|
|
72124
|
+
/**
|
|
72125
|
+
* Optional callback the runtime forwards to SelfHealingManager so that
|
|
72126
|
+
* stale-merge recovery can re-enqueue tasks immediately. Set by ProjectEngine
|
|
72127
|
+
* before `start()` via `setMergeEnqueuer`.
|
|
72128
|
+
*/
|
|
72129
|
+
mergeEnqueuer;
|
|
70806
72130
|
/**
|
|
70807
72131
|
* Start the runtime and initialize all subsystems.
|
|
70808
72132
|
*
|
|
@@ -70923,8 +72247,7 @@ var init_in_process_runtime = __esm({
|
|
|
70923
72247
|
this.recordActivity();
|
|
70924
72248
|
runtimeLog.log(`Scheduled task ${task.id}`);
|
|
70925
72249
|
},
|
|
70926
|
-
onBlocked: (
|
|
70927
|
-
runtimeLog.log(`Task ${task.id} blocked by: ${blockedBy.join(", ")}`);
|
|
72250
|
+
onBlocked: () => {
|
|
70928
72251
|
}
|
|
70929
72252
|
});
|
|
70930
72253
|
this.stuckTaskDetector = new StuckTaskDetector(this.taskStore, {
|
|
@@ -71244,7 +72567,8 @@ var init_in_process_runtime = __esm({
|
|
|
71244
72567
|
getExecutingTaskIds: () => this.executor.getExecutingTaskIds(),
|
|
71245
72568
|
recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
|
|
71246
72569
|
getPlanningTaskIds: () => this.triageProcessor?.getProcessingTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
71247
|
-
evictStaleTriageProcessing: () => this.triageProcessor?.evictStaleProcessing() ?? /* @__PURE__ */ new Set()
|
|
72570
|
+
evictStaleTriageProcessing: () => this.triageProcessor?.evictStaleProcessing() ?? /* @__PURE__ */ new Set(),
|
|
72571
|
+
enqueueMerge: this.mergeEnqueuer ? (taskId) => this.mergeEnqueuer?.(taskId) : void 0
|
|
71248
72572
|
});
|
|
71249
72573
|
this.selfHealingManager.start();
|
|
71250
72574
|
this.stuckTaskDetector.start();
|
|
@@ -71421,6 +72745,14 @@ var init_in_process_runtime = __esm({
|
|
|
71421
72745
|
getStatus() {
|
|
71422
72746
|
return this.status;
|
|
71423
72747
|
}
|
|
72748
|
+
/**
|
|
72749
|
+
* Register a callback used by SelfHealingManager to re-enqueue tasks for
|
|
72750
|
+
* auto-merge after clearing a stale `merging` status. Must be called before
|
|
72751
|
+
* `start()` because SelfHealingManager is constructed during startup.
|
|
72752
|
+
*/
|
|
72753
|
+
setMergeEnqueuer(enqueueMerge) {
|
|
72754
|
+
this.mergeEnqueuer = enqueueMerge;
|
|
72755
|
+
}
|
|
71424
72756
|
/**
|
|
71425
72757
|
* Get the project's TaskStore instance.
|
|
71426
72758
|
* @throws Error if runtime has not been started
|
|
@@ -73190,6 +74522,132 @@ var init_project_manager = __esm({
|
|
|
73190
74522
|
}
|
|
73191
74523
|
});
|
|
73192
74524
|
|
|
74525
|
+
// ../engine/src/gridlock-detector.ts
|
|
74526
|
+
var gridlockLog, GridlockDetector;
|
|
74527
|
+
var init_gridlock_detector = __esm({
|
|
74528
|
+
"../engine/src/gridlock-detector.ts"() {
|
|
74529
|
+
"use strict";
|
|
74530
|
+
init_logger2();
|
|
74531
|
+
init_scheduler();
|
|
74532
|
+
gridlockLog = createLogger2("gridlock-detector");
|
|
74533
|
+
GridlockDetector = class {
|
|
74534
|
+
constructor(store, options = {}) {
|
|
74535
|
+
this.store = store;
|
|
74536
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 3e4;
|
|
74537
|
+
this.missionStore = options.missionStore;
|
|
74538
|
+
this.onGridlock = options.onGridlock;
|
|
74539
|
+
}
|
|
74540
|
+
interval = null;
|
|
74541
|
+
pollIntervalMs;
|
|
74542
|
+
missionStore;
|
|
74543
|
+
onGridlock;
|
|
74544
|
+
lastGridlockKey = null;
|
|
74545
|
+
start() {
|
|
74546
|
+
if (this.interval) return;
|
|
74547
|
+
this.interval = setInterval(() => {
|
|
74548
|
+
this.detectGridlock().catch((error) => {
|
|
74549
|
+
gridlockLog.error("Failed gridlock detection cycle:", error);
|
|
74550
|
+
});
|
|
74551
|
+
}, this.pollIntervalMs);
|
|
74552
|
+
gridlockLog.log(`Started (poll interval: ${this.pollIntervalMs}ms)`);
|
|
74553
|
+
}
|
|
74554
|
+
stop() {
|
|
74555
|
+
if (!this.interval) return;
|
|
74556
|
+
clearInterval(this.interval);
|
|
74557
|
+
this.interval = null;
|
|
74558
|
+
gridlockLog.log("Stopped");
|
|
74559
|
+
}
|
|
74560
|
+
async detectGridlock() {
|
|
74561
|
+
const [tasks, settings] = await Promise.all([
|
|
74562
|
+
this.store.listTasks({ slim: true, includeArchived: false }),
|
|
74563
|
+
this.store.getSettings()
|
|
74564
|
+
]);
|
|
74565
|
+
const now = Date.now();
|
|
74566
|
+
const schedulable = tasks.filter((task) => {
|
|
74567
|
+
if (task.column !== "todo" || task.paused) return false;
|
|
74568
|
+
if (task.nextRecoveryAt && new Date(task.nextRecoveryAt).getTime() > now) return false;
|
|
74569
|
+
if (this.isMissionBlocked(task)) return false;
|
|
74570
|
+
return true;
|
|
74571
|
+
});
|
|
74572
|
+
if (schedulable.length === 0) {
|
|
74573
|
+
this.lastGridlockKey = null;
|
|
74574
|
+
return null;
|
|
74575
|
+
}
|
|
74576
|
+
const active = tasks.filter((task) => task.column === "in-progress" || task.column === "in-review" && Boolean(task.worktree));
|
|
74577
|
+
if (active.length === 0) {
|
|
74578
|
+
this.lastGridlockKey = null;
|
|
74579
|
+
return null;
|
|
74580
|
+
}
|
|
74581
|
+
const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
|
|
74582
|
+
const activeScopes = /* @__PURE__ */ new Map();
|
|
74583
|
+
if (settings.groupOverlappingFiles) {
|
|
74584
|
+
for (const task of active) {
|
|
74585
|
+
const scope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths);
|
|
74586
|
+
if (scope.length > 0) {
|
|
74587
|
+
activeScopes.set(task.id, scope);
|
|
74588
|
+
}
|
|
74589
|
+
}
|
|
74590
|
+
}
|
|
74591
|
+
const reasons = {};
|
|
74592
|
+
const blockingTaskIds = /* @__PURE__ */ new Set();
|
|
74593
|
+
for (const task of schedulable) {
|
|
74594
|
+
const unmetDeps = task.dependencies.filter((depId) => {
|
|
74595
|
+
const dep = tasks.find((candidate) => candidate.id === depId);
|
|
74596
|
+
return dep && dep.column !== "done" && dep.column !== "in-review" && dep.column !== "archived";
|
|
74597
|
+
});
|
|
74598
|
+
if (unmetDeps.length > 0) {
|
|
74599
|
+
reasons[task.id] = "dependency";
|
|
74600
|
+
for (const depId of unmetDeps) blockingTaskIds.add(depId);
|
|
74601
|
+
continue;
|
|
74602
|
+
}
|
|
74603
|
+
if (!settings.groupOverlappingFiles) continue;
|
|
74604
|
+
const taskScope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths);
|
|
74605
|
+
if (taskScope.length === 0) continue;
|
|
74606
|
+
for (const [activeId, activeScope] of activeScopes) {
|
|
74607
|
+
if (pathsOverlap2(taskScope, activeScope)) {
|
|
74608
|
+
reasons[task.id] = "overlap";
|
|
74609
|
+
blockingTaskIds.add(activeId);
|
|
74610
|
+
break;
|
|
74611
|
+
}
|
|
74612
|
+
}
|
|
74613
|
+
}
|
|
74614
|
+
const blockedTaskIds = Object.keys(reasons).sort();
|
|
74615
|
+
if (blockedTaskIds.length !== schedulable.length) {
|
|
74616
|
+
this.lastGridlockKey = null;
|
|
74617
|
+
return null;
|
|
74618
|
+
}
|
|
74619
|
+
const gridlockKey = blockedTaskIds.join(",");
|
|
74620
|
+
const event = {
|
|
74621
|
+
blockedTaskCount: blockedTaskIds.length,
|
|
74622
|
+
reasons,
|
|
74623
|
+
blockedTaskIds,
|
|
74624
|
+
blockingTaskIds: Array.from(blockingTaskIds).sort()
|
|
74625
|
+
};
|
|
74626
|
+
if (this.lastGridlockKey !== gridlockKey) {
|
|
74627
|
+
this.lastGridlockKey = gridlockKey;
|
|
74628
|
+
gridlockLog.warn(`Gridlock detected: blocked=${event.blockedTaskIds.join(",")}; blocking=${event.blockingTaskIds.join(",")}`);
|
|
74629
|
+
this.onGridlock?.(event);
|
|
74630
|
+
}
|
|
74631
|
+
return event;
|
|
74632
|
+
}
|
|
74633
|
+
isMissionBlocked(task) {
|
|
74634
|
+
if (!this.missionStore || !task.sliceId) return false;
|
|
74635
|
+
try {
|
|
74636
|
+
const slice = this.missionStore.getSlice(task.sliceId);
|
|
74637
|
+
if (!slice) return false;
|
|
74638
|
+
const milestone = this.missionStore.getMilestone(slice.milestoneId);
|
|
74639
|
+
if (!milestone) return false;
|
|
74640
|
+
const mission = this.missionStore.getMission(milestone.missionId);
|
|
74641
|
+
return mission?.status === "blocked";
|
|
74642
|
+
} catch (error) {
|
|
74643
|
+
gridlockLog.warn(`Mission lookup failed for ${task.id}:`, error);
|
|
74644
|
+
return false;
|
|
74645
|
+
}
|
|
74646
|
+
}
|
|
74647
|
+
};
|
|
74648
|
+
}
|
|
74649
|
+
});
|
|
74650
|
+
|
|
73193
74651
|
// ../engine/src/remote-access/provider-adapters.ts
|
|
73194
74652
|
import { accessSync, constants as fsConstants } from "node:fs";
|
|
73195
74653
|
function isAbsoluteOrPathLike(input) {
|
|
@@ -73772,6 +75230,8 @@ var init_project_engine = __esm({
|
|
|
73772
75230
|
init_pr_monitor();
|
|
73773
75231
|
init_pr_comment_handler();
|
|
73774
75232
|
init_notifier();
|
|
75233
|
+
init_notification();
|
|
75234
|
+
init_gridlock_detector();
|
|
73775
75235
|
init_cron_runner();
|
|
73776
75236
|
init_merger();
|
|
73777
75237
|
init_concurrency();
|
|
@@ -73785,11 +75245,24 @@ var init_project_engine = __esm({
|
|
|
73785
75245
|
this.options = options;
|
|
73786
75246
|
const runtimeConfig = options.externalTaskStore ? { ...config, externalTaskStore: options.externalTaskStore } : config;
|
|
73787
75247
|
this.runtime = new InProcessRuntime(runtimeConfig, centralCore);
|
|
75248
|
+
this.runtime.setMergeEnqueuer?.((taskId) => {
|
|
75249
|
+
if (this.activeMergeTaskId === taskId) {
|
|
75250
|
+
this.mergeAbortController?.abort();
|
|
75251
|
+
this.mergeAbortController = null;
|
|
75252
|
+
this.activeMergeSession?.dispose();
|
|
75253
|
+
this.activeMergeSession = null;
|
|
75254
|
+
this.activeMergeTaskId = null;
|
|
75255
|
+
}
|
|
75256
|
+
this.mergeActive.delete(taskId);
|
|
75257
|
+
this.internalEnqueueMerge(taskId);
|
|
75258
|
+
});
|
|
73788
75259
|
}
|
|
73789
75260
|
runtime;
|
|
73790
75261
|
prMonitor;
|
|
73791
75262
|
prCommentHandler;
|
|
73792
75263
|
notifier;
|
|
75264
|
+
notificationService;
|
|
75265
|
+
gridlockDetector;
|
|
73793
75266
|
cronRunner;
|
|
73794
75267
|
automationStore;
|
|
73795
75268
|
remoteTunnelManager;
|
|
@@ -73854,12 +75327,21 @@ var init_project_engine = __esm({
|
|
|
73854
75327
|
(taskId, prInfo, comments) => this.prCommentHandler.handleNewComments(taskId, prInfo, comments)
|
|
73855
75328
|
);
|
|
73856
75329
|
if (!this.options.skipNotifier) {
|
|
75330
|
+
this.notificationService = new NotificationService(store, {
|
|
75331
|
+
projectId: this.options.projectId,
|
|
75332
|
+
ntfyBaseUrl: this.options.ntfyBaseUrl
|
|
75333
|
+
});
|
|
75334
|
+
await this.notificationService.start();
|
|
73857
75335
|
this.notifier = new NtfyNotifier(store, {
|
|
73858
75336
|
projectId: this.options.projectId,
|
|
73859
75337
|
ntfyBaseUrl: this.options.ntfyBaseUrl
|
|
73860
75338
|
});
|
|
73861
75339
|
await this.notifier.start();
|
|
73862
75340
|
}
|
|
75341
|
+
this.gridlockDetector = new GridlockDetector(store, {
|
|
75342
|
+
onGridlock: (event) => this.notifier?.notifyGridlock(event)
|
|
75343
|
+
});
|
|
75344
|
+
this.gridlockDetector.start();
|
|
73863
75345
|
this.setAutomationSubsystemHealth(
|
|
73864
75346
|
"initializing",
|
|
73865
75347
|
"Initializing AutomationStore and CronRunner"
|
|
@@ -73990,7 +75472,9 @@ ${detail}`
|
|
|
73990
75472
|
}
|
|
73991
75473
|
} catch {
|
|
73992
75474
|
}
|
|
75475
|
+
this.notificationService?.stop();
|
|
73993
75476
|
this.notifier?.stop();
|
|
75477
|
+
this.gridlockDetector?.stop();
|
|
73994
75478
|
this.cronRunner?.stop();
|
|
73995
75479
|
this.setAutomationSubsystemHealth("not-initialized", "Automation subsystem stopped");
|
|
73996
75480
|
const tunnelManager = this.remoteTunnelManager;
|
|
@@ -75809,6 +77293,8 @@ __export(src_exports2, {
|
|
|
75809
77293
|
MissionAutopilot: () => MissionAutopilot,
|
|
75810
77294
|
MissionExecutionLoop: () => MissionExecutionLoop,
|
|
75811
77295
|
NodeHealthMonitor: () => NodeHealthMonitor,
|
|
77296
|
+
NotificationService: () => NotificationService,
|
|
77297
|
+
NtfyNotificationProvider: () => NtfyNotificationProvider,
|
|
75812
77298
|
NtfyNotifier: () => NtfyNotifier,
|
|
75813
77299
|
PRIORITY_EXECUTE: () => PRIORITY_EXECUTE,
|
|
75814
77300
|
PRIORITY_MERGE: () => PRIORITY_MERGE,
|
|
@@ -75833,6 +77319,7 @@ __export(src_exports2, {
|
|
|
75833
77319
|
TriageProcessor: () => TriageProcessor,
|
|
75834
77320
|
TunnelProcessManager: () => TunnelProcessManager,
|
|
75835
77321
|
UsageLimitPauser: () => UsageLimitPauser,
|
|
77322
|
+
WebhookNotificationProvider: () => WebhookNotificationProvider,
|
|
75836
77323
|
WorktreePool: () => WorktreePool,
|
|
75837
77324
|
aiMergeTask: () => aiMergeTask,
|
|
75838
77325
|
buildAgentChatPrompt: () => buildAgentChatPrompt,
|
|
@@ -75851,6 +77338,7 @@ __export(src_exports2, {
|
|
|
75851
77338
|
createTaskLogTool: () => createTaskLogTool,
|
|
75852
77339
|
describeAgentModel: () => describeAgentModel,
|
|
75853
77340
|
describeModel: () => describeModel,
|
|
77341
|
+
formatTaskIdentifier: () => formatTaskIdentifier,
|
|
75854
77342
|
getDefaultPiRuntime: () => getDefaultPiRuntime,
|
|
75855
77343
|
getHostExtensionPaths: () => getHostExtensionPaths,
|
|
75856
77344
|
getTunnelProviderAdapter: () => getTunnelProviderAdapter,
|
|
@@ -75902,6 +77390,7 @@ var init_src2 = __esm({
|
|
|
75902
77390
|
init_pr_monitor();
|
|
75903
77391
|
init_pr_comment_handler();
|
|
75904
77392
|
init_notifier();
|
|
77393
|
+
init_notification();
|
|
75905
77394
|
init_cron_runner();
|
|
75906
77395
|
init_routine_runner();
|
|
75907
77396
|
init_routine_scheduler();
|
|
@@ -76087,6 +77576,7 @@ async function ensureNtfyHelpersReady() {
|
|
|
76087
77576
|
}
|
|
76088
77577
|
try {
|
|
76089
77578
|
const engine = await Promise.resolve().then(() => (init_src2(), src_exports2));
|
|
77579
|
+
const hasNotificationService = "NotificationService" in engine && typeof engine.NotificationService === "function";
|
|
76090
77580
|
const hasAllHelpers = "isNtfyEventEnabled" in engine && "buildNtfyClickUrl" in engine && "sendNtfyNotification" in engine && typeof engine.isNtfyEventEnabled === "function" && typeof engine.buildNtfyClickUrl === "function" && typeof engine.sendNtfyNotification === "function";
|
|
76091
77581
|
if (!hasAllHelpers) {
|
|
76092
77582
|
return;
|
|
@@ -76096,6 +77586,12 @@ async function ensureNtfyHelpersReady() {
|
|
|
76096
77586
|
buildNtfyClickUrl: engine.buildNtfyClickUrl,
|
|
76097
77587
|
sendNtfyNotification: engine.sendNtfyNotification
|
|
76098
77588
|
};
|
|
77589
|
+
if (hasNotificationService) {
|
|
77590
|
+
diagnostics.info(
|
|
77591
|
+
"NotificationService abstraction detected in engine",
|
|
77592
|
+
{ operation: "notification-service-detection" }
|
|
77593
|
+
);
|
|
77594
|
+
}
|
|
76099
77595
|
} catch {
|
|
76100
77596
|
}
|
|
76101
77597
|
}
|
|
@@ -76118,6 +77614,11 @@ function cleanupInMemorySession(sessionId) {
|
|
|
76118
77614
|
if (!session) {
|
|
76119
77615
|
return false;
|
|
76120
77616
|
}
|
|
77617
|
+
const activeGeneration = activeGenerations.get(sessionId);
|
|
77618
|
+
if (activeGeneration) {
|
|
77619
|
+
clearTimeout(activeGeneration.timer);
|
|
77620
|
+
activeGenerations.delete(sessionId);
|
|
77621
|
+
}
|
|
76121
77622
|
if (session.agent) {
|
|
76122
77623
|
try {
|
|
76123
77624
|
session.agent.session.dispose?.();
|
|
@@ -76451,109 +77952,157 @@ async function maybeNotifyPlanningAwaitingInput(session, question) {
|
|
|
76451
77952
|
});
|
|
76452
77953
|
}
|
|
76453
77954
|
}
|
|
77955
|
+
function setSessionError(session, message) {
|
|
77956
|
+
session.error = message;
|
|
77957
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
77958
|
+
persistSession(session, "error", message);
|
|
77959
|
+
planningStreamManager.broadcast(session.id, {
|
|
77960
|
+
type: "error",
|
|
77961
|
+
data: message
|
|
77962
|
+
});
|
|
77963
|
+
}
|
|
77964
|
+
function createAbortError() {
|
|
77965
|
+
const error = new Error("Generation aborted");
|
|
77966
|
+
error.name = "AbortError";
|
|
77967
|
+
return error;
|
|
77968
|
+
}
|
|
77969
|
+
async function runGenerationWithTimeout(session, operation) {
|
|
77970
|
+
const existing = activeGenerations.get(session.id);
|
|
77971
|
+
if (existing) {
|
|
77972
|
+
clearTimeout(existing.timer);
|
|
77973
|
+
existing.abortController.abort();
|
|
77974
|
+
}
|
|
77975
|
+
const abortController = new AbortController();
|
|
77976
|
+
let timeoutTriggered = false;
|
|
77977
|
+
const timer = setTimeout(() => {
|
|
77978
|
+
timeoutTriggered = true;
|
|
77979
|
+
setSessionError(session, "AI generation timed out. You can retry or start a new session.");
|
|
77980
|
+
abortController.abort();
|
|
77981
|
+
}, GENERATION_TIMEOUT_MS);
|
|
77982
|
+
activeGenerations.set(session.id, { abortController, timer });
|
|
77983
|
+
const abortPromise = new Promise((_, reject) => {
|
|
77984
|
+
abortController.signal.addEventListener(
|
|
77985
|
+
"abort",
|
|
77986
|
+
() => reject(createAbortError()),
|
|
77987
|
+
{ once: true }
|
|
77988
|
+
);
|
|
77989
|
+
});
|
|
77990
|
+
try {
|
|
77991
|
+
return await Promise.race([operation(), abortPromise]);
|
|
77992
|
+
} catch (error) {
|
|
77993
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
77994
|
+
if (!timeoutTriggered && !session.error) {
|
|
77995
|
+
setSessionError(session, "Generation stopped by user. You can retry or start a new session.");
|
|
77996
|
+
}
|
|
77997
|
+
}
|
|
77998
|
+
throw error;
|
|
77999
|
+
} finally {
|
|
78000
|
+
clearTimeout(timer);
|
|
78001
|
+
activeGenerations.delete(session.id);
|
|
78002
|
+
}
|
|
78003
|
+
}
|
|
76454
78004
|
async function continueAgentConversation(session, message) {
|
|
76455
78005
|
if (!session.agent) {
|
|
76456
78006
|
throw new InvalidSessionStateError("AI agent not initialized");
|
|
76457
78007
|
}
|
|
76458
78008
|
try {
|
|
76459
|
-
session
|
|
76460
|
-
|
|
76461
|
-
|
|
76462
|
-
|
|
76463
|
-
|
|
76464
|
-
if (
|
|
76465
|
-
|
|
76466
|
-
|
|
76467
|
-
|
|
78009
|
+
await runGenerationWithTimeout(session, async () => {
|
|
78010
|
+
session.thinkingOutput = "";
|
|
78011
|
+
await session.agent.session.prompt(message);
|
|
78012
|
+
const lastMessage = session.agent.session.state.messages.filter((m) => m.role === "assistant").pop();
|
|
78013
|
+
let responseText = session.thinkingOutput;
|
|
78014
|
+
if (lastMessage?.content) {
|
|
78015
|
+
if (typeof lastMessage.content === "string") {
|
|
78016
|
+
responseText = lastMessage.content;
|
|
78017
|
+
} else if (Array.isArray(lastMessage.content)) {
|
|
78018
|
+
responseText = lastMessage.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
78019
|
+
}
|
|
76468
78020
|
}
|
|
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.'
|
|
78021
|
+
let parsed;
|
|
78022
|
+
let lastError;
|
|
78023
|
+
for (let attempt = 0; attempt <= MAX_PARSE_RETRIES; attempt++) {
|
|
78024
|
+
try {
|
|
78025
|
+
parsed = parseAgentResponse(responseText);
|
|
78026
|
+
break;
|
|
78027
|
+
} catch (err) {
|
|
78028
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
78029
|
+
if (attempt < MAX_PARSE_RETRIES) {
|
|
78030
|
+
diagnostics.warn(
|
|
78031
|
+
"Parse attempt failed, requesting reformat",
|
|
78032
|
+
{ sessionId: session.id, attempt: attempt + 1, operation: "parse-retry" }
|
|
76487
78033
|
);
|
|
76488
|
-
|
|
76489
|
-
|
|
76490
|
-
|
|
76491
|
-
|
|
76492
|
-
|
|
76493
|
-
|
|
76494
|
-
|
|
78034
|
+
try {
|
|
78035
|
+
session.thinkingOutput = "";
|
|
78036
|
+
await session.agent.session.prompt(
|
|
78037
|
+
'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.'
|
|
78038
|
+
);
|
|
78039
|
+
const retryMessage = session.agent.session.state.messages.filter((m) => m.role === "assistant").pop();
|
|
78040
|
+
let retryText = session.thinkingOutput;
|
|
78041
|
+
if (retryMessage?.content) {
|
|
78042
|
+
if (typeof retryMessage.content === "string") {
|
|
78043
|
+
retryText = retryMessage.content;
|
|
78044
|
+
} else if (Array.isArray(retryMessage.content)) {
|
|
78045
|
+
retryText = retryMessage.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
78046
|
+
}
|
|
76495
78047
|
}
|
|
78048
|
+
responseText = retryText;
|
|
78049
|
+
} catch (retryErr) {
|
|
78050
|
+
diagnostics.errorFromException(
|
|
78051
|
+
"Retry prompt failed for session",
|
|
78052
|
+
retryErr,
|
|
78053
|
+
{ sessionId: session.id, operation: "retry-prompt" }
|
|
78054
|
+
);
|
|
78055
|
+
break;
|
|
76496
78056
|
}
|
|
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
78057
|
}
|
|
76506
78058
|
}
|
|
76507
78059
|
}
|
|
76508
|
-
|
|
76509
|
-
|
|
76510
|
-
|
|
76511
|
-
|
|
76512
|
-
|
|
76513
|
-
|
|
76514
|
-
|
|
76515
|
-
|
|
76516
|
-
|
|
76517
|
-
|
|
76518
|
-
|
|
76519
|
-
|
|
76520
|
-
|
|
76521
|
-
|
|
78060
|
+
if (!parsed) {
|
|
78061
|
+
const errorMsg = `${lastError?.message || "Failed to parse AI response"} You can try responding again or start a new planning session.`;
|
|
78062
|
+
diagnostics.error(
|
|
78063
|
+
"All parse attempts exhausted for session",
|
|
78064
|
+
{ sessionId: session.id, message: errorMsg, operation: "parse-exhausted" }
|
|
78065
|
+
);
|
|
78066
|
+
session.error = errorMsg;
|
|
78067
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78068
|
+
persistSession(session, "error", errorMsg);
|
|
78069
|
+
planningStreamManager.broadcast(session.id, {
|
|
78070
|
+
type: "error",
|
|
78071
|
+
data: errorMsg
|
|
78072
|
+
});
|
|
78073
|
+
return;
|
|
78074
|
+
}
|
|
78075
|
+
if (parsed.type === "question") {
|
|
78076
|
+
session.currentQuestion = parsed.data;
|
|
78077
|
+
session.error = void 0;
|
|
78078
|
+
session.lastGeneratedThinking = session.thinkingOutput;
|
|
78079
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78080
|
+
persistSession(session, "awaiting_input");
|
|
78081
|
+
void maybeNotifyPlanningAwaitingInput(session, parsed.data);
|
|
78082
|
+
planningStreamManager.broadcast(session.id, {
|
|
78083
|
+
type: "question",
|
|
78084
|
+
data: parsed.data
|
|
78085
|
+
});
|
|
78086
|
+
} else if (parsed.type === "complete") {
|
|
78087
|
+
session.summary = parsed.data;
|
|
78088
|
+
session.currentQuestion = void 0;
|
|
78089
|
+
session.error = void 0;
|
|
78090
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
78091
|
+
persistSession(session, "complete");
|
|
78092
|
+
planningStreamManager.broadcast(session.id, {
|
|
78093
|
+
type: "summary",
|
|
78094
|
+
data: parsed.data
|
|
78095
|
+
});
|
|
78096
|
+
planningStreamManager.broadcast(session.id, { type: "complete" });
|
|
78097
|
+
}
|
|
78098
|
+
});
|
|
78099
|
+
} catch (err) {
|
|
78100
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
76522
78101
|
return;
|
|
76523
78102
|
}
|
|
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
78103
|
const errorMessage = err instanceof Error ? err.message : "AI processing failed";
|
|
76549
78104
|
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
|
-
});
|
|
78105
|
+
setSessionError(session, errorMessage);
|
|
76557
78106
|
}
|
|
76558
78107
|
}
|
|
76559
78108
|
function extractJsonCandidate(text) {
|
|
@@ -76825,7 +78374,7 @@ function getSession(sessionId) {
|
|
|
76825
78374
|
return void 0;
|
|
76826
78375
|
}
|
|
76827
78376
|
}
|
|
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;
|
|
78377
|
+
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
78378
|
var init_planning = __esm({
|
|
76830
78379
|
"../dashboard/src/planning.ts"() {
|
|
76831
78380
|
"use strict";
|
|
@@ -76896,8 +78445,10 @@ For completion:
|
|
|
76896
78445
|
CLEANUP_INTERVAL_MS2 = 5 * 60 * 1e3;
|
|
76897
78446
|
MAX_SESSIONS_PER_IP_PER_HOUR = 5;
|
|
76898
78447
|
RATE_LIMIT_WINDOW_MS2 = 60 * 60 * 1e3;
|
|
78448
|
+
GENERATION_TIMEOUT_MS = 12e4;
|
|
76899
78449
|
sessions = /* @__PURE__ */ new Map();
|
|
76900
78450
|
rateLimits2 = /* @__PURE__ */ new Map();
|
|
78451
|
+
activeGenerations = /* @__PURE__ */ new Map();
|
|
76901
78452
|
cleanupInterval2 = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS2);
|
|
76902
78453
|
cleanupInterval2.unref?.();
|
|
76903
78454
|
process.on("beforeExit", () => {
|
|
@@ -77035,6 +78586,108 @@ var init_ai_session_store = __esm({
|
|
|
77035
78586
|
}
|
|
77036
78587
|
});
|
|
77037
78588
|
|
|
78589
|
+
// ../dashboard/src/ai-session-timeout.ts
|
|
78590
|
+
function createAbortError2() {
|
|
78591
|
+
const error = new Error("Generation aborted");
|
|
78592
|
+
error.name = "AbortError";
|
|
78593
|
+
return error;
|
|
78594
|
+
}
|
|
78595
|
+
function isAbortError(err) {
|
|
78596
|
+
return err instanceof Error && err.name === "AbortError";
|
|
78597
|
+
}
|
|
78598
|
+
var GenerationGuard;
|
|
78599
|
+
var init_ai_session_timeout = __esm({
|
|
78600
|
+
"../dashboard/src/ai-session-timeout.ts"() {
|
|
78601
|
+
"use strict";
|
|
78602
|
+
GenerationGuard = class {
|
|
78603
|
+
active = /* @__PURE__ */ new Map();
|
|
78604
|
+
/**
|
|
78605
|
+
* Tracks the cause of a pending abort so the original `run()` can
|
|
78606
|
+
* distinguish a user-initiated stop from re-entrant displacement. The flag
|
|
78607
|
+
* is consumed (deleted) by the catch block of the displaced `run()`.
|
|
78608
|
+
*/
|
|
78609
|
+
abortCause = /* @__PURE__ */ new Map();
|
|
78610
|
+
/**
|
|
78611
|
+
* Wrap `op` with a timeout + abort. If a previous generation is still
|
|
78612
|
+
* registered for the same id, it is aborted first (the prior `run()`
|
|
78613
|
+
* rejects with `AbortError`, marked as `displaced` so its `onUserStop`
|
|
78614
|
+
* handler does NOT fire).
|
|
78615
|
+
*/
|
|
78616
|
+
async run(sessionId, timeoutMs, handlers, op) {
|
|
78617
|
+
this.cancelInternal(sessionId, "displaced");
|
|
78618
|
+
const abort = new AbortController();
|
|
78619
|
+
const timer = setTimeout(() => {
|
|
78620
|
+
this.abortCause.set(abort, "timeout");
|
|
78621
|
+
try {
|
|
78622
|
+
handlers.onTimeout();
|
|
78623
|
+
} catch {
|
|
78624
|
+
}
|
|
78625
|
+
abort.abort();
|
|
78626
|
+
}, timeoutMs);
|
|
78627
|
+
const entry = { abort, timer };
|
|
78628
|
+
this.active.set(sessionId, entry);
|
|
78629
|
+
const abortPromise = new Promise((_, reject) => {
|
|
78630
|
+
abort.signal.addEventListener(
|
|
78631
|
+
"abort",
|
|
78632
|
+
() => reject(createAbortError2()),
|
|
78633
|
+
{ once: true }
|
|
78634
|
+
);
|
|
78635
|
+
});
|
|
78636
|
+
try {
|
|
78637
|
+
return await Promise.race([op(), abortPromise]);
|
|
78638
|
+
} catch (err) {
|
|
78639
|
+
if (isAbortError(err)) {
|
|
78640
|
+
const cause = this.abortCause.get(abort) ?? "user-stop";
|
|
78641
|
+
if (cause === "user-stop") {
|
|
78642
|
+
try {
|
|
78643
|
+
handlers.onUserStop?.();
|
|
78644
|
+
} catch {
|
|
78645
|
+
}
|
|
78646
|
+
}
|
|
78647
|
+
}
|
|
78648
|
+
throw err;
|
|
78649
|
+
} finally {
|
|
78650
|
+
clearTimeout(timer);
|
|
78651
|
+
this.abortCause.delete(abort);
|
|
78652
|
+
if (this.active.get(sessionId) === entry) {
|
|
78653
|
+
this.active.delete(sessionId);
|
|
78654
|
+
}
|
|
78655
|
+
}
|
|
78656
|
+
}
|
|
78657
|
+
/** AbortSignal of the in-flight generation, if any — for tools that honor it. */
|
|
78658
|
+
signal(sessionId) {
|
|
78659
|
+
return this.active.get(sessionId)?.abort.signal;
|
|
78660
|
+
}
|
|
78661
|
+
has(sessionId) {
|
|
78662
|
+
return this.active.has(sessionId);
|
|
78663
|
+
}
|
|
78664
|
+
/**
|
|
78665
|
+
* Manually abort the active generation. Returns true if there was one.
|
|
78666
|
+
* The wrapped operation will reject with `AbortError`, and the `onUserStop`
|
|
78667
|
+
* handler from the original `run()` call will fire.
|
|
78668
|
+
*/
|
|
78669
|
+
stop(sessionId) {
|
|
78670
|
+
return this.cancelInternal(sessionId, "user-stop");
|
|
78671
|
+
}
|
|
78672
|
+
/** Reset all in-flight generations (test/shutdown only). */
|
|
78673
|
+
reset() {
|
|
78674
|
+
for (const sessionId of [...this.active.keys()]) {
|
|
78675
|
+
this.cancelInternal(sessionId, "user-stop");
|
|
78676
|
+
}
|
|
78677
|
+
}
|
|
78678
|
+
cancelInternal(sessionId, cause) {
|
|
78679
|
+
const entry = this.active.get(sessionId);
|
|
78680
|
+
if (!entry) return false;
|
|
78681
|
+
clearTimeout(entry.timer);
|
|
78682
|
+
this.abortCause.set(entry.abort, cause);
|
|
78683
|
+
entry.abort.abort();
|
|
78684
|
+
this.active.delete(sessionId);
|
|
78685
|
+
return true;
|
|
78686
|
+
}
|
|
78687
|
+
};
|
|
78688
|
+
}
|
|
78689
|
+
});
|
|
78690
|
+
|
|
77038
78691
|
// ../dashboard/src/subtask-breakdown.ts
|
|
77039
78692
|
import { EventEmitter as EventEmitter24 } from "node:events";
|
|
77040
78693
|
function cleanupInMemorySubtaskSession(sessionId) {
|
|
@@ -77042,6 +78695,7 @@ function cleanupInMemorySubtaskSession(sessionId) {
|
|
|
77042
78695
|
if (!session) {
|
|
77043
78696
|
return false;
|
|
77044
78697
|
}
|
|
78698
|
+
generationGuard.stop(sessionId);
|
|
77045
78699
|
try {
|
|
77046
78700
|
session.agent?.session?.dispose?.();
|
|
77047
78701
|
} catch {
|
|
@@ -77058,17 +78712,19 @@ function cleanupExpiredSessions2() {
|
|
|
77058
78712
|
}
|
|
77059
78713
|
}
|
|
77060
78714
|
}
|
|
77061
|
-
var diagnostics3, SESSION_TTL_MS2, CLEANUP_INTERVAL_MS3, sessions2, cleanupInterval3, SubtaskStreamManager, subtaskStreamManager;
|
|
78715
|
+
var diagnostics3, SESSION_TTL_MS2, CLEANUP_INTERVAL_MS3, generationGuard, sessions2, cleanupInterval3, SubtaskStreamManager, subtaskStreamManager;
|
|
77062
78716
|
var init_subtask_breakdown = __esm({
|
|
77063
78717
|
"../dashboard/src/subtask-breakdown.ts"() {
|
|
77064
78718
|
"use strict";
|
|
77065
78719
|
init_src();
|
|
77066
78720
|
init_sse_buffer();
|
|
77067
78721
|
init_ai_session_diagnostics();
|
|
78722
|
+
init_ai_session_timeout();
|
|
77068
78723
|
init_src2();
|
|
77069
78724
|
diagnostics3 = createSessionDiagnostics("subtask-breakdown");
|
|
77070
78725
|
SESSION_TTL_MS2 = 7 * 24 * 60 * 60 * 1e3;
|
|
77071
78726
|
CLEANUP_INTERVAL_MS3 = 5 * 60 * 1e3;
|
|
78727
|
+
generationGuard = new GenerationGuard();
|
|
77072
78728
|
sessions2 = /* @__PURE__ */ new Map();
|
|
77073
78729
|
cleanupInterval3 = setInterval(cleanupExpiredSessions2, CLEANUP_INTERVAL_MS3);
|
|
77074
78730
|
cleanupInterval3.unref?.();
|
|
@@ -77143,6 +78799,7 @@ function cleanupInMemoryMissionSession(sessionId) {
|
|
|
77143
78799
|
if (!session) {
|
|
77144
78800
|
return false;
|
|
77145
78801
|
}
|
|
78802
|
+
generationGuard2.stop(sessionId);
|
|
77146
78803
|
if (session.agent) {
|
|
77147
78804
|
try {
|
|
77148
78805
|
session.agent.session.dispose?.();
|
|
@@ -77167,18 +78824,20 @@ function cleanupExpiredSessions3() {
|
|
|
77167
78824
|
}
|
|
77168
78825
|
}
|
|
77169
78826
|
}
|
|
77170
|
-
var diagnostics4, SESSION_TTL_MS3, CLEANUP_INTERVAL_MS4, RATE_LIMIT_WINDOW_MS3, sessions3, rateLimits3, cleanupInterval4, MissionInterviewStreamManager, missionInterviewStreamManager;
|
|
78827
|
+
var diagnostics4, SESSION_TTL_MS3, CLEANUP_INTERVAL_MS4, RATE_LIMIT_WINDOW_MS3, generationGuard2, sessions3, rateLimits3, cleanupInterval4, MissionInterviewStreamManager, missionInterviewStreamManager;
|
|
77171
78828
|
var init_mission_interview = __esm({
|
|
77172
78829
|
"../dashboard/src/mission-interview.ts"() {
|
|
77173
78830
|
"use strict";
|
|
77174
78831
|
init_src();
|
|
77175
78832
|
init_sse_buffer();
|
|
77176
78833
|
init_ai_session_diagnostics();
|
|
78834
|
+
init_ai_session_timeout();
|
|
77177
78835
|
init_src2();
|
|
77178
78836
|
diagnostics4 = createSessionDiagnostics("mission-interview");
|
|
77179
78837
|
SESSION_TTL_MS3 = 7 * 24 * 60 * 60 * 1e3;
|
|
77180
78838
|
CLEANUP_INTERVAL_MS4 = 5 * 60 * 1e3;
|
|
77181
78839
|
RATE_LIMIT_WINDOW_MS3 = 60 * 60 * 1e3;
|
|
78840
|
+
generationGuard2 = new GenerationGuard();
|
|
77182
78841
|
sessions3 = /* @__PURE__ */ new Map();
|
|
77183
78842
|
rateLimits3 = /* @__PURE__ */ new Map();
|
|
77184
78843
|
cleanupInterval4 = setInterval(cleanupExpiredSessions3, CLEANUP_INTERVAL_MS4);
|
|
@@ -77258,6 +78917,7 @@ function cleanupInMemorySession2(sessionId) {
|
|
|
77258
78917
|
if (!session) {
|
|
77259
78918
|
return false;
|
|
77260
78919
|
}
|
|
78920
|
+
generationGuard3.stop(sessionId);
|
|
77261
78921
|
if (session.agent) {
|
|
77262
78922
|
try {
|
|
77263
78923
|
session.agent.session.dispose?.();
|
|
@@ -77282,19 +78942,21 @@ function cleanupExpiredSessions4() {
|
|
|
77282
78942
|
}
|
|
77283
78943
|
}
|
|
77284
78944
|
}
|
|
77285
|
-
var diagnostics5, SESSION_TTL_MS4, CLEANUP_INTERVAL_MS5, RATE_LIMIT_WINDOW_MS4, sessions4, rateLimits4, cleanupInterval5, MilestoneSliceInterviewStreamManager, milestoneSliceInterviewStreamManager;
|
|
78945
|
+
var diagnostics5, SESSION_TTL_MS4, CLEANUP_INTERVAL_MS5, RATE_LIMIT_WINDOW_MS4, generationGuard3, sessions4, rateLimits4, cleanupInterval5, MilestoneSliceInterviewStreamManager, milestoneSliceInterviewStreamManager;
|
|
77286
78946
|
var init_milestone_slice_interview = __esm({
|
|
77287
78947
|
"../dashboard/src/milestone-slice-interview.ts"() {
|
|
77288
78948
|
"use strict";
|
|
77289
78949
|
init_sse_buffer();
|
|
77290
78950
|
init_mission_interview();
|
|
77291
78951
|
init_ai_session_diagnostics();
|
|
78952
|
+
init_ai_session_timeout();
|
|
77292
78953
|
init_mission_interview();
|
|
77293
78954
|
init_src2();
|
|
77294
78955
|
diagnostics5 = createSessionDiagnostics("milestone-slice-interview");
|
|
77295
78956
|
SESSION_TTL_MS4 = 7 * 24 * 60 * 60 * 1e3;
|
|
77296
78957
|
CLEANUP_INTERVAL_MS5 = 5 * 60 * 1e3;
|
|
77297
78958
|
RATE_LIMIT_WINDOW_MS4 = 60 * 60 * 1e3;
|
|
78959
|
+
generationGuard3 = new GenerationGuard();
|
|
77298
78960
|
sessions4 = /* @__PURE__ */ new Map();
|
|
77299
78961
|
rateLimits4 = /* @__PURE__ */ new Map();
|
|
77300
78962
|
cleanupInterval5 = setInterval(cleanupExpiredSessions4, CLEANUP_INTERVAL_MS5);
|
|
@@ -78139,6 +79801,7 @@ var init_register_task_workflow_routes = __esm({
|
|
|
78139
79801
|
var init_register_planning_subtask_routes = __esm({
|
|
78140
79802
|
"../dashboard/src/routes/register-planning-subtask-routes.ts"() {
|
|
78141
79803
|
"use strict";
|
|
79804
|
+
init_src();
|
|
78142
79805
|
init_api_error();
|
|
78143
79806
|
init_sse_buffer();
|
|
78144
79807
|
}
|
|
@@ -80381,16 +82044,26 @@ var init_register_agents_projects_nodes = __esm({
|
|
|
80381
82044
|
}
|
|
80382
82045
|
});
|
|
80383
82046
|
|
|
80384
|
-
// ../dashboard/src/
|
|
82047
|
+
// ../dashboard/src/exec-file.ts
|
|
80385
82048
|
import { execFile as execFile5 } from "node:child_process";
|
|
80386
|
-
import * as fsPromises from "node:fs/promises";
|
|
80387
82049
|
import { promisify as promisify12 } from "node:util";
|
|
80388
|
-
var
|
|
82050
|
+
var execFileAsync3;
|
|
82051
|
+
var init_exec_file = __esm({
|
|
82052
|
+
"../dashboard/src/exec-file.ts"() {
|
|
82053
|
+
"use strict";
|
|
82054
|
+
execFileAsync3 = promisify12(execFile5);
|
|
82055
|
+
}
|
|
82056
|
+
});
|
|
82057
|
+
|
|
82058
|
+
// ../dashboard/src/routes/register-project-routes.ts
|
|
82059
|
+
import * as fsPromises from "node:fs/promises";
|
|
82060
|
+
var access3, stat6, mkdir12, readdir8, rm2;
|
|
80389
82061
|
var init_register_project_routes = __esm({
|
|
80390
82062
|
"../dashboard/src/routes/register-project-routes.ts"() {
|
|
80391
82063
|
"use strict";
|
|
80392
82064
|
init_src();
|
|
80393
82065
|
init_api_error();
|
|
82066
|
+
init_exec_file();
|
|
80394
82067
|
init_project_store_resolver();
|
|
80395
82068
|
({
|
|
80396
82069
|
access: access3,
|
|
@@ -80399,7 +82072,6 @@ var init_register_project_routes = __esm({
|
|
|
80399
82072
|
readdir: readdir8,
|
|
80400
82073
|
rm: rm2
|
|
80401
82074
|
} = fsPromises);
|
|
80402
|
-
execFileAsync3 = promisify12(execFile5);
|
|
80403
82075
|
}
|
|
80404
82076
|
});
|
|
80405
82077
|
|
|
@@ -85738,12 +87410,29 @@ var init_project_context = __esm({
|
|
|
85738
87410
|
}
|
|
85739
87411
|
});
|
|
85740
87412
|
|
|
87413
|
+
// src/commands/node.ts
|
|
87414
|
+
import { createInterface } from "node:readline/promises";
|
|
87415
|
+
async function findNodeByNameOrId(central, nameOrId) {
|
|
87416
|
+
const byId = await central.getNode(nameOrId);
|
|
87417
|
+
if (byId) {
|
|
87418
|
+
return byId;
|
|
87419
|
+
}
|
|
87420
|
+
return central.getNodeByName(nameOrId);
|
|
87421
|
+
}
|
|
87422
|
+
var init_node = __esm({
|
|
87423
|
+
"src/commands/node.ts"() {
|
|
87424
|
+
"use strict";
|
|
87425
|
+
init_src();
|
|
87426
|
+
}
|
|
87427
|
+
});
|
|
87428
|
+
|
|
85741
87429
|
// src/commands/task.ts
|
|
85742
87430
|
var task_exports = {};
|
|
85743
87431
|
__export(task_exports, {
|
|
85744
87432
|
fetchGitHubIssues: () => fetchGitHubIssues,
|
|
85745
87433
|
runTaskArchive: () => runTaskArchive,
|
|
85746
87434
|
runTaskAttach: () => runTaskAttach,
|
|
87435
|
+
runTaskClearNode: () => runTaskClearNode,
|
|
85747
87436
|
runTaskComment: () => runTaskComment,
|
|
85748
87437
|
runTaskComments: () => runTaskComments,
|
|
85749
87438
|
runTaskCreate: () => runTaskCreate,
|
|
@@ -85761,13 +87450,14 @@ __export(task_exports, {
|
|
|
85761
87450
|
runTaskPrCreate: () => runTaskPrCreate,
|
|
85762
87451
|
runTaskRefine: () => runTaskRefine,
|
|
85763
87452
|
runTaskRetry: () => runTaskRetry,
|
|
87453
|
+
runTaskSetNode: () => runTaskSetNode,
|
|
85764
87454
|
runTaskShow: () => runTaskShow,
|
|
85765
87455
|
runTaskSteer: () => runTaskSteer,
|
|
85766
87456
|
runTaskUnarchive: () => runTaskUnarchive,
|
|
85767
87457
|
runTaskUnpause: () => runTaskUnpause,
|
|
85768
87458
|
runTaskUpdate: () => runTaskUpdate
|
|
85769
87459
|
});
|
|
85770
|
-
import { createInterface } from "node:readline/promises";
|
|
87460
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
85771
87461
|
import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync30, readFileSync as readFileSync9 } from "node:fs";
|
|
85772
87462
|
import { join as join37 } from "node:path";
|
|
85773
87463
|
function asLocalProjectContext(store) {
|
|
@@ -85832,11 +87522,28 @@ async function getProjectContext(projectName) {
|
|
|
85832
87522
|
async function getProjectPath(projectName) {
|
|
85833
87523
|
return (await getCommandContext(projectName)).projectPath;
|
|
85834
87524
|
}
|
|
85835
|
-
async function
|
|
87525
|
+
async function resolveNodeByNameOrId(nodeNameOrId) {
|
|
87526
|
+
const central = new CentralCore();
|
|
87527
|
+
await central.init();
|
|
87528
|
+
try {
|
|
87529
|
+
const looksLikeNodeId = nodeNameOrId.includes("-") && nodeNameOrId.length > 20;
|
|
87530
|
+
let node = looksLikeNodeId ? await central.getNode(nodeNameOrId) : await central.getNodeByName(nodeNameOrId);
|
|
87531
|
+
if (!node) {
|
|
87532
|
+
node = await findNodeByNameOrId(central, nodeNameOrId);
|
|
87533
|
+
}
|
|
87534
|
+
if (!node) {
|
|
87535
|
+
throw new Error(`Node not found: ${nodeNameOrId}`);
|
|
87536
|
+
}
|
|
87537
|
+
return { id: node.id, name: node.name };
|
|
87538
|
+
} finally {
|
|
87539
|
+
await central.close();
|
|
87540
|
+
}
|
|
87541
|
+
}
|
|
87542
|
+
async function runTaskCreate(descriptionArg, attachFiles, depends, projectName, nodeName) {
|
|
85836
87543
|
let description = descriptionArg;
|
|
85837
87544
|
const projectContext = await getProjectContext(projectName);
|
|
85838
87545
|
if (!description) {
|
|
85839
|
-
const rl =
|
|
87546
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
85840
87547
|
description = await rl.question("Task description: ");
|
|
85841
87548
|
rl.close();
|
|
85842
87549
|
}
|
|
@@ -85846,6 +87553,16 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
|
|
|
85846
87553
|
}
|
|
85847
87554
|
const store = projectContext?.store ?? await getStore(projectName);
|
|
85848
87555
|
const task = await store.createTask({ description: description.trim(), dependencies: depends });
|
|
87556
|
+
let resolvedNode;
|
|
87557
|
+
if (nodeName) {
|
|
87558
|
+
try {
|
|
87559
|
+
resolvedNode = await resolveNodeByNameOrId(nodeName);
|
|
87560
|
+
await store.updateTask(task.id, { nodeId: resolvedNode.id });
|
|
87561
|
+
} catch (error) {
|
|
87562
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
87563
|
+
process.exit(1);
|
|
87564
|
+
}
|
|
87565
|
+
}
|
|
85849
87566
|
const label = task.description.length > 60 ? task.description.slice(0, 60) + "\u2026" : task.description;
|
|
85850
87567
|
console.log();
|
|
85851
87568
|
if (projectContext) {
|
|
@@ -85856,6 +87573,9 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
|
|
|
85856
87573
|
if (task.dependencies.length > 0) {
|
|
85857
87574
|
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
|
85858
87575
|
}
|
|
87576
|
+
if (resolvedNode) {
|
|
87577
|
+
console.log(` Node: ${resolvedNode.name || resolvedNode.id}`);
|
|
87578
|
+
}
|
|
85859
87579
|
console.log(` Path: .fusion/tasks/${task.id}/`);
|
|
85860
87580
|
if (attachFiles && attachFiles.length > 0) {
|
|
85861
87581
|
const { readFile: readFile19 } = await import("node:fs/promises");
|
|
@@ -86047,15 +87767,62 @@ async function runTaskLogs(id, options = {}, projectName) {
|
|
|
86047
87767
|
});
|
|
86048
87768
|
}
|
|
86049
87769
|
}
|
|
87770
|
+
async function runTaskSetNode(id, nodeNameOrId, projectName) {
|
|
87771
|
+
const store = await getStore(projectName);
|
|
87772
|
+
const task = await store.getTask(id);
|
|
87773
|
+
if (task.column === "in-progress") {
|
|
87774
|
+
console.error(`Cannot change node override: task ${id} is in progress`);
|
|
87775
|
+
process.exit(1);
|
|
87776
|
+
}
|
|
87777
|
+
let resolvedNode;
|
|
87778
|
+
try {
|
|
87779
|
+
resolvedNode = await resolveNodeByNameOrId(nodeNameOrId);
|
|
87780
|
+
} catch (error) {
|
|
87781
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
87782
|
+
process.exit(1);
|
|
87783
|
+
return;
|
|
87784
|
+
}
|
|
87785
|
+
await store.updateTask(id, { nodeId: resolvedNode.id });
|
|
87786
|
+
console.log(`\u2713 Set node override for ${id}: ${resolvedNode.name || resolvedNode.id}`);
|
|
87787
|
+
}
|
|
87788
|
+
async function runTaskClearNode(id, projectName) {
|
|
87789
|
+
const store = await getStore(projectName);
|
|
87790
|
+
const task = await store.getTask(id);
|
|
87791
|
+
if (task.column === "in-progress") {
|
|
87792
|
+
console.error(`Cannot change node override: task ${id} is in progress`);
|
|
87793
|
+
process.exit(1);
|
|
87794
|
+
}
|
|
87795
|
+
await store.updateTask(id, { nodeId: null });
|
|
87796
|
+
console.log(`\u2713 Cleared node override for ${id}`);
|
|
87797
|
+
}
|
|
86050
87798
|
async function runTaskShow(id, projectName) {
|
|
86051
87799
|
const store = await getStore(projectName);
|
|
86052
87800
|
const task = await store.getTask(id);
|
|
87801
|
+
const settings = "getSettings" in store ? await store.getSettings() : {};
|
|
87802
|
+
let nodeSummary = "(default local)";
|
|
87803
|
+
if (task.nodeId) {
|
|
87804
|
+
let nodeName;
|
|
87805
|
+
const central = new CentralCore();
|
|
87806
|
+
await central.init();
|
|
87807
|
+
try {
|
|
87808
|
+
nodeName = (await central.getNode(task.nodeId))?.name;
|
|
87809
|
+
} finally {
|
|
87810
|
+
await central.close();
|
|
87811
|
+
}
|
|
87812
|
+
nodeSummary = nodeName ? `${nodeName} (${task.nodeId})` : task.nodeId;
|
|
87813
|
+
} else if (settings.defaultNodeId) {
|
|
87814
|
+
nodeSummary = `project default: ${settings.defaultNodeId}`;
|
|
87815
|
+
}
|
|
86053
87816
|
console.log();
|
|
86054
87817
|
console.log(` ${task.id}: ${task.title || task.description}`);
|
|
86055
87818
|
console.log(` Column: ${COLUMN_LABELS[task.column]}${task.size ? ` \xB7 Size: ${task.size}` : ""}${task.reviewLevel !== void 0 ? ` \xB7 Review: ${task.reviewLevel}` : ""}`);
|
|
86056
87819
|
if (task.dependencies.length) {
|
|
86057
87820
|
console.log(` Dependencies: ${task.dependencies.join(", ")}`);
|
|
86058
87821
|
}
|
|
87822
|
+
console.log(` Node: ${nodeSummary}`);
|
|
87823
|
+
if (settings.unavailableNodePolicy) {
|
|
87824
|
+
console.log(` Unavailable Node Policy: ${settings.unavailableNodePolicy}`);
|
|
87825
|
+
}
|
|
86059
87826
|
console.log();
|
|
86060
87827
|
if (task.steps.length > 0) {
|
|
86061
87828
|
console.log(` Steps (${task.steps.filter((s) => s.status === "done").length}/${task.steps.length}):`);
|
|
@@ -86172,7 +87939,7 @@ async function runTaskRefine(id, feedbackArg, projectName) {
|
|
|
86172
87939
|
const store = await getStore(projectName);
|
|
86173
87940
|
let feedback = feedbackArg;
|
|
86174
87941
|
if (feedback === void 0) {
|
|
86175
|
-
const rl =
|
|
87942
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86176
87943
|
feedback = await rl.question("What needs to be refined? ");
|
|
86177
87944
|
rl.close();
|
|
86178
87945
|
}
|
|
@@ -86243,7 +88010,7 @@ async function runTaskDelete(id, force, projectName) {
|
|
|
86243
88010
|
return;
|
|
86244
88011
|
}
|
|
86245
88012
|
if (!force) {
|
|
86246
|
-
const rl =
|
|
88013
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86247
88014
|
const answer = await rl.question(`Are you sure you want to delete ${id}? [y/N] `);
|
|
86248
88015
|
rl.close();
|
|
86249
88016
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -86307,7 +88074,7 @@ async function runTaskImportGitHubInteractive(ownerRepo, options = {}, projectNa
|
|
|
86307
88074
|
console.log(` ${i + 1}. #${issue.number} ${issue.title.slice(0, 80)}${issue.title.length > 80 ? "\u2026" : ""}${status}`);
|
|
86308
88075
|
}
|
|
86309
88076
|
console.log();
|
|
86310
|
-
const rl =
|
|
88077
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86311
88078
|
let selectedIndices = [];
|
|
86312
88079
|
let validInput = false;
|
|
86313
88080
|
while (!validInput) {
|
|
@@ -86464,7 +88231,7 @@ async function runTaskComment(id, message, author = "user", projectName) {
|
|
|
86464
88231
|
const store = await getStore(projectName);
|
|
86465
88232
|
let text = message;
|
|
86466
88233
|
if (text === void 0) {
|
|
86467
|
-
const rl =
|
|
88234
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86468
88235
|
text = await rl.question("Comment: ");
|
|
86469
88236
|
rl.close();
|
|
86470
88237
|
}
|
|
@@ -86507,7 +88274,7 @@ async function runTaskSteer(id, message, projectName) {
|
|
|
86507
88274
|
const store = await getStore(projectName);
|
|
86508
88275
|
let text = message;
|
|
86509
88276
|
if (text === void 0) {
|
|
86510
|
-
const rl =
|
|
88277
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86511
88278
|
text = await rl.question("Message: ");
|
|
86512
88279
|
rl.close();
|
|
86513
88280
|
}
|
|
@@ -86638,7 +88405,7 @@ async function promptText(question) {
|
|
|
86638
88405
|
console.log(` ${question.description}`);
|
|
86639
88406
|
}
|
|
86640
88407
|
console.log(" (Enter your response. Type DONE on its own line when finished):\n");
|
|
86641
|
-
const rl =
|
|
88408
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86642
88409
|
const lines = [];
|
|
86643
88410
|
return new Promise((resolve17) => {
|
|
86644
88411
|
const askLine = () => {
|
|
@@ -86672,7 +88439,7 @@ async function promptSingleSelect(question) {
|
|
|
86672
88439
|
console.log(` ${opt.description}`);
|
|
86673
88440
|
}
|
|
86674
88441
|
}
|
|
86675
|
-
const rl =
|
|
88442
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86676
88443
|
while (true) {
|
|
86677
88444
|
const answer = await rl.question("\n Select (1-" + question.options.length + "): ");
|
|
86678
88445
|
const num = parseInt(answer.trim(), 10);
|
|
@@ -86700,7 +88467,7 @@ async function promptMultiSelect(question) {
|
|
|
86700
88467
|
console.log(` ${opt.description}`);
|
|
86701
88468
|
}
|
|
86702
88469
|
}
|
|
86703
|
-
const rl =
|
|
88470
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86704
88471
|
while (true) {
|
|
86705
88472
|
const answer = await rl.question("\n Select (comma-separated): ");
|
|
86706
88473
|
const nums = answer.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
@@ -86723,7 +88490,7 @@ async function promptConfirm(question) {
|
|
|
86723
88490
|
if (question.description) {
|
|
86724
88491
|
console.log(` ${question.description}`);
|
|
86725
88492
|
}
|
|
86726
|
-
const rl =
|
|
88493
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86727
88494
|
const answer = await rl.question("\n [Y/n]: ");
|
|
86728
88495
|
rl.close();
|
|
86729
88496
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -86788,7 +88555,7 @@ function wrapText(text, width) {
|
|
|
86788
88555
|
async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
|
|
86789
88556
|
let initialPlan = initialPlanArg;
|
|
86790
88557
|
if (!initialPlan) {
|
|
86791
|
-
const rl =
|
|
88558
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86792
88559
|
console.log("\n Let's plan your task. What would you like to accomplish?\n");
|
|
86793
88560
|
initialPlan = await rl.question(" Describe your idea: ");
|
|
86794
88561
|
rl.close();
|
|
@@ -86889,7 +88656,7 @@ async function runTaskPlan(initialPlanArg, yesFlag = false, projectName) {
|
|
|
86889
88656
|
displaySummary(result.data);
|
|
86890
88657
|
let confirmed = yesFlag;
|
|
86891
88658
|
if (!yesFlag) {
|
|
86892
|
-
const rl =
|
|
88659
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
86893
88660
|
const answer = await rl.question(" Create this task? [Y/n]: ");
|
|
86894
88661
|
rl.close();
|
|
86895
88662
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -86931,6 +88698,7 @@ var init_task = __esm({
|
|
|
86931
88698
|
init_src4();
|
|
86932
88699
|
init_gh_cli();
|
|
86933
88700
|
init_project_context();
|
|
88701
|
+
init_node();
|
|
86934
88702
|
STEP_STATUSES2 = ["pending", "in-progress", "done", "skipped"];
|
|
86935
88703
|
ANSI = {
|
|
86936
88704
|
reset: "\x1B[0m",
|
|
@@ -87143,17 +88911,17 @@ async function fetchGitHubIssuesViaGh(owner, repo, options = {}) {
|
|
|
87143
88911
|
}
|
|
87144
88912
|
const path2 = `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?${queryParams.toString()}`;
|
|
87145
88913
|
try {
|
|
87146
|
-
const issues = await runGhJsonAsync(["api", path2]);
|
|
88914
|
+
const issues = await runGhJsonAsync(["api", path2], { signal: options.signal });
|
|
87147
88915
|
return issues.filter((issue) => !issue.pull_request);
|
|
87148
88916
|
} catch (error) {
|
|
87149
88917
|
throw new Error(getGhErrorMessage(error));
|
|
87150
88918
|
}
|
|
87151
88919
|
}
|
|
87152
|
-
async function fetchGitHubIssueViaGh(owner, repo, issueNumber) {
|
|
88920
|
+
async function fetchGitHubIssueViaGh(owner, repo, issueNumber, options = {}) {
|
|
87153
88921
|
ensureGhCliAuth();
|
|
87154
88922
|
const path2 = `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`;
|
|
87155
88923
|
try {
|
|
87156
|
-
return await runGhJsonAsync(["api", path2]);
|
|
88924
|
+
return await runGhJsonAsync(["api", path2], { signal: options.signal });
|
|
87157
88925
|
} catch (error) {
|
|
87158
88926
|
throw new Error(getGhErrorMessage(error));
|
|
87159
88927
|
}
|
|
@@ -87234,12 +89002,18 @@ Column: triage
|
|
|
87234
89002
|
], {
|
|
87235
89003
|
description: "Agent ID to assign this task to, or null to clear (e.g. 'agent-abc123')"
|
|
87236
89004
|
})
|
|
89005
|
+
),
|
|
89006
|
+
nodeId: Type7.Optional(
|
|
89007
|
+
Type7.Union([Type7.String(), Type7.Null()], {
|
|
89008
|
+
description: "Node ID override for this task, or null to clear"
|
|
89009
|
+
})
|
|
87237
89010
|
)
|
|
87238
89011
|
}),
|
|
87239
89012
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
87240
89013
|
const store = await getStore2(ctx.cwd);
|
|
89014
|
+
let task;
|
|
87241
89015
|
try {
|
|
87242
|
-
await store.getTask(params.id);
|
|
89016
|
+
task = await store.getTask(params.id);
|
|
87243
89017
|
} catch {
|
|
87244
89018
|
return {
|
|
87245
89019
|
content: [{ type: "text", text: `Task ${params.id} not found` }],
|
|
@@ -87265,9 +89039,21 @@ Column: triage
|
|
|
87265
89039
|
updates.assignedAgentId = params.agentId;
|
|
87266
89040
|
updatedFields.push("agentId");
|
|
87267
89041
|
}
|
|
89042
|
+
if (params.nodeId !== void 0) {
|
|
89043
|
+
const validation = validateNodeOverrideChange(task, params.nodeId ?? null);
|
|
89044
|
+
if (!validation.allowed) {
|
|
89045
|
+
return {
|
|
89046
|
+
content: [{ type: "text", text: validation.message ?? "Node override change blocked" }],
|
|
89047
|
+
isError: true,
|
|
89048
|
+
details: { error: validation.reason }
|
|
89049
|
+
};
|
|
89050
|
+
}
|
|
89051
|
+
updates.nodeId = params.nodeId;
|
|
89052
|
+
updatedFields.push("nodeId");
|
|
89053
|
+
}
|
|
87268
89054
|
if (updatedFields.length === 0) {
|
|
87269
89055
|
return {
|
|
87270
|
-
content: [{ type: "text", text: "No fields to update. Provide at least one of: title, description, depends, agentId." }],
|
|
89056
|
+
content: [{ type: "text", text: "No fields to update. Provide at least one of: title, description, depends, agentId, nodeId." }],
|
|
87271
89057
|
isError: true,
|
|
87272
89058
|
details: { error: "No fields provided" }
|
|
87273
89059
|
};
|
|
@@ -87648,11 +89434,11 @@ Path: .fusion/tasks/${params.id}/attachments/${attachment.filename}`
|
|
|
87648
89434
|
})
|
|
87649
89435
|
)
|
|
87650
89436
|
}),
|
|
87651
|
-
async execute(_toolCallId, params,
|
|
89437
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
87652
89438
|
const [owner, repo] = params.ownerRepo.split("/");
|
|
87653
89439
|
const limit = params.limit ?? 30;
|
|
87654
89440
|
const labels = params.labels;
|
|
87655
|
-
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels });
|
|
89441
|
+
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels, signal });
|
|
87656
89442
|
if (issues.length === 0) {
|
|
87657
89443
|
return {
|
|
87658
89444
|
content: [{ type: "text", text: `No open issues found in ${owner}/${repo}.` }],
|
|
@@ -87727,9 +89513,9 @@ ${createdTasks.map((task) => ` ${task.id}: ${task.title}`).join("\n") || " Non
|
|
|
87727
89513
|
minimum: 1
|
|
87728
89514
|
})
|
|
87729
89515
|
}),
|
|
87730
|
-
async execute(_toolCallId, params,
|
|
89516
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
87731
89517
|
const { owner, repo, issueNumber } = params;
|
|
87732
|
-
const issue = await fetchGitHubIssueViaGh(owner, repo, issueNumber);
|
|
89518
|
+
const issue = await fetchGitHubIssueViaGh(owner, repo, issueNumber, { signal });
|
|
87733
89519
|
if (issue.pull_request) {
|
|
87734
89520
|
throw new Error(`#${issueNumber} is a pull request, not an issue`);
|
|
87735
89521
|
}
|
|
@@ -87813,9 +89599,9 @@ ${sourceUrl}`
|
|
|
87813
89599
|
})
|
|
87814
89600
|
)
|
|
87815
89601
|
}),
|
|
87816
|
-
async execute(_toolCallId, params,
|
|
89602
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
87817
89603
|
const { owner, repo, limit = 30, labels } = params;
|
|
87818
|
-
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels });
|
|
89604
|
+
const issues = await fetchGitHubIssuesViaGh(owner, repo, { limit, labels, signal });
|
|
87819
89605
|
if (issues.length === 0) {
|
|
87820
89606
|
return {
|
|
87821
89607
|
content: [{ type: "text", text: `No open issues found in ${owner}/${repo}.` }],
|