huddle60 0.0.0-stage → 0.1.1

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/action.js ADDED
@@ -0,0 +1,1601 @@
1
+ // src/entry/action.ts
2
+ import { appendFile, readFile } from "node:fs/promises";
3
+
4
+ // src/adapters/git-evidence.ts
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+
7
+ // src/core/canonical.ts
8
+ import { createHash } from "node:crypto";
9
+ var CANONICAL_VERSION = 1;
10
+ function normalizeText(value) {
11
+ return value.normalize("NFC").replace(/\r\n?/g, "\n").split("\n").map((line) => line.replace(/[ \t]+$/u, "")).join("\n").replace(/^\n+/u, "").replace(/\n+$/u, "");
12
+ }
13
+ function normalizeDeep(value, path) {
14
+ if (value === null) return null;
15
+ if (typeof value === "string") return normalizeText(value);
16
+ if (typeof value === "boolean") return value;
17
+ if (typeof value === "number") {
18
+ if (!Number.isFinite(value)) throw new TypeError(`Non-finite number at ${path}`);
19
+ return value;
20
+ }
21
+ if (Array.isArray(value))
22
+ return value.map((item, index) => normalizeDeep(item, `${path}[${index}]`));
23
+ if (typeof value === "object") {
24
+ const result = {};
25
+ for (const [key, item] of Object.entries(value)) {
26
+ if (item === void 0) continue;
27
+ result[key] = normalizeDeep(item, `${path}.${key}`);
28
+ }
29
+ return result;
30
+ }
31
+ throw new TypeError(`Unsupported value of type ${typeof value} at ${path}`);
32
+ }
33
+ function canonicalJson(value) {
34
+ return serialize(normalizeDeep(value, "$"));
35
+ }
36
+ function serialize(value) {
37
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
38
+ if (Array.isArray(value)) return `[${value.map(serialize).join(",")}]`;
39
+ const keys = Object.keys(value).sort();
40
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${serialize(value[key])}`).join(",")}}`;
41
+ }
42
+ function sha256(text) {
43
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
44
+ }
45
+ function contentHash(payload) {
46
+ const { contentHash: _ignored, ...rest } = payload;
47
+ return sha256(canonicalJson(rest));
48
+ }
49
+ function shortHash(hash, length = 12) {
50
+ return hash.replace(/^sha256:/u, "").slice(0, length);
51
+ }
52
+
53
+ // src/core/errors.ts
54
+ var AiDlcError = class extends Error {
55
+ code;
56
+ constructor(code, message, options) {
57
+ super(message, options);
58
+ this.name = "AiDlcError";
59
+ this.code = code;
60
+ }
61
+ };
62
+ function isAiDlcError(error) {
63
+ return error instanceof AiDlcError;
64
+ }
65
+
66
+ // src/adapters/github.ts
67
+ import { createAppAuth } from "@octokit/auth-app";
68
+ import { retry } from "@octokit/plugin-retry";
69
+ import { Octokit } from "@octokit/rest";
70
+ var RetryingOctokit = Octokit.plugin(retry);
71
+ var USER_AGENT = "huddle60/0.1.0";
72
+ var defaultLog = process.env.AI_DLC_DEBUG ? void 0 : {
73
+ debug: () => {
74
+ },
75
+ info: () => {
76
+ },
77
+ warn: () => {
78
+ },
79
+ error: () => {
80
+ }
81
+ };
82
+ function octokitFromToken(token) {
83
+ return new RetryingOctokit({ auth: token, userAgent: USER_AGENT, log: defaultLog });
84
+ }
85
+ function isStatus(error, ...statuses) {
86
+ return typeof error === "object" && error !== null && statuses.includes(error.status ?? -1);
87
+ }
88
+ function toActor(user) {
89
+ return user ? { id: user.id, login: user.login, type: user.type } : { id: 0, login: "ghost", type: "User" };
90
+ }
91
+ function toIssue(raw) {
92
+ return {
93
+ id: raw.id,
94
+ nodeId: raw.node_id,
95
+ number: raw.number,
96
+ title: raw.title,
97
+ body: raw.body ?? "",
98
+ state: raw.state === "closed" ? "closed" : "open",
99
+ stateReason: raw.state_reason ?? null,
100
+ labels: raw.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
101
+ author: toActor(raw.user),
102
+ isPullRequest: raw.pull_request !== void 0 && raw.pull_request !== null
103
+ };
104
+ }
105
+ async function connectRepository(octokit, owner, repo) {
106
+ const { data } = await octokit.rest.repos.get({ owner, repo });
107
+ return new OctokitGitHub(octokit, { owner, repo, repositoryId: data.id }, data.default_branch);
108
+ }
109
+ var OctokitGitHub = class {
110
+ constructor(octokit, repo, defaultBranch) {
111
+ this.octokit = octokit;
112
+ this.repo = repo;
113
+ this.defaultBranch = defaultBranch;
114
+ }
115
+ octokit;
116
+ repo;
117
+ defaultBranch;
118
+ projectIds = /* @__PURE__ */ new Map();
119
+ get base() {
120
+ return { owner: this.repo.owner, repo: this.repo.repo };
121
+ }
122
+ async getDefaultBranch() {
123
+ if (!this.defaultBranch) {
124
+ const { data } = await this.octokit.rest.repos.get(this.base);
125
+ this.defaultBranch = data.default_branch;
126
+ }
127
+ return this.defaultBranch;
128
+ }
129
+ async readFile(path, ref) {
130
+ try {
131
+ const { data } = await this.octokit.rest.repos.getContent({
132
+ ...this.base,
133
+ path,
134
+ ...ref ? { ref } : {}
135
+ });
136
+ if (Array.isArray(data) || data.type !== "file" || !("content" in data)) return null;
137
+ return Buffer.from(data.content, "base64").toString("utf8");
138
+ } catch (error) {
139
+ if (isStatus(error, 404)) return null;
140
+ throw error;
141
+ }
142
+ }
143
+ async getIssue(number) {
144
+ try {
145
+ const { data } = await this.octokit.rest.issues.get({ ...this.base, issue_number: number });
146
+ return toIssue(data);
147
+ } catch (error) {
148
+ if (isStatus(error, 404, 410))
149
+ throw new AiDlcError("E_GITHUB_API", `Issue #${number} was not found.`);
150
+ throw error;
151
+ }
152
+ }
153
+ async getComment(id) {
154
+ try {
155
+ const { data } = await this.octokit.rest.issues.getComment({ ...this.base, comment_id: id });
156
+ return { id: data.id, body: data.body ?? "", author: toActor(data.user) };
157
+ } catch (error) {
158
+ if (isStatus(error, 404)) return null;
159
+ throw error;
160
+ }
161
+ }
162
+ async createIssue(input) {
163
+ const { data } = await this.octokit.rest.issues.create({ ...this.base, ...input });
164
+ return toIssue(data);
165
+ }
166
+ async listIssuesWithLabel(label) {
167
+ const items = await this.octokit.paginate(this.octokit.rest.issues.listForRepo, {
168
+ ...this.base,
169
+ labels: label,
170
+ state: "all",
171
+ per_page: 100
172
+ });
173
+ return items.map(toIssue).filter((issue) => !issue.isPullRequest);
174
+ }
175
+ async addLabels(issueNumber, labels) {
176
+ await this.octokit.rest.issues.addLabels({ ...this.base, issue_number: issueNumber, labels });
177
+ }
178
+ async removeLabel(issueNumber, label) {
179
+ try {
180
+ await this.octokit.rest.issues.removeLabel({
181
+ ...this.base,
182
+ issue_number: issueNumber,
183
+ name: label
184
+ });
185
+ } catch (error) {
186
+ if (!isStatus(error, 404)) throw error;
187
+ }
188
+ }
189
+ async upsertComment(issueNumber, marker2, body) {
190
+ const comments = await this.octokit.paginate(this.octokit.rest.issues.listComments, {
191
+ ...this.base,
192
+ issue_number: issueNumber,
193
+ per_page: 100
194
+ });
195
+ const existing = comments.find(
196
+ (comment) => comment.user?.type === "Bot" && (comment.body ?? "").includes(marker2)
197
+ );
198
+ const text = body.includes(marker2) ? body : `${body}
199
+ ${marker2}`;
200
+ if (existing) {
201
+ await this.octokit.rest.issues.updateComment({
202
+ ...this.base,
203
+ comment_id: existing.id,
204
+ body: text
205
+ });
206
+ } else {
207
+ await this.octokit.rest.issues.createComment({
208
+ ...this.base,
209
+ issue_number: issueNumber,
210
+ body: text
211
+ });
212
+ }
213
+ }
214
+ async closeIssue(issueNumber, reason) {
215
+ await this.octokit.rest.issues.update({
216
+ ...this.base,
217
+ issue_number: issueNumber,
218
+ state: "closed",
219
+ state_reason: reason
220
+ });
221
+ }
222
+ async addSubIssue(parentNumber, childId) {
223
+ try {
224
+ await this.octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues", {
225
+ ...this.base,
226
+ issue_number: parentNumber,
227
+ sub_issue_id: childId
228
+ });
229
+ return "linked";
230
+ } catch (error) {
231
+ if (isStatus(error, 404)) return "unsupported";
232
+ throw error;
233
+ }
234
+ }
235
+ async projectId(owner, number) {
236
+ const key = `${owner}/${number}`;
237
+ const cached = this.projectIds.get(key);
238
+ if (cached) return cached;
239
+ const query = `query($owner: String!, $number: Int!) {
240
+ organization(login: $owner) { projectV2(number: $number) { id } }
241
+ }`;
242
+ let id;
243
+ try {
244
+ const result = await this.octokit.graphql(query, { owner, number });
245
+ id = result.organization?.projectV2?.id;
246
+ } catch {
247
+ const userQuery = `query($owner: String!, $number: Int!) { user(login: $owner) { projectV2(number: $number) { id } } }`;
248
+ const result = await this.octokit.graphql(userQuery, { owner, number });
249
+ id = result.user?.projectV2?.id;
250
+ }
251
+ if (!id)
252
+ throw new AiDlcError("E_GITHUB_API", `Project ${key} was not found or is not accessible.`);
253
+ this.projectIds.set(key, id);
254
+ return id;
255
+ }
256
+ async addToProject(project, contentNodeId) {
257
+ const projectId = await this.projectId(project.owner, project.number);
258
+ await this.octokit.graphql(
259
+ `mutation($projectId: ID!, $contentId: ID!) {
260
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
261
+ }`,
262
+ { projectId, contentId: contentNodeId }
263
+ );
264
+ }
265
+ async getPullRequest(number) {
266
+ const { data } = await this.octokit.rest.pulls.get({ ...this.base, pull_number: number });
267
+ return {
268
+ number: data.number,
269
+ body: data.body ?? "",
270
+ headSha: data.head.sha,
271
+ labels: data.labels.map((label) => label.name)
272
+ };
273
+ }
274
+ async listPullRequestFiles(number) {
275
+ const files = await this.octokit.paginate(this.octokit.rest.pulls.listFiles, {
276
+ ...this.base,
277
+ pull_number: number,
278
+ per_page: 100
279
+ });
280
+ return files.flatMap(
281
+ (file) => file.previous_filename ? [file.filename, file.previous_filename] : [file.filename]
282
+ );
283
+ }
284
+ async setCommitStatus(sha, status) {
285
+ await this.octokit.rest.repos.createCommitStatus({ ...this.base, sha, ...status });
286
+ }
287
+ async getWorkflowRun(runId) {
288
+ try {
289
+ const { data } = await this.octokit.rest.actions.getWorkflowRun({
290
+ ...this.base,
291
+ run_id: runId
292
+ });
293
+ return toRun(data);
294
+ } catch (error) {
295
+ if (isStatus(error, 404)) return null;
296
+ throw error;
297
+ }
298
+ }
299
+ async viewer() {
300
+ const { data } = await this.octokit.rest.users.getAuthenticated();
301
+ return { id: data.id, login: data.login, type: data.type };
302
+ }
303
+ async listOpenIssuesWithLabel(label) {
304
+ const items = await this.octokit.paginate(this.octokit.rest.issues.listForRepo, {
305
+ ...this.base,
306
+ labels: label,
307
+ state: "open",
308
+ per_page: 100
309
+ });
310
+ return items.map(toIssue).filter((issue) => !issue.isPullRequest);
311
+ }
312
+ async createComment(issueNumber, body) {
313
+ const { data } = await this.octokit.rest.issues.createComment({
314
+ ...this.base,
315
+ issue_number: issueNumber,
316
+ body
317
+ });
318
+ return { id: data.id, body: data.body ?? "", author: toActor(data.user) };
319
+ }
320
+ async findBotComment(issueNumber, marker2) {
321
+ const comments = await this.octokit.paginate(this.octokit.rest.issues.listComments, {
322
+ ...this.base,
323
+ issue_number: issueNumber,
324
+ per_page: 100
325
+ });
326
+ const found = comments.filter((comment) => comment.user?.type === "Bot" && (comment.body ?? "").includes(marker2)).at(-1);
327
+ return found ? { id: found.id, body: found.body ?? "", author: toActor(found.user) } : null;
328
+ }
329
+ async listSuccessfulRuns(workflowPath, branch, limit) {
330
+ try {
331
+ const { data } = await this.octokit.rest.actions.listWorkflowRuns({
332
+ ...this.base,
333
+ workflow_id: workflowPath.split("/").pop() ?? workflowPath,
334
+ branch,
335
+ event: "push",
336
+ status: "success",
337
+ per_page: limit
338
+ });
339
+ return data.workflow_runs.map(toRun);
340
+ } catch (error) {
341
+ if (isStatus(error, 404)) return [];
342
+ throw error;
343
+ }
344
+ }
345
+ async ensureLabel(label) {
346
+ try {
347
+ await this.octokit.rest.issues.createLabel({ ...this.base, ...label });
348
+ return "created";
349
+ } catch (error) {
350
+ if (isStatus(error, 422)) return "exists";
351
+ throw error;
352
+ }
353
+ }
354
+ };
355
+ function toRun(data) {
356
+ return {
357
+ id: data.id,
358
+ runAttempt: data.run_attempt ?? 1,
359
+ path: data.path.split("@")[0] ?? data.path,
360
+ event: data.event,
361
+ headBranch: data.head_branch,
362
+ headSha: data.head_sha,
363
+ status: data.status,
364
+ conclusion: data.conclusion,
365
+ repositoryId: data.repository.id
366
+ };
367
+ }
368
+
369
+ // src/adapters/git-evidence.ts
370
+ var MAX_ATTEMPTS = 5;
371
+ var GitBranchEvidenceStore = class {
372
+ constructor(octokit, owner, repo, branch) {
373
+ this.octokit = octokit;
374
+ this.owner = owner;
375
+ this.repo = repo;
376
+ this.branch = branch;
377
+ }
378
+ octokit;
379
+ owner;
380
+ repo;
381
+ branch;
382
+ get base() {
383
+ return { owner: this.owner, repo: this.repo };
384
+ }
385
+ async readAt(path, ref) {
386
+ try {
387
+ const { data } = await this.octokit.rest.repos.getContent({ ...this.base, path, ref });
388
+ if (Array.isArray(data) || data.type !== "file" || !("content" in data)) return null;
389
+ return JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
390
+ } catch (error) {
391
+ if (isStatus(error, 404)) return null;
392
+ throw error;
393
+ }
394
+ }
395
+ async read(path) {
396
+ return this.readAt(path, this.branch);
397
+ }
398
+ async list(prefix) {
399
+ const directory = prefix.replace(/\/+$/u, "");
400
+ try {
401
+ const { data } = await this.octokit.rest.repos.getContent({
402
+ ...this.base,
403
+ path: directory,
404
+ ref: this.branch
405
+ });
406
+ if (!Array.isArray(data)) return [];
407
+ return data.filter((entry) => entry.type === "file").map((entry) => entry.path).sort();
408
+ } catch (error) {
409
+ if (isStatus(error, 404)) return [];
410
+ throw error;
411
+ }
412
+ }
413
+ async write(path, value) {
414
+ const canonical = canonicalJson(value);
415
+ const content = `${JSON.stringify(JSON.parse(canonical), null, 2)}
416
+ `;
417
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
418
+ const head = await this.headCommit();
419
+ const existing = await this.readAt(path, head.sha);
420
+ if (existing !== null) {
421
+ if (canonicalJson(existing) === canonical) return "unchanged";
422
+ throw new AiDlcError(
423
+ "E_CONFLICT",
424
+ `Evidence already exists with different content at ${path}.`
425
+ );
426
+ }
427
+ const blob = await this.octokit.rest.git.createBlob({
428
+ ...this.base,
429
+ content,
430
+ encoding: "utf-8"
431
+ });
432
+ const tree = await this.octokit.rest.git.createTree({
433
+ ...this.base,
434
+ base_tree: head.treeSha,
435
+ tree: [{ path, mode: "100644", type: "blob", sha: blob.data.sha }]
436
+ });
437
+ const commit = await this.octokit.rest.git.createCommit({
438
+ ...this.base,
439
+ message: `huddle60: add ${path}`,
440
+ tree: tree.data.sha,
441
+ parents: [head.sha]
442
+ });
443
+ try {
444
+ await this.octokit.rest.git.updateRef({
445
+ ...this.base,
446
+ ref: `heads/${this.branch}`,
447
+ sha: commit.data.sha,
448
+ force: false
449
+ });
450
+ return "created";
451
+ } catch (error) {
452
+ if (!isStatus(error, 409, 422) || attempt === MAX_ATTEMPTS) throw error;
453
+ await delay(250 * 2 ** attempt);
454
+ }
455
+ }
456
+ throw new AiDlcError("E_GITHUB_API", `Could not write ${path} after ${MAX_ATTEMPTS} attempts.`);
457
+ }
458
+ async headCommit() {
459
+ try {
460
+ const ref = await this.octokit.rest.git.getRef({ ...this.base, ref: `heads/${this.branch}` });
461
+ const commit = await this.octokit.rest.git.getCommit({
462
+ ...this.base,
463
+ commit_sha: ref.data.object.sha
464
+ });
465
+ return { sha: commit.data.sha, treeSha: commit.data.tree.sha };
466
+ } catch (error) {
467
+ if (isStatus(error, 404)) {
468
+ throw new AiDlcError(
469
+ "E_GITHUB_API",
470
+ `Evidence branch \`${this.branch}\` does not exist. Run \`huddle60 init\`.`
471
+ );
472
+ }
473
+ throw error;
474
+ }
475
+ }
476
+ /** Creates the evidence branch as an orphan commit. Safe to call when it already exists. */
477
+ async initialize() {
478
+ try {
479
+ await this.octokit.rest.git.getRef({ ...this.base, ref: `heads/${this.branch}` });
480
+ return "exists";
481
+ } catch (error) {
482
+ if (!isStatus(error, 404)) throw error;
483
+ }
484
+ const readme = [
485
+ "# AI-DLC evidence",
486
+ "",
487
+ "Append-only baselines, decision receipts and revocations written by the AI-DLC GitHub App.",
488
+ "Do not edit or force-push this branch; protect it with a ruleset.",
489
+ ""
490
+ ].join("\n");
491
+ const blob = await this.octokit.rest.git.createBlob({
492
+ ...this.base,
493
+ content: readme,
494
+ encoding: "utf-8"
495
+ });
496
+ const tree = await this.octokit.rest.git.createTree({
497
+ ...this.base,
498
+ tree: [{ path: "README.md", mode: "100644", type: "blob", sha: blob.data.sha }]
499
+ });
500
+ const commit = await this.octokit.rest.git.createCommit({
501
+ ...this.base,
502
+ message: "huddle60: initialize evidence branch",
503
+ tree: tree.data.sha,
504
+ parents: []
505
+ });
506
+ await this.octokit.rest.git.createRef({
507
+ ...this.base,
508
+ ref: `refs/heads/${this.branch}`,
509
+ sha: commit.data.sha
510
+ });
511
+ return "created";
512
+ }
513
+ };
514
+
515
+ // src/adapters/memory.ts
516
+ function createJsonLogger(base = {}) {
517
+ const write = (severity, message, fields) => {
518
+ const line = JSON.stringify({ severity, message, ...base, ...fields });
519
+ if (severity === "ERROR") process.stderr.write(`${line}
520
+ `);
521
+ else process.stdout.write(`${line}
522
+ `);
523
+ };
524
+ return {
525
+ info: (message, fields) => write("INFO", message, fields),
526
+ warn: (message, fields) => write("WARNING", message, fields),
527
+ error: (message, fields) => write("ERROR", message, fields)
528
+ };
529
+ }
530
+
531
+ // src/core/policy.ts
532
+ import { parse as parseYaml } from "yaml";
533
+ import { z } from "zod";
534
+ var Member = z.object({
535
+ id: z.number().int().positive(),
536
+ login: z.string().min(1)
537
+ });
538
+ var Lane = z.object({
539
+ requires_po_approval: z.boolean()
540
+ });
541
+ var PolicySchema = z.object({
542
+ version: z.literal(1),
543
+ roles: z.object({
544
+ po: z.array(Member).min(1),
545
+ dev: z.array(Member).default([]),
546
+ maintainer: z.array(Member).default([])
547
+ }),
548
+ project: z.object({
549
+ owner: z.string().min(1),
550
+ number: z.number().int().positive()
551
+ }).optional(),
552
+ lanes: z.object({
553
+ fast: Lane,
554
+ standard: Lane,
555
+ strict: Lane
556
+ }).default({
557
+ fast: { requires_po_approval: false },
558
+ standard: { requires_po_approval: true },
559
+ strict: { requires_po_approval: true }
560
+ }),
561
+ sensitive_paths: z.array(z.string().min(1)).default([]),
562
+ evidence: z.object({ branch: z.string().min(1).default("huddle60-evidence") }).default({ branch: "huddle60-evidence" }),
563
+ acceptance: z.object({ build_workflows: z.array(z.string().min(1)).min(1) }).default({ build_workflows: [".github/workflows/build.yml"] }),
564
+ planner: z.object({
565
+ max_nodes: z.number().int().min(1).max(200).default(60),
566
+ plan_ttl_days: z.number().int().min(1).max(90).default(30)
567
+ }).default({ max_nodes: 60, plan_ttl_days: 30 })
568
+ }).superRefine((policy, ctx) => {
569
+ const bots = [...policy.roles.po, ...policy.roles.dev, ...policy.roles.maintainer].filter(
570
+ (member) => member.login.endsWith("[bot]")
571
+ );
572
+ for (const bot of bots) {
573
+ ctx.addIssue({ code: "custom", message: `Bot account ${bot.login} cannot hold a role` });
574
+ }
575
+ });
576
+ var POLICY_PATH = ".github/huddle60/policy.yml";
577
+ var LEGACY_POLICY_PATH = ".github/ai-dlc/policy.yml";
578
+ function parsePolicy(source) {
579
+ let raw;
580
+ try {
581
+ raw = parseYaml(source);
582
+ } catch (error) {
583
+ throw new AiDlcError("E_POLICY_INVALID", "Policy file is not valid YAML.", { cause: error });
584
+ }
585
+ const result = PolicySchema.safeParse(raw);
586
+ if (!result.success) {
587
+ throw new AiDlcError(
588
+ "E_POLICY_INVALID",
589
+ `Policy file is invalid:
590
+ ${z.prettifyError(result.error)}`
591
+ );
592
+ }
593
+ return result.data;
594
+ }
595
+ function hasRole(policy, userId, role) {
596
+ return policy.roles[role].some((member) => member.id === userId);
597
+ }
598
+ function matchesPath(pattern, path) {
599
+ let regex = "";
600
+ for (let index = 0; index < pattern.length; index += 1) {
601
+ const char = pattern[index];
602
+ if (char === "*") {
603
+ if (pattern[index + 1] === "*") {
604
+ regex += ".*";
605
+ index += 1;
606
+ if (pattern[index + 1] === "/") index += 1;
607
+ } else {
608
+ regex += "[^/]*";
609
+ }
610
+ } else if (char === "?") {
611
+ regex += "[^/]";
612
+ } else {
613
+ regex += char.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
614
+ }
615
+ }
616
+ return new RegExp(`^${regex}$`, "u").test(path);
617
+ }
618
+ function isSensitive(policy, path) {
619
+ return policy.sensitive_paths.some((pattern) => matchesPath(pattern, path));
620
+ }
621
+
622
+ // src/ports.ts
623
+ var systemClock = { now: () => /* @__PURE__ */ new Date() };
624
+
625
+ // src/core/issue-form.ts
626
+ var NO_RESPONSE = "_No response_";
627
+ function parseIssueForm(body) {
628
+ const sections = /* @__PURE__ */ new Map();
629
+ const lines = (body ?? "").replace(/\r\n?/gu, "\n").split("\n");
630
+ let current = null;
631
+ let buffer = [];
632
+ const flush = () => {
633
+ if (current === null) return;
634
+ const value = buffer.join("\n").trim();
635
+ sections.set(current, value === NO_RESPONSE ? "" : value);
636
+ };
637
+ for (const line of lines) {
638
+ const heading = /^###\s+(.+?)\s*$/u.exec(line);
639
+ if (heading) {
640
+ flush();
641
+ current = heading[1];
642
+ buffer = [];
643
+ } else if (current !== null) {
644
+ buffer.push(line);
645
+ }
646
+ }
647
+ flush();
648
+ return sections;
649
+ }
650
+ var CRITERION_LINE = /^-\s+((?:AC|EC)-\d{2,3}):\s+(.+)$/u;
651
+ function parseCriteria(text) {
652
+ const criteria = [];
653
+ const seen = /* @__PURE__ */ new Set();
654
+ for (const rawLine of text.split("\n")) {
655
+ const line = rawLine.trim();
656
+ if (line.length === 0) continue;
657
+ const match = CRITERION_LINE.exec(line);
658
+ if (!match) {
659
+ if (/^-\s+(AC|EC)/u.test(line)) {
660
+ throw new AiDlcError(
661
+ "E_FORM",
662
+ `Malformed criterion line: \`${line}\`. Use \`- AC-01: ...\`.`
663
+ );
664
+ }
665
+ continue;
666
+ }
667
+ const id = match[1];
668
+ if (seen.has(id)) throw new AiDlcError("E_FORM", `Duplicate criterion id \`${id}\`.`);
669
+ seen.add(id);
670
+ criteria.push({ id, text: match[2].trim() });
671
+ }
672
+ return criteria;
673
+ }
674
+ function parseIssueRefs(text) {
675
+ if (!text) return [];
676
+ const refs = /* @__PURE__ */ new Set();
677
+ for (const part of text.split(/[\s,]+/u)) {
678
+ if (part.length === 0) continue;
679
+ const match = /^#(\d+)$/u.exec(part);
680
+ if (!match) {
681
+ throw new AiDlcError("E_FORM", `Invalid issue reference \`${part}\`. Use \`#123\`.`);
682
+ }
683
+ refs.add(Number(match[1]));
684
+ }
685
+ return [...refs].sort((a, b) => a - b);
686
+ }
687
+
688
+ // src/core/vocabulary.ts
689
+ var LABELS = {
690
+ request: "huddle60:request",
691
+ planDraft: "huddle60:plan-draft",
692
+ planCreated: "huddle60:plan-created",
693
+ planPartial: "huddle60:plan-partial",
694
+ fromPlan: "huddle60:from-plan",
695
+ stale: "huddle60:stale",
696
+ exempt: "huddle60:exempt",
697
+ epic: "type:epic",
698
+ story: "type:story",
699
+ task: "type:task"
700
+ };
701
+ var KIND_LABEL = {
702
+ epic: LABELS.epic,
703
+ story: LABELS.story,
704
+ task: LABELS.task
705
+ };
706
+ var SECTIONS = {
707
+ request: "Y\xEAu c\u1EA7u",
708
+ goal: "M\u1EE5c ti\xEAu v\xE0 ng\u01B0\u1EDDi d\xF9ng",
709
+ scope: "Ph\u1EA1m vi",
710
+ parent: "Issue cha",
711
+ blockedBy: "Blocked by",
712
+ epicCriteria: "Ti\xEAu ch\xED nghi\u1EC7m thu Epic",
713
+ storyCriteria: "Acceptance Criteria",
714
+ acRefs: "AC li\xEAn quan",
715
+ technicalRefs: "Tham chi\u1EBFu k\u1EF9 thu\u1EADt",
716
+ risks: "C\xE2u h\u1ECFi v\xE0 r\u1EE7i ro"
717
+ };
718
+ var REQUIREMENT_SECTIONS = {
719
+ epic: [
720
+ SECTIONS.goal,
721
+ SECTIONS.scope,
722
+ SECTIONS.epicCriteria,
723
+ SECTIONS.technicalRefs,
724
+ SECTIONS.risks
725
+ ],
726
+ story: [
727
+ SECTIONS.goal,
728
+ SECTIONS.scope,
729
+ SECTIONS.parent,
730
+ SECTIONS.blockedBy,
731
+ SECTIONS.storyCriteria,
732
+ SECTIONS.technicalRefs,
733
+ SECTIONS.risks
734
+ ],
735
+ task: [
736
+ SECTIONS.goal,
737
+ SECTIONS.scope,
738
+ SECTIONS.parent,
739
+ SECTIONS.blockedBy,
740
+ SECTIONS.acRefs,
741
+ SECTIONS.technicalRefs,
742
+ SECTIONS.risks
743
+ ]
744
+ };
745
+ var CRITERIA_SECTION = {
746
+ epic: SECTIONS.epicCriteria,
747
+ story: SECTIONS.storyCriteria,
748
+ task: null
749
+ };
750
+ function kindFromLabels(labels) {
751
+ const kinds = Object.keys(KIND_LABEL).filter(
752
+ (kind) => labels.includes(KIND_LABEL[kind])
753
+ );
754
+ return kinds.length === 1 ? kinds[0] : null;
755
+ }
756
+
757
+ // src/core/baseline.ts
758
+ var BASELINE_SCHEMA_VERSION = 1;
759
+ function hashedPart(snapshot) {
760
+ return {
761
+ schemaVersion: snapshot.schemaVersion,
762
+ canonicalVersion: snapshot.canonicalVersion,
763
+ kind: snapshot.kind,
764
+ source: snapshot.source,
765
+ requirements: snapshot.requirements,
766
+ parent: snapshot.parent,
767
+ blockedBy: snapshot.blockedBy,
768
+ acRefs: snapshot.acRefs
769
+ };
770
+ }
771
+ function buildBaseline(issue) {
772
+ const kind = kindFromLabels(issue.labels);
773
+ if (!kind) {
774
+ throw new AiDlcError(
775
+ "E_FORM",
776
+ "Issue needs exactly one of the labels type:epic, type:story or type:task."
777
+ );
778
+ }
779
+ const parsed = parseIssueForm(issue.body);
780
+ const sections = {};
781
+ for (const label of REQUIREMENT_SECTIONS[kind]) sections[label] = parsed.get(label) ?? "";
782
+ if (!sections[SECTIONS.goal]) {
783
+ throw new AiDlcError("E_FORM", `Section \`### ${SECTIONS.goal}\` is required.`);
784
+ }
785
+ const criteriaSection = CRITERIA_SECTION[kind];
786
+ const criteria = criteriaSection ? parseCriteria(sections[criteriaSection] ?? "") : [];
787
+ if (criteriaSection && criteria.length === 0) {
788
+ throw new AiDlcError(
789
+ "E_FORM",
790
+ `Section \`### ${criteriaSection}\` needs at least one criterion line.`
791
+ );
792
+ }
793
+ const expectedPrefix = kind === "epic" ? "EC-" : "AC-";
794
+ if (criteria.some((criterion) => !criterion.id.startsWith(expectedPrefix))) {
795
+ throw new AiDlcError("E_FORM", `${kind} criteria must use the ${expectedPrefix} prefix.`);
796
+ }
797
+ const parentRefs = parseIssueRefs(sections[SECTIONS.parent]);
798
+ if (parentRefs.length > 1) throw new AiDlcError("E_FORM", "Only one parent Issue is allowed.");
799
+ const acRefs = kind === "task" ? (sections[SECTIONS.acRefs] ?? "").split(/[\s,]+/u).filter((value) => value.length > 0).map((value) => {
800
+ if (!/^AC-\d{2,3}$/u.test(value))
801
+ throw new AiDlcError("E_FORM", `Invalid AC reference \`${value}\`.`);
802
+ return value;
803
+ }) : [];
804
+ const base = {
805
+ schemaVersion: BASELINE_SCHEMA_VERSION,
806
+ canonicalVersion: CANONICAL_VERSION,
807
+ kind,
808
+ source: {
809
+ repositoryId: issue.repositoryId,
810
+ issueNodeId: issue.issueNodeId,
811
+ issueNumber: issue.number
812
+ },
813
+ requirements: { title: issue.title, sections, criteria },
814
+ parent: parentRefs[0] ?? null,
815
+ blockedBy: parseIssueRefs(sections[SECTIONS.blockedBy]),
816
+ acRefs
817
+ };
818
+ const hash = contentHash(hashedPart(base));
819
+ return { ...base, baselineId: `bl-${issue.number}-${shortHash(hash)}`, contentHash: hash };
820
+ }
821
+ function verifySnapshot(snapshot) {
822
+ return contentHash(hashedPart(snapshot)) === snapshot.contentHash;
823
+ }
824
+ function baselinePath(repositoryId, issueNumber, baselineId) {
825
+ return `baselines/${repositoryId}/${issueNumber}/${baselineId}.json`;
826
+ }
827
+
828
+ // src/core/command.ts
829
+ var COMMAND_PREFIX = "/huddle60";
830
+ var LEGACY_COMMAND_PREFIX = "/ai-dlc";
831
+ var COMMAND_SPECS = {
832
+ baseline: { required: [], optional: [], handler: "deterministic" },
833
+ approve: { required: ["baseline", "hash"], optional: [], handler: "deterministic" },
834
+ ready: { required: ["baseline", "hash", "lane"], optional: [], handler: "deterministic" },
835
+ accept: { required: ["baseline", "run", "commit"], optional: [], handler: "deterministic" },
836
+ revoke: { required: ["decision", "reason"], optional: [], handler: "deterministic" },
837
+ refine: { required: ["plan"], optional: [], handler: "planner" },
838
+ "create-plan": { required: ["plan", "hash"], optional: [], handler: "planner" },
839
+ "resume-plan": { required: ["plan"], optional: [], handler: "planner" }
840
+ };
841
+ function commandHandler(name) {
842
+ return COMMAND_SPECS[name].handler;
843
+ }
844
+ function tokenize(input) {
845
+ const tokens = [];
846
+ const pattern = /([a-z][a-z-]*)=(?:"((?:[^"\\]|\\.)*)"|(\S+))|(\S+)/gu;
847
+ for (const match of input.matchAll(pattern)) {
848
+ if (match[4] !== void 0) {
849
+ throw new AiDlcError("E_SYNTAX", `Unexpected token \`${match[4]}\`. Use key=value.`);
850
+ }
851
+ const value = match[2] !== void 0 ? match[2].replace(/\\(.)/gu, "$1") : match[3];
852
+ tokens.push(`${match[1]}=${value}`);
853
+ }
854
+ return tokens;
855
+ }
856
+ function parseCommand(body) {
857
+ const normalized = body.replace(/\r\n?/gu, "\n");
858
+ const [head = "", ...rest] = normalized.split("\n");
859
+ const line = head.trim();
860
+ let prefix = "";
861
+ if (line.startsWith(COMMAND_PREFIX)) {
862
+ prefix = COMMAND_PREFIX;
863
+ } else if (line.startsWith(LEGACY_COMMAND_PREFIX)) {
864
+ prefix = LEGACY_COMMAND_PREFIX;
865
+ } else {
866
+ throw new AiDlcError(
867
+ "E_SYNTAX",
868
+ `Commands must start with \`${COMMAND_PREFIX}\` on the first line.`
869
+ );
870
+ }
871
+ const remainder = line.slice(prefix.length).trim();
872
+ const [rawName = "", ...argParts] = remainder.split(/\s+/u);
873
+ if (!(rawName in COMMAND_SPECS)) {
874
+ throw new AiDlcError(
875
+ "E_SYNTAX",
876
+ `Unknown command \`${rawName || "(empty)"}\`. Known commands: ${Object.keys(COMMAND_SPECS).join(", ")}.`
877
+ );
878
+ }
879
+ const name = rawName;
880
+ const spec = COMMAND_SPECS[name];
881
+ const args = {};
882
+ for (const token of tokenize(argParts.join(" "))) {
883
+ const separator = token.indexOf("=");
884
+ const key = token.slice(0, separator);
885
+ const value = token.slice(separator + 1);
886
+ if (!spec.required.includes(key) && !spec.optional.includes(key)) {
887
+ throw new AiDlcError("E_SYNTAX", `Unknown argument \`${key}\` for \`${name}\`.`);
888
+ }
889
+ if (key in args) throw new AiDlcError("E_SYNTAX", `Duplicate argument \`${key}\`.`);
890
+ if (value.length === 0) throw new AiDlcError("E_SYNTAX", `Argument \`${key}\` is empty.`);
891
+ args[key] = value;
892
+ }
893
+ const missing = spec.required.filter((key) => !(key in args));
894
+ if (missing.length > 0) {
895
+ throw new AiDlcError("E_SYNTAX", `Missing argument(s) for \`${name}\`: ${missing.join(", ")}.`);
896
+ }
897
+ return { name, args, note: rest.join("\n").trim() };
898
+ }
899
+
900
+ // src/core/decision.ts
901
+ var DECISION_SCHEMA_VERSION = 1;
902
+ var REQUIRED_ROLE = {
903
+ approve: "po",
904
+ ready: "dev",
905
+ accept: "po"
906
+ };
907
+ function decisionId(kind, commentId) {
908
+ return `dec-${shortHash(sha256(`${kind}:${commentId}`), 16)}`;
909
+ }
910
+ function commentDigest(body) {
911
+ return sha256(body.replace(/\r\n?/gu, "\n"));
912
+ }
913
+ function decisionPath(repositoryId, issueNumber, id) {
914
+ return `decisions/${repositoryId}/${issueNumber}/${id}.json`;
915
+ }
916
+ function revocationPath(repositoryId, issueNumber, id) {
917
+ return `revocations/${repositoryId}/${issueNumber}/${id}.json`;
918
+ }
919
+ function evaluateReceipt(receipt, facts) {
920
+ const reasons = [];
921
+ if (facts.revoked) reasons.push("decision was revoked");
922
+ if (facts.currentCommentBody === null) reasons.push("source comment was deleted");
923
+ else if (commentDigest(facts.currentCommentBody) !== receipt.source.commentBodyDigest) {
924
+ reasons.push("source comment was edited");
925
+ }
926
+ if (!hasRole(facts.policy, receipt.actor.id, REQUIRED_ROLE[receipt.kind])) {
927
+ reasons.push(`actor no longer holds the ${REQUIRED_ROLE[receipt.kind]} role`);
928
+ }
929
+ if (facts.snapshotHash === null) reasons.push("baseline snapshot is missing or corrupt");
930
+ else if (facts.snapshotHash !== receipt.baselineHash) reasons.push("baseline hash mismatch");
931
+ if (receipt.kind === "accept" && facts.buildValid !== true)
932
+ reasons.push("accepted build is no longer valid");
933
+ return { valid: reasons.length === 0, reasons };
934
+ }
935
+
936
+ // src/core/pr-block.ts
937
+ var PR_BLOCK_BEGIN = "<!-- huddle60:begin -->";
938
+ var PR_BLOCK_END = "<!-- huddle60:end -->";
939
+ var LEGACY_PR_BLOCK_BEGIN = "<!-- ai-dlc:begin -->";
940
+ var LEGACY_PR_BLOCK_END = "<!-- ai-dlc:end -->";
941
+ function parsePrBlock(body) {
942
+ const text = (body ?? "").replace(/\r\n?/gu, "\n");
943
+ let begin = text.indexOf(PR_BLOCK_BEGIN);
944
+ let beginMarker = PR_BLOCK_BEGIN;
945
+ let endMarker = PR_BLOCK_END;
946
+ if (begin === -1) {
947
+ begin = text.indexOf(LEGACY_PR_BLOCK_BEGIN);
948
+ if (begin !== -1) {
949
+ beginMarker = LEGACY_PR_BLOCK_BEGIN;
950
+ endMarker = LEGACY_PR_BLOCK_END;
951
+ }
952
+ }
953
+ if (begin === -1) return null;
954
+ if (text.indexOf(beginMarker, begin + 1) !== -1) {
955
+ throw new AiDlcError("E_PR_BLOCK", "PR body contains more than one huddle60 block.");
956
+ }
957
+ const end = text.indexOf(endMarker, begin);
958
+ if (end === -1) throw new AiDlcError("E_PR_BLOCK", "huddle60 block is not closed.");
959
+ const fields = /* @__PURE__ */ new Map();
960
+ for (const line of text.slice(begin + beginMarker.length, end).split("\n")) {
961
+ const match = /^(?:HUDDLE60|AI-DLC)-([A-Za-z]+):\s*(.*)$/iu.exec(line.trim());
962
+ if (match) fields.set(match[1].toLowerCase(), match[2].trim());
963
+ }
964
+ const issue = /^#(\d+)$/u.exec(fields.get("issue") ?? "");
965
+ const baselineId = fields.get("baseline") ?? "";
966
+ const hash = fields.get("hash") ?? "";
967
+ if (!issue)
968
+ throw new AiDlcError(
969
+ "E_PR_BLOCK",
970
+ "HUDDLE60-Issue must be `#<number>` in the same repository."
971
+ );
972
+ if (!/^bl-\d+-[0-9a-f]{12}$/u.test(baselineId))
973
+ throw new AiDlcError("E_PR_BLOCK", "HUDDLE60-Baseline is missing or invalid.");
974
+ if (!/^sha256:[0-9a-f]{64}$/u.test(hash))
975
+ throw new AiDlcError("E_PR_BLOCK", "HUDDLE60-Hash is missing or invalid.");
976
+ const acceptanceCriteria = (fields.get("ac") ?? "").split(/[\s,]+/u).filter((value) => value.length > 0);
977
+ for (const id of acceptanceCriteria) {
978
+ if (!/^AC-\d{2,3}$/u.test(id))
979
+ throw new AiDlcError("E_PR_BLOCK", `Invalid AC id \`${id}\` in HUDDLE60-AC.`);
980
+ }
981
+ return { issueNumber: Number(issue[1]), baselineId, hash, acceptanceCriteria };
982
+ }
983
+ function renderPrBlock(block) {
984
+ return [
985
+ PR_BLOCK_BEGIN,
986
+ `HUDDLE60-Issue: #${block.issueNumber}`,
987
+ `HUDDLE60-Baseline: ${block.baselineId}`,
988
+ `HUDDLE60-Hash: ${block.hash}`,
989
+ `HUDDLE60-AC: ${block.acceptanceCriteria.join(", ")}`,
990
+ PR_BLOCK_END
991
+ ].join("\n");
992
+ }
993
+ function closingReferences(body) {
994
+ const pattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+#(\d+)\b/giu;
995
+ return [...new Set([...(body ?? "").matchAll(pattern)].map((match) => Number(match[1])))];
996
+ }
997
+
998
+ // src/core/plan.ts
999
+ import { z as z2 } from "zod";
1000
+ var LOCAL_ID = /^[a-z0-9][a-z0-9-]{0,47}$/u;
1001
+ var Text = (max) => z2.string().trim().min(1).max(max);
1002
+ var CriterionSchema = z2.object({
1003
+ id: z2.string().regex(/^(AC|EC)-\d{2,3}$/u),
1004
+ text: Text(500)
1005
+ });
1006
+ var NodeSchema = z2.object({
1007
+ localId: z2.string().regex(LOCAL_ID),
1008
+ kind: z2.enum(["epic", "story", "task"]),
1009
+ parentLocalId: z2.string().regex(LOCAL_ID).nullable(),
1010
+ title: Text(120),
1011
+ goal: Text(1e3),
1012
+ scope: z2.array(Text(300)).max(15),
1013
+ outOfScope: z2.array(Text(300)).max(10),
1014
+ criteria: z2.array(CriterionSchema).max(20),
1015
+ acRefs: z2.array(z2.string().regex(/^AC-\d{2,3}$/u)).max(20),
1016
+ blockedBy: z2.array(z2.string().regex(LOCAL_ID)).max(10),
1017
+ riskNotes: z2.array(Text(300)).max(10)
1018
+ });
1019
+ var ModelPlanSchema = z2.object({
1020
+ rootKind: z2.enum(["epic", "story"]),
1021
+ summary: Text(1e3),
1022
+ assumptions: z2.array(Text(300)).max(15),
1023
+ openQuestions: z2.array(z2.object({ text: Text(300), blocking: z2.boolean() })).max(15),
1024
+ nodes: z2.array(NodeSchema).min(1).max(200)
1025
+ });
1026
+
1027
+ // src/core/render.ts
1028
+ function sanitize(text) {
1029
+ return text.replace(/<!--/gu, "&lt;!--").replace(/</gu, "&lt;").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/(?:huddle60|ai-dlc)/gmu, "\\/huddle60");
1030
+ }
1031
+ function marker(name, value) {
1032
+ return `<!-- huddle60:${name}:${value} -->`;
1033
+ }
1034
+
1035
+ // src/workflow/service.ts
1036
+ var STATUS_CONTEXT = "huddle60/baseline";
1037
+ var LANES = ["fast", "standard", "strict"];
1038
+ var WorkflowService = class {
1039
+ constructor(deps) {
1040
+ this.deps = deps;
1041
+ }
1042
+ deps;
1043
+ policyCache = null;
1044
+ get repositoryId() {
1045
+ return this.deps.github.repo.repositoryId;
1046
+ }
1047
+ async policy() {
1048
+ if (this.policyCache) return this.policyCache;
1049
+ const branch = await this.deps.github.getDefaultBranch();
1050
+ let source = await this.deps.github.readFile(POLICY_PATH, branch);
1051
+ if (source === null) {
1052
+ source = await this.deps.github.readFile(LEGACY_POLICY_PATH, branch);
1053
+ }
1054
+ if (source === null)
1055
+ throw new AiDlcError("E_POLICY_INVALID", `Missing ${POLICY_PATH} on ${branch}.`);
1056
+ this.policyCache = parsePolicy(source);
1057
+ return this.policyCache;
1058
+ }
1059
+ /** Entry point for `issue_comment` events on Issues. */
1060
+ async handleComment(issueNumber, commentId, action) {
1061
+ if (!this.deps.enabled) return;
1062
+ if (action !== "created") {
1063
+ await this.reportInvalidatedDecisions(issueNumber, commentId, action);
1064
+ return;
1065
+ }
1066
+ const comment = await this.deps.github.getComment(commentId);
1067
+ if (!comment || comment.author.type !== "User") return;
1068
+ let command;
1069
+ try {
1070
+ command = parseCommand(comment.body);
1071
+ } catch (error) {
1072
+ if (isAiDlcError(error)) await this.reply(issueNumber, commentId, errorReply(error));
1073
+ return;
1074
+ }
1075
+ if (commandHandler(command.name) !== "deterministic") return;
1076
+ try {
1077
+ const policy = await this.policy();
1078
+ const issue = await this.deps.github.getIssue(issueNumber);
1079
+ if (issue.isPullRequest)
1080
+ throw new AiDlcError("E_SYNTAX", "AI-DLC commands are only accepted on Issues.");
1081
+ const message = await this.runCommand(command, issue, comment, policy);
1082
+ await this.reply(issueNumber, commentId, message);
1083
+ } catch (error) {
1084
+ if (!isAiDlcError(error)) throw error;
1085
+ await this.reply(issueNumber, commentId, errorReply(error));
1086
+ }
1087
+ }
1088
+ async reply(issueNumber, commentId, body) {
1089
+ await this.deps.github.upsertComment(issueNumber, marker("reply", String(commentId)), body);
1090
+ }
1091
+ async runCommand(command, issue, comment, policy) {
1092
+ const actor = comment.author;
1093
+ switch (command.name) {
1094
+ case "baseline":
1095
+ this.requireRole(policy, actor.id, ["po", "dev"], "baseline");
1096
+ return this.createBaseline(issue);
1097
+ case "approve":
1098
+ case "ready":
1099
+ case "accept":
1100
+ this.requireRole(policy, actor.id, [REQUIRED_ROLE[command.name]], command.name);
1101
+ return this.recordDecision(command.name, command.args, issue, comment, policy);
1102
+ case "revoke":
1103
+ return this.revoke(command.args, issue, comment, policy);
1104
+ default:
1105
+ throw new AiDlcError(
1106
+ "E_SYNTAX",
1107
+ `\`${command.name}\` is handled by the planner, not this workflow.`
1108
+ );
1109
+ }
1110
+ }
1111
+ requireRole(policy, userId, roles, action) {
1112
+ if (!roles.some((role) => hasRole(policy, userId, role))) {
1113
+ throw new AiDlcError("E_ROLE", `\`${action}\` requires the ${roles.join(" or ")} role.`);
1114
+ }
1115
+ }
1116
+ snapshotOf(issue) {
1117
+ return buildBaseline({
1118
+ repositoryId: this.repositoryId,
1119
+ issueNodeId: issue.nodeId,
1120
+ number: issue.number,
1121
+ title: issue.title,
1122
+ body: issue.body,
1123
+ labels: issue.labels
1124
+ });
1125
+ }
1126
+ async createBaseline(issue) {
1127
+ const snapshot = this.snapshotOf(issue);
1128
+ const result = await this.deps.evidence.write(
1129
+ baselinePath(this.repositoryId, issue.number, snapshot.baselineId),
1130
+ snapshot
1131
+ );
1132
+ const criteria = snapshot.requirements.criteria.map(
1133
+ (criterion) => `- ${criterion.id}: ${sanitize(criterion.text)}`
1134
+ );
1135
+ return [
1136
+ `**Baseline \`${snapshot.baselineId}\`** ${result === "created" ? "\u0111\xE3 \u0111\u01B0\u1EE3c t\u1EA1o" : "\u0111\xE3 t\u1ED3n t\u1EA1i v\u1EDBi c\xF9ng n\u1ED9i dung"}.`,
1137
+ "",
1138
+ `Hash: \`${snapshot.contentHash}\``,
1139
+ ...criteria.length > 0 ? ["", "Ti\xEAu ch\xED:", ...criteria] : [],
1140
+ "",
1141
+ "PO duy\u1EC7t nghi\u1EC7p v\u1EE5:",
1142
+ "```text",
1143
+ `/huddle60 approve baseline=${snapshot.baselineId} hash=${snapshot.contentHash}`,
1144
+ "```",
1145
+ "DEV x\xE1c nh\u1EADn s\u1EB5n s\xE0ng (lane: fast | standard | strict):",
1146
+ "```text",
1147
+ `/huddle60 ready baseline=${snapshot.baselineId} hash=${snapshot.contentHash} lane=standard`,
1148
+ "```"
1149
+ ].join("\n");
1150
+ }
1151
+ async loadSnapshot(issueNumber, baselineId) {
1152
+ if (!baselineId.startsWith(`bl-${issueNumber}-`)) {
1153
+ throw new AiDlcError(
1154
+ "E_BASELINE_NOT_FOUND",
1155
+ `Baseline \`${baselineId}\` does not belong to #${issueNumber}.`
1156
+ );
1157
+ }
1158
+ const snapshot = await this.deps.evidence.read(
1159
+ baselinePath(this.repositoryId, issueNumber, baselineId)
1160
+ );
1161
+ if (!snapshot || !verifySnapshot(snapshot)) {
1162
+ throw new AiDlcError(
1163
+ "E_BASELINE_NOT_FOUND",
1164
+ `Baseline \`${baselineId}\` is missing or corrupt.`
1165
+ );
1166
+ }
1167
+ return snapshot;
1168
+ }
1169
+ /**
1170
+ * Loads the snapshot for a decision. When it was never stored but the id and hash
1171
+ * match the Issue's current content, it is stored now, so one command suffices.
1172
+ */
1173
+ async snapshotForDecision(issue, baselineId, hash) {
1174
+ const stored = await this.deps.evidence.read(
1175
+ baselinePath(this.repositoryId, issue.number, baselineId)
1176
+ );
1177
+ if (stored || hash === void 0) return this.loadSnapshot(issue.number, baselineId);
1178
+ const current = this.snapshotOf(issue);
1179
+ if (current.baselineId !== baselineId || current.contentHash !== hash) {
1180
+ return this.loadSnapshot(issue.number, baselineId);
1181
+ }
1182
+ await this.deps.evidence.write(
1183
+ baselinePath(this.repositoryId, issue.number, current.baselineId),
1184
+ current
1185
+ );
1186
+ return current;
1187
+ }
1188
+ /** Throws when the Issue no longer matches the baseline it is being decided on. */
1189
+ assertCurrent(issue, snapshot) {
1190
+ if (this.snapshotOf(issue).contentHash !== snapshot.contentHash) {
1191
+ throw new AiDlcError(
1192
+ "E_STALE",
1193
+ "The Issue changed after this baseline was created. Run `/huddle60 baseline` again and decide on the new baseline."
1194
+ );
1195
+ }
1196
+ }
1197
+ async recordDecision(kind, args, issue, comment, policy) {
1198
+ const { github, evidence, clock } = this.deps;
1199
+ const snapshot = kind === "accept" ? await this.loadSnapshot(issue.number, args.baseline) : await this.snapshotForDecision(issue, args.baseline, args.hash);
1200
+ let lane = null;
1201
+ let build = null;
1202
+ if (kind === "approve" || kind === "ready") {
1203
+ if (args.hash !== snapshot.contentHash) {
1204
+ throw new AiDlcError(
1205
+ "E_HASH_MISMATCH",
1206
+ "The hash does not match the baseline. Copy it from the baseline reply."
1207
+ );
1208
+ }
1209
+ this.assertCurrent(issue, snapshot);
1210
+ }
1211
+ if (kind === "ready") {
1212
+ if (!LANES.includes(args.lane)) {
1213
+ throw new AiDlcError("E_SYNTAX", `Lane must be one of ${LANES.join(", ")}.`);
1214
+ }
1215
+ lane = args.lane;
1216
+ await this.assertApproved(snapshot, lane, policy);
1217
+ const open = [];
1218
+ for (const dependency of snapshot.blockedBy) {
1219
+ const blocker = await github.getIssue(dependency);
1220
+ if (blocker.state !== "closed" || blocker.stateReason !== "completed")
1221
+ open.push(dependency);
1222
+ }
1223
+ if (open.length > 0) {
1224
+ throw new AiDlcError(
1225
+ "E_DEPENDENCY_OPEN",
1226
+ `Blocked by unfinished Issue(s): ${open.map((n) => `#${n}`).join(", ")}.`
1227
+ );
1228
+ }
1229
+ }
1230
+ if (kind === "accept") {
1231
+ if (snapshot.kind === "task") {
1232
+ throw new AiDlcError(
1233
+ "E_WRONG_KIND",
1234
+ "PO accepts stories and epics. A task is done when its PR is merged and its story is accepted."
1235
+ );
1236
+ }
1237
+ const ready = await this.findValidDecision(
1238
+ issue.number,
1239
+ snapshot.baselineId,
1240
+ "ready",
1241
+ policy
1242
+ );
1243
+ if (!ready)
1244
+ throw new AiDlcError("E_NOT_READY", "There is no valid ready decision for this baseline.");
1245
+ build = await this.verifyBuild(Number(args.run), args.commit, policy);
1246
+ }
1247
+ const receipt = {
1248
+ schemaVersion: DECISION_SCHEMA_VERSION,
1249
+ decisionId: decisionId(kind, comment.id),
1250
+ kind,
1251
+ issueNumber: issue.number,
1252
+ baselineId: snapshot.baselineId,
1253
+ baselineHash: snapshot.contentHash,
1254
+ actor: { id: comment.author.id, login: comment.author.login },
1255
+ source: { commentId: comment.id, commentBodyDigest: commentDigest(comment.body) },
1256
+ lane,
1257
+ build,
1258
+ policyVersion: policy.version,
1259
+ recordedAt: clock.now().toISOString()
1260
+ };
1261
+ await evidence.write(
1262
+ decisionPath(this.repositoryId, issue.number, receipt.decisionId),
1263
+ receipt
1264
+ );
1265
+ if (kind === "accept") {
1266
+ await github.closeIssue(issue.number, "completed");
1267
+ return `**\u0110\xE3 ghi nh\u1EADn nghi\u1EC7m thu** \`${receipt.decisionId}\` cho \`${snapshot.baselineId}\` tr\xEAn build ${build?.runId} (\`${build?.headSha.slice(0, 7)}\`). Issue \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\xF3ng.`;
1268
+ }
1269
+ if (kind === "ready") {
1270
+ const block = renderPrBlock({
1271
+ issueNumber: issue.number,
1272
+ baselineId: snapshot.baselineId,
1273
+ hash: snapshot.contentHash,
1274
+ acceptanceCriteria: snapshot.kind === "task" ? snapshot.acRefs : snapshot.requirements.criteria.map((c) => c.id)
1275
+ });
1276
+ return [
1277
+ `**S\u1EB5n s\xE0ng tri\u1EC3n khai** \`${receipt.decisionId}\` (lane \`${lane}\`).`,
1278
+ "",
1279
+ "D\xE1n kh\u1ED1i sau v\xE0o m\xF4 t\u1EA3 Pull Request:",
1280
+ "```text",
1281
+ block,
1282
+ "```"
1283
+ ].join("\n");
1284
+ }
1285
+ return `**\u0110\xE3 duy\u1EC7t nghi\u1EC7p v\u1EE5** \`${receipt.decisionId}\` cho \`${snapshot.baselineId}\`. DEV c\xF3 th\u1EC3 x\xE1c nh\u1EADn \`ready\`.`;
1286
+ }
1287
+ async verifyBuild(runId, commit, policy) {
1288
+ if (!Number.isSafeInteger(runId) || runId <= 0)
1289
+ throw new AiDlcError("E_SYNTAX", "`run` must be a workflow run id.");
1290
+ const problems = await this.buildProblems(runId, commit, policy);
1291
+ if (problems.problems.length > 0 || !problems.run) {
1292
+ throw new AiDlcError(
1293
+ "E_BUILD_INVALID",
1294
+ `Build cannot be accepted:
1295
+ - ${problems.problems.join("\n- ")}`
1296
+ );
1297
+ }
1298
+ const { run } = problems;
1299
+ return {
1300
+ runId: run.id,
1301
+ runAttempt: run.runAttempt,
1302
+ workflowPath: run.path,
1303
+ headSha: run.headSha
1304
+ };
1305
+ }
1306
+ async buildProblems(runId, commit, policy) {
1307
+ const run = await this.deps.github.getWorkflowRun(runId);
1308
+ const problems = [];
1309
+ if (!run) return { run: null, problems: [`Workflow run ${runId} was not found.`] };
1310
+ const branch = await this.deps.github.getDefaultBranch();
1311
+ if (run.repositoryId !== this.repositoryId) problems.push("run belongs to another repository");
1312
+ if (!policy.acceptance.build_workflows.includes(run.path))
1313
+ problems.push(`workflow \`${run.path}\` is not an acceptance workflow`);
1314
+ if (run.event !== "push" || run.headBranch !== branch)
1315
+ problems.push(`run must come from a push to \`${branch}\``);
1316
+ if (run.headSha !== commit) problems.push("run commit does not match `commit`");
1317
+ if (run.status !== "completed" || run.conclusion !== "success")
1318
+ problems.push("run did not complete successfully");
1319
+ return { run, problems };
1320
+ }
1321
+ async revoke(args, issue, comment, policy) {
1322
+ const id = args.decision;
1323
+ const receipt = await this.deps.evidence.read(
1324
+ decisionPath(this.repositoryId, issue.number, id)
1325
+ );
1326
+ if (!receipt)
1327
+ throw new AiDlcError(
1328
+ "E_DECISION_NOT_FOUND",
1329
+ `Decision \`${id}\` was not found on this Issue.`
1330
+ );
1331
+ const actorId = comment.author.id;
1332
+ const allowed = receipt.actor.id === actorId || hasRole(policy, actorId, REQUIRED_ROLE[receipt.kind]);
1333
+ if (!allowed)
1334
+ throw new AiDlcError(
1335
+ "E_ROLE",
1336
+ "Only the author or a holder of the same role may revoke this decision."
1337
+ );
1338
+ const revocation = {
1339
+ schemaVersion: 1,
1340
+ decisionId: id,
1341
+ actor: { id: actorId, login: comment.author.login },
1342
+ reason: args.reason,
1343
+ source: { commentId: comment.id },
1344
+ recordedAt: this.deps.clock.now().toISOString()
1345
+ };
1346
+ const result = await this.deps.evidence.write(
1347
+ revocationPath(this.repositoryId, issue.number, id),
1348
+ revocation
1349
+ );
1350
+ return result === "created" ? `**\u0110\xE3 thu h\u1ED3i** \`${id}\`.` : `\`${id}\` \u0111\xE3 b\u1ECB thu h\u1ED3i tr\u01B0\u1EDBc \u0111\xF3.`;
1351
+ }
1352
+ async listDecisions(issueNumber) {
1353
+ const paths = await this.deps.evidence.list(`decisions/${this.repositoryId}/${issueNumber}/`);
1354
+ const receipts = [];
1355
+ for (const path of paths) {
1356
+ const receipt = await this.deps.evidence.read(path);
1357
+ if (receipt) receipts.push(receipt);
1358
+ }
1359
+ return receipts;
1360
+ }
1361
+ async isValid(receipt, policy) {
1362
+ const { evidence, github } = this.deps;
1363
+ const revoked = await evidence.read(
1364
+ revocationPath(this.repositoryId, receipt.issueNumber, receipt.decisionId)
1365
+ ) !== null;
1366
+ const comment = await github.getComment(receipt.source.commentId);
1367
+ const snapshot = await evidence.read(
1368
+ baselinePath(this.repositoryId, receipt.issueNumber, receipt.baselineId)
1369
+ );
1370
+ const snapshotHash = snapshot && verifySnapshot(snapshot) ? snapshot.contentHash : null;
1371
+ let buildValid;
1372
+ if (receipt.kind === "accept" && receipt.build) {
1373
+ const { problems } = await this.buildProblems(
1374
+ receipt.build.runId,
1375
+ receipt.build.headSha,
1376
+ policy
1377
+ );
1378
+ buildValid = problems.length === 0;
1379
+ }
1380
+ return evaluateReceipt(receipt, {
1381
+ policy,
1382
+ revoked,
1383
+ currentCommentBody: comment ? comment.body : null,
1384
+ snapshotHash,
1385
+ ...buildValid === void 0 ? {} : { buildValid }
1386
+ });
1387
+ }
1388
+ async findValidDecision(issueNumber, baselineId, kind, policy) {
1389
+ for (const receipt of await this.listDecisions(issueNumber)) {
1390
+ if (receipt.kind !== kind || receipt.baselineId !== baselineId) continue;
1391
+ if ((await this.isValid(receipt, policy)).valid) return receipt;
1392
+ }
1393
+ return null;
1394
+ }
1395
+ async reportInvalidatedDecisions(issueNumber, commentId, action) {
1396
+ const affected = (await this.listDecisions(issueNumber)).filter(
1397
+ (receipt) => receipt.source.commentId === commentId
1398
+ );
1399
+ if (affected.length === 0) return;
1400
+ await this.deps.github.upsertComment(
1401
+ issueNumber,
1402
+ marker("invalidated", String(commentId)),
1403
+ `Comment quy\u1EBFt \u0111\u1ECBnh \u0111\xE3 b\u1ECB ${action === "deleted" ? "x\xF3a" : "s\u1EEDa"}; c\xE1c quy\u1EBFt \u0111\u1ECBnh sau kh\xF4ng c\xF2n hi\u1EC7u l\u1EF1c: ${affected.map((receipt) => `\`${receipt.decisionId}\` (${receipt.kind})`).join(", ")}.`
1404
+ );
1405
+ }
1406
+ /** Evaluates the `ai-dlc/baseline` gate for a pull request and publishes a commit status. */
1407
+ async checkPullRequest(prNumber) {
1408
+ const pr = await this.deps.github.getPullRequest(prNumber);
1409
+ const result = this.deps.enabled ? await this.evaluatePullRequest(prNumber).catch((error) => {
1410
+ if (!isAiDlcError(error)) throw error;
1411
+ return {
1412
+ state: "failure",
1413
+ description: `${error.code}: ${firstLine(error.message)}`,
1414
+ details: [error.message]
1415
+ };
1416
+ }) : {
1417
+ state: "pending",
1418
+ description: "huddle60 is disabled by the operator",
1419
+ details: []
1420
+ };
1421
+ await this.deps.github.setCommitStatus(pr.headSha, {
1422
+ state: result.state,
1423
+ context: STATUS_CONTEXT,
1424
+ description: truncate(result.description, 140)
1425
+ });
1426
+ return result;
1427
+ }
1428
+ async evaluatePullRequest(prNumber) {
1429
+ const { github } = this.deps;
1430
+ const policy = await this.policy();
1431
+ const pr = await github.getPullRequest(prNumber);
1432
+ for (const reference of closingReferences(pr.body)) {
1433
+ const referenced = await github.getIssue(reference).catch(() => null);
1434
+ const kind = referenced ? kindFromLabels(referenced.labels) : null;
1435
+ if (kind === "story" || kind === "epic") {
1436
+ throw new AiDlcError(
1437
+ "E_CLOSING_KEYWORD",
1438
+ `Use \`Refs #${reference}\`; closing keywords would close the ${kind} without acceptance.`
1439
+ );
1440
+ }
1441
+ }
1442
+ if (pr.labels.includes(LABELS.exempt)) {
1443
+ return { state: "success", description: "Exempt by maintainer label", details: [] };
1444
+ }
1445
+ const block = parsePrBlock(pr.body);
1446
+ if (!block)
1447
+ throw new AiDlcError(
1448
+ "E_PR_BLOCK",
1449
+ "PR body has no huddle60 block. Run the context CLI or use the ready reply."
1450
+ );
1451
+ const issue = await github.getIssue(block.issueNumber);
1452
+ const snapshot = await this.loadSnapshot(issue.number, block.baselineId);
1453
+ if (snapshot.contentHash !== block.hash)
1454
+ throw new AiDlcError("E_HASH_MISMATCH", "HUDDLE60-Hash does not match the baseline.");
1455
+ if (snapshot.kind === "epic")
1456
+ throw new AiDlcError("E_PR_BLOCK", "A PR must reference a story or task, not an epic.");
1457
+ this.assertCurrent(issue, snapshot);
1458
+ const ready = await this.findValidDecision(issue.number, snapshot.baselineId, "ready", policy);
1459
+ if (!ready || !ready.lane)
1460
+ throw new AiDlcError(
1461
+ "E_NOT_READY",
1462
+ `#${issue.number} has no valid ready decision for this baseline.`
1463
+ );
1464
+ await this.assertApproved(snapshot, ready.lane, policy);
1465
+ const allowedAc = new Set(
1466
+ snapshot.kind === "task" ? snapshot.acRefs : snapshot.requirements.criteria.map((c) => c.id)
1467
+ );
1468
+ const unknown = block.acceptanceCriteria.filter((id) => !allowedAc.has(id));
1469
+ if (unknown.length > 0)
1470
+ throw new AiDlcError("E_PR_BLOCK", `Unknown AC for this baseline: ${unknown.join(", ")}.`);
1471
+ if (snapshot.kind === "task" && snapshot.parent !== null)
1472
+ await this.assertParentReady(snapshot.parent, policy);
1473
+ if (ready.lane === "fast") {
1474
+ const sensitive = (await github.listPullRequestFiles(prNumber)).filter(
1475
+ (path) => isSensitive(policy, path)
1476
+ );
1477
+ if (sensitive.length > 0) {
1478
+ throw new AiDlcError(
1479
+ "E_SENSITIVE_PATH",
1480
+ `Lane fast cannot change sensitive paths: ${sensitive.slice(0, 5).join(", ")}.`
1481
+ );
1482
+ }
1483
+ }
1484
+ return {
1485
+ state: "success",
1486
+ description: `${snapshot.baselineId} ready (${ready.lane})`,
1487
+ details: []
1488
+ };
1489
+ }
1490
+ /** PO approves stories; a task inherits the approval of its parent story's current content. */
1491
+ async assertApproved(snapshot, lane, policy) {
1492
+ if (!policy.lanes[lane].requires_po_approval) return;
1493
+ if (snapshot.kind === "task") {
1494
+ if (snapshot.parent === null) {
1495
+ throw new AiDlcError("E_FORM", "A task needs a parent story in `### Issue cha`.");
1496
+ }
1497
+ if (!await this.parentDecision(snapshot.parent, "approve", policy)) {
1498
+ throw new AiDlcError(
1499
+ "E_NOT_APPROVED",
1500
+ `Lane \`${lane}\` requires a valid PO approval of parent story #${snapshot.parent} for its current content.`
1501
+ );
1502
+ }
1503
+ return;
1504
+ }
1505
+ if (!await this.findValidDecision(
1506
+ snapshot.source.issueNumber,
1507
+ snapshot.baselineId,
1508
+ "approve",
1509
+ policy
1510
+ )) {
1511
+ throw new AiDlcError(
1512
+ "E_NOT_APPROVED",
1513
+ `Lane \`${lane}\` requires a valid PO approval for this baseline.`
1514
+ );
1515
+ }
1516
+ }
1517
+ async parentDecision(parentNumber, kind, policy) {
1518
+ const parent = await this.deps.github.getIssue(parentNumber);
1519
+ if (kindFromLabels(parent.labels) !== "story") {
1520
+ throw new AiDlcError("E_WRONG_KIND", `Parent #${parentNumber} of a task must be a story.`);
1521
+ }
1522
+ const current = this.snapshotOf(parent);
1523
+ return this.findValidDecision(parentNumber, current.baselineId, kind, policy);
1524
+ }
1525
+ async assertParentReady(parentNumber, policy) {
1526
+ if (!await this.parentDecision(parentNumber, "ready", policy)) {
1527
+ throw new AiDlcError(
1528
+ "E_NOT_READY",
1529
+ `Parent story #${parentNumber} has no valid ready decision for its current content.`
1530
+ );
1531
+ }
1532
+ }
1533
+ };
1534
+ function errorReply(error) {
1535
+ return [`**huddle60: \`${error.code}\`**`, "", sanitize(error.message)].join("\n");
1536
+ }
1537
+ function firstLine(text) {
1538
+ return text.split("\n")[0] ?? "";
1539
+ }
1540
+ function truncate(text, max) {
1541
+ return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
1542
+ }
1543
+
1544
+ // src/entry/action.ts
1545
+ async function main() {
1546
+ const eventName = requireEnv("GITHUB_EVENT_NAME");
1547
+ const payload = JSON.parse(await readFile(requireEnv("GITHUB_EVENT_PATH"), "utf8"));
1548
+ const [owner, repo] = requireEnv("GITHUB_REPOSITORY").split("/");
1549
+ const logger = createJsonLogger({ component: "huddle60-action", event: eventName });
1550
+ const token = process.env.HUDDLE60_TOKEN || process.env.AI_DLC_TOKEN || requireEnv("HUDDLE60_TOKEN or AI_DLC_TOKEN");
1551
+ const octokit = octokitFromToken(token);
1552
+ const github = await connectRepository(octokit, owner, repo);
1553
+ let policySource = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
1554
+ if (!policySource) {
1555
+ policySource = await github.readFile(LEGACY_POLICY_PATH, await github.getDefaultBranch());
1556
+ }
1557
+ const branch = policySource ? parsePolicy(policySource).evidence.branch : "huddle60-evidence";
1558
+ const workflow = new WorkflowService({
1559
+ github,
1560
+ evidence: new GitBranchEvidenceStore(octokit, owner, repo, branch),
1561
+ clock: systemClock,
1562
+ logger,
1563
+ enabled: process.env.HUDDLE60_ENABLED !== "false" && process.env.AI_DLC_ENABLED !== "false"
1564
+ });
1565
+ if (eventName === "issue_comment") {
1566
+ if (!payload.issue || payload.issue.pull_request || !payload.comment || payload.sender?.type === "Bot")
1567
+ return;
1568
+ const action = payload.action;
1569
+ await workflow.handleComment(payload.issue.number, payload.comment.id, action);
1570
+ return;
1571
+ }
1572
+ if (eventName === "pull_request_target" && payload.pull_request) {
1573
+ const result = await workflow.checkPullRequest(payload.pull_request.number);
1574
+ await summary([
1575
+ `### huddle60/baseline: ${result.state}`,
1576
+ "",
1577
+ result.description,
1578
+ ...result.details.map((line) => `> ${line}`)
1579
+ ]);
1580
+ return;
1581
+ }
1582
+ logger.info("event ignored");
1583
+ }
1584
+ function requireEnv(name) {
1585
+ const value = process.env[name];
1586
+ if (!value) throw new Error(`Missing environment variable ${name}`);
1587
+ return value;
1588
+ }
1589
+ async function summary(lines) {
1590
+ const file = process.env.GITHUB_STEP_SUMMARY;
1591
+ if (file) await appendFile(file, `${lines.join("\n")}
1592
+ `);
1593
+ }
1594
+ main().catch((error) => {
1595
+ process.stderr.write(
1596
+ `ai-dlc action failed: ${error instanceof Error ? error.stack : String(error)}
1597
+ `
1598
+ );
1599
+ process.exitCode = 1;
1600
+ });
1601
+ //# sourceMappingURL=action.js.map