engineering-memory 1.11.22 → 1.11.24
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/dispatcher/sections.mjs +2 -0
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +11 -0
- package/runtime/dist/src/git/verification-gate.js +2 -0
- package/runtime/dist/src/localization/catalogue.generated.js +26 -0
- package/runtime/dist/src/mcp/delivery-tools.js +313 -100
- package/runtime/dist/src/mcp/live-status-tools.js +37 -0
- package/runtime/dist/src/mcp/server.js +1 -0
- package/runtime/dist/src/mcp/tool-annotations.js +6 -0
- package/runtime/dist/src/mcp/tool-definitions.js +27 -1
- package/runtime/dist/src/mcp/worktree-tools.js +16 -2
- package/runtime/dist/src/runtime/api-client.js +7 -0
- package/runtime/dist/src/runtime/branch-preferences.js +15 -0
- package/runtime/dist/src/runtime/bridge-service.js +291 -30
- package/runtime/dist/src/runtime/create-bridge-service.js +8 -2
- package/runtime/dist/src/runtime/live-signals.js +178 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +7 -0
- package/runtime/dist/src/runtime/task-start.js +45 -15
- package/runtime/dist/src/runtime/worktree-pool.js +97 -20
- package/skill/SKILL.md +2 -0
- package/skill/references/lifecycle.md +32 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { liveQuestionKind } from './live-signals.js';
|
|
1
2
|
import { ignoredRuntimeFiles, localRelativePath } from './worktree-preparation.js';
|
|
2
3
|
import { decisionModeSchema } from './decision-mode-store.js';
|
|
3
|
-
import { delegatedReasonSchema } from './questionnaire-store.js';
|
|
4
|
+
import { answerSourceName, delegatedReasonSchema } from './questionnaire-store.js';
|
|
4
5
|
import { unreadableWorktree, } from './worktree-pool.js';
|
|
5
6
|
import { validWorktreePolicy } from './worktree-policy.js';
|
|
6
7
|
import { resolveTaskBase } from './branch-preferences.js';
|
|
@@ -56,6 +57,9 @@ export class BridgeService {
|
|
|
56
57
|
managesWorktrees() {
|
|
57
58
|
return Boolean(this.dependencies.worktreePool);
|
|
58
59
|
}
|
|
60
|
+
get live() {
|
|
61
|
+
return this.dependencies.live;
|
|
62
|
+
}
|
|
59
63
|
async language(told) {
|
|
60
64
|
const accessToken = await this.dependencies.credentials.get('access-token');
|
|
61
65
|
const principalHash = accessToken ? principalFingerprint(accessToken) : sha256('anonymous');
|
|
@@ -219,7 +223,7 @@ export class BridgeService {
|
|
|
219
223
|
alternativesConsidered: input.alternativesConsidered,
|
|
220
224
|
userInterestReview: input.userInterestReview,
|
|
221
225
|
});
|
|
222
|
-
const resolved = await this.
|
|
226
|
+
const resolved = await this.acceptWithScope(scope, record.questionnaireId, record.requestKey, answer, {
|
|
223
227
|
kind: 'delegated_agent',
|
|
224
228
|
externalTaskId,
|
|
225
229
|
mode: policy.state.mode,
|
|
@@ -242,7 +246,17 @@ export class BridgeService {
|
|
|
242
246
|
: await this.checkoutTask(scope.repoFingerprint, await this.dependencies.repositories.git.findRoot(input.repoRoot ?? process.cwd()));
|
|
243
247
|
const record = await this.dependencies.questionnaires.ask(scope, definition, previousDefinitions, owner ?? (askingTask ? { tool: 'questionnaire.ask', externalTaskId: askingTask } : undefined));
|
|
244
248
|
await this.assertDecisionCurrent(record);
|
|
245
|
-
return
|
|
249
|
+
return await this.liveQuestion({
|
|
250
|
+
...record,
|
|
251
|
+
language: record.language ?? (await this.language()),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async liveQuestion(record) {
|
|
255
|
+
if (record.status === 'pending')
|
|
256
|
+
await this.live.questionOpened(liveQuestionKind(record));
|
|
257
|
+
else
|
|
258
|
+
await this.live.questionClosed();
|
|
259
|
+
return record;
|
|
246
260
|
}
|
|
247
261
|
async checkoutTask(repoFingerprint, repoRoot) {
|
|
248
262
|
const worktreeId = sha256(await canonicalPath(repoRoot));
|
|
@@ -259,11 +273,14 @@ export class BridgeService {
|
|
|
259
273
|
if (!record)
|
|
260
274
|
throw refuse('No questionnaire exists for this account and repository binding.', 'session.entry');
|
|
261
275
|
await this.assertDecisionCurrent(record);
|
|
262
|
-
return
|
|
276
|
+
return await this.liveQuestion({
|
|
277
|
+
...record,
|
|
278
|
+
language: record.language ?? (await this.language()),
|
|
279
|
+
});
|
|
263
280
|
}
|
|
264
281
|
async questionnaireWithdraw(input) {
|
|
265
282
|
const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
|
|
266
|
-
return this.dependencies.questionnaires.withdraw(scope, input.questionnaireId);
|
|
283
|
+
return await this.liveQuestion(await this.dependencies.questionnaires.withdraw(scope, input.questionnaireId));
|
|
267
284
|
}
|
|
268
285
|
async questionnaireAccept(input) {
|
|
269
286
|
const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
|
|
@@ -273,7 +290,9 @@ export class BridgeService {
|
|
|
273
290
|
return await this.acceptWithScope(scope, input.questionnaireId, input.requestKey, input.answer, input.answerSource);
|
|
274
291
|
}
|
|
275
292
|
async acceptWithScope(scope, questionnaireId, requestKey, answer, answerSource) {
|
|
276
|
-
|
|
293
|
+
const resolved = await this.dependencies.questionnaires.accept(scope, questionnaireId, requestKey, answer, answerSource);
|
|
294
|
+
await this.liveQuestion(resolved.record);
|
|
295
|
+
return resolved;
|
|
277
296
|
}
|
|
278
297
|
questionnaireRetry(record, repoRoot, attemptOffset = 0) {
|
|
279
298
|
if (!record.owner || record.owner.tool === 'questionnaire.ask')
|
|
@@ -574,6 +593,7 @@ export class BridgeService {
|
|
|
574
593
|
};
|
|
575
594
|
await this.dependencies.activeContexts.save(pointer);
|
|
576
595
|
this.workedTasks.set(sha256(repository.repoRoot), input.externalTaskId);
|
|
596
|
+
await this.live.taskStarted(task.id);
|
|
577
597
|
await this.dependencies.journal.apply({
|
|
578
598
|
eventId: checkpointId,
|
|
579
599
|
taskId: task.id,
|
|
@@ -964,6 +984,7 @@ export class BridgeService {
|
|
|
964
984
|
? { closedAt: pointer?.closedAt ?? new Date().toISOString() }
|
|
965
985
|
: {}),
|
|
966
986
|
});
|
|
987
|
+
await this.live.taskStarted(backendTask.id);
|
|
967
988
|
}
|
|
968
989
|
const backendFresh = responseSource !== 'stale_cache';
|
|
969
990
|
return asJsonValue({
|
|
@@ -1023,16 +1044,19 @@ export class BridgeService {
|
|
|
1023
1044
|
}
|
|
1024
1045
|
async contextPrepareChange(input) {
|
|
1025
1046
|
const pointer = await this.dependencies.activeContexts.findBySessionId(input.sessionId);
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
try {
|
|
1029
|
-
return await this.taskExclusive(pointer.taskId, () => this.contextPrepareChangeUnlocked(input));
|
|
1030
|
-
}
|
|
1031
|
-
catch (error) {
|
|
1032
|
-
return this.execute(async () => {
|
|
1047
|
+
const result = pointer
|
|
1048
|
+
? await this.taskExclusive(pointer.taskId, () => this.contextPrepareChangeUnlocked(input)).catch((error) => this.execute(async () => {
|
|
1033
1049
|
throw error;
|
|
1034
|
-
})
|
|
1035
|
-
|
|
1050
|
+
}))
|
|
1051
|
+
: await this.contextPrepareChangeUnlocked(input);
|
|
1052
|
+
return this.liveTaskCall(result);
|
|
1053
|
+
}
|
|
1054
|
+
liveTaskCall(result) {
|
|
1055
|
+
if (result.ok)
|
|
1056
|
+
this.live.settled();
|
|
1057
|
+
else if (result.error?.recovery)
|
|
1058
|
+
this.live.refused(result.error.recovery);
|
|
1059
|
+
return result;
|
|
1036
1060
|
}
|
|
1037
1061
|
async contextPrepareChangeUnlocked(input) {
|
|
1038
1062
|
return await this.execute(async () => {
|
|
@@ -1689,10 +1713,31 @@ export class BridgeService {
|
|
|
1689
1713
|
catch (error) {
|
|
1690
1714
|
throw refuse(error instanceof Error ? error.message : 'The resulting commit must be inspected.', 'memory.sync_start');
|
|
1691
1715
|
}
|
|
1716
|
+
if (!proof)
|
|
1717
|
+
throw refuse('This task has nothing to publish: its project had no inspected source snapshot when the task was verified.', 'memory.sync_start');
|
|
1692
1718
|
const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
|
|
1693
1719
|
return response.data;
|
|
1694
1720
|
});
|
|
1695
1721
|
}
|
|
1722
|
+
async publishDelivered(repoRoot, taskId) {
|
|
1723
|
+
try {
|
|
1724
|
+
const proof = await this.dependencies.gate.publicationProof(repoRoot, taskId);
|
|
1725
|
+
if (!proof)
|
|
1726
|
+
return null;
|
|
1727
|
+
const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
|
|
1728
|
+
const answer = objectValue(response.data);
|
|
1729
|
+
return answer?.published === false
|
|
1730
|
+
? { published: false, reason: String(answer.reason) }
|
|
1731
|
+
: { published: true };
|
|
1732
|
+
}
|
|
1733
|
+
catch (error) {
|
|
1734
|
+
return {
|
|
1735
|
+
published: false,
|
|
1736
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1737
|
+
nextAction: 'The delivery is recorded; only the source memory of this task was not published. The member who opened the task can call memory.publish_task in this folder before another task reuses it; otherwise memory.sync_start inspects the result.',
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1696
1741
|
async onboardingRequest(operation, body) {
|
|
1697
1742
|
return this.execute(async () => {
|
|
1698
1743
|
if (operation === 'next' || operation === 'submit') {
|
|
@@ -1778,7 +1823,7 @@ export class BridgeService {
|
|
|
1778
1823
|
});
|
|
1779
1824
|
}
|
|
1780
1825
|
async taskCheckpoint(input) {
|
|
1781
|
-
return await this.writeTaskEvent(input, false);
|
|
1826
|
+
return this.liveTaskCall(await this.writeTaskEvent(input, false));
|
|
1782
1827
|
}
|
|
1783
1828
|
async taskRecordCorrection(input) {
|
|
1784
1829
|
const reference = input.correctionRef ?? input.summary;
|
|
@@ -1864,7 +1909,7 @@ export class BridgeService {
|
|
|
1864
1909
|
}));
|
|
1865
1910
|
}
|
|
1866
1911
|
async taskSelfReview(input) {
|
|
1867
|
-
|
|
1912
|
+
const reviewed = await this.execute(async () => {
|
|
1868
1913
|
const files = await this.approvedDeviations(input);
|
|
1869
1914
|
assertSafeToPersist(cleanJson({ files }));
|
|
1870
1915
|
const checkout = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
@@ -1896,6 +1941,7 @@ export class BridgeService {
|
|
|
1896
1941
|
},
|
|
1897
1942
|
});
|
|
1898
1943
|
});
|
|
1944
|
+
return this.liveTaskCall(reviewed);
|
|
1899
1945
|
}
|
|
1900
1946
|
async approvedDeviations(input) {
|
|
1901
1947
|
const files = reviewedFiles(input);
|
|
@@ -1934,7 +1980,7 @@ export class BridgeService {
|
|
|
1934
1980
|
})));
|
|
1935
1981
|
}
|
|
1936
1982
|
async taskReconcile(input) {
|
|
1937
|
-
|
|
1983
|
+
const reconciled = await this.execute(async () => {
|
|
1938
1984
|
const repoRoot = input.repoRoot ?? process.cwd();
|
|
1939
1985
|
const entries = (input.entries ?? [
|
|
1940
1986
|
{
|
|
@@ -1965,6 +2011,7 @@ export class BridgeService {
|
|
|
1965
2011
|
});
|
|
1966
2012
|
return asJsonValue({ reconciliations: response.data });
|
|
1967
2013
|
});
|
|
2014
|
+
return this.liveTaskCall(reconciled);
|
|
1968
2015
|
}
|
|
1969
2016
|
async taskResolvePendingDelivery(input) {
|
|
1970
2017
|
return await this.execute(async () => {
|
|
@@ -2075,7 +2122,7 @@ export class BridgeService {
|
|
|
2075
2122
|
}, true);
|
|
2076
2123
|
}
|
|
2077
2124
|
async taskVerify(input) {
|
|
2078
|
-
|
|
2125
|
+
const verified = await this.execute(async () => {
|
|
2079
2126
|
const timer = startPhaseTimer('task.verify');
|
|
2080
2127
|
return await this.taskExclusive(input.taskId, async () => {
|
|
2081
2128
|
await this.recoverJournalOutbox();
|
|
@@ -2268,11 +2315,7 @@ export class BridgeService {
|
|
|
2268
2315
|
questionnaireId,
|
|
2269
2316
|
reason: waiver.reason,
|
|
2270
2317
|
paths,
|
|
2271
|
-
answerSource: question.answerSource
|
|
2272
|
-
? 'delegated_agent'
|
|
2273
|
-
: question.answerSource?.kind === 'host_native_relay'
|
|
2274
|
-
? question.answerSource.hostTool
|
|
2275
|
-
: 'mcp_form',
|
|
2318
|
+
answerSource: answerSourceName(question.answerSource),
|
|
2276
2319
|
...(delegatedEvidence(question)
|
|
2277
2320
|
? { delegatedDecision: delegatedEvidence(question) }
|
|
2278
2321
|
: {}),
|
|
@@ -2365,6 +2408,7 @@ export class BridgeService {
|
|
|
2365
2408
|
});
|
|
2366
2409
|
});
|
|
2367
2410
|
});
|
|
2411
|
+
return this.liveTaskCall(verified);
|
|
2368
2412
|
}
|
|
2369
2413
|
async validationWaiverSubject(input) {
|
|
2370
2414
|
return await this.execute(async () => {
|
|
@@ -2407,7 +2451,7 @@ export class BridgeService {
|
|
|
2407
2451
|
});
|
|
2408
2452
|
}
|
|
2409
2453
|
async taskClose(input) {
|
|
2410
|
-
|
|
2454
|
+
const closed = await this.execute(async () => {
|
|
2411
2455
|
return await this.taskExclusive(input.taskId, async () => {
|
|
2412
2456
|
const checkout = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
2413
2457
|
const pointer = await this.requireActivePointer(checkout.repoFingerprint, input.taskId, false, 'close');
|
|
@@ -2452,6 +2496,7 @@ export class BridgeService {
|
|
|
2452
2496
|
return await this.deliverCloseIntent(repository, closeBody, taskChanges, false);
|
|
2453
2497
|
});
|
|
2454
2498
|
});
|
|
2499
|
+
return this.liveTaskCall(closed);
|
|
2455
2500
|
}
|
|
2456
2501
|
async worktreeTaskAllocation(input) {
|
|
2457
2502
|
return this.execute(async () => {
|
|
@@ -2545,9 +2590,44 @@ export class BridgeService {
|
|
|
2545
2590
|
currentCommit: repository.git.head,
|
|
2546
2591
|
folder,
|
|
2547
2592
|
suggestedName,
|
|
2593
|
+
...(input.workItemId
|
|
2594
|
+
? await this.workItemBranches(repository.repoRoot, repository.projectId, input.workItemProjectId ?? repository.projectId, input.workItemId, preferences.data)
|
|
2595
|
+
: { planBranch: null, continueBranches: [] }),
|
|
2548
2596
|
});
|
|
2549
2597
|
});
|
|
2550
2598
|
}
|
|
2599
|
+
async workItemBranches(repoRoot, projectId, itemProjectId, workItemId, preferences) {
|
|
2600
|
+
const git = this.dependencies.repositories.git;
|
|
2601
|
+
const [plan, runs, reserved, openDeliveries] = await Promise.all([
|
|
2602
|
+
this.dependencies.client.request(endpoints.workItemPlan(itemProjectId, workItemId)),
|
|
2603
|
+
this.dependencies.client.request(`${endpoints.workItemRuns(itemProjectId, workItemId)}?offset=0&limit=50`),
|
|
2604
|
+
git.protectedBranchNames(repoRoot),
|
|
2605
|
+
this.dependencies.client
|
|
2606
|
+
.request(`${endpoints.projectDeliveries(projectId)}?state=open&limit=100`)
|
|
2607
|
+
.then((response) => response.data?.items ?? [], () => []),
|
|
2608
|
+
]);
|
|
2609
|
+
for (const preference of Object.values(preferences.branches))
|
|
2610
|
+
if (preference.branch)
|
|
2611
|
+
reserved.add(preference.branch);
|
|
2612
|
+
const used = [
|
|
2613
|
+
...new Set((runs.data?.items ?? [])
|
|
2614
|
+
.filter((run) => run.projectId === projectId && run.mode !== 'read_only')
|
|
2615
|
+
.map((run) => run.branch)
|
|
2616
|
+
.filter((branch) => !!branch && !reserved.has(branch))),
|
|
2617
|
+
];
|
|
2618
|
+
const planBranch = plan.data?.confirmed ? plan.data.branch : null;
|
|
2619
|
+
const continueBranches = !planBranch
|
|
2620
|
+
? used.slice(0, 2)
|
|
2621
|
+
: used.includes(planBranch) || (await git.branchExists(repoRoot, planBranch))
|
|
2622
|
+
? [planBranch]
|
|
2623
|
+
: [];
|
|
2624
|
+
const open = new Set(openDeliveries.map((delivery) => delivery.taskId));
|
|
2625
|
+
return {
|
|
2626
|
+
planBranch,
|
|
2627
|
+
continueBranches,
|
|
2628
|
+
openDeliveryBranches: continueBranches.filter((branch) => (runs.data?.items ?? []).some((run) => run.branch === branch && open.has(run.id))),
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2551
2631
|
async suggestedBranchName(repoRoot, externalTaskId) {
|
|
2552
2632
|
const candidates = suggestedBranchNames(externalTaskId);
|
|
2553
2633
|
for (const candidate of candidates)
|
|
@@ -2770,11 +2850,22 @@ export class BridgeService {
|
|
|
2770
2850
|
if (operation === 'pause') {
|
|
2771
2851
|
await pool.pause(projectId, repository.repoRoot, input.generation);
|
|
2772
2852
|
this.poolAllocations.delete(repository.repoRoot);
|
|
2853
|
+
await this.live.taskPaused(entry.taskId);
|
|
2773
2854
|
}
|
|
2774
2855
|
if (operation === 'release') {
|
|
2775
2856
|
await this.requireDeliveredTaskWork(entry);
|
|
2857
|
+
const sourcePublication = outcome === 'delivered' && entry.taskId && entry.pendingDelivery
|
|
2858
|
+
? await this.publishDelivered(repository.repoRoot, entry.taskId)
|
|
2859
|
+
: null;
|
|
2776
2860
|
await pool.release(projectId, repository.repoRoot, input.generation, outcome === 'delivered' ? outcome : undefined);
|
|
2777
2861
|
this.poolAllocations.delete(repository.repoRoot);
|
|
2862
|
+
await this.live.taskPaused(entry.taskId);
|
|
2863
|
+
return asJsonValue({
|
|
2864
|
+
operation,
|
|
2865
|
+
complete: true,
|
|
2866
|
+
repoRoot: repository.repoRoot,
|
|
2867
|
+
...(sourcePublication ? { sourcePublication } : {}),
|
|
2868
|
+
});
|
|
2778
2869
|
}
|
|
2779
2870
|
if (operation === 'cancel_delivery') {
|
|
2780
2871
|
if (!entry.pendingDelivery)
|
|
@@ -2795,6 +2886,7 @@ export class BridgeService {
|
|
|
2795
2886
|
await this.requireDeliveredTaskWork(entry);
|
|
2796
2887
|
const cancelled = await pool.cancelDelivery(projectId, repository.repoRoot, input.generation, choice);
|
|
2797
2888
|
this.poolAllocations.delete(repository.repoRoot);
|
|
2889
|
+
await this.live.taskPaused(entry.taskId);
|
|
2798
2890
|
return asJsonValue({
|
|
2799
2891
|
operation,
|
|
2800
2892
|
complete: true,
|
|
@@ -2882,7 +2974,8 @@ export class BridgeService {
|
|
|
2882
2974
|
throw refuse(`This folder is held by task ${holder.externalTaskId}. Call task.branch again: its start form asks whether ${holder.externalTaskId} should move out of this folder before the new branch starts here, or opens the new task in a separate worktree.`, 'task.branch');
|
|
2883
2975
|
if (repository.git.changedPaths.length > 0)
|
|
2884
2976
|
throw refuse(`This folder has uncommitted changes, so the new branch cannot start here and ${holder.externalTaskId} stays in it. Call task.branch with a new decisionAttempt and open the task in a separate worktree.`, 'task.branch');
|
|
2885
|
-
if (
|
|
2977
|
+
if (input.base.kind !== 'existing' &&
|
|
2978
|
+
(await git.branchExists(repository.repoRoot, input.name)))
|
|
2886
2979
|
throw refuse(`Branch ${input.name} already exists, so it cannot start here and ${holder.externalTaskId} stays in this folder. Call task.branch with a new decisionAttempt and choose a new branch name.`, 'task.branch');
|
|
2887
2980
|
if (holder.generation) {
|
|
2888
2981
|
await pool.moveOut(projectId, repository.repoRoot, holder.generation, holder.oid ?? undefined);
|
|
@@ -2914,6 +3007,7 @@ export class BridgeService {
|
|
|
2914
3007
|
keepCurrent: input.keepCurrent,
|
|
2915
3008
|
inPlace: !existing && input.inPlace,
|
|
2916
3009
|
resume: Boolean(existing),
|
|
3010
|
+
continued: !existing && input.base?.kind === 'existing',
|
|
2917
3011
|
}, allocationPolicy));
|
|
2918
3012
|
allocationTimer.mark('pool_allocate');
|
|
2919
3013
|
allocationTimer.finish();
|
|
@@ -3215,6 +3309,7 @@ export class BridgeService {
|
|
|
3215
3309
|
.release(input.taskId);
|
|
3216
3310
|
const forgotten = await this.dependencies.activeContexts.forget(repository.repoFingerprint, input.taskId);
|
|
3217
3311
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
3312
|
+
this.live.taskEnded(input.taskId);
|
|
3218
3313
|
return asJsonValue({
|
|
3219
3314
|
...abandoned,
|
|
3220
3315
|
localPointerRemoved: forgotten,
|
|
@@ -3725,6 +3820,22 @@ export class BridgeService {
|
|
|
3725
3820
|
return asJsonValue(response.data);
|
|
3726
3821
|
});
|
|
3727
3822
|
}
|
|
3823
|
+
async workItemComment(input) {
|
|
3824
|
+
return this.execute(async () => {
|
|
3825
|
+
const response = await this.dependencies.client.request(endpoints.workItemComments(input.projectId, input.workItemId), { method: 'POST', body: cleanJson(input.data) });
|
|
3826
|
+
return asJsonValue(response.data);
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
async workItemComments(input) {
|
|
3830
|
+
return this.execute(async () => {
|
|
3831
|
+
const query = new URLSearchParams({
|
|
3832
|
+
offset: String(input.offset ?? 0),
|
|
3833
|
+
limit: String(input.limit ?? 50),
|
|
3834
|
+
});
|
|
3835
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemComments(input.projectId, input.workItemId)}?${query}`);
|
|
3836
|
+
return asJsonValue(response.data);
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3728
3839
|
async workItemPlan(input) {
|
|
3729
3840
|
return this.execute(async () => {
|
|
3730
3841
|
const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId));
|
|
@@ -4023,7 +4134,7 @@ export class BridgeService {
|
|
|
4023
4134
|
const livePointers = authenticated && state === 'bound'
|
|
4024
4135
|
? this.dependencies.activeContexts.list(repository.repoFingerprint)
|
|
4025
4136
|
: Promise.resolve([]);
|
|
4026
|
-
const [client, liveTasksRaw, workItems, gitPreferences, worktrees, pendingQuestionnaires, moved,] = await Promise.all([
|
|
4137
|
+
const [client, liveTasksRaw, workItems, gitPreferences, worktrees, pendingQuestionnaires, moved, deliveries,] = await Promise.all([
|
|
4027
4138
|
this.clientUpdate(authenticated),
|
|
4028
4139
|
livePointers,
|
|
4029
4140
|
authenticated && state === 'bound' && projectId
|
|
@@ -4044,6 +4155,9 @@ export class BridgeService {
|
|
|
4044
4155
|
authenticated && state === 'bound' && projectId
|
|
4045
4156
|
? this.movedAway(repository, projectId)
|
|
4046
4157
|
: Promise.resolve({}),
|
|
4158
|
+
authenticated && state === 'bound' && projectId
|
|
4159
|
+
? this.openDeliveries(projectId)
|
|
4160
|
+
: Promise.resolve(null),
|
|
4047
4161
|
]);
|
|
4048
4162
|
timer.mark('independent_lookups');
|
|
4049
4163
|
const liveTasks = liveTasksRaw.map(describePointer);
|
|
@@ -4056,9 +4170,11 @@ export class BridgeService {
|
|
|
4056
4170
|
.map(([role]) => role)
|
|
4057
4171
|
: [];
|
|
4058
4172
|
const taskSelectionRequired = liveTasks.length > 0 || workItems.length > 0;
|
|
4059
|
-
const
|
|
4173
|
+
const open = authenticated && state === 'bound' && !moved.nextAction;
|
|
4174
|
+
const whatsNew = open
|
|
4060
4175
|
? await this.dependencies.releaseNotes?.prepare(this.dependencies.clientVersion, language)
|
|
4061
4176
|
: null;
|
|
4177
|
+
const liveStatusNotice = open && projectId ? await this.liveStatusNotice(projectId, language) : null;
|
|
4062
4178
|
timer.finish();
|
|
4063
4179
|
return asJsonValue({
|
|
4064
4180
|
authenticated,
|
|
@@ -4069,6 +4185,12 @@ export class BridgeService {
|
|
|
4069
4185
|
workItems,
|
|
4070
4186
|
taskSelectionRequired,
|
|
4071
4187
|
...(whatsNew ? { whatsNew } : {}),
|
|
4188
|
+
...(liveStatusNotice
|
|
4189
|
+
? {
|
|
4190
|
+
liveStatusNotice,
|
|
4191
|
+
liveStatusNoticeAction: 'Show this text to the user once as a short paragraph in their language, then silently call live_status.notice_presented with this exact noticeVersion. It asks nothing, needs no answer and never blocks their work.',
|
|
4192
|
+
}
|
|
4193
|
+
: {}),
|
|
4072
4194
|
...(worktrees ? { worktrees: worktrees.ok ? worktrees.data : worktrees.error } : {}),
|
|
4073
4195
|
...(gitPreferences
|
|
4074
4196
|
? {
|
|
@@ -4083,6 +4205,7 @@ export class BridgeService {
|
|
|
4083
4205
|
...(pendingQuestionnaires.deferred.length > 0
|
|
4084
4206
|
? { deferredTaskStarts: pendingQuestionnaires.deferred }
|
|
4085
4207
|
: {}),
|
|
4208
|
+
...(deliveries ? { openDeliveries: deliveries } : {}),
|
|
4086
4209
|
client,
|
|
4087
4210
|
...moved,
|
|
4088
4211
|
...(incomplete && state === 'bound'
|
|
@@ -4113,6 +4236,58 @@ export class BridgeService {
|
|
|
4113
4236
|
recorded: (await this.dependencies.releaseNotes?.presented(input.reportId, input.claimId)) ?? false,
|
|
4114
4237
|
}));
|
|
4115
4238
|
}
|
|
4239
|
+
async liveStatusNotice(projectId, language) {
|
|
4240
|
+
const response = await this.dependencies.client
|
|
4241
|
+
.request(endpoints.liveStatusNotice(projectId), {
|
|
4242
|
+
networkTimeoutMs: 2000,
|
|
4243
|
+
retryRefresh: false,
|
|
4244
|
+
})
|
|
4245
|
+
.catch(() => null);
|
|
4246
|
+
const notice = liveStatusNoticeSchema.safeParse(response?.data);
|
|
4247
|
+
if (!notice.success || notice.data.presented)
|
|
4248
|
+
return null;
|
|
4249
|
+
const { copy } = copies(liveStatusNoticeWording, 'liveStatusNotice', language)[0];
|
|
4250
|
+
return {
|
|
4251
|
+
projectId: notice.data.projectId,
|
|
4252
|
+
organizationId: notice.data.organizationId,
|
|
4253
|
+
noticeVersion: notice.data.noticeVersion,
|
|
4254
|
+
sharing: notice.data.sharing,
|
|
4255
|
+
text: copy[notice.data.sharing],
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
async liveStatusList(input) {
|
|
4259
|
+
return await this.execute(async () => {
|
|
4260
|
+
const scope = input.organizationId
|
|
4261
|
+
? new URLSearchParams({ organizationId: input.organizationId })
|
|
4262
|
+
: new URLSearchParams({
|
|
4263
|
+
projectId: input.projectId ?? (await this.boundProject(input.repoRoot)),
|
|
4264
|
+
});
|
|
4265
|
+
const response = await this.dependencies.client.request(`${endpoints.liveStatusList}?${scope}`);
|
|
4266
|
+
return asJsonValue(response.data);
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
async liveStatusNoticePresented(input) {
|
|
4270
|
+
return await this.execute(async () => {
|
|
4271
|
+
const projectId = input.projectId ?? (await this.boundProject(input.repoRoot));
|
|
4272
|
+
const response = await this.dependencies.client.request(endpoints.liveStatusNoticePresented(projectId), { method: 'POST', body: { noticeVersion: input.noticeVersion } });
|
|
4273
|
+
return asJsonValue(response.data);
|
|
4274
|
+
});
|
|
4275
|
+
}
|
|
4276
|
+
async liveStatusSetSharing(input) {
|
|
4277
|
+
return await this.execute(async () => {
|
|
4278
|
+
const response = await this.dependencies.client.request(endpoints.liveStatusSharing(input.organizationId), {
|
|
4279
|
+
method: 'PUT',
|
|
4280
|
+
body: { sharing: input.sharing, expectedSharing: input.expectedSharing },
|
|
4281
|
+
});
|
|
4282
|
+
return asJsonValue(response.data);
|
|
4283
|
+
});
|
|
4284
|
+
}
|
|
4285
|
+
async boundProject(repoRoot) {
|
|
4286
|
+
const repository = await this.dependencies.repositories.resolveIdentity(repoRoot ?? process.cwd());
|
|
4287
|
+
if (!repository.projectId)
|
|
4288
|
+
throw refuse('This repository has no selected project, so there is no default scope for live status. Name a projectId or an organizationId.', 'session.entry');
|
|
4289
|
+
return repository.projectId;
|
|
4290
|
+
}
|
|
4116
4291
|
async movedAway(repository, projectId) {
|
|
4117
4292
|
try {
|
|
4118
4293
|
const response = await this.dependencies.client.request(endpoints.projectResolve, {
|
|
@@ -4126,6 +4301,30 @@ export class BridgeService {
|
|
|
4126
4301
|
return {};
|
|
4127
4302
|
}
|
|
4128
4303
|
}
|
|
4304
|
+
async openDeliveries(projectId) {
|
|
4305
|
+
try {
|
|
4306
|
+
const response = await this.dependencies.client.request(`${endpoints.projectDeliveries(projectId)}?state=open&limit=5`);
|
|
4307
|
+
const page = objectValue(response.data);
|
|
4308
|
+
const items = Array.isArray(page?.items) ? page.items : [];
|
|
4309
|
+
if (!items.length)
|
|
4310
|
+
return null;
|
|
4311
|
+
const pool = this.dependencies.worktreePool;
|
|
4312
|
+
return {
|
|
4313
|
+
total: page.total ?? items.length,
|
|
4314
|
+
items: await Promise.all(items.map(async (item) => {
|
|
4315
|
+
const folder = await pool?.forTask(String(objectValue(item)?.taskId));
|
|
4316
|
+
return {
|
|
4317
|
+
...objectOrEmpty(item),
|
|
4318
|
+
folderHere: folder?.pendingDelivery ? folder.repoRoot : null,
|
|
4319
|
+
};
|
|
4320
|
+
})),
|
|
4321
|
+
nextAction: 'These closed tasks have no concluded delivery. Mention them to the user in one line. For one whose folderHere is set, task.close with its taskId in that folder asks and performs the Git delivery there. Any other can be answered or concluded with task.delivery when the user asks.',
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
catch {
|
|
4325
|
+
return null;
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4129
4328
|
async actionableWorkItems(projectId) {
|
|
4130
4329
|
try {
|
|
4131
4330
|
const response = await this.dependencies.client.request(`${endpoints.workItemList(projectId)}?limit=100&actionable=true`);
|
|
@@ -4431,6 +4630,44 @@ export class BridgeService {
|
|
|
4431
4630
|
return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
|
|
4432
4631
|
}
|
|
4433
4632
|
}
|
|
4633
|
+
async taskDeliveryAnswer(input) {
|
|
4634
|
+
return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryAnswer(input.projectId, input.taskId), cleanJson({
|
|
4635
|
+
expectedVersion: input.expectedVersion,
|
|
4636
|
+
choice: input.choice,
|
|
4637
|
+
baseBranch: input.baseBranch,
|
|
4638
|
+
answerSource: input.answerSource,
|
|
4639
|
+
})));
|
|
4640
|
+
}
|
|
4641
|
+
async taskDeliveryCancel(input) {
|
|
4642
|
+
return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryCancel(input.projectId, input.taskId), { reason: input.reason }));
|
|
4643
|
+
}
|
|
4644
|
+
async taskDeliveryRecord(input) {
|
|
4645
|
+
return this.execute(async () => {
|
|
4646
|
+
const response = await this.dependencies.client.request(endpoints.taskDelivery(input.projectId, input.taskId));
|
|
4647
|
+
const folder = await this.dependencies.worktreePool?.forTask(input.taskId);
|
|
4648
|
+
return asJsonValue({
|
|
4649
|
+
record: response.data,
|
|
4650
|
+
folder: folder?.pendingDelivery ? folder.repoRoot : null,
|
|
4651
|
+
});
|
|
4652
|
+
});
|
|
4653
|
+
}
|
|
4654
|
+
async reportDeliveryOutcome(report) {
|
|
4655
|
+
const path = report.outcome === 'delivered'
|
|
4656
|
+
? endpoints.taskDeliveryDeliver(report.projectId, report.taskId)
|
|
4657
|
+
: endpoints.taskDeliveryCancel(report.projectId, report.taskId);
|
|
4658
|
+
const body = report.outcome === 'delivered'
|
|
4659
|
+
? { commit: report.commit, pushed: report.pushed }
|
|
4660
|
+
: { reason: report.reason };
|
|
4661
|
+
await this.dependencies.outbox.enqueue({
|
|
4662
|
+
operation: 'task.delivery',
|
|
4663
|
+
method: 'POST',
|
|
4664
|
+
path,
|
|
4665
|
+
body,
|
|
4666
|
+
projectId: report.projectId,
|
|
4667
|
+
idempotencyKey: sha256(stableStringify({ path, body })),
|
|
4668
|
+
});
|
|
4669
|
+
this.deliverInBackground();
|
|
4670
|
+
}
|
|
4434
4671
|
async enqueueTaskReceipt(operation, path, taskId, repoRoot, content) {
|
|
4435
4672
|
const repository = await this.dependencies.repositories.resolve(repoRoot);
|
|
4436
4673
|
return this.taskExclusive(taskId, async () => {
|
|
@@ -4479,7 +4716,11 @@ export class BridgeService {
|
|
|
4479
4716
|
for (const entry of entries) {
|
|
4480
4717
|
const owner = taskIdFromDeliverySafe(entry);
|
|
4481
4718
|
const body = objectOrEmpty(entry.body);
|
|
4482
|
-
const key = owner
|
|
4719
|
+
const key = owner
|
|
4720
|
+
? 'task:' + owner
|
|
4721
|
+
: entry.operation === 'task.delivery'
|
|
4722
|
+
? 'delivery:' + entry.path.slice(0, entry.path.lastIndexOf('/'))
|
|
4723
|
+
: 'project:' + String(body.projectId ?? 'unscoped');
|
|
4483
4724
|
const group = groups.get(key) ?? [];
|
|
4484
4725
|
group.push(entry);
|
|
4485
4726
|
groups.set(key, group);
|
|
@@ -4505,6 +4746,12 @@ export class BridgeService {
|
|
|
4505
4746
|
synchronized.push(entry.id);
|
|
4506
4747
|
}
|
|
4507
4748
|
catch (error) {
|
|
4749
|
+
if (entry.operation === 'task.delivery' &&
|
|
4750
|
+
error instanceof ApiResponseError &&
|
|
4751
|
+
(error.httpStatus === 404 || error.httpStatus === 409)) {
|
|
4752
|
+
await this.dependencies.outbox.acknowledge(entry.id);
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
4508
4755
|
const errorKind = classifyError(error);
|
|
4509
4756
|
await this.dependencies.outbox.markAttempt(entry.id, errorKind);
|
|
4510
4757
|
if (entry.journalRef)
|
|
@@ -4874,6 +5121,9 @@ export class BridgeService {
|
|
|
4874
5121
|
response = await this.dependencies.client.request(endpoints.taskClose, {
|
|
4875
5122
|
method: 'POST',
|
|
4876
5123
|
body,
|
|
5124
|
+
...(this.dependencies.clientVersion
|
|
5125
|
+
? { headers: { 'x-client-version': this.dependencies.clientVersion } }
|
|
5126
|
+
: {}),
|
|
4877
5127
|
});
|
|
4878
5128
|
}
|
|
4879
5129
|
catch (error) {
|
|
@@ -5139,6 +5389,17 @@ const modeWording = z.strictObject({
|
|
|
5139
5389
|
autonomous: option,
|
|
5140
5390
|
ask: option,
|
|
5141
5391
|
});
|
|
5392
|
+
const liveStatusNoticeWording = z.strictObject({
|
|
5393
|
+
managers: text,
|
|
5394
|
+
off: text,
|
|
5395
|
+
});
|
|
5396
|
+
const liveStatusNoticeSchema = z.object({
|
|
5397
|
+
projectId: z.uuid(),
|
|
5398
|
+
organizationId: z.uuid(),
|
|
5399
|
+
sharing: z.enum(['managers', 'off']),
|
|
5400
|
+
noticeVersion: z.string().regex(/^[0-9a-f]{64}$/),
|
|
5401
|
+
presented: z.boolean(),
|
|
5402
|
+
});
|
|
5142
5403
|
const ruleDeviationWording = z.strictObject({
|
|
5143
5404
|
ask: text,
|
|
5144
5405
|
rule: text,
|
|
@@ -5371,7 +5632,7 @@ function deliveryQuestion(touchedContract, destination, publishApplicable) {
|
|
|
5371
5632
|
: {}),
|
|
5372
5633
|
afterPullRequest: 'Do not end the turn once a pull request exists. Check whether it merges cleanly, report the conflicting files if it does not, and ask whether to resolve them before touching anything.',
|
|
5373
5634
|
afterCommit: publishApplicable
|
|
5374
|
-
? '
|
|
5635
|
+
? "After the authorized delivery, call worktree.release in this folder with deliveryOutcome delivered: it publishes this task's source memory and then records the delivery for the project. Its sourcePublication says whether publication happened; call memory.publish_task only when it names a reason you can fix. A task without a worktree to release calls memory.publish_task after the commit instead."
|
|
5375
5636
|
: 'This task has nothing to publish (see sourcePublication on the close response); do not call memory.publish_task.',
|
|
5376
5637
|
});
|
|
5377
5638
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LiveSignals } from './live-signals.js';
|
|
1
2
|
import { ReleaseNotes } from './release-notes.js';
|
|
2
3
|
import { WorktreePool } from './worktree-pool.js';
|
|
3
4
|
import { WorktreePolicyCache } from './worktree-policy.js';
|
|
@@ -42,13 +43,16 @@ export function createBridgeService(options = {}) {
|
|
|
42
43
|
credentials,
|
|
43
44
|
cache,
|
|
44
45
|
refreshPath: endpoints.authRefresh,
|
|
46
|
+
liveHeader: (path) => live.header(path),
|
|
45
47
|
});
|
|
48
|
+
const live = new LiveSignals(client);
|
|
46
49
|
const git = new GitInspector();
|
|
47
50
|
const outbox = new OfflineOutbox(stateRoot);
|
|
48
51
|
const activeContexts = new ActiveContextStore(stateRoot);
|
|
49
52
|
const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
|
|
50
53
|
const updateChoices = new UpdateChoiceStore(stateRoot);
|
|
51
54
|
const shadowNotices = new ShadowNoticeStore(stateRoot);
|
|
55
|
+
let service;
|
|
52
56
|
const worktreePool = new WorktreePool(stateRoot, {
|
|
53
57
|
...(options.ownerId ? { ownerId: options.ownerId } : {}),
|
|
54
58
|
pendingWork: async (entry) => {
|
|
@@ -59,11 +63,13 @@ export function createBridgeService(options = {}) {
|
|
|
59
63
|
pointer?.closeIntent ||
|
|
60
64
|
(await outbox.listForTask(entry.taskId)).length);
|
|
61
65
|
},
|
|
66
|
+
onDeliveryOutcome: async (report) => service?.reportDeliveryOutcome(report),
|
|
62
67
|
});
|
|
63
68
|
const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
|
|
64
69
|
const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
|
|
65
70
|
const languages = new LanguageStore(stateRoot);
|
|
66
|
-
return new BridgeService({
|
|
71
|
+
return (service = new BridgeService({
|
|
72
|
+
live,
|
|
67
73
|
releaseNotes: new ReleaseNotes(stateRoot, client, credentials),
|
|
68
74
|
worktreePool,
|
|
69
75
|
worktreePolicy: new WorktreePolicyCache(stateRoot),
|
|
@@ -83,6 +89,6 @@ export function createBridgeService(options = {}) {
|
|
|
83
89
|
onboarding: new OnboardingStore(stateRoot),
|
|
84
90
|
clientVersion: options.clientVersion === undefined ? config.clientVersion : options.clientVersion,
|
|
85
91
|
principalState,
|
|
86
|
-
});
|
|
92
|
+
}));
|
|
87
93
|
}
|
|
88
94
|
//# sourceMappingURL=create-bridge-service.js.map
|