@atolis-hq/wake 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/orchestration/application/advance-workflow.js +19 -18
- package/dist/src/orchestration/application/coordination-claims.js +30 -31
- package/dist/src/orchestration/application/durable-append.js +36 -0
- package/dist/src/orchestration/application/resource-transition-matching.js +14 -17
- package/dist/src/orchestration/application/trigger-workflow-instance.js +20 -0
- package/dist/src/orchestration/application/watch-reactor.js +1 -33
- package/dist/src/orchestration/domain/compiler.js +1 -0
- package/dist/src/orchestration/domain/signal-policy.js +6 -15
- package/dist/src/orchestration/domain/transition.js +7 -2
- package/dist/src/orchestration/index.js +2 -0
- package/package.json +8 -2
|
@@ -5,6 +5,7 @@ import { workflowInstanceStream } from '../contracts/streams.js';
|
|
|
5
5
|
import { WorkflowStatus } from '../contracts/vocabulary.js';
|
|
6
6
|
import { requestChangesResume as decideChangesResume, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, } from '../domain/interpreter.js';
|
|
7
7
|
import { isAuthorisedActor } from '../domain/supplemental-policy.js';
|
|
8
|
+
import { appendWithIntentRecovery } from './durable-append.js';
|
|
8
9
|
import { acceptResourceTransition, matchResourceTransitions, } from './resource-transition-matching.js';
|
|
9
10
|
import { matchWatches } from './watch-matching.js';
|
|
10
11
|
export class OperatorRetryIneligibleError extends Error {
|
|
@@ -126,15 +127,15 @@ export class AdvanceWorkflow {
|
|
|
126
127
|
});
|
|
127
128
|
if (decision.kind === 'ignored')
|
|
128
129
|
throw new OperatorRetryIneligibleError(decision.reason);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
const recovered = await appendWithIntentRecovery({
|
|
131
|
+
append: async () => {
|
|
132
|
+
await this.repository.append(id, loaded.sequence, decision.events);
|
|
133
|
+
},
|
|
134
|
+
load: () => this.repository.loadRequired(id),
|
|
135
|
+
alreadyApplied: (reloaded) => reloaded.view.operatorRetryCommandIds.includes(context.commandId),
|
|
136
|
+
});
|
|
137
|
+
if (recovered !== undefined)
|
|
138
|
+
return recovered.view;
|
|
138
139
|
return (await this.repository.loadRequired(id)).view;
|
|
139
140
|
}
|
|
140
141
|
async resumeBlockedStageForChanges(id, context) {
|
|
@@ -153,15 +154,15 @@ export class AdvanceWorkflow {
|
|
|
153
154
|
});
|
|
154
155
|
if (decision.kind === 'ignored')
|
|
155
156
|
return loaded.view;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
const recovered = await appendWithIntentRecovery({
|
|
158
|
+
append: async () => {
|
|
159
|
+
await this.repository.append(id, loaded.sequence, decision.events);
|
|
160
|
+
},
|
|
161
|
+
load: () => this.repository.loadRequired(id),
|
|
162
|
+
alreadyApplied: (reloaded) => reloaded.view.operatorRetryCommandIds.includes(context.commandId),
|
|
163
|
+
});
|
|
164
|
+
if (recovered !== undefined)
|
|
165
|
+
return recovered.view;
|
|
165
166
|
return (await this.repository.loadRequired(id)).view;
|
|
166
167
|
}
|
|
167
168
|
async get(id) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createEventDraft, EventSourceKind,
|
|
1
|
+
import { createEventDraft, EventSourceKind, } from '../../kernel/index.js';
|
|
2
2
|
import { selectOrchestrationEvent } from '../contracts/event-decoder.js';
|
|
3
3
|
import { OrchestrationEventType } from '../contracts/events.js';
|
|
4
4
|
import { isOrchestrationGroupStream, primaryOrchestrationGroupStream, } from '../contracts/streams.js';
|
|
5
|
+
import { claimWithCasRetry } from './durable-append.js';
|
|
5
6
|
export class CoordinationClaims {
|
|
6
7
|
journal;
|
|
7
8
|
constructor(journal) {
|
|
@@ -9,16 +10,19 @@ export class CoordinationClaims {
|
|
|
9
10
|
}
|
|
10
11
|
async claimPrimary(workItemId, workflowInstanceId, context) {
|
|
11
12
|
const stream = primaryOrchestrationGroupStream(workItemId);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
await claimWithCasRetry({
|
|
14
|
+
read: () => this.journal.readStream(stream),
|
|
15
|
+
decode: groupEvents,
|
|
16
|
+
alreadyClaimed: (events) => {
|
|
17
|
+
const owner = primaryOwner(events);
|
|
18
|
+
if (owner === undefined)
|
|
19
|
+
return false;
|
|
16
20
|
if (owner === workflowInstanceId)
|
|
17
|
-
return;
|
|
21
|
+
return true;
|
|
18
22
|
throw new Error(`WorkItem already has an active primary workflow owned by ${owner}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
await this.journal.append(stream,
|
|
23
|
+
},
|
|
24
|
+
append: async (sequence) => {
|
|
25
|
+
await this.journal.append(stream, sequence, [
|
|
22
26
|
createEventDraft({
|
|
23
27
|
eventId: `${context.commandId}:${OrchestrationEventType.PrimaryClaimed}:${workItemId}`,
|
|
24
28
|
eventType: OrchestrationEventType.PrimaryClaimed,
|
|
@@ -31,26 +35,23 @@ export class CoordinationClaims {
|
|
|
31
35
|
payload: { workItemId, workflowInstanceId },
|
|
32
36
|
}),
|
|
33
37
|
]);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (!(error instanceof WrongExpectedSequenceError))
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
38
|
+
},
|
|
39
|
+
});
|
|
41
40
|
}
|
|
42
41
|
async primaryWorkflowInstanceId(workItemId) {
|
|
43
42
|
return primaryOwner(groupEvents(await this.journal.readStream(primaryOrchestrationGroupStream(workItemId))));
|
|
44
43
|
}
|
|
45
44
|
async claimWithinBudget(stream, request, context) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
const existing = groupEvents(await this.journal.readStream(stream));
|
|
46
|
+
if (claimedRequestIds(existing).has(request.requestId))
|
|
47
|
+
return true;
|
|
48
|
+
const claimed = await claimWithCasRetry({
|
|
49
|
+
read: () => this.journal.readStream(stream),
|
|
50
|
+
decode: groupEvents,
|
|
51
|
+
alreadyClaimed: (events) => claimedRequestIds(events).has(request.requestId),
|
|
52
|
+
canAppend: (events) => events.length < request.maxPerGroup,
|
|
53
|
+
append: async (sequence) => {
|
|
54
|
+
await this.journal.append(stream, sequence, [
|
|
54
55
|
createEventDraft({
|
|
55
56
|
eventId: `${context.commandId}:${OrchestrationEventType.GroupClaimed}:${request.requestId}`,
|
|
56
57
|
eventType: OrchestrationEventType.GroupClaimed,
|
|
@@ -63,13 +64,11 @@ export class CoordinationClaims {
|
|
|
63
64
|
payload: { key: stream.id, requestId: request.requestId },
|
|
64
65
|
}),
|
|
65
66
|
]);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (claimed)
|
|
70
|
+
return true;
|
|
71
|
+
return claimedRequestIds(groupEvents(await this.journal.readStream(stream))).has(request.requestId);
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
function primaryOwner(events) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { WrongExpectedSequenceError } from '../../kernel/index.js';
|
|
2
|
+
/** Recover a competing append only when the caller's durable intent is now visible. */
|
|
3
|
+
export async function appendWithIntentRecovery(input) {
|
|
4
|
+
try {
|
|
5
|
+
await input.append();
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const reloaded = await input.load();
|
|
10
|
+
if (input.alreadyApplied(reloaded))
|
|
11
|
+
return reloaded;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Append a single claim with optimistic concurrency. The claim is inspected
|
|
17
|
+
* before every append, so a losing racer terminates on its next read.
|
|
18
|
+
*/
|
|
19
|
+
export async function claimWithCasRetry(input) {
|
|
20
|
+
for (;;) {
|
|
21
|
+
const events = await input.read();
|
|
22
|
+
const decoded = input.decode(events);
|
|
23
|
+
if (input.alreadyClaimed(decoded))
|
|
24
|
+
return false;
|
|
25
|
+
if (input.canAppend?.(events, decoded) === false)
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
await input.append(events.length);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(error instanceof WrongExpectedSequenceError))
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { OrchestrationEventType } from '../contracts/events.js';
|
|
2
|
-
import { isWorkflowInstanceStream } from '../contracts/streams.js';
|
|
3
1
|
import { WorkflowStatus } from '../contracts/vocabulary.js';
|
|
4
2
|
import { acceptSignal as decideSignal } from '../domain/interpreter.js';
|
|
3
|
+
import { appendWithIntentRecovery } from './durable-append.js';
|
|
4
|
+
import { resolveTriggerWorkflowInstanceId } from './trigger-workflow-instance.js';
|
|
5
5
|
// Generic: no resource-kind knowledge lives here. A `signal-wait-started`
|
|
6
6
|
// trigger on the instance's own stream returns every declared transition
|
|
7
7
|
// unfiltered, so the reactor's evidence policy can check prior journal
|
|
8
8
|
// history for a fact that arrived before the wait began. Any other event
|
|
9
9
|
// is matched against each instance's declared predicates directly.
|
|
10
|
-
export function matchResourceTransitions(loaded, event) {
|
|
11
|
-
const waitStart = event
|
|
12
|
-
isWorkflowInstanceStream(event.stream)
|
|
13
|
-
? event.stream.id
|
|
14
|
-
: undefined;
|
|
10
|
+
export async function matchResourceTransitions(loaded, event) {
|
|
11
|
+
const waitStart = await resolveTriggerWorkflowInstanceId(event, undefined);
|
|
15
12
|
return loaded.flatMap(({ view }) => {
|
|
16
13
|
if (view.status !== WorkflowStatus.Waiting)
|
|
17
14
|
return [];
|
|
@@ -67,17 +64,17 @@ export async function acceptResourceTransition(repository, workflows, id, target
|
|
|
67
64
|
consent: true,
|
|
68
65
|
});
|
|
69
66
|
if (decision.kind === 'append') {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
const recovered = await appendWithIntentRecovery({
|
|
68
|
+
append: async () => {
|
|
69
|
+
await repository.append(id, loaded.sequence, decision.events);
|
|
70
|
+
},
|
|
71
|
+
load: () => repository.load(id),
|
|
72
|
+
alreadyApplied: (reloaded) => reloaded.view !== null &&
|
|
76
73
|
(reloaded.view.acceptedSignalIds.includes(evidenceId) ||
|
|
77
|
-
reloaded.view.waitingFor === undefined)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
reloaded.view.waitingFor === undefined),
|
|
75
|
+
});
|
|
76
|
+
if (recovered !== undefined)
|
|
77
|
+
return recovered.view;
|
|
81
78
|
}
|
|
82
79
|
return (await repository.load(id)).view;
|
|
83
80
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { selectRunExecutionEvent } from '../../execution/index.js';
|
|
2
|
+
import { OrchestrationEventType } from '../contracts/events.js';
|
|
3
|
+
import { isWorkflowInstanceStream } from '../contracts/streams.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the one instance an event belongs to, when it has one. Run events
|
|
6
|
+
* inherit their scope from the run. `ChildCompleted` deliberately does not:
|
|
7
|
+
* it is a cross-instance coordination fact and stream-scoping it would hide
|
|
8
|
+
* valid parent/child coordination from other eligible instances.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveTriggerWorkflowInstanceId(event, runs) {
|
|
11
|
+
if (event.eventType === OrchestrationEventType.SignalWaitStarted &&
|
|
12
|
+
isWorkflowInstanceStream(event.stream))
|
|
13
|
+
return event.stream.id;
|
|
14
|
+
if (runs === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const runEvent = selectRunExecutionEvent(event);
|
|
17
|
+
if (runEvent === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
return (await runs.load(runEvent.stream.id)).view?.workflowInstanceId;
|
|
20
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { selectRunExecutionEvent } from '../../execution/index.js';
|
|
2
1
|
import { correlationId, EventActorKind, } from '../../kernel/index.js';
|
|
3
2
|
import { selectOrchestrationEvent } from '../contracts/event-decoder.js';
|
|
4
|
-
import { OrchestrationEventType, } from '../contracts/events.js';
|
|
5
3
|
import { workflowInstanceId, workflowName, } from '../contracts/identifiers.js';
|
|
6
|
-
import {
|
|
4
|
+
import { resolveTriggerWorkflowInstanceId } from './trigger-workflow-instance.js';
|
|
7
5
|
const checkpoint = 'reactor:orchestration.watch';
|
|
8
6
|
export function createWatchReactor(orchestration, journal, checkpoints, runs) {
|
|
9
7
|
return {
|
|
@@ -57,36 +55,6 @@ function commandContext(event) {
|
|
|
57
55
|
actor: { kind: EventActorKind.System, id: 'watch-reactor' },
|
|
58
56
|
};
|
|
59
57
|
}
|
|
60
|
-
/**
|
|
61
|
-
* A run-lifecycle event (e.g. `execution.run-succeeded`) fires for every run
|
|
62
|
-
* in the system, including a watch's own spawned child re-running its own
|
|
63
|
-
* activity on retry. Without scoping, `listWatchMatches` would treat any
|
|
64
|
-
* currently-eligible parent's declared event type as a match regardless of
|
|
65
|
-
* which run actually produced it, causing a child's own retry (or an
|
|
66
|
-
* unrelated workflow's run) to spuriously re-trigger the same watch.
|
|
67
|
-
*
|
|
68
|
-
* For orchestration events, only `SignalWaitStarted` is scoped to its
|
|
69
|
-
* stream's instance; this is currently the only orchestration event type
|
|
70
|
-
* known to benefit from stream-based filtering. Other orchestration events
|
|
71
|
-
* like `ChildCompleted` are cross-instance coordination facts by design and
|
|
72
|
-
* must fall through unchanged to the async resolver to preserve their
|
|
73
|
-
* existing multi-instance coordination paths.
|
|
74
|
-
*/
|
|
75
|
-
async function resolveTriggerWorkflowInstanceId(event, runs) {
|
|
76
|
-
if (event.eventType === OrchestrationEventType.SignalWaitStarted &&
|
|
77
|
-
isWorkflowInstanceStream(event.stream))
|
|
78
|
-
return event.stream.id;
|
|
79
|
-
return resolveRunWorkflowInstanceId(event, runs);
|
|
80
|
-
}
|
|
81
|
-
async function resolveRunWorkflowInstanceId(event, runs) {
|
|
82
|
-
if (runs === undefined)
|
|
83
|
-
return undefined;
|
|
84
|
-
const runEvent = selectRunExecutionEvent(event);
|
|
85
|
-
if (runEvent === null)
|
|
86
|
-
return undefined;
|
|
87
|
-
const run = (await runs.load(runEvent.stream.id)).view;
|
|
88
|
-
return run?.workflowInstanceId;
|
|
89
|
-
}
|
|
90
58
|
function orchestrationCausalCycleId(event) {
|
|
91
59
|
return event !== null && 'causalCycleId' in event.payload
|
|
92
60
|
? event.payload.causalCycleId
|
|
@@ -98,6 +98,7 @@ function compileStage(context, rawStageName, stage, allStages) {
|
|
|
98
98
|
const effectiveAwait = defaultApprovalAwait(stage, outcomeKind, route);
|
|
99
99
|
const compiled = Object.freeze({
|
|
100
100
|
target,
|
|
101
|
+
reentryTarget: { kind: TransitionTargetKind.Stage, stage: stageName(rawStageName) },
|
|
101
102
|
...(route.repeat === undefined ? {} : { repeat: route.repeat }),
|
|
102
103
|
...(route.retry === undefined ? {} : { retry: route.retry }),
|
|
103
104
|
...(followOns === undefined ? {} : { activities: Object.freeze(followOns) }),
|
|
@@ -24,23 +24,14 @@ export function acceptSignal(definition, state, input) {
|
|
|
24
24
|
const events = [
|
|
25
25
|
stateDraft(state, input, OrchestrationEventType.SignalAccepted, { ...signal, authority }, 1),
|
|
26
26
|
];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
requestCurrentStage(events, definition, state, input);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else if (expected.resume !== undefined) {
|
|
36
|
-
resumeToTarget(events, definition, state, input, expected.resume);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
requestCurrentStage(events, definition, state, input);
|
|
40
|
-
}
|
|
27
|
+
const target = signal.outcome === ActivityOutcomeKind.Rejected ? expected.onRejectResume : expected.resume;
|
|
28
|
+
if (target !== undefined)
|
|
29
|
+
resumeToTarget(events, definition, state, input, target);
|
|
30
|
+
else
|
|
31
|
+
requestLegacyReentry(events, definition, state, input);
|
|
41
32
|
return { kind: 'append', events };
|
|
42
33
|
}
|
|
43
|
-
function
|
|
34
|
+
function requestLegacyReentry(events, definition, state, input) {
|
|
44
35
|
const stage = definition.stages[stageName(state.currentStage)];
|
|
45
36
|
events.push(stateDraft(state, input, OrchestrationEventType.ActivityRequested, activation(state.workflowInstanceId, nextOrdinal(state), stage.activity, stage.with, {
|
|
46
37
|
execution: stage.execution,
|
|
@@ -2,7 +2,7 @@ import { OrchestrationEventType, ResourceTransitionSignal, WatchGateVerdictSigna
|
|
|
2
2
|
import { ApprovalAuthorityKind, TransitionTargetKind } from '../contracts/vocabulary.js';
|
|
3
3
|
import { activation, nextOrdinal, stateDraft } from './decision-events.js';
|
|
4
4
|
// Route completion combines the mutually exclusive wait, await, and target policies.
|
|
5
|
-
// eslint-disable-next-line complexity
|
|
5
|
+
// eslint-disable-next-line complexity, max-lines-per-function
|
|
6
6
|
export function finishRoute(events, definition, state, input, route) {
|
|
7
7
|
if (route.watchGates !== undefined || route.resourceTransitions !== undefined) {
|
|
8
8
|
const gate = route.watchGates?.[0];
|
|
@@ -25,7 +25,12 @@ export function finishRoute(events, definition, state, input, route) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
if (route.await !== undefined) {
|
|
28
|
-
events.push(stateDraft(state, input, OrchestrationEventType.SignalWaitStarted, {
|
|
28
|
+
events.push(stateDraft(state, input, OrchestrationEventType.SignalWaitStarted, {
|
|
29
|
+
signalKind: route.await.signal,
|
|
30
|
+
from: route.await.from,
|
|
31
|
+
resume: route.await.resume,
|
|
32
|
+
onRejectResume: route.reentryTarget,
|
|
33
|
+
}, events.length + 1));
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
if (route.target.kind !== TransitionTargetKind.Stage) {
|
|
@@ -2,12 +2,14 @@ export * from './application/orchestration-projection.js';
|
|
|
2
2
|
export * from './application/orchestration-repository.js';
|
|
3
3
|
export * from './application/orchestration-service.js';
|
|
4
4
|
export * from './application/advance-workflow.js';
|
|
5
|
+
export * from './application/durable-append.js';
|
|
5
6
|
export * from './application/pull-request-transition-evidence.js';
|
|
6
7
|
export * from './application/resource-transition-evidence.js';
|
|
7
8
|
export * from './application/resource-transition-matching.js';
|
|
8
9
|
export * from './application/resource-transition-reactor.js';
|
|
9
10
|
export * from './application/signal-reactor.js';
|
|
10
11
|
export * from './application/watch-reactor.js';
|
|
12
|
+
export * from './application/trigger-workflow-instance.js';
|
|
11
13
|
export * from './application/workflow-definition-registry.js';
|
|
12
14
|
export * from './contracts/activity-outcome.js';
|
|
13
15
|
export * from './contracts/commands.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolis-hq/wake",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"description": "Local autonomous agent control plane for software development",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,11 @@
|
|
|
64
64
|
"smoke:codex": "tsx src/main.ts smoke codex",
|
|
65
65
|
"smoke:cursor": "tsx src/main.ts smoke cursor",
|
|
66
66
|
"verify": "npm run check:catalogue && npm run check:scenarios && npm run check:specs:report && npm run lint:architecture && npm run lint && npm run format:check && npm run build && npm run test:unit",
|
|
67
|
-
"verify:ci": "npm run verify && npm run test:architecture && npm run test:integration && npm run test:e2e && npm run knip && npm run test:web"
|
|
67
|
+
"verify:ci": "npm run verify && npm run test:architecture && npm run test:integration && npm run test:e2e && npm run knip && npm run test:web",
|
|
68
|
+
"prepare": "husky"
|
|
69
|
+
},
|
|
70
|
+
"lint-staged": {
|
|
71
|
+
"*.{ts,tsx,js,mjs,cjs,json,yaml,yml,css,html}": "prettier --write"
|
|
68
72
|
},
|
|
69
73
|
"dependencies": {
|
|
70
74
|
"@octokit/rest": "^22.0.0",
|
|
@@ -84,7 +88,9 @@
|
|
|
84
88
|
"eslint": "^10.8.0",
|
|
85
89
|
"eslint-config-prettier": "^10.1.8",
|
|
86
90
|
"globals": "^17.7.0",
|
|
91
|
+
"husky": "^9.1.7",
|
|
87
92
|
"knip": "^5.88.1",
|
|
93
|
+
"lint-staged": "^17.3.0",
|
|
88
94
|
"prettier": "^3.9.6",
|
|
89
95
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
90
96
|
"tsx": "^4.23.1",
|