@ikon85/agent-workflow-kit 0.32.1 → 0.33.0

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.
@@ -0,0 +1,134 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const exec = promisify(execFile);
5
+ const CONTRACT_VERSION = 1;
6
+
7
+ async function git(repoRoot, args, options = {}) {
8
+ return exec('git', args, { cwd: repoRoot, encoding: 'utf8', ...options });
9
+ }
10
+
11
+ // execFile has no `input` option — the child's stdin would stay open and a
12
+ // stdin-reading plumbing command (`git mktag`) would block forever. Spawn and
13
+ // close stdin explicitly instead.
14
+ function gitWithInput(repoRoot, args, input) {
15
+ return new Promise((resolve, reject) => {
16
+ const child = spawn('git', args, { cwd: repoRoot });
17
+ let stdout = '';
18
+ let stderr = '';
19
+ child.stdout.setEncoding('utf8');
20
+ child.stderr.setEncoding('utf8');
21
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
22
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
23
+ child.on('error', reject);
24
+ child.on('close', (code) => {
25
+ if (code === 0) resolve({ stdout, stderr });
26
+ else reject(new Error(`git ${args.join(' ')} failed (${code}): ${stderr.trim()}`));
27
+ });
28
+ child.stdin.end(input);
29
+ });
30
+ }
31
+
32
+ function claimRef(anchor) {
33
+ if (typeof anchor !== 'string' || !/^[1-9][0-9]*$/.test(anchor)) {
34
+ throw new TypeError('anchor must be a positive issue number string');
35
+ }
36
+ return `refs/tags/wave-active/${anchor}`;
37
+ }
38
+
39
+ function validateOwner(owner) {
40
+ if (typeof owner !== 'string' || owner.trim() === '') {
41
+ throw new TypeError('owner must be a non-empty string');
42
+ }
43
+ }
44
+
45
+ async function resolveRef(repoRoot, ref) {
46
+ try {
47
+ const { stdout } = await git(repoRoot, ['rev-parse', '--verify', ref]);
48
+ return stdout.trim();
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ async function readClaimAt(repoRoot, ref, tagOid) {
55
+ const { stdout: type } = await git(repoRoot, ['cat-file', '-t', tagOid]);
56
+ if (type.trim() !== 'tag') throw new Error(`${ref} is not an annotated wave claim`);
57
+ const { stdout } = await git(repoRoot, ['cat-file', '-p', tagOid]);
58
+ const separator = stdout.indexOf('\n\n');
59
+ if (separator < 0) throw new Error(`${ref} has no ownership payload`);
60
+ let claim;
61
+ try {
62
+ claim = JSON.parse(stdout.slice(separator + 2).trim());
63
+ } catch (error) {
64
+ throw new Error(`${ref} has an invalid ownership payload`, { cause: error });
65
+ }
66
+ if (claim?.contractVersion !== CONTRACT_VERSION || typeof claim.owner !== 'string') {
67
+ throw new Error(`${ref} has an unsupported ownership payload`);
68
+ }
69
+ return claim;
70
+ }
71
+
72
+ export async function readWaveClaim({ repoRoot, anchor }) {
73
+ const ref = claimRef(anchor);
74
+ const tagOid = await resolveRef(repoRoot, ref);
75
+ return tagOid ? readClaimAt(repoRoot, ref, tagOid) : null;
76
+ }
77
+
78
+ async function createTagObject(repoRoot, anchor, claim) {
79
+ const ref = claimRef(anchor);
80
+ const [{ stdout: head }, { stdout: objectFormat }] = await Promise.all([
81
+ git(repoRoot, ['rev-parse', '--verify', 'HEAD^{commit}']),
82
+ git(repoRoot, ['rev-parse', '--show-object-format']),
83
+ ]);
84
+ const epoch = Math.floor(Date.parse(claim.createdAt) / 1000);
85
+ const content = [
86
+ `object ${head.trim()}`,
87
+ 'type commit',
88
+ `tag ${ref.slice('refs/tags/'.length)}`,
89
+ `tagger agent-workflow-kit waveClaim <wave-claim@localhost> ${epoch} +0000`,
90
+ '',
91
+ JSON.stringify(claim),
92
+ '',
93
+ ].join('\n');
94
+ const { stdout: tagOid } = await gitWithInput(repoRoot, ['mktag'], content);
95
+ const zeroOid = '0'.repeat(objectFormat.trim() === 'sha256' ? 64 : 40);
96
+ return { ref, tagOid: tagOid.trim(), zeroOid };
97
+ }
98
+
99
+ /** Atomically acquire a local annotated-tag claim and read back its owner. */
100
+ export async function claimWave({ repoRoot, anchor, owner, sliceBranches = [], now = new Date() }) {
101
+ validateOwner(owner);
102
+ if (!Array.isArray(sliceBranches) || !sliceBranches.every((branch) => typeof branch === 'string')) {
103
+ throw new TypeError('sliceBranches must be an array of strings');
104
+ }
105
+ const createdAt = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
106
+ const proposed = { contractVersion: CONTRACT_VERSION, anchor, owner, createdAt, sliceBranches };
107
+ const { ref, tagOid, zeroOid } = await createTagObject(repoRoot, anchor, proposed);
108
+ let acquired = true;
109
+ try {
110
+ await git(repoRoot, ['update-ref', ref, tagOid, zeroOid]);
111
+ } catch {
112
+ acquired = false;
113
+ }
114
+ const currentOid = await resolveRef(repoRoot, ref);
115
+ if (!currentOid) throw new Error(`wave claim ${ref} was not readable after creation`);
116
+ const claim = await readClaimAt(repoRoot, ref, currentOid);
117
+ return { acquired: acquired && currentOid === tagOid && claim.owner === owner, claim };
118
+ }
119
+
120
+ /** Remove a claim only while the annotated payload and ref still belong to owner. */
121
+ export async function releaseWaveClaim({ repoRoot, anchor, owner }) {
122
+ validateOwner(owner);
123
+ const ref = claimRef(anchor);
124
+ const tagOid = await resolveRef(repoRoot, ref);
125
+ if (!tagOid) return false;
126
+ const claim = await readClaimAt(repoRoot, ref, tagOid);
127
+ if (claim.owner !== owner) return false;
128
+ try {
129
+ await git(repoRoot, ['update-ref', '-d', ref, tagOid]);
130
+ } catch {
131
+ return false;
132
+ }
133
+ return await resolveRef(repoRoot, ref) === null;
134
+ }