agentic-orchestrator 0.1.26 → 0.1.28
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/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +47 -14
- package/agentic/orchestrator/agents.yaml +13 -0
- package/agentic/orchestrator/policy.yaml +3 -0
- package/agentic/orchestrator/schemas/agents.schema.json +76 -0
- package/agentic/orchestrator/schemas/policy.schema.json +16 -0
- package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
- package/agentic/orchestrator/schemas/state.schema.json +53 -0
- package/apps/control-plane/src/application/configuration-service.ts +181 -0
- package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
- package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
- package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
- package/apps/control-plane/src/application/services/patch-service.ts +29 -5
- package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
- package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
- package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
- package/apps/control-plane/src/cli/types.ts +3 -0
- package/apps/control-plane/src/core/kernel-types.ts +55 -0
- package/apps/control-plane/src/core/kernel.ts +61 -878
- package/apps/control-plane/src/core/tool-caller.ts +10 -0
- package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
- package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
- package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
- package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
- package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
- package/apps/control-plane/src/providers/providers.ts +45 -24
- package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
- package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
- package/apps/control-plane/src/supervisor/runtime.ts +107 -1
- package/apps/control-plane/src/supervisor/types.ts +9 -0
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
- package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
- package/apps/control-plane/test/cli.unit.spec.ts +52 -0
- package/apps/control-plane/test/configuration-service.spec.ts +466 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
- package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
- package/apps/control-plane/test/init-wizard.spec.ts +35 -0
- package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
- package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
- package/apps/control-plane/test/resume-command.spec.ts +33 -0
- package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
- package/apps/control-plane/test/rollback-command.spec.ts +208 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
- package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
- package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
- package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
- package/config/agentic/orchestrator/agents.yaml +13 -0
- package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
- package/dist/apps/control-plane/application/configuration-service.js +123 -0
- package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
- package/dist/apps/control-plane/application/services/patch-service.js +11 -2
- package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +3 -0
- package/dist/apps/control-plane/cli/types.js +1 -0
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
- package/dist/apps/control-plane/core/configuration-service.js +130 -0
- package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
- package/dist/apps/control-plane/core/kernel-types.js +2 -0
- package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
- package/dist/apps/control-plane/core/kernel.d.ts +17 -48
- package/dist/apps/control-plane/core/kernel.js +44 -539
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
- package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
- package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
- package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
- package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +31 -24
- package/dist/apps/control-plane/providers/providers.js +10 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
- package/dist/apps/control-plane/supervisor/runtime.js +90 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/package.json +1 -1
- package/packages/web-dashboard/package.json +2 -0
- package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
- package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
- package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
- package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
- package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
- package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
- package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
- package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
- package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
- package/packages/web-dashboard/src/lib/types.ts +131 -0
- package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
- package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
- package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
- package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
- package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
- package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
- package/spec-files/progress.md +269 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { pathExists, ensureDir } from '../../core/fs.js';
|
|
4
|
+
import { runGit, runCommand } from '../../core/git.js';
|
|
5
|
+
import { normalizeRepoPath } from '../../core/path-rules.js';
|
|
6
|
+
import { ERROR_CODES } from '../../core/error-codes.js';
|
|
7
|
+
import { fail } from '../../core/response.js';
|
|
8
|
+
import {
|
|
9
|
+
applyWorktreeSymlinks,
|
|
10
|
+
formatWorkspaceHookWarning,
|
|
11
|
+
runWorktreePostCreate,
|
|
12
|
+
type WorkspaceHookWarning,
|
|
13
|
+
} from '../../core/workspace-hooks.js';
|
|
14
|
+
import { normalizeFromWorktree } from '../../core/utils/path-normalizers.js';
|
|
15
|
+
import { asArray } from '../../core/utils/field-readers.js';
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
type AnyRecord = Record<string, any>;
|
|
19
|
+
|
|
20
|
+
export interface RepoOperationsServicePort {
|
|
21
|
+
getRepoRoot(): string;
|
|
22
|
+
worktreePath(featureId: string): string;
|
|
23
|
+
getPolicySnapshot(): AnyRecord;
|
|
24
|
+
evidenceLatest(featureId: string): Promise<{ data?: { latest?: unknown } }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class RepoOperationsService {
|
|
28
|
+
private readonly port: RepoOperationsServicePort;
|
|
29
|
+
|
|
30
|
+
constructor(port: RepoOperationsServicePort) {
|
|
31
|
+
this.port = port;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async repoEnsureWorktree(featureId: string | null) {
|
|
35
|
+
const repoRoot = this.port.getRepoRoot();
|
|
36
|
+
const worktree = this.port.worktreePath(featureId);
|
|
37
|
+
const branch = featureId;
|
|
38
|
+
await ensureDir(path.join(repoRoot, '.worktrees'));
|
|
39
|
+
|
|
40
|
+
if (await pathExists(worktree)) {
|
|
41
|
+
return {
|
|
42
|
+
data: {
|
|
43
|
+
feature_id: featureId,
|
|
44
|
+
branch,
|
|
45
|
+
worktree_path_abs: worktree,
|
|
46
|
+
existed: true,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const policy = this.port.getPolicySnapshot();
|
|
52
|
+
const baseBranch = (policy.worktree as { base_branch: string }).base_branch;
|
|
53
|
+
const baseCheck = await runGit(repoRoot, ['rev-parse', '--verify', baseBranch]);
|
|
54
|
+
const baseRef = baseCheck.code === 0 ? baseBranch : 'HEAD';
|
|
55
|
+
|
|
56
|
+
const branchCheck = await runGit(repoRoot, ['rev-parse', '--verify', branch]);
|
|
57
|
+
if (branchCheck.code !== 0) {
|
|
58
|
+
const branchCreate = await runGit(repoRoot, ['branch', branch, baseRef]);
|
|
59
|
+
if (branchCreate.code !== 0) {
|
|
60
|
+
throw {
|
|
61
|
+
normalizedResponse: fail(
|
|
62
|
+
ERROR_CODES.GIT_FAILURE,
|
|
63
|
+
'Unable to create feature branch',
|
|
64
|
+
{
|
|
65
|
+
feature_id: featureId,
|
|
66
|
+
stderr: branchCreate.stderr,
|
|
67
|
+
retryable: false,
|
|
68
|
+
requires_human: true,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
command: ['git', 'branch', branch, baseRef],
|
|
72
|
+
exit_code: branchCreate.code,
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const addWorktree = await runGit(repoRoot, ['worktree', 'add', worktree, branch]);
|
|
80
|
+
if (addWorktree.code !== 0) {
|
|
81
|
+
throw {
|
|
82
|
+
normalizedResponse: fail(
|
|
83
|
+
ERROR_CODES.GIT_FAILURE,
|
|
84
|
+
'Unable to create git worktree',
|
|
85
|
+
{
|
|
86
|
+
feature_id: featureId,
|
|
87
|
+
stderr: addWorktree.stderr,
|
|
88
|
+
retryable: false,
|
|
89
|
+
requires_human: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
command: ['git', 'worktree', 'add', worktree, branch],
|
|
93
|
+
exit_code: addWorktree.code,
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const worktreeConfig = policy.worktree as
|
|
100
|
+
| {
|
|
101
|
+
base_branch: string;
|
|
102
|
+
symlinks?: string[];
|
|
103
|
+
post_create?: string[];
|
|
104
|
+
}
|
|
105
|
+
| undefined;
|
|
106
|
+
const hookWarnings: WorkspaceHookWarning[] = [];
|
|
107
|
+
const collectHookWarning = (warning: WorkspaceHookWarning) => {
|
|
108
|
+
hookWarnings.push(warning);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (worktreeConfig?.symlinks?.length) {
|
|
112
|
+
await applyWorktreeSymlinks(repoRoot, worktree, worktreeConfig.symlinks, collectHookWarning);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (worktreeConfig?.post_create?.length) {
|
|
116
|
+
await runWorktreePostCreate(worktree, worktreeConfig.post_create, collectHookWarning);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const warning of hookWarnings) {
|
|
120
|
+
// Preserve non-fatal behavior while making hook failures observable.
|
|
121
|
+
console.warn(`[aop] workspace hook warning: ${formatWorkspaceHookWarning(warning)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
data: {
|
|
126
|
+
feature_id: featureId,
|
|
127
|
+
branch,
|
|
128
|
+
worktree_path_abs: worktree,
|
|
129
|
+
existed: false,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async repoStatus(featureId: string | null) {
|
|
135
|
+
const repoRoot = this.port.getRepoRoot();
|
|
136
|
+
const worktree = this.port.worktreePath(featureId);
|
|
137
|
+
const status = await runGit(repoRoot, ['status', '--porcelain'], { cwd: worktree });
|
|
138
|
+
const branch = await runGit(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
139
|
+
cwd: worktree,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
data: {
|
|
144
|
+
feature_id: featureId,
|
|
145
|
+
branch: branch.stdout.trim(),
|
|
146
|
+
status_porcelain: status.stdout.trim().split('\n').filter(Boolean),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async repoDiff(featureId: string | null, options: string[] = []) {
|
|
152
|
+
const repoRoot = this.port.getRepoRoot();
|
|
153
|
+
const safeOptions = asArray<string>(options).filter(
|
|
154
|
+
(option) => typeof option === 'string' && option.startsWith('--'),
|
|
155
|
+
);
|
|
156
|
+
const diff = await runGit(repoRoot, ['diff', ...safeOptions], {
|
|
157
|
+
cwd: this.port.worktreePath(featureId),
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
data: {
|
|
161
|
+
feature_id: featureId,
|
|
162
|
+
diff: diff.stdout,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async repoReadFile(featureId: string | null, filePath: string | null) {
|
|
168
|
+
const repoRoot = this.port.getRepoRoot();
|
|
169
|
+
const worktree = this.port.worktreePath(featureId);
|
|
170
|
+
const policy = this.port.getPolicySnapshot();
|
|
171
|
+
const normalized = await normalizeRepoPath(
|
|
172
|
+
repoRoot,
|
|
173
|
+
path.join(worktree, filePath),
|
|
174
|
+
(policy.path_rules as { allow_symlink_traversal?: boolean } | undefined)
|
|
175
|
+
?.allow_symlink_traversal,
|
|
176
|
+
).then((relative) => normalizeFromWorktree(worktree, repoRoot, relative));
|
|
177
|
+
const absolute = path.join(repoRoot, normalized);
|
|
178
|
+
const exists = await pathExists(absolute);
|
|
179
|
+
if (!exists) {
|
|
180
|
+
throw {
|
|
181
|
+
normalizedResponse: fail(ERROR_CODES.FILE_NOT_FOUND, 'File not found', {
|
|
182
|
+
path: normalized,
|
|
183
|
+
retryable: false,
|
|
184
|
+
requires_human: false,
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const content = await fs.readFile(absolute, 'utf8');
|
|
189
|
+
return {
|
|
190
|
+
data: {
|
|
191
|
+
feature_id: featureId,
|
|
192
|
+
path: normalized,
|
|
193
|
+
content,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async repoSearch(featureId: string | null, query: string | null) {
|
|
199
|
+
const worktree = this.port.worktreePath(featureId);
|
|
200
|
+
const rgResult = await runCommand('rg', ['-n', '--no-heading', query, '.'], {
|
|
201
|
+
cwd: worktree,
|
|
202
|
+
timeoutMs: 30_000,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (rgResult.code === 127) {
|
|
206
|
+
throw {
|
|
207
|
+
normalizedResponse: fail(
|
|
208
|
+
ERROR_CODES.GIT_FAILURE,
|
|
209
|
+
'ripgrep (rg) not found - required for search functionality',
|
|
210
|
+
{
|
|
211
|
+
stderr: rgResult.stderr,
|
|
212
|
+
retryable: false,
|
|
213
|
+
requires_human: true,
|
|
214
|
+
},
|
|
215
|
+
),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (rgResult.code !== 0 && rgResult.code !== 1) {
|
|
220
|
+
throw {
|
|
221
|
+
normalizedResponse: fail(ERROR_CODES.GIT_FAILURE, 'Search failed', {
|
|
222
|
+
stderr: rgResult.stderr,
|
|
223
|
+
retryable: true,
|
|
224
|
+
requires_human: false,
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const matches = rgResult.stdout
|
|
230
|
+
.trim()
|
|
231
|
+
.split('\n')
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
.map((line) => {
|
|
234
|
+
const firstColon = line.indexOf(':');
|
|
235
|
+
const secondColon = line.indexOf(':', firstColon + 1);
|
|
236
|
+
if (firstColon === -1 || secondColon === -1) {
|
|
237
|
+
return { raw: line };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
path: line.slice(0, firstColon),
|
|
241
|
+
line: Number(line.slice(firstColon + 1, secondColon)),
|
|
242
|
+
snippet: line.slice(secondColon + 1),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
data: {
|
|
248
|
+
feature_id: featureId,
|
|
249
|
+
query,
|
|
250
|
+
matches,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async repoDiffBundle(featureId: string | null) {
|
|
256
|
+
const repoRoot = this.port.getRepoRoot();
|
|
257
|
+
const worktree = this.port.worktreePath(featureId);
|
|
258
|
+
const stat = await runGit(repoRoot, ['diff', '--stat'], { cwd: worktree });
|
|
259
|
+
const full = await runGit(repoRoot, ['diff'], { cwd: worktree });
|
|
260
|
+
const names = await runGit(repoRoot, ['diff', '--name-only'], { cwd: worktree });
|
|
261
|
+
const latest = await this.port.evidenceLatest(featureId);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
data: {
|
|
265
|
+
feature_id: featureId,
|
|
266
|
+
diff_stat: stat.stdout,
|
|
267
|
+
diff: full.stdout,
|
|
268
|
+
touched_files: names.stdout
|
|
269
|
+
.split('\n')
|
|
270
|
+
.map((x) => x.trim())
|
|
271
|
+
.filter(Boolean),
|
|
272
|
+
last_gate_summary: latest.data?.latest ?? null,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { runGit } from '../../core/git.js';
|
|
3
|
+
|
|
4
|
+
interface WatchStartInput {
|
|
5
|
+
featureId: string;
|
|
6
|
+
repoRoot: string;
|
|
7
|
+
worktreePath: string;
|
|
8
|
+
pollIntervalMs: number;
|
|
9
|
+
maxUncommittedChanges: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface WatcherState {
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
worktreePath: string;
|
|
15
|
+
maxUncommittedChanges: number;
|
|
16
|
+
timer: NodeJS.Timeout;
|
|
17
|
+
changedFiles: Set<string>;
|
|
18
|
+
thresholdEmitted: boolean;
|
|
19
|
+
pollInFlight: boolean;
|
|
20
|
+
stopped: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseStatusPaths(statusPorcelain: string): string[] {
|
|
24
|
+
const files: string[] = [];
|
|
25
|
+
const lines = statusPorcelain
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((line) => line.trimEnd())
|
|
28
|
+
.filter((line) => line.length >= 4);
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const candidate = line.slice(3).trim();
|
|
32
|
+
if (!candidate) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const renameMarker = candidate.indexOf(' -> ');
|
|
36
|
+
const pathValue = renameMarker >= 0 ? candidate.slice(renameMarker + 4) : candidate;
|
|
37
|
+
const normalized = pathValue.replaceAll('\\\\', '/').trim();
|
|
38
|
+
if (normalized.length > 0) {
|
|
39
|
+
files.push(normalized);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Polling watchdog that tracks feature-worktree changes and emits threshold events.
|
|
48
|
+
*/
|
|
49
|
+
export class WorktreeWatchdogService extends EventEmitter {
|
|
50
|
+
private readonly watchers = new Map<string, WatcherState>();
|
|
51
|
+
|
|
52
|
+
async startWatching(input: WatchStartInput): Promise<void> {
|
|
53
|
+
await this.stopWatching(input.featureId);
|
|
54
|
+
|
|
55
|
+
const intervalMs =
|
|
56
|
+
Number.isFinite(input.pollIntervalMs) && input.pollIntervalMs > 0
|
|
57
|
+
? Math.floor(input.pollIntervalMs)
|
|
58
|
+
: 2_000;
|
|
59
|
+
const maxUncommittedChanges =
|
|
60
|
+
Number.isFinite(input.maxUncommittedChanges) && input.maxUncommittedChanges > 0
|
|
61
|
+
? Math.floor(input.maxUncommittedChanges)
|
|
62
|
+
: 50;
|
|
63
|
+
|
|
64
|
+
const state: WatcherState = {
|
|
65
|
+
repoRoot: input.repoRoot,
|
|
66
|
+
worktreePath: input.worktreePath,
|
|
67
|
+
maxUncommittedChanges,
|
|
68
|
+
timer: setInterval(() => {
|
|
69
|
+
void this.pollFeature(input.featureId);
|
|
70
|
+
}, intervalMs),
|
|
71
|
+
changedFiles: new Set<string>(),
|
|
72
|
+
thresholdEmitted: false,
|
|
73
|
+
pollInFlight: false,
|
|
74
|
+
stopped: false,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
state.timer.unref?.();
|
|
78
|
+
this.watchers.set(input.featureId, state);
|
|
79
|
+
await this.pollFeature(input.featureId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stopWatching(featureId: string): Promise<void> {
|
|
83
|
+
const existing = this.watchers.get(featureId);
|
|
84
|
+
if (!existing) {
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
}
|
|
87
|
+
existing.stopped = true;
|
|
88
|
+
clearInterval(existing.timer);
|
|
89
|
+
this.watchers.delete(featureId);
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getChangedFiles(featureId: string): Promise<string[]> {
|
|
94
|
+
await this.pollFeature(featureId);
|
|
95
|
+
const state = this.watchers.get(featureId);
|
|
96
|
+
if (!state) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return [...state.changedFiles].sort((a, b) => a.localeCompare(b));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getChangeCount(featureId: string): number {
|
|
103
|
+
const state = this.watchers.get(featureId);
|
|
104
|
+
if (!state) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
return state.changedFiles.size;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
resetChangeCount(featureId: string): void {
|
|
111
|
+
const state = this.watchers.get(featureId);
|
|
112
|
+
if (!state) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
state.changedFiles.clear();
|
|
116
|
+
state.thresholdEmitted = false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
on(event: 'changeThreshold', listener: (featureId: string) => void): this {
|
|
120
|
+
return super.on(event, listener);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
off(event: 'changeThreshold', listener: (featureId: string) => void): this {
|
|
124
|
+
return super.off(event, listener);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async pollFeature(featureId: string): Promise<void> {
|
|
128
|
+
const state = this.watchers.get(featureId);
|
|
129
|
+
if (!state || state.stopped || state.pollInFlight) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
state.pollInFlight = true;
|
|
134
|
+
try {
|
|
135
|
+
const status = await runGit(state.repoRoot, ['status', '--porcelain'], {
|
|
136
|
+
cwd: state.worktreePath,
|
|
137
|
+
});
|
|
138
|
+
if (status.code !== 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const files = parseStatusPaths(status.stdout);
|
|
143
|
+
state.changedFiles = new Set(files);
|
|
144
|
+
|
|
145
|
+
if (state.changedFiles.size >= state.maxUncommittedChanges && !state.thresholdEmitted) {
|
|
146
|
+
state.thresholdEmitted = true;
|
|
147
|
+
this.emit('changeThreshold', featureId);
|
|
148
|
+
}
|
|
149
|
+
if (state.changedFiles.size < state.maxUncommittedChanges) {
|
|
150
|
+
state.thresholdEmitted = false;
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
state.pollInFlight = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -55,6 +55,11 @@ export class CliArgumentParser {
|
|
|
55
55
|
index += 1;
|
|
56
56
|
continue;
|
|
57
57
|
}
|
|
58
|
+
if (token === '--checkpoint') {
|
|
59
|
+
options.checkpoint = next;
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
58
63
|
if (token === '--dry-run') {
|
|
59
64
|
const parsed = parseOptionalBooleanToken(next);
|
|
60
65
|
if (parsed === null) {
|
|
@@ -106,6 +111,13 @@ export class CliArgumentParser {
|
|
|
106
111
|
}
|
|
107
112
|
continue;
|
|
108
113
|
}
|
|
114
|
+
if (token === '--execution-mode') {
|
|
115
|
+
if (next === 'deterministic' || next === 'interactive') {
|
|
116
|
+
options.execution_mode = next;
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
109
121
|
if (token === '--transport') {
|
|
110
122
|
options.transport = next;
|
|
111
123
|
index += 1;
|
|
@@ -31,6 +31,10 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
|
|
|
31
31
|
flag: '--worker-provider-mode <live|stub>',
|
|
32
32
|
description: 'Worker execution mode override (default: live for run/resume)',
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
flag: '--execution-mode <deterministic|interactive>',
|
|
36
|
+
description: 'Execution mode override for worker decision flow (default: deterministic)',
|
|
37
|
+
},
|
|
34
38
|
{ flag: '--transport <inprocess|mcp>', description: 'Tool transport layer (default: mcp)' },
|
|
35
39
|
{ flag: '--takeover-stale-run', description: 'Take over a stale run lease' },
|
|
36
40
|
{ flag: '--project <name>', description: 'Select project from multi-project.yaml' },
|
|
@@ -55,6 +59,10 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
|
|
|
55
59
|
flag: '--worker-provider-mode <live|stub>',
|
|
56
60
|
description: 'Worker execution mode override (default: live)',
|
|
57
61
|
},
|
|
62
|
+
{
|
|
63
|
+
flag: '--execution-mode <deterministic|interactive>',
|
|
64
|
+
description: 'Execution mode override for worker decision flow (default: deterministic)',
|
|
65
|
+
},
|
|
58
66
|
{ flag: '--transport <inprocess|mcp>', description: 'Tool transport layer (default: mcp)' },
|
|
59
67
|
],
|
|
60
68
|
},
|
|
@@ -96,6 +104,15 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
|
|
|
96
104
|
},
|
|
97
105
|
],
|
|
98
106
|
},
|
|
107
|
+
[CliCommand.Rollback]: {
|
|
108
|
+
usage: 'aop rollback [flags]',
|
|
109
|
+
description: 'Rollback a feature worktree by applying the inverse of a checkpoint snapshot.',
|
|
110
|
+
flags: [
|
|
111
|
+
{ flag: '--feature-id <id>', description: 'Feature to roll back' },
|
|
112
|
+
{ flag: '--checkpoint <id>', description: 'Checkpoint id to reverse-apply' },
|
|
113
|
+
{ flag: '--dry-run', description: 'Preview rollback inputs without applying git changes' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
99
116
|
[CliCommand.Init]: {
|
|
100
117
|
usage: 'aop init [flags]',
|
|
101
118
|
description: 'Initialise agentic orchestrator configuration in the current directory.',
|
|
@@ -30,6 +30,7 @@ export interface InitOptions {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
type TestFramework = 'vitest' | 'jest' | 'pytest' | 'maven' | 'gradle';
|
|
33
|
+
type ExecutionMode = 'deterministic' | 'interactive';
|
|
33
34
|
|
|
34
35
|
interface PromptSession {
|
|
35
36
|
question(query: string): Promise<string>;
|
|
@@ -46,6 +47,7 @@ interface WizardConfig {
|
|
|
46
47
|
maxParallelGateRuns: number;
|
|
47
48
|
dashboardPort: number;
|
|
48
49
|
framework: TestFramework;
|
|
50
|
+
executionMode: ExecutionMode;
|
|
49
51
|
defaultProvider: string;
|
|
50
52
|
defaultModel: string;
|
|
51
53
|
providerConfigEnv: string | null;
|
|
@@ -423,6 +425,19 @@ missing_prompt_behavior: ignore
|
|
|
423
425
|
runtime:
|
|
424
426
|
default_provider: ${wizard.defaultProvider}
|
|
425
427
|
default_model: ${wizard.defaultModel}
|
|
428
|
+
execution_mode: ${wizard.executionMode}
|
|
429
|
+
interactive:
|
|
430
|
+
watchdog_poll_interval_ms: 2000
|
|
431
|
+
checkpoint_interval_ms: 30000
|
|
432
|
+
max_uncommitted_changes: 50
|
|
433
|
+
validation_on_checkpoint: true
|
|
434
|
+
revert_on_violation: false
|
|
435
|
+
violation_severity: warning
|
|
436
|
+
shadow_workspace:
|
|
437
|
+
enabled: false
|
|
438
|
+
promotion_strategy: atomic
|
|
439
|
+
cleanup_on_failure: true
|
|
440
|
+
max_shadow_size_mb: 2048
|
|
426
441
|
worker_provider_mode: live
|
|
427
442
|
worker_response_timeout_ms: 600000
|
|
428
443
|
worker_spawn_timeout_ms: 15000
|
|
@@ -478,6 +493,14 @@ function parseFramework(raw: string, fallback: TestFramework): TestFramework {
|
|
|
478
493
|
return fallback;
|
|
479
494
|
}
|
|
480
495
|
|
|
496
|
+
function parseExecutionMode(raw: string, fallback: ExecutionMode): ExecutionMode {
|
|
497
|
+
const value = raw.trim().toLowerCase();
|
|
498
|
+
if (value === 'deterministic' || value === 'interactive') {
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
return fallback;
|
|
502
|
+
}
|
|
503
|
+
|
|
481
504
|
function parseNotificationChannels(raw: string): {
|
|
482
505
|
desktop: boolean;
|
|
483
506
|
slack: boolean;
|
|
@@ -643,6 +666,12 @@ async function collectWizardConfig(
|
|
|
643
666
|
}
|
|
644
667
|
}
|
|
645
668
|
|
|
669
|
+
const executionModeRaw = await askWithDefault(
|
|
670
|
+
prompt,
|
|
671
|
+
'Default execution mode (deterministic|interactive)',
|
|
672
|
+
'deterministic',
|
|
673
|
+
);
|
|
674
|
+
|
|
646
675
|
return {
|
|
647
676
|
baseBranch,
|
|
648
677
|
defaultProvider: parseAdapterName(
|
|
@@ -651,6 +680,7 @@ async function collectWizardConfig(
|
|
|
651
680
|
defaults.defaultProvider,
|
|
652
681
|
),
|
|
653
682
|
defaultModel,
|
|
683
|
+
executionMode: parseExecutionMode(executionModeRaw, 'deterministic'),
|
|
654
684
|
providerConfigEnv,
|
|
655
685
|
providerCredentialBootstrapped,
|
|
656
686
|
scmProvider: parseAdapterName(
|
|
@@ -711,6 +741,7 @@ export class InitCommandHandler {
|
|
|
711
741
|
baseBranch: gitContext.defaultBranch,
|
|
712
742
|
defaultProvider: DEFAULT_AGENT_PROVIDER,
|
|
713
743
|
defaultModel: DEFAULT_AGENT_MODEL,
|
|
744
|
+
executionMode: 'deterministic' as const,
|
|
714
745
|
providerConfigEnv: null,
|
|
715
746
|
providerCredentialBootstrapped: false,
|
|
716
747
|
scmProvider: DEFAULT_SCM_PROVIDER,
|
|
@@ -3,6 +3,7 @@ import { resolveProviderSelection } from '../providers/providers.js';
|
|
|
3
3
|
import {
|
|
4
4
|
DefaultWorkerProviderFactory,
|
|
5
5
|
resolveWorkerProviderMode,
|
|
6
|
+
resolveWorkerProviderObservability,
|
|
6
7
|
resolveWorkerProviderPolicy,
|
|
7
8
|
resolveWorkerProviderRuntime,
|
|
8
9
|
type WorkerProviderFactory,
|
|
@@ -52,6 +53,14 @@ function asStringArray(value: unknown): string[] {
|
|
|
52
53
|
return normalized;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function asFeatureId(value: unknown): string | null {
|
|
57
|
+
if (typeof value !== 'string') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
function toResumePhase(status: string): string {
|
|
56
65
|
if (status === STATUS.BUILDING) {
|
|
57
66
|
return STATUS.BUILDING;
|
|
@@ -98,9 +107,10 @@ export class ResumeCommandHandler {
|
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
async execute(context: ResumeCommandContext): Promise<unknown> {
|
|
101
|
-
const { env, runId, transport, options, kernel, toolClient } = context;
|
|
110
|
+
const { repoRoot, env, runId, transport, options, kernel, toolClient } = context;
|
|
102
111
|
const recovery = await kernel.recoverFromState();
|
|
103
|
-
const
|
|
112
|
+
const requestedFeatureId = asFeatureId(options.feature_id);
|
|
113
|
+
const resumePlan = await this.buildResumePlan(kernel, runId, requestedFeatureId);
|
|
104
114
|
|
|
105
115
|
if (resumePlan.features.length === 0) {
|
|
106
116
|
return {
|
|
@@ -127,6 +137,10 @@ export class ResumeCommandHandler {
|
|
|
127
137
|
policySnapshot.execution && typeof policySnapshot.execution === 'object'
|
|
128
138
|
? (policySnapshot.execution as Record<string, unknown>)
|
|
129
139
|
: null;
|
|
140
|
+
const observabilityPolicy =
|
|
141
|
+
policySnapshot.observability && typeof policySnapshot.observability === 'object'
|
|
142
|
+
? (policySnapshot.observability as Record<string, unknown>)
|
|
143
|
+
: null;
|
|
130
144
|
const provider = this.workerProviderFactory.create({
|
|
131
145
|
selection,
|
|
132
146
|
mode: resolveWorkerProviderMode(
|
|
@@ -136,7 +150,9 @@ export class ResumeCommandHandler {
|
|
|
136
150
|
),
|
|
137
151
|
context: 'resume',
|
|
138
152
|
policy: resolveWorkerProviderPolicy(executionPolicy),
|
|
153
|
+
observability: resolveWorkerProviderObservability(observabilityPolicy),
|
|
139
154
|
runtime: runtimeConfig,
|
|
155
|
+
repoRoot,
|
|
140
156
|
});
|
|
141
157
|
const supervisor = new SupervisorRuntime(kernel, provider, toolClient, {
|
|
142
158
|
max_active_features: 5,
|
|
@@ -144,6 +160,7 @@ export class ResumeCommandHandler {
|
|
|
144
160
|
run_id: resumePlan.run_id,
|
|
145
161
|
owner_instance_id: resumePlan.owner_instance_id,
|
|
146
162
|
takeover_stale_run: options.takeover_stale_run,
|
|
163
|
+
execution_mode: options.execution_mode,
|
|
147
164
|
});
|
|
148
165
|
|
|
149
166
|
const startResult = await supervisor.start(
|
|
@@ -168,7 +185,11 @@ export class ResumeCommandHandler {
|
|
|
168
185
|
};
|
|
169
186
|
}
|
|
170
187
|
|
|
171
|
-
private async buildResumePlan(
|
|
188
|
+
private async buildResumePlan(
|
|
189
|
+
kernel: AopKernel,
|
|
190
|
+
fallbackRunId: string,
|
|
191
|
+
requestedFeatureId?: string | null,
|
|
192
|
+
): Promise<ResumePlan> {
|
|
172
193
|
const index = await kernel.readIndex();
|
|
173
194
|
const runtimeSessions = await kernel.getRuntimeSessions();
|
|
174
195
|
const discovered = await kernel.featureDiscoverSpecs();
|
|
@@ -201,7 +222,13 @@ export class ResumeCommandHandler {
|
|
|
201
222
|
}
|
|
202
223
|
|
|
203
224
|
const resumable: ResumeFeature[] = [];
|
|
204
|
-
const
|
|
225
|
+
const normalizedRequestedFeatureId = asFeatureId(requestedFeatureId);
|
|
226
|
+
if (normalizedRequestedFeatureId) {
|
|
227
|
+
this.addSource(sourcesByFeature, normalizedRequestedFeatureId, 'cli.feature_id');
|
|
228
|
+
}
|
|
229
|
+
const sortedFeatureIds = normalizedRequestedFeatureId
|
|
230
|
+
? [normalizedRequestedFeatureId]
|
|
231
|
+
: [...sourcesByFeature.keys()].sort((a, b) => a.localeCompare(b));
|
|
205
232
|
for (const featureId of sortedFeatureIds) {
|
|
206
233
|
try {
|
|
207
234
|
const state = await kernel.featureStateGet(featureId);
|