@atolis-hq/wake 0.3.20 → 0.3.22

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.
@@ -31,7 +31,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
31
31
  const maintenance = createUpdateMaintenanceLease(paths.wakeRoot);
32
32
  const clock = options.clock ?? new SystemClock();
33
33
  const ids = new UlidIdGenerator();
34
- const { journal, projections, checkpoints, resourceTransitionOrdering, resourceTransitionTriggers, } = composePersistence(paths, clock, options);
34
+ const { journal, projections, checkpoints } = composePersistence(paths, clock, options);
35
35
  const work = createWorkService(journal);
36
36
  const lookup = createResourceLookup({ journal, projections });
37
37
  const resources = createResourceService(journal, lookup);
@@ -125,8 +125,6 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
125
125
  ids,
126
126
  wakeRoot,
127
127
  scheduleCheckpoints: options.scheduleCheckpoints ?? new FileScheduleCheckpointStore(paths.dataRoot),
128
- resourceTransitionOrdering,
129
- resourceTransitionTriggers,
130
128
  ...(options.decorateDeliveryAdapter === undefined
131
129
  ? {}
132
130
  : { decorateDeliveryAdapter: options.decorateDeliveryAdapter }),
@@ -33,7 +33,6 @@ export function composeControlPlaneHosts(advanceOnce, sleep) {
33
33
  }
34
34
  export * from './composition-root.js';
35
35
  export * from './resource-transition-evidence.js';
36
- export * from './resource-transition-ordering.js';
37
36
  export * from './analytics-projection.js';
38
37
  export * from './board-projection.js';
39
38
  export * from './config/load-config.js';
@@ -110,13 +110,11 @@ export async function composeIntegrationRuntime(input) {
110
110
  },
111
111
  ],
112
112
  });
113
- input.resourceTransitionTriggers.register(resourceTransitionEvidence.triggers);
114
- input.resourceTransitionTriggers.freeze();
115
- const resourceTransitions = createResourceTransitionReactor(input.orchestration, resourceTransitionEvidence, input.journal, input.checkpoints, input.resourceTransitionOrdering);
116
- input.orchestration.setAcceptSignalOperationCoordinator((operation) => input.resourceTransitionOrdering(async () => {
113
+ const resourceTransitions = createResourceTransitionReactor(input.orchestration, resourceTransitionEvidence, input.journal, input.checkpoints);
114
+ input.orchestration.setAcceptSignalOperationCoordinator(async (operation) => {
117
115
  await resourceTransitions.drain();
118
116
  return operation();
119
- }));
117
+ });
120
118
  const outcomes = new DeliveryOutcomeReactor(input.journal, input.checkpoints, input.orchestration);
121
119
  const catchUpProjections = async () => {
122
120
  await projectionRunner.runRegisteredOnce();
@@ -1,19 +1,11 @@
1
1
  import { FileCheckpointStore, FileEventJournal, FileProjectionStore, } from '../persistence/index.js';
2
- import { createResourceTransitionOrdering, createTriggerAwareEventJournal, createTriggerRegistry, } from './resource-transition-ordering.js';
3
2
  function identity(value) {
4
3
  return value;
5
4
  }
6
5
  export function composePersistence(paths, clock, options) {
7
- const resourceTransitionOrdering = createResourceTransitionOrdering(options.journal === undefined
8
- ? `${paths.locksRoot}/resource-transition-ordering.lock`
9
- : undefined);
10
- const resourceTransitionTriggers = createTriggerRegistry();
11
- const journal = createTriggerAwareEventJournal((options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)), resourceTransitionTriggers, resourceTransitionOrdering);
12
6
  return {
13
- journal,
7
+ journal: (options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)),
14
8
  projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
15
9
  checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
16
- resourceTransitionOrdering,
17
- resourceTransitionTriggers,
18
10
  };
19
11
  }
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "gea46475";
111
+ export const wakeVersion = "gd39350a";
@@ -1,4 +1,5 @@
1
1
  import { ActivityOutcomeKind, ReviewActorKind, ReviewDecisionKind, ReviewerAuthorizationSource, createPullRequestService, } from '../../../activities/index.js';
2
+ import { ApprovalAuthorityKind } from '../../../orchestration/index.js';
2
3
  import { BuiltInResourceKind, ResourceCorrelationRole, ResourceStreamKind, resourceId, } from '../../../resources/index.js';
3
4
  import { UnknownGitHubIdentity } from '../contracts/vocabulary.js';
4
5
  import { commandContext } from './inbound-context.js';
@@ -143,6 +144,9 @@ async function applyWorkflowSignal(input) {
143
144
  evidenceId: event.eventId,
144
145
  },
145
146
  providerEventId: event.eventId,
147
+ ...(event.payload.actor.kind === ReviewActorKind.Human
148
+ ? { authority: { kind: ApprovalAuthorityKind.Human } }
149
+ : {}),
146
150
  }, commandContext(event));
147
151
  }
148
152
  else if (resumeBlockedOnChanges) {
@@ -66,7 +66,18 @@ export async function acceptResourceTransition(repository, workflows, id, target
66
66
  causationId: `${context.commandId}:${evidenceId}`,
67
67
  consent: true,
68
68
  });
69
- if (decision.kind === 'append')
70
- await repository.append(id, loaded.sequence, decision.events);
69
+ if (decision.kind === 'append') {
70
+ try {
71
+ await repository.append(id, loaded.sequence, decision.events);
72
+ }
73
+ catch (error) {
74
+ const reloaded = await repository.load(id);
75
+ if (reloaded.view !== null &&
76
+ (reloaded.view.acceptedSignalIds.includes(evidenceId) ||
77
+ reloaded.view.waitingFor === undefined))
78
+ return reloaded.view;
79
+ throw error;
80
+ }
81
+ }
71
82
  return (await repository.load(id)).view;
72
83
  }
@@ -3,7 +3,7 @@ import { selectOrchestrationEvent } from '../contracts/event-decoder.js';
3
3
  import { OrchestrationEventType } from '../contracts/events.js';
4
4
  const checkpoint = 'reactor:orchestration.resource-transition';
5
5
  const batchSize = 100;
6
- export function createResourceTransitionReactor(orchestration, evidence, journal, checkpoints, coordinate) {
6
+ export function createResourceTransitionReactor(orchestration, evidence, journal, checkpoints) {
7
7
  let queue = Promise.resolve();
8
8
  const react = async (event, context) => {
9
9
  const orchestrationEvent = selectOrchestrationEvent(event);
@@ -40,10 +40,10 @@ export function createResourceTransitionReactor(orchestration, evidence, journal
40
40
  return {
41
41
  react,
42
42
  runOnce(limit = batchSize) {
43
- return (coordinate ?? serialize)(() => runBatch(limit));
43
+ return serialize(() => runBatch(limit));
44
44
  },
45
45
  drain() {
46
- return (coordinate ?? serialize)(async () => {
46
+ return serialize(async () => {
47
47
  let total = 0;
48
48
  let processed;
49
49
  do {
@@ -52,7 +52,10 @@ function signalRejectionReason(state, input) {
52
52
  if (state.acceptedSignalIds.includes(signal.providerEventId))
53
53
  return 'provider signal was already accepted';
54
54
  const expected = state.waitingFor;
55
- if (state.status !== WorkflowStatus.Waiting || expected === undefined)
55
+ if (expected === undefined)
56
+ return 'WorkflowInstance is not waiting for a signal';
57
+ if (state.status !== WorkflowStatus.Waiting &&
58
+ !isBlockedHumanAuthorizedWait(state, signal, expected))
56
59
  return 'WorkflowInstance is not waiting for a signal';
57
60
  if (!matchesExpectation(signal, expected))
58
61
  return 'signal does not match the current expectation';
@@ -66,6 +69,11 @@ function signalRejectionReason(state, input) {
66
69
  return 'watch-gate signal outcome is neither done nor rejected';
67
70
  return null;
68
71
  }
72
+ function isBlockedHumanAuthorizedWait(state, signal, expected) {
73
+ return (state.status === WorkflowStatus.Blocked &&
74
+ signal.authority?.kind === ApprovalAuthorityKind.Human &&
75
+ expected.from?.some((authority) => authority.kind === ApprovalAuthorityKind.Human) === true);
76
+ }
69
77
  function signalAuthorityAccepted(signal, expected, consent) {
70
78
  if (expected.from === undefined)
71
79
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/prompts/refine.md CHANGED
@@ -26,6 +26,14 @@ Your job here is only to:
26
26
  response (do not try to save it to a file).
27
27
  - If underspecified, ask the smallest set of clarifying questions needed.
28
28
 
29
+ "Underspecified" means the requirement or acceptance criteria are unclear or
30
+ contradictory — not that an implementation detail is undecided. Investigate
31
+ the codebase and decide design questions (module ownership, event/projection
32
+ shape, which pattern to extend); state the decision as an assumption in your
33
+ plan instead of asking. plan-review checks stated assumptions and rejects a
34
+ wrong one. Block only when the ticket's own intent can't be determined by
35
+ reading the repository.
36
+
29
37
  Wake will provide the issue data and comments below in a delimited untrusted
30
38
  data block.
31
39
  {{else}}
@@ -1,67 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
- import { acquireFileLock } from '../persistence/index.js';
3
- export function createTriggerRegistry() {
4
- const triggers = new Set();
5
- let frozen = false;
6
- return {
7
- register(values) {
8
- if (frozen)
9
- throw new Error('Resource transition trigger registry is frozen');
10
- values.forEach((value) => triggers.add(value));
11
- },
12
- freeze() {
13
- frozen = true;
14
- },
15
- includes(eventType) {
16
- return triggers.has(eventType);
17
- },
18
- };
19
- }
20
- export function createResourceTransitionOrdering(lockPath) {
21
- const active = new AsyncLocalStorage();
22
- let queue = Promise.resolve();
23
- const coordinate = (operation) => {
24
- if (active.getStore() === true)
25
- return operation();
26
- const result = queue.then(async () => {
27
- const lock = lockPath === undefined ? undefined : await waitForFileLock(lockPath);
28
- try {
29
- return await active.run(true, operation);
30
- }
31
- finally {
32
- await lock?.release();
33
- }
34
- });
35
- queue = result.catch(() => { });
36
- return result;
37
- };
38
- return coordinate;
39
- }
40
- async function waitForFileLock(lockPath) {
41
- for (;;) {
42
- const lock = await acquireFileLock(lockPath, {
43
- staleAfterMs: 60_000,
44
- staleRequiresDeadProcess: true,
45
- });
46
- if (lock.acquired)
47
- return lock;
48
- await new Promise((resolve) => setTimeout(resolve, 10));
49
- }
50
- }
51
- export function createTriggerAwareEventJournal(journal, registry, coordinate) {
52
- return {
53
- append(stream, expectedSequence, events) {
54
- const append = () => journal.append(stream, expectedSequence, events);
55
- return events.some(({ eventType }) => registry.includes(eventType))
56
- ? coordinate(append)
57
- : append();
58
- },
59
- readStream: (stream) => journal.readStream(stream),
60
- readAll: (afterGlobalPosition, limit) => journal.readAll(afterGlobalPosition, limit),
61
- ...(journal.readLatest === undefined
62
- ? {}
63
- : {
64
- readLatest: (beforeGlobalPosition, limit) => journal.readLatest(beforeGlobalPosition, limit),
65
- }),
66
- };
67
- }