@innofeight/global-workflow 0.0.1 → 0.0.2

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,65 @@
1
+ import { URL } from 'node:url'
2
+
3
+ const PAGE_SIZE = 25
4
+
5
+ function fail(reason) {
6
+ throw new Error(`clickup-comments.${reason}`)
7
+ }
8
+
9
+ function normalizeComment(comment) {
10
+ if (!comment || typeof comment !== 'object' || Array.isArray(comment))
11
+ fail('invalid-comment')
12
+ const id = String(comment.id ?? '')
13
+ const date = String(comment.date ?? '')
14
+ if (!id || !/^\d+$/u.test(date)) fail('invalid-comment-cursor')
15
+ if (typeof comment.comment_text !== 'string') fail('invalid-comment-text')
16
+ return { date, id, text: comment.comment_text }
17
+ }
18
+
19
+ async function page(fetchImpl, token, taskId, cursor) {
20
+ const url = new URL(`https://api.clickup.com/api/v2/task/${taskId}/comment`)
21
+ if (cursor) {
22
+ url.searchParams.set('start', cursor.start)
23
+ url.searchParams.set('start_id', cursor.startId)
24
+ }
25
+ let response
26
+ try {
27
+ response = await fetchImpl(url.toString(), {
28
+ headers: { Authorization: token }
29
+ })
30
+ } catch (error) {
31
+ fail(
32
+ `request-failed:${error instanceof Error ? error.message : String(error)}`
33
+ )
34
+ }
35
+ if (!response.ok) fail(`request-failed:${response.status}`)
36
+ const body = await response.json()
37
+ if (!body || !Array.isArray(body.comments)) fail('invalid-response')
38
+ return body.comments.map(normalizeComment)
39
+ }
40
+
41
+ export async function readAllTaskComments({ fetchImpl, taskId, token }) {
42
+ const byId = new Map()
43
+ const cursors = new Set()
44
+ let cursor
45
+ for (;;) {
46
+ const comments = await page(fetchImpl, token, taskId, cursor)
47
+ for (const comment of comments) {
48
+ const existing = byId.get(comment.id)
49
+ if (existing && JSON.stringify(existing) !== JSON.stringify(comment))
50
+ fail(`conflicting-duplicate:${comment.id}`)
51
+ byId.set(comment.id, comment)
52
+ }
53
+ if (comments.length < PAGE_SIZE) break
54
+ const oldest = comments.at(-1)
55
+ if (!oldest) fail('missing-oldest-comment')
56
+ const next = `${oldest.date}:${oldest.id}`
57
+ if (cursors.has(next)) fail('repeating-cursor')
58
+ cursors.add(next)
59
+ cursor = { start: oldest.date, startId: oldest.id }
60
+ }
61
+ return [...byId.values()].sort(
62
+ (left, right) =>
63
+ Number(right.date) - Number(left.date) || right.id.localeCompare(left.id)
64
+ )
65
+ }
@@ -0,0 +1,114 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ import { stableAetV2Json, validateAetV2Event } from './aet-v2.mjs'
4
+
5
+ export const RECORD_PREFIXES = Object.freeze({
6
+ automaticEligibility: 'POST_MERGE_AUTOMATIC_ELIGIBILITY_V1 ',
7
+ authorized: 'POST_MERGE_RECONCILIATION_AUTHORIZED_V2 ',
8
+ calibration: 'POST_MERGE_CALIBRATION_EVIDENCE_V1 ',
9
+ durableReconciliation: 'POST_MERGE_DURABLE_RECONCILIATION_V1 ',
10
+ finalized: 'POST_MERGE_RECONCILIATION_FINALIZED_V2 ',
11
+ introducingRecovery: 'POST_MERGE_INTRODUCING_RECOVERY_V1 ',
12
+ introducingStaging: 'POST_MERGE_INTRODUCING_STAGING_EXCEPTION_V1 ',
13
+ skippedIdentityRecovery: 'POST_MERGE_SKIPPED_IDENTITY_RECOVERY_V1 ',
14
+ packet: 'POST_MERGE_COMPLETION_REQUEST_V2 ',
15
+ staging: 'POST_MERGE_STAGING_V1 '
16
+ })
17
+
18
+ export function fingerprint(value) {
19
+ return `sha256:${createHash('sha256').update(stableAetV2Json(value)).digest('hex')}`
20
+ }
21
+
22
+ export function formatDurableRecord(prefix, schema, payload) {
23
+ const envelope = { fingerprint: fingerprint(payload), payload, schema }
24
+ return `${prefix}${stableAetV2Json(envelope)}`
25
+ }
26
+
27
+ export function parseDurableRecord(text, prefix, schema) {
28
+ if (typeof text !== 'string' || !text.startsWith(prefix)) return null
29
+ let envelope
30
+ try {
31
+ envelope = JSON.parse(text.slice(prefix.length))
32
+ } catch {
33
+ throw new Error(`post-merge.invalid-durable-json:${prefix.trim()}`)
34
+ }
35
+ if (
36
+ !envelope ||
37
+ typeof envelope !== 'object' ||
38
+ Array.isArray(envelope) ||
39
+ Object.keys(envelope).sort().join(',') !== 'fingerprint,payload,schema' ||
40
+ envelope.schema !== schema ||
41
+ envelope.fingerprint !== fingerprint(envelope.payload)
42
+ )
43
+ throw new Error('post-merge.invalid-durable-envelope')
44
+ return envelope
45
+ }
46
+
47
+ export function commentsToTexts(comments) {
48
+ return comments.map((comment) => comment.text)
49
+ }
50
+
51
+ export function durableRecords(texts, prefix, schema) {
52
+ return texts
53
+ .map((text) => parseDurableRecord(text, prefix, schema))
54
+ .filter(Boolean)
55
+ }
56
+
57
+ export function durableAetEvents(texts) {
58
+ const events = []
59
+ for (const text of texts) {
60
+ if (typeof text !== 'string' || !text.trimStart().startsWith('{')) continue
61
+ try {
62
+ const event = JSON.parse(text)
63
+ if (event?.schema !== 'rn-launcher/aet-v2-event/v1') continue
64
+ events.push(validateAetV2Event(event))
65
+ } catch (error) {
66
+ if (text.includes('rn-launcher/aet-v2-event/v1')) throw error
67
+ }
68
+ }
69
+ return events
70
+ }
71
+
72
+ export function durableTaskAetEvents(texts, taskId) {
73
+ const events = durableAetEvents(texts).filter(
74
+ (event) => event.taskId === taskId
75
+ )
76
+ const ids = new Set()
77
+ for (const event of events) {
78
+ if (ids.has(event.eventId))
79
+ throw new Error(`post-merge.duplicate-durable-event: ${event.eventId}`)
80
+ ids.add(event.eventId)
81
+ }
82
+ return events
83
+ }
84
+
85
+ export function closedWorldLedger(events, eventIds, terminalEventIds = []) {
86
+ const seen = new Set()
87
+ for (const event of events) {
88
+ if (seen.has(event.eventId))
89
+ throw new Error(`post-merge.duplicate-durable-event: ${event.eventId}`)
90
+ seen.add(event.eventId)
91
+ }
92
+ const terminal = new Set(terminalEventIds)
93
+ const applicable = events.filter((event) => !terminal.has(event.eventId))
94
+ const expected = applicable.map((event) => event.eventId).sort()
95
+ const requested = [...eventIds].sort()
96
+ if (JSON.stringify(expected) !== JSON.stringify(requested))
97
+ throw new Error('post-merge.stale-or-incomplete-event-set')
98
+ return canonicalLedger(applicable, requested)
99
+ }
100
+
101
+ export function canonicalLedger(events, eventIds) {
102
+ const byId = new Map()
103
+ for (const event of events) {
104
+ if (byId.has(event.eventId))
105
+ throw new Error(`post-merge.duplicate-durable-event: ${event.eventId}`)
106
+ byId.set(event.eventId, event)
107
+ }
108
+ const selected = [...eventIds].sort().map((eventId) => {
109
+ const event = byId.get(eventId)
110
+ if (!event) throw new Error(`post-merge.missing-durable-event: ${eventId}`)
111
+ return event
112
+ })
113
+ return selected.map(stableAetV2Json).join('\n')
114
+ }
@@ -0,0 +1,370 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { URL } from 'node:url'
5
+
6
+ export const TASK_SLUG_MAX_LENGTH = 48
7
+
8
+ export class TaskBranchStop extends Error {
9
+ constructor(reason, details = {}) {
10
+ super(reason)
11
+ this.name = 'TaskBranchStop'
12
+ this.details = sanitizeDiagnosticDetails(details)
13
+ }
14
+ }
15
+
16
+ function sanitizeDiagnosticDetails(value) {
17
+ if (typeof value === 'string') return redactGitDiagnostic(value)
18
+ if (Array.isArray(value)) return value.map(sanitizeDiagnosticDetails)
19
+ if (value && typeof value === 'object')
20
+ return Object.fromEntries(
21
+ Object.entries(value).map(([key, entry]) => [
22
+ key,
23
+ sanitizeDiagnosticDetails(entry)
24
+ ])
25
+ )
26
+ return value
27
+ }
28
+
29
+ export function normalizeTaskSlug(title) {
30
+ const slug = title
31
+ .normalize('NFKD')
32
+ .toLowerCase()
33
+ .replace(/[\u0300-\u036f]/gu, '')
34
+ .replace(/[^a-z0-9]+/gu, '-')
35
+ .replace(/-+/gu, '-')
36
+ .replace(/^-|-$/gu, '')
37
+ .slice(0, TASK_SLUG_MAX_LENGTH)
38
+ .replace(/-$/gu, '')
39
+ return slug || 'task'
40
+ }
41
+
42
+ export function validateTaskId(taskId) {
43
+ if (!/^[a-z0-9]+$/u.test(taskId))
44
+ throw new TaskBranchStop('INVALID CLICKUP TASK ID', { taskId })
45
+ return taskId
46
+ }
47
+
48
+ export function taskBranchName(taskId, title) {
49
+ return `codex/CU-${validateTaskId(taskId)}-${normalizeTaskSlug(title)}`
50
+ }
51
+
52
+ export function validatePredecessorBinding(predecessor) {
53
+ if (!predecessor || typeof predecessor !== 'object')
54
+ throw new TaskBranchStop('PREVIOUS GOVERNED TICKET REQUIRED')
55
+ const keys = Object.keys(predecessor).sort()
56
+ if (
57
+ JSON.stringify(keys) !==
58
+ JSON.stringify(['authority', 'status', 'taskId', 'title'])
59
+ )
60
+ throw new TaskBranchStop('PREDECESSOR BINDING IS INVALID')
61
+ if (predecessor.taskId === 'NONE') {
62
+ if (
63
+ predecessor.title !== 'NONE' ||
64
+ predecessor.status !== 'NONE' ||
65
+ predecessor.authority !== 'POSITIVELY_NO_PREDECESSOR'
66
+ )
67
+ throw new TaskBranchStop('NO-PREDECESSOR AUTHORITY IS INVALID')
68
+ return { result: 'NO PREDECESSOR — POSITIVELY PROVEN' }
69
+ }
70
+ validateTaskId(predecessor.taskId)
71
+ if (typeof predecessor.title !== 'string' || !predecessor.title.trim())
72
+ throw new TaskBranchStop('PREDECESSOR TITLE IS MISSING')
73
+ if (predecessor.authority !== 'FRESH_CLICKUP_GITHUB_REPOSITORY_READ')
74
+ throw new TaskBranchStop('PREDECESSOR AUTHORITY IS STALE OR UNKNOWN')
75
+ if (predecessor.status === 'COMPLETE')
76
+ return { result: 'PREDECESSOR COMPLETE', taskId: predecessor.taskId }
77
+ if (predecessor.status === 'AUTO_AFTER_MERGE_RESUMABLE')
78
+ throw new TaskBranchStop('PREDECESSOR MUST BE RECONCILED FIRST', {
79
+ taskId: predecessor.taskId
80
+ })
81
+ throw new TaskBranchStop('PREDECESSOR IS NOT COMPLETE', {
82
+ status: predecessor.status,
83
+ taskId: predecessor.taskId
84
+ })
85
+ }
86
+
87
+ export function normalizeRepositoryRemote(url) {
88
+ const trimmed = url.trim()
89
+ const match = trimmed
90
+ .replace(/\.git$/u, '')
91
+ .match(/(?:github\.com[/:])([^/]+\/[^/]+)$/u)
92
+ if (match) return match[1]
93
+
94
+ try {
95
+ const parsed = new URL(trimmed)
96
+ const pathname = parsed.pathname.replace(/\.git$/u, '')
97
+ if (parsed.protocol === 'file:') return pathname
98
+ return `unrecognized-remote:${parsed.hostname}${pathname}`
99
+ } catch {
100
+ const scpLike = trimmed.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/u)
101
+ if (scpLike)
102
+ return `unrecognized-remote:${scpLike[1]}/${scpLike[2].replace(
103
+ /\.git$/u,
104
+ ''
105
+ )}`
106
+ return `unrecognized-remote:${redactGitDiagnostic(trimmed).replace(
107
+ /\.git$/u,
108
+ ''
109
+ )}`
110
+ }
111
+ }
112
+
113
+ export function redactGitDiagnostic(value) {
114
+ return String(value).replace(
115
+ /([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/giu,
116
+ '$1[REDACTED]@'
117
+ )
118
+ }
119
+
120
+ export function createGitRunner(cwd) {
121
+ return (args, options = {}) => {
122
+ const result = spawnSync('git', args, {
123
+ cwd,
124
+ encoding: 'utf8',
125
+ stdio: options.inherit ? 'inherit' : 'pipe'
126
+ })
127
+ if (result.error || result.status !== 0) {
128
+ if (options.allowFailure) return null
129
+ throw new TaskBranchStop(options.reason ?? 'GIT COMMAND FAILED', {
130
+ args,
131
+ cause: redactGitDiagnostic(
132
+ result.error?.message ??
133
+ result.stderr?.trim() ??
134
+ `git exited ${result.status}`
135
+ )
136
+ })
137
+ }
138
+ return options.inherit ? '' : result.stdout.trim()
139
+ }
140
+ }
141
+
142
+ function operationState(git, cwd) {
143
+ const gitDirectory = path.resolve(cwd, git(['rev-parse', '--git-dir']))
144
+ const commonDirectory = path.resolve(
145
+ cwd,
146
+ git(['rev-parse', '--git-common-dir'])
147
+ )
148
+ const checks = [
149
+ ['MERGE IN PROGRESS', path.join(gitDirectory, 'MERGE_HEAD')],
150
+ ['REBASE IN PROGRESS', path.join(gitDirectory, 'rebase-merge')],
151
+ ['REBASE IN PROGRESS', path.join(gitDirectory, 'rebase-apply')],
152
+ ['CHERRY-PICK IN PROGRESS', path.join(gitDirectory, 'CHERRY_PICK_HEAD')],
153
+ ['REVERT IN PROGRESS', path.join(gitDirectory, 'REVERT_HEAD')],
154
+ ['BISECT IN PROGRESS', path.join(commonDirectory, 'BISECT_LOG')]
155
+ ]
156
+ return checks.find(([, marker]) => existsSync(marker))?.[0] ?? null
157
+ }
158
+
159
+ export function inspectRepository({
160
+ cwd,
161
+ expectedRepository,
162
+ git = createGitRunner(cwd)
163
+ }) {
164
+ const inside = git(['rev-parse', '--is-inside-work-tree'], {
165
+ allowFailure: true
166
+ })
167
+ if (inside !== 'true') throw new TaskBranchStop('WRONG REPOSITORY')
168
+
169
+ const originUrl = git(['remote', 'get-url', 'origin'], {
170
+ reason: 'REMOTE UNAVAILABLE'
171
+ })
172
+ const repository = normalizeRepositoryRemote(originUrl)
173
+ if (repository !== expectedRepository)
174
+ throw new TaskBranchStop('WRONG REPOSITORY', {
175
+ expected: expectedRepository,
176
+ observed: repository
177
+ })
178
+
179
+ const currentBranch = git(['symbolic-ref', '--quiet', '--short', 'HEAD'], {
180
+ allowFailure: true
181
+ })
182
+ if (!currentBranch) throw new TaskBranchStop('DETACHED HEAD')
183
+
184
+ const operation = operationState(git, cwd)
185
+ if (operation) throw new TaskBranchStop(operation)
186
+
187
+ const status = git(['status', '--porcelain=v2', '--untracked-files=all'])
188
+ const lines = status ? status.split(/\r?\n/u) : []
189
+ if (lines.some((line) => line.startsWith('u ')))
190
+ throw new TaskBranchStop('UNRESOLVED CONFLICTS')
191
+ if (lines.some((line) => /^[12] [^.]/u.test(line)))
192
+ throw new TaskBranchStop('STAGED CHANGES')
193
+ if (lines.some((line) => /^[12] .[^.]/u.test(line)))
194
+ throw new TaskBranchStop('TRACKED MODIFICATIONS')
195
+ if (lines.some((line) => line.startsWith('? ')))
196
+ throw new TaskBranchStop('UNTRACKED FILES')
197
+
198
+ return {
199
+ currentBranch,
200
+ head: git(['rev-parse', 'HEAD']),
201
+ repository,
202
+ clean: true
203
+ }
204
+ }
205
+
206
+ export function classifyMainRelation(git) {
207
+ const counts = git([
208
+ 'rev-list',
209
+ '--left-right',
210
+ '--count',
211
+ 'main...origin/main'
212
+ ])
213
+ .split(/\s+/u)
214
+ .map(Number)
215
+ const [ahead, behind] = counts
216
+ if (ahead === 0 && behind === 0) return 'equal'
217
+ if (ahead === 0 && Number(behind) > 0) return 'behind'
218
+ if (Number(ahead) > 0 && behind === 0) return 'ahead'
219
+ return 'divergent'
220
+ }
221
+
222
+ export function synchronizeMain(git) {
223
+ const relation = classifyMainRelation(git)
224
+ if (relation === 'ahead') throw new TaskBranchStop('LOCAL MAIN AHEAD')
225
+ if (relation === 'divergent') throw new TaskBranchStop('LOCAL MAIN DIVERGENT')
226
+ if (relation === 'behind')
227
+ git(['merge', '--ff-only', 'origin/main'], {
228
+ reason: 'MAIN FAST-FORWARD FAILED'
229
+ })
230
+ return relation
231
+ }
232
+
233
+ function remoteBranchSha(git, branch) {
234
+ const output = git(
235
+ ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`],
236
+ {
237
+ reason: 'REMOTE UNAVAILABLE'
238
+ }
239
+ )
240
+ return output ? output.split(/\s+/u)[0] : null
241
+ }
242
+
243
+ export function pickupPlan({ cwd, expectedRepository, taskId, title }) {
244
+ const git = createGitRunner(cwd)
245
+ const state = inspectRepository({ cwd, expectedRepository, git })
246
+ const branch = taskBranchName(taskId, title)
247
+ const localCollision =
248
+ git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], {
249
+ allowFailure: true
250
+ }) !== null
251
+ const remoteSha = remoteBranchSha(git, branch)
252
+ return {
253
+ mode: 'dry-run',
254
+ state,
255
+ branch,
256
+ localCollision,
257
+ remoteCollision: Boolean(remoteSha),
258
+ intendedActions: [
259
+ 'fetch origin',
260
+ 'switch to main',
261
+ 'verify or fast-forward main with --ff-only',
262
+ `create ${branch} from verified main`,
263
+ `push --set-upstream origin ${branch}`,
264
+ 'attempt ClickUp/GitHub linkage verification in the agent layer',
265
+ 'stop for mandatory branch confirmed reply before Entry Review'
266
+ ],
267
+ mutationsPerformed: false
268
+ }
269
+ }
270
+
271
+ export function startPickup({
272
+ cwd,
273
+ expectedRepository,
274
+ predecessor,
275
+ taskId,
276
+ title
277
+ }) {
278
+ validatePredecessorBinding(predecessor)
279
+ const git = createGitRunner(cwd)
280
+ inspectRepository({ cwd, expectedRepository, git })
281
+ const branch = taskBranchName(taskId, title)
282
+
283
+ git(['fetch', 'origin', '--prune'], { reason: 'REMOTE UNAVAILABLE' })
284
+ inspectRepository({ cwd, expectedRepository, git })
285
+ git(['switch', 'main'], { reason: 'CANNOT SWITCH TO MAIN SAFELY' })
286
+ inspectRepository({ cwd, expectedRepository, git })
287
+
288
+ synchronizeMain(git)
289
+ inspectRepository({ cwd, expectedRepository, git })
290
+
291
+ if (
292
+ git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], {
293
+ allowFailure: true
294
+ }) !== null
295
+ )
296
+ throw new TaskBranchStop('LOCAL BRANCH COLLISION', { branch })
297
+ if (remoteBranchSha(git, branch))
298
+ throw new TaskBranchStop('REMOTE BRANCH COLLISION', { branch })
299
+
300
+ git(['switch', '-c', branch], { reason: 'BRANCH CREATION FAILED' })
301
+ git(['push', '--set-upstream', 'origin', branch], {
302
+ reason: 'BRANCH PUSH FAILED'
303
+ })
304
+ return {
305
+ result: 'BRANCH STARTED',
306
+ branch,
307
+ head: git(['rev-parse', 'HEAD']),
308
+ next: [
309
+ 'attempt ClickUp/GitHub linkage verification',
310
+ 'provide the readable task title/link and exact branch to the human',
311
+ 'stop for mandatory branch confirmed reply before Entry Review'
312
+ ]
313
+ }
314
+ }
315
+
316
+ export function resumeCheck({ cwd, expectedRepository, taskId, title }) {
317
+ const git = createGitRunner(cwd)
318
+ const state = inspectRepository({ cwd, expectedRepository, git })
319
+ const branch = taskBranchName(taskId, title)
320
+ if (state.currentBranch !== branch)
321
+ throw new TaskBranchStop('TASK/BRANCH MISMATCH', {
322
+ expected: branch,
323
+ observed: state.currentBranch
324
+ })
325
+ if (!branch.startsWith(`codex/CU-${taskId}-`))
326
+ throw new TaskBranchStop('NAMESPACE VIOLATION', { branch })
327
+
328
+ const mainSha = git(['rev-parse', 'main'])
329
+ const remoteMain = git(
330
+ ['ls-remote', '--heads', 'origin', 'refs/heads/main'],
331
+ {
332
+ reason: 'REMOTE UNAVAILABLE'
333
+ }
334
+ )
335
+ const remoteMainSha = remoteMain ? remoteMain.split(/\s+/u)[0] : null
336
+ if (!remoteMainSha) throw new TaskBranchStop('REMOTE MAIN MISSING')
337
+ if (mainSha !== remoteMainSha)
338
+ throw new TaskBranchStop('MAIN BASELINE MISMATCH', {
339
+ local: mainSha,
340
+ remote: remoteMainSha
341
+ })
342
+ if (git(['merge-base', branch, 'main']) !== mainSha)
343
+ throw new TaskBranchStop('UNSAFE RESUME LINEAGE')
344
+ if (git(['rev-list', '--count', `main..${branch}`]) !== '0')
345
+ throw new TaskBranchStop('UNEXPECTED PICKUP COMMITS')
346
+
347
+ const remoteSha = remoteBranchSha(git, branch)
348
+ const upstream = git(
349
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
350
+ { allowFailure: true }
351
+ )
352
+ if (remoteSha && remoteSha !== state.head)
353
+ throw new TaskBranchStop('REMOTE BRANCH DIVERGED')
354
+ if (remoteSha && upstream !== `origin/${branch}`)
355
+ throw new TaskBranchStop('UPSTREAM MISMATCH')
356
+ if (!remoteSha && upstream)
357
+ throw new TaskBranchStop('UPSTREAM WITHOUT REMOTE BRANCH')
358
+
359
+ return {
360
+ result: 'SAFE RESUME CANDIDATE',
361
+ branch,
362
+ head: state.head,
363
+ relationship: remoteSha ? 'remote-matching-with-upstream' : 'local-only',
364
+ stillRequires: [
365
+ 'agent verification of the exact live ClickUp task',
366
+ 'agent or human verification of task/branch/PR linkage',
367
+ 'human review for any ambiguous collision'
368
+ ]
369
+ }
370
+ }
package/hello-world.mjs DELETED
@@ -1 +0,0 @@
1
- export const helloWorld = 'hello world'