@atolis-hq/wake 0.3.19 → 0.3.21

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.
Files changed (31) hide show
  1. package/dist/src/activities/pr/application.js +5 -1
  2. package/dist/src/activities/pr/policy.js +10 -0
  3. package/dist/src/bootstrap/activity-registry.js +32 -0
  4. package/dist/src/bootstrap/composition-root.js +17 -243
  5. package/dist/src/bootstrap/index.js +1 -0
  6. package/dist/src/bootstrap/integration-runtime.js +182 -0
  7. package/dist/src/bootstrap/persistence-composition.js +11 -0
  8. package/dist/src/bootstrap/resource-transition-evidence.js +17 -0
  9. package/dist/src/bootstrap/transcript-retention.js +29 -0
  10. package/dist/src/bootstrap/version.js +1 -1
  11. package/dist/src/orchestration/application/advance-workflow.js +12 -22
  12. package/dist/src/orchestration/application/orchestration-repository.js +19 -4
  13. package/dist/src/orchestration/application/orchestration-service.js +12 -2
  14. package/dist/src/orchestration/application/pull-request-transition-evidence.js +70 -0
  15. package/dist/src/orchestration/application/resource-transition-evidence.js +1 -0
  16. package/dist/src/orchestration/application/resource-transition-matching.js +83 -0
  17. package/dist/src/orchestration/application/resource-transition-reactor.js +65 -0
  18. package/dist/src/orchestration/application/watch-matching.js +27 -0
  19. package/dist/src/orchestration/contracts/config.js +24 -1
  20. package/dist/src/orchestration/contracts/event-decoder.js +18 -1
  21. package/dist/src/orchestration/contracts/events.js +1 -0
  22. package/dist/src/orchestration/domain/approval-defaults.js +1 -0
  23. package/dist/src/orchestration/domain/compiler.js +14 -1
  24. package/dist/src/orchestration/domain/resource-transition-compiler.js +12 -0
  25. package/dist/src/orchestration/domain/transition.js +18 -9
  26. package/dist/src/orchestration/domain/workflow-graph.js +5 -1
  27. package/dist/src/orchestration/domain/workflow-instance-events.js +3 -0
  28. package/dist/src/orchestration/index.js +4 -0
  29. package/dist/src/persistence/filesystem/file-lock.js +199 -27
  30. package/package.json +1 -1
  31. package/prompts/refine.md +8 -0
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, open, readFile, rm } from 'node:fs/promises';
3
- import { dirname } from 'node:path';
2
+ import { mkdir, open, readdir, readFile, rm, rmdir } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ const maximumOwnerRecords = 1024;
5
+ const compatibilityAcquiredAt = '9999-12-31T23:59:59.999Z';
4
6
  export async function acquireFileLock(path, options) {
5
7
  await mkdir(dirname(path), { recursive: true });
6
8
  const metadata = {
@@ -8,51 +10,219 @@ export async function acquireFileLock(path, options) {
8
10
  acquiredAt: (options?.now ?? new Date()).toISOString(),
9
11
  lockId: randomUUID(),
10
12
  };
13
+ return options?.staleRequiresDeadProcess
14
+ ? acquireStrictFileLock(path, metadata, options)
15
+ : acquireLegacyFileLock(path, metadata, options);
16
+ }
17
+ async function acquireStrictFileLock(path, metadata, options) {
18
+ const ownersPath = `${path}.owners`;
19
+ await mkdir(ownersPath, { recursive: true });
20
+ const legacy = await inspectLegacyOwner(path, ownersPath, options);
21
+ if (legacy.blocks)
22
+ return unavailableFileLock();
23
+ if (legacy.reclaimableProtectedOwnerRecord !== undefined)
24
+ await rm(join(ownersPath, legacy.reclaimableProtectedOwnerRecord), { force: true });
25
+ const ownerName = ownerRecordName(metadata);
26
+ const ownerPath = join(ownersPath, ownerName);
27
+ await createOwnerRecord(ownerPath, metadata);
11
28
  try {
12
- return await createFileLock(path, metadata);
29
+ const candidates = await readdir(ownersPath);
30
+ if (candidates.length > maximumOwnerRecords) {
31
+ await rm(ownerPath, { force: true });
32
+ return unavailableFileLock();
33
+ }
34
+ if (await hasBlockingOwner(candidates, ownersPath, ownerName, legacy, options)) {
35
+ await rm(ownerPath, { force: true });
36
+ return unavailableFileLock();
37
+ }
38
+ const ownsCompatibilityPath = await createCompatibilityOwner(path, metadata, ownerName);
39
+ if (!ownsCompatibilityPath && (await inspectLegacyOwner(path, ownersPath, options)).blocks) {
40
+ await rm(ownerPath, { force: true });
41
+ return unavailableFileLock();
42
+ }
43
+ return {
44
+ acquired: true,
45
+ metadata,
46
+ async release() {
47
+ let peers;
48
+ try {
49
+ peers = (await readdir(ownersPath)).filter((candidate) => candidate !== ownerName);
50
+ }
51
+ catch (error) {
52
+ if (error.code === 'ENOENT')
53
+ return;
54
+ throw error;
55
+ }
56
+ if (peers.length === 0)
57
+ await releaseCompatibilityOwner(path);
58
+ await rm(ownerPath, { force: true });
59
+ await removeEmptyOwnerDirectory(ownersPath);
60
+ },
61
+ };
13
62
  }
14
63
  catch (error) {
15
- if (error.code !== 'EEXIST')
16
- throw error;
17
- return reclaimStaleFileLock(path, metadata, options);
64
+ await rm(ownerPath, { force: true });
65
+ throw error;
18
66
  }
19
67
  }
20
- async function createFileLock(path, metadata) {
21
- const handle = await open(path, 'wx');
68
+ async function removeEmptyOwnerDirectory(path) {
22
69
  try {
23
- await handle.writeFile(`${JSON.stringify(metadata)}\n`, 'utf8');
24
- await handle.sync();
70
+ await rmdir(path);
25
71
  }
26
- finally {
27
- await handle.close();
72
+ catch {
73
+ // A concurrent owner may have populated the directory.
28
74
  }
29
- return { acquired: true, metadata, release: () => releaseFileLock(path, metadata) };
30
75
  }
31
- async function releaseFileLock(path, metadata) {
76
+ async function acquireLegacyFileLock(path, metadata, options) {
32
77
  try {
33
- const current = JSON.parse(await readFile(path, 'utf8'));
34
- if (current.lockId === metadata.lockId)
35
- await rm(path, { force: true });
78
+ return await createLegacyFileLock(path, metadata);
36
79
  }
37
- catch {
38
- /* already released */
80
+ catch (error) {
81
+ if (error.code !== 'EEXIST')
82
+ throw error;
39
83
  }
40
- }
41
- async function reclaimStaleFileLock(path, metadata, options) {
42
84
  if (options?.staleAfterMs === undefined)
43
85
  return unavailableFileLock();
44
86
  try {
45
87
  const prior = JSON.parse(await readFile(path, 'utf8'));
46
88
  if (!isStale(prior, options))
47
89
  return unavailableFileLock();
48
- if (options.staleRequiresDeadProcess && ownerMayBeAlive(options, prior.pid))
49
- return unavailableFileLock();
50
90
  }
51
91
  catch {
52
- // A corrupt or vanished lock has no trustworthy owner and can be reclaimed.
92
+ // Preserve the historical time-only recovery contract for malformed or vanished locks.
53
93
  }
54
94
  await rm(path, { force: true });
55
- return createFileLock(path, metadata);
95
+ try {
96
+ return await createLegacyFileLock(path, metadata);
97
+ }
98
+ catch (error) {
99
+ if (error.code === 'EEXIST')
100
+ return unavailableFileLock();
101
+ throw error;
102
+ }
103
+ }
104
+ async function createLegacyFileLock(path, metadata) {
105
+ await createOwnerRecord(path, metadata);
106
+ return {
107
+ acquired: true,
108
+ metadata,
109
+ release: () => releaseLegacyFileLock(path, metadata.lockId),
110
+ };
111
+ }
112
+ async function releaseLegacyFileLock(path, lockId) {
113
+ try {
114
+ const current = JSON.parse(await readFile(path, 'utf8'));
115
+ if (current.lockId === lockId)
116
+ await rm(path, { force: true });
117
+ }
118
+ catch {
119
+ /* already released */
120
+ }
121
+ }
122
+ async function hasBlockingOwner(candidates, ownersPath, ownerName, legacy, options) {
123
+ for (const candidate of candidates) {
124
+ if (candidate === ownerName || candidate === legacy.protectedOwnerRecord)
125
+ continue;
126
+ const candidatePath = join(ownersPath, candidate);
127
+ if (await ownerBlocksAcquisition(candidatePath, options))
128
+ return true;
129
+ if (options?.staleRequiresDeadProcess)
130
+ await rm(candidatePath, { force: true });
131
+ }
132
+ return false;
133
+ }
134
+ async function inspectLegacyOwner(path, ownersPath, options) {
135
+ let legacy;
136
+ try {
137
+ legacy = JSON.parse(await readFile(path, 'utf8'));
138
+ }
139
+ catch (error) {
140
+ return { blocks: error.code !== 'ENOENT' };
141
+ }
142
+ if (!('compatibilityOwner' in legacy)) {
143
+ return { blocks: true };
144
+ }
145
+ const ownerPath = join(ownersPath, legacy.ownerRecord);
146
+ const blocks = await ownerBlocksAcquisition(ownerPath, options);
147
+ return {
148
+ blocks,
149
+ protectedOwnerRecord: legacy.ownerRecord,
150
+ ...(blocks ? {} : { reclaimableProtectedOwnerRecord: legacy.ownerRecord }),
151
+ };
152
+ }
153
+ async function createCompatibilityOwner(path, metadata, ownerRecord) {
154
+ try {
155
+ await createOwnerRecord(path, {
156
+ ...metadata,
157
+ acquiredAt: compatibilityAcquiredAt,
158
+ compatibilityOwner: true,
159
+ ownerRecord,
160
+ });
161
+ return true;
162
+ }
163
+ catch (error) {
164
+ if (error.code === 'EEXIST')
165
+ return false;
166
+ throw error;
167
+ }
168
+ }
169
+ async function releaseCompatibilityOwner(path) {
170
+ try {
171
+ const owner = JSON.parse(await readFile(path, 'utf8'));
172
+ if (owner.compatibilityOwner)
173
+ await rm(path, { force: true });
174
+ }
175
+ catch {
176
+ /* already released */
177
+ }
178
+ }
179
+ async function createOwnerRecord(path, metadata) {
180
+ let handle;
181
+ try {
182
+ handle = await open(path, 'wx');
183
+ }
184
+ catch (error) {
185
+ if (error.code !== 'ENOENT')
186
+ throw error;
187
+ await mkdir(dirname(path), { recursive: true });
188
+ handle = await open(path, 'wx');
189
+ }
190
+ try {
191
+ await handle.writeFile(`${JSON.stringify(metadata)}\n`, 'utf8');
192
+ await handle.sync();
193
+ }
194
+ finally {
195
+ await handle.close();
196
+ }
197
+ }
198
+ async function ownerBlocksAcquisition(path, options) {
199
+ let owner;
200
+ try {
201
+ owner = JSON.parse(await readFile(path, 'utf8'));
202
+ }
203
+ catch (error) {
204
+ if (error.code === 'ENOENT')
205
+ return false;
206
+ const encoded = /^(\d+)-(\d+)-([^.]+)\.json$/.exec(path.split(/[\\/]/).at(-1) ?? '');
207
+ if (encoded === null)
208
+ return true;
209
+ owner = {
210
+ pid: Number(encoded[1]),
211
+ acquiredAt: new Date(Number(encoded[2])).toISOString(),
212
+ lockId: encoded[3],
213
+ };
214
+ }
215
+ if (options?.staleAfterMs === undefined || !isStale(owner, options))
216
+ return true;
217
+ return ownerMetadataBlocks(owner, options);
218
+ }
219
+ function ownerMetadataBlocks(owner, options) {
220
+ if (options?.staleAfterMs === undefined || !isStale(owner, options))
221
+ return true;
222
+ return Boolean(options.staleRequiresDeadProcess && ownerMayBeAlive(options, owner.pid));
223
+ }
224
+ function ownerRecordName(metadata) {
225
+ return `${metadata.pid}-${Date.parse(metadata.acquiredAt)}-${metadata.lockId}.json`;
56
226
  }
57
227
  function isStale(prior, options) {
58
228
  return ((options.now ?? new Date()).getTime() - Date.parse(prior.acquiredAt) >= options.staleAfterMs);
@@ -74,12 +244,14 @@ function ownerMayBeAlive(options, pid) {
74
244
  return (options.isProcessAlive ?? isProcessAlive)(pid);
75
245
  }
76
246
  catch {
77
- // Permission and platform-probe failures are indeterminate: never steal a live owner's lock.
78
247
  return true;
79
248
  }
80
249
  }
81
250
  export async function withFileLock(path, operation) {
82
- const lock = await acquireFileLock(path, { staleAfterMs: 60_000 });
251
+ const lock = await acquireFileLock(path, {
252
+ staleAfterMs: 60_000,
253
+ staleRequiresDeadProcess: true,
254
+ });
83
255
  if (!lock.acquired)
84
256
  throw new Error(`File lock is already held: ${path}`);
85
257
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
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}}