huddle60 0.1.0 → 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/README.md +50 -67
- package/dist/action.js +87 -49
- package/dist/action.js.map +2 -2
- package/dist/cli.js +220 -80
- package/dist/cli.js.map +4 -4
- package/package.json +6 -6
- package/templates/.github/ISSUE_TEMPLATE/request.yml +4 -4
- package/templates/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- package/templates/.github/{ai-dlc → huddle60}/policy.yml +2 -2
- package/templates/.github/workflows/{ai-dlc.yml → huddle60.yml} +5 -6
- package/templates/AGENTS.md +2 -2
- package/ui/app.js +1 -1
- package/dist/planner-server.js +0 -1912
- package/dist/planner-server.js.map +0 -7
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/entry/cli.ts
|
|
4
4
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { mkdir, readdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
7
|
+
import { dirname, join as join2, relative } from "node:path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
9
|
import { parseArgs } from "node:util";
|
|
7
10
|
|
|
8
11
|
// src/adapters/gemini.ts
|
|
@@ -129,8 +132,18 @@ import { retry } from "@octokit/plugin-retry";
|
|
|
129
132
|
import { Octokit } from "@octokit/rest";
|
|
130
133
|
var RetryingOctokit = Octokit.plugin(retry);
|
|
131
134
|
var USER_AGENT = "huddle60/0.1.0";
|
|
135
|
+
var defaultLog = process.env.AI_DLC_DEBUG ? void 0 : {
|
|
136
|
+
debug: () => {
|
|
137
|
+
},
|
|
138
|
+
info: () => {
|
|
139
|
+
},
|
|
140
|
+
warn: () => {
|
|
141
|
+
},
|
|
142
|
+
error: () => {
|
|
143
|
+
}
|
|
144
|
+
};
|
|
132
145
|
function octokitFromToken(token2) {
|
|
133
|
-
return new RetryingOctokit({ auth: token2, userAgent: USER_AGENT });
|
|
146
|
+
return new RetryingOctokit({ auth: token2, userAgent: USER_AGENT, log: defaultLog });
|
|
134
147
|
}
|
|
135
148
|
function isStatus(error, ...statuses) {
|
|
136
149
|
return typeof error === "object" && error !== null && statuses.includes(error.status ?? -1);
|
|
@@ -486,7 +499,7 @@ var GitBranchEvidenceStore = class {
|
|
|
486
499
|
});
|
|
487
500
|
const commit = await this.octokit.rest.git.createCommit({
|
|
488
501
|
...this.base,
|
|
489
|
-
message: `
|
|
502
|
+
message: `huddle60: add ${path}`,
|
|
490
503
|
tree: tree.data.sha,
|
|
491
504
|
parents: [head.sha]
|
|
492
505
|
});
|
|
@@ -549,7 +562,7 @@ var GitBranchEvidenceStore = class {
|
|
|
549
562
|
});
|
|
550
563
|
const commit = await this.octokit.rest.git.createCommit({
|
|
551
564
|
...this.base,
|
|
552
|
-
message: "
|
|
565
|
+
message: "huddle60: initialize evidence branch",
|
|
553
566
|
tree: tree.data.sha,
|
|
554
567
|
parents: []
|
|
555
568
|
});
|
|
@@ -659,7 +672,7 @@ var PolicySchema = z.object({
|
|
|
659
672
|
strict: { requires_po_approval: true }
|
|
660
673
|
}),
|
|
661
674
|
sensitive_paths: z.array(z.string().min(1)).default([]),
|
|
662
|
-
evidence: z.object({ branch: z.string().min(1).default("
|
|
675
|
+
evidence: z.object({ branch: z.string().min(1).default("huddle60-evidence") }).default({ branch: "huddle60-evidence" }),
|
|
663
676
|
acceptance: z.object({ build_workflows: z.array(z.string().min(1)).min(1) }).default({ build_workflows: [".github/workflows/build.yml"] }),
|
|
664
677
|
planner: z.object({
|
|
665
678
|
max_nodes: z.number().int().min(1).max(200).default(60),
|
|
@@ -673,7 +686,8 @@ var PolicySchema = z.object({
|
|
|
673
686
|
ctx.addIssue({ code: "custom", message: `Bot account ${bot.login} cannot hold a role` });
|
|
674
687
|
}
|
|
675
688
|
});
|
|
676
|
-
var POLICY_PATH = ".github/
|
|
689
|
+
var POLICY_PATH = ".github/huddle60/policy.yml";
|
|
690
|
+
var LEGACY_POLICY_PATH = ".github/ai-dlc/policy.yml";
|
|
677
691
|
function parsePolicy(source) {
|
|
678
692
|
let raw;
|
|
679
693
|
try {
|
|
@@ -918,13 +932,13 @@ function blockingQuestions(plan) {
|
|
|
918
932
|
|
|
919
933
|
// src/core/vocabulary.ts
|
|
920
934
|
var LABELS = {
|
|
921
|
-
request: "
|
|
922
|
-
planDraft: "
|
|
923
|
-
planCreated: "
|
|
924
|
-
planPartial: "
|
|
925
|
-
fromPlan: "
|
|
926
|
-
stale: "
|
|
927
|
-
exempt: "
|
|
935
|
+
request: "huddle60:request",
|
|
936
|
+
planDraft: "huddle60:plan-draft",
|
|
937
|
+
planCreated: "huddle60:plan-created",
|
|
938
|
+
planPartial: "huddle60:plan-partial",
|
|
939
|
+
fromPlan: "huddle60:from-plan",
|
|
940
|
+
stale: "huddle60:stale",
|
|
941
|
+
exempt: "huddle60:exempt",
|
|
928
942
|
epic: "type:epic",
|
|
929
943
|
story: "type:story",
|
|
930
944
|
task: "type:task"
|
|
@@ -987,13 +1001,13 @@ function kindFromLabels(labels) {
|
|
|
987
1001
|
|
|
988
1002
|
// src/core/render.ts
|
|
989
1003
|
function sanitize(text) {
|
|
990
|
-
return text.replace(/<!--/gu, "<!--").replace(/</gu, "<").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/ai-dlc/gmu, "\\/
|
|
1004
|
+
return text.replace(/<!--/gu, "<!--").replace(/</gu, "<").replace(/(^|[^\w`])@(?=[\w-])/gu, "$1@\u200B").replace(/^\/(?:huddle60|ai-dlc)/gmu, "\\/huddle60");
|
|
991
1005
|
}
|
|
992
1006
|
function marker(name, value) {
|
|
993
|
-
return `<!--
|
|
1007
|
+
return `<!-- huddle60:${name}:${value} -->`;
|
|
994
1008
|
}
|
|
995
1009
|
function findMarker(body, name) {
|
|
996
|
-
const match = new RegExp(`<!-- ai-dlc:${name}:([^\\s>]+) -->`, "u").exec(body ?? "");
|
|
1010
|
+
const match = new RegExp(`<!-- (?:huddle60|ai-dlc):${name}:([^\\s>]+) -->`, "u").exec(body ?? "");
|
|
997
1011
|
return match ? match[1] : null;
|
|
998
1012
|
}
|
|
999
1013
|
function planNodeMarker(planId, localId) {
|
|
@@ -1081,17 +1095,17 @@ function renderPlanComment(record) {
|
|
|
1081
1095
|
"**C\u1EA7n PO tr\u1EA3 l\u1EDDi c\xE1c c\xE2u h\u1ECFi ch\u1EB7n tr\u01B0\u1EDBc khi t\u1EA1o Issues.** Tr\u1EA3 l\u1EDDi b\u1EB1ng m\u1ED9t comment m\u1EDBi:",
|
|
1082
1096
|
"",
|
|
1083
1097
|
"```text",
|
|
1084
|
-
`/
|
|
1098
|
+
`/huddle60 refine plan=${record.planId}`,
|
|
1085
1099
|
"<c\xE2u tr\u1EA3 l\u1EDDi c\u1EE7a b\u1EA1n, m\u1ED7i \xFD m\u1ED9t d\xF2ng>",
|
|
1086
1100
|
"```"
|
|
1087
1101
|
] : [
|
|
1088
1102
|
"**N\u1EBFu \u0111\u1ED3ng \xFD, PO x\xE1c nh\u1EADn \u0111\u1EC3 App t\u1EA1o c\xE1c Issues:**",
|
|
1089
1103
|
"",
|
|
1090
1104
|
"```text",
|
|
1091
|
-
`/
|
|
1105
|
+
`/huddle60 create-plan plan=${record.planId} hash=${record.planHash}`,
|
|
1092
1106
|
"```",
|
|
1093
1107
|
"",
|
|
1094
|
-
`Mu\u1ED1n ch\u1EC9nh s\u1EEDa: \`/
|
|
1108
|
+
`Mu\u1ED1n ch\u1EC9nh s\u1EEDa: \`/huddle60 refine plan=${record.planId}\` k\xE8m y\xEAu c\u1EA7u \u1EDF c\xE1c d\xF2ng ti\u1EBFp theo.`
|
|
1095
1109
|
];
|
|
1096
1110
|
return [
|
|
1097
1111
|
`## K\u1EBF ho\u1EA1ch \u0111\u1EC1 xu\u1EA5t (revision ${record.revision})`,
|
|
@@ -1133,7 +1147,7 @@ function renderCreationSummary(record, numbers, complete, failures) {
|
|
|
1133
1147
|
...lines,
|
|
1134
1148
|
"",
|
|
1135
1149
|
...failures.length > 0 ? ["**L\u1ED7i:**", "", bulletList(failures), ""] : [],
|
|
1136
|
-
complete ? "B\u01B0\u1EDBc ti\u1EBFp theo: PO \u01B0u ti\xEAn trong Project; DEV review kh\u1EA3 thi v\xE0 chia vi\u1EC7c; ch\u1ED1t baseline khi Story s\u1EB5n s\xE0ng (`/
|
|
1150
|
+
complete ? "B\u01B0\u1EDBc ti\u1EBFp theo: PO \u01B0u ti\xEAn trong Project; DEV review kh\u1EA3 thi v\xE0 chia vi\u1EC7c; ch\u1ED1t baseline khi Story s\u1EB5n s\xE0ng (`/huddle60 baseline`)." : `Kh\xF4ng x\xF3a c\xE1c Issue \u0111\xE3 t\u1EA1o. Th\u1EED l\u1EA1i an to\xE0n b\u1EB1ng \`/huddle60 resume-plan plan=${record.planId}\`.`,
|
|
1137
1151
|
marker("plan-result", record.planId)
|
|
1138
1152
|
].join("\n");
|
|
1139
1153
|
}
|
|
@@ -1514,45 +1528,59 @@ function baselinePath(repositoryId, issueNumber, baselineId) {
|
|
|
1514
1528
|
}
|
|
1515
1529
|
|
|
1516
1530
|
// src/core/pr-block.ts
|
|
1517
|
-
var PR_BLOCK_BEGIN = "<!--
|
|
1518
|
-
var PR_BLOCK_END = "<!--
|
|
1531
|
+
var PR_BLOCK_BEGIN = "<!-- huddle60:begin -->";
|
|
1532
|
+
var PR_BLOCK_END = "<!-- huddle60:end -->";
|
|
1533
|
+
var LEGACY_PR_BLOCK_BEGIN = "<!-- ai-dlc:begin -->";
|
|
1534
|
+
var LEGACY_PR_BLOCK_END = "<!-- ai-dlc:end -->";
|
|
1519
1535
|
function parsePrBlock(body) {
|
|
1520
1536
|
const text = (body ?? "").replace(/\r\n?/gu, "\n");
|
|
1521
|
-
|
|
1537
|
+
let begin = text.indexOf(PR_BLOCK_BEGIN);
|
|
1538
|
+
let beginMarker = PR_BLOCK_BEGIN;
|
|
1539
|
+
let endMarker = PR_BLOCK_END;
|
|
1540
|
+
if (begin === -1) {
|
|
1541
|
+
begin = text.indexOf(LEGACY_PR_BLOCK_BEGIN);
|
|
1542
|
+
if (begin !== -1) {
|
|
1543
|
+
beginMarker = LEGACY_PR_BLOCK_BEGIN;
|
|
1544
|
+
endMarker = LEGACY_PR_BLOCK_END;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1522
1547
|
if (begin === -1) return null;
|
|
1523
|
-
if (text.indexOf(
|
|
1524
|
-
throw new AiDlcError("E_PR_BLOCK", "PR body contains more than one
|
|
1548
|
+
if (text.indexOf(beginMarker, begin + 1) !== -1) {
|
|
1549
|
+
throw new AiDlcError("E_PR_BLOCK", "PR body contains more than one huddle60 block.");
|
|
1525
1550
|
}
|
|
1526
|
-
const end = text.indexOf(
|
|
1527
|
-
if (end === -1) throw new AiDlcError("E_PR_BLOCK", "
|
|
1551
|
+
const end = text.indexOf(endMarker, begin);
|
|
1552
|
+
if (end === -1) throw new AiDlcError("E_PR_BLOCK", "huddle60 block is not closed.");
|
|
1528
1553
|
const fields = /* @__PURE__ */ new Map();
|
|
1529
|
-
for (const line of text.slice(begin +
|
|
1530
|
-
const match = /^AI-DLC-([A-Za-z]+):\s*(.*)$/
|
|
1554
|
+
for (const line of text.slice(begin + beginMarker.length, end).split("\n")) {
|
|
1555
|
+
const match = /^(?:HUDDLE60|AI-DLC)-([A-Za-z]+):\s*(.*)$/iu.exec(line.trim());
|
|
1531
1556
|
if (match) fields.set(match[1].toLowerCase(), match[2].trim());
|
|
1532
1557
|
}
|
|
1533
1558
|
const issue = /^#(\d+)$/u.exec(fields.get("issue") ?? "");
|
|
1534
1559
|
const baselineId = fields.get("baseline") ?? "";
|
|
1535
1560
|
const hash = fields.get("hash") ?? "";
|
|
1536
1561
|
if (!issue)
|
|
1537
|
-
throw new AiDlcError(
|
|
1562
|
+
throw new AiDlcError(
|
|
1563
|
+
"E_PR_BLOCK",
|
|
1564
|
+
"HUDDLE60-Issue must be `#<number>` in the same repository."
|
|
1565
|
+
);
|
|
1538
1566
|
if (!/^bl-\d+-[0-9a-f]{12}$/u.test(baselineId))
|
|
1539
|
-
throw new AiDlcError("E_PR_BLOCK", "
|
|
1567
|
+
throw new AiDlcError("E_PR_BLOCK", "HUDDLE60-Baseline is missing or invalid.");
|
|
1540
1568
|
if (!/^sha256:[0-9a-f]{64}$/u.test(hash))
|
|
1541
|
-
throw new AiDlcError("E_PR_BLOCK", "
|
|
1569
|
+
throw new AiDlcError("E_PR_BLOCK", "HUDDLE60-Hash is missing or invalid.");
|
|
1542
1570
|
const acceptanceCriteria = (fields.get("ac") ?? "").split(/[\s,]+/u).filter((value) => value.length > 0);
|
|
1543
1571
|
for (const id of acceptanceCriteria) {
|
|
1544
1572
|
if (!/^AC-\d{2,3}$/u.test(id))
|
|
1545
|
-
throw new AiDlcError("E_PR_BLOCK", `Invalid AC id \`${id}\` in
|
|
1573
|
+
throw new AiDlcError("E_PR_BLOCK", `Invalid AC id \`${id}\` in HUDDLE60-AC.`);
|
|
1546
1574
|
}
|
|
1547
1575
|
return { issueNumber: Number(issue[1]), baselineId, hash, acceptanceCriteria };
|
|
1548
1576
|
}
|
|
1549
1577
|
function renderPrBlock(block) {
|
|
1550
1578
|
return [
|
|
1551
1579
|
PR_BLOCK_BEGIN,
|
|
1552
|
-
`
|
|
1553
|
-
`
|
|
1554
|
-
`
|
|
1555
|
-
`
|
|
1580
|
+
`HUDDLE60-Issue: #${block.issueNumber}`,
|
|
1581
|
+
`HUDDLE60-Baseline: ${block.baselineId}`,
|
|
1582
|
+
`HUDDLE60-Hash: ${block.hash}`,
|
|
1583
|
+
`HUDDLE60-AC: ${block.acceptanceCriteria.join(", ")}`,
|
|
1556
1584
|
PR_BLOCK_END
|
|
1557
1585
|
].join("\n");
|
|
1558
1586
|
}
|
|
@@ -1587,7 +1615,7 @@ async function renderAgentContext(deps, policy, issueNumber, draft = false) {
|
|
|
1587
1615
|
|
|
1588
1616
|
${value}`);
|
|
1589
1617
|
return [
|
|
1590
|
-
`#
|
|
1618
|
+
`# huddle60 context for #${issueNumber}`,
|
|
1591
1619
|
"",
|
|
1592
1620
|
`Baseline \`${snapshot.baselineId}\` \xB7 lane \`${receipt.lane}\` \xB7 ${snapshot.kind}`,
|
|
1593
1621
|
...stale ? [
|
|
@@ -1595,7 +1623,7 @@ ${value}`);
|
|
|
1595
1623
|
"> WARNING: the Issue changed after this baseline. Stop and ask the DEV/PO before implementing."
|
|
1596
1624
|
] : [],
|
|
1597
1625
|
"",
|
|
1598
|
-
"Implement only what this baseline describes. Do not run /
|
|
1626
|
+
"Implement only what this baseline describes. Do not run /huddle60 approve, ready or accept.",
|
|
1599
1627
|
"",
|
|
1600
1628
|
`## ${snapshot.requirements.title}`,
|
|
1601
1629
|
"",
|
|
@@ -1615,12 +1643,13 @@ ${value}`);
|
|
|
1615
1643
|
}
|
|
1616
1644
|
throw new AiDlcError(
|
|
1617
1645
|
"E_NOT_READY",
|
|
1618
|
-
`#${issueNumber} has no valid ready decision. Ask a DEV to run /
|
|
1646
|
+
`#${issueNumber} has no valid ready decision. Ask a DEV to run /huddle60 ready, or use --draft.`
|
|
1619
1647
|
);
|
|
1620
1648
|
}
|
|
1621
1649
|
|
|
1622
1650
|
// src/core/command.ts
|
|
1623
|
-
var COMMAND_PREFIX = "/
|
|
1651
|
+
var COMMAND_PREFIX = "/huddle60";
|
|
1652
|
+
var LEGACY_COMMAND_PREFIX = "/ai-dlc";
|
|
1624
1653
|
var COMMAND_SPECS = {
|
|
1625
1654
|
baseline: { required: [], optional: [], handler: "deterministic" },
|
|
1626
1655
|
approve: { required: ["baseline", "hash"], optional: [], handler: "deterministic" },
|
|
@@ -1650,13 +1679,18 @@ function parseCommand(body) {
|
|
|
1650
1679
|
const normalized = body.replace(/\r\n?/gu, "\n");
|
|
1651
1680
|
const [head = "", ...rest] = normalized.split("\n");
|
|
1652
1681
|
const line = head.trim();
|
|
1653
|
-
|
|
1682
|
+
let prefix = "";
|
|
1683
|
+
if (line.startsWith(COMMAND_PREFIX)) {
|
|
1684
|
+
prefix = COMMAND_PREFIX;
|
|
1685
|
+
} else if (line.startsWith(LEGACY_COMMAND_PREFIX)) {
|
|
1686
|
+
prefix = LEGACY_COMMAND_PREFIX;
|
|
1687
|
+
} else {
|
|
1654
1688
|
throw new AiDlcError(
|
|
1655
1689
|
"E_SYNTAX",
|
|
1656
1690
|
`Commands must start with \`${COMMAND_PREFIX}\` on the first line.`
|
|
1657
1691
|
);
|
|
1658
1692
|
}
|
|
1659
|
-
const remainder = line.slice(
|
|
1693
|
+
const remainder = line.slice(prefix.length).trim();
|
|
1660
1694
|
const [rawName = "", ...argParts] = remainder.split(/\s+/u);
|
|
1661
1695
|
if (!(rawName in COMMAND_SPECS)) {
|
|
1662
1696
|
throw new AiDlcError(
|
|
@@ -1686,7 +1720,7 @@ function parseCommand(body) {
|
|
|
1686
1720
|
}
|
|
1687
1721
|
|
|
1688
1722
|
// src/workflow/service.ts
|
|
1689
|
-
var STATUS_CONTEXT = "
|
|
1723
|
+
var STATUS_CONTEXT = "huddle60/baseline";
|
|
1690
1724
|
var LANES = ["fast", "standard", "strict"];
|
|
1691
1725
|
var WorkflowService = class {
|
|
1692
1726
|
constructor(deps) {
|
|
@@ -1700,7 +1734,10 @@ var WorkflowService = class {
|
|
|
1700
1734
|
async policy() {
|
|
1701
1735
|
if (this.policyCache) return this.policyCache;
|
|
1702
1736
|
const branch = await this.deps.github.getDefaultBranch();
|
|
1703
|
-
|
|
1737
|
+
let source = await this.deps.github.readFile(POLICY_PATH, branch);
|
|
1738
|
+
if (source === null) {
|
|
1739
|
+
source = await this.deps.github.readFile(LEGACY_POLICY_PATH, branch);
|
|
1740
|
+
}
|
|
1704
1741
|
if (source === null)
|
|
1705
1742
|
throw new AiDlcError("E_POLICY_INVALID", `Missing ${POLICY_PATH} on ${branch}.`);
|
|
1706
1743
|
this.policyCache = parsePolicy(source);
|
|
@@ -1790,11 +1827,11 @@ var WorkflowService = class {
|
|
|
1790
1827
|
"",
|
|
1791
1828
|
"PO duy\u1EC7t nghi\u1EC7p v\u1EE5:",
|
|
1792
1829
|
"```text",
|
|
1793
|
-
`/
|
|
1830
|
+
`/huddle60 approve baseline=${snapshot.baselineId} hash=${snapshot.contentHash}`,
|
|
1794
1831
|
"```",
|
|
1795
1832
|
"DEV x\xE1c nh\u1EADn s\u1EB5n s\xE0ng (lane: fast | standard | strict):",
|
|
1796
1833
|
"```text",
|
|
1797
|
-
`/
|
|
1834
|
+
`/huddle60 ready baseline=${snapshot.baselineId} hash=${snapshot.contentHash} lane=standard`,
|
|
1798
1835
|
"```"
|
|
1799
1836
|
].join("\n");
|
|
1800
1837
|
}
|
|
@@ -1840,7 +1877,7 @@ var WorkflowService = class {
|
|
|
1840
1877
|
if (this.snapshotOf(issue).contentHash !== snapshot.contentHash) {
|
|
1841
1878
|
throw new AiDlcError(
|
|
1842
1879
|
"E_STALE",
|
|
1843
|
-
"The Issue changed after this baseline was created. Run `/
|
|
1880
|
+
"The Issue changed after this baseline was created. Run `/huddle60 baseline` again and decide on the new baseline."
|
|
1844
1881
|
);
|
|
1845
1882
|
}
|
|
1846
1883
|
}
|
|
@@ -2065,7 +2102,7 @@ var WorkflowService = class {
|
|
|
2065
2102
|
};
|
|
2066
2103
|
}) : {
|
|
2067
2104
|
state: "pending",
|
|
2068
|
-
description: "
|
|
2105
|
+
description: "huddle60 is disabled by the operator",
|
|
2069
2106
|
details: []
|
|
2070
2107
|
};
|
|
2071
2108
|
await this.deps.github.setCommitStatus(pr.headSha, {
|
|
@@ -2096,12 +2133,12 @@ var WorkflowService = class {
|
|
|
2096
2133
|
if (!block)
|
|
2097
2134
|
throw new AiDlcError(
|
|
2098
2135
|
"E_PR_BLOCK",
|
|
2099
|
-
"PR body has no
|
|
2136
|
+
"PR body has no huddle60 block. Run the context CLI or use the ready reply."
|
|
2100
2137
|
);
|
|
2101
2138
|
const issue = await github.getIssue(block.issueNumber);
|
|
2102
2139
|
const snapshot = await this.loadSnapshot(issue.number, block.baselineId);
|
|
2103
2140
|
if (snapshot.contentHash !== block.hash)
|
|
2104
|
-
throw new AiDlcError("E_HASH_MISMATCH", "
|
|
2141
|
+
throw new AiDlcError("E_HASH_MISMATCH", "HUDDLE60-Hash does not match the baseline.");
|
|
2105
2142
|
if (snapshot.kind === "epic")
|
|
2106
2143
|
throw new AiDlcError("E_PR_BLOCK", "A PR must reference a story or task, not an epic.");
|
|
2107
2144
|
this.assertCurrent(issue, snapshot);
|
|
@@ -2182,7 +2219,7 @@ var WorkflowService = class {
|
|
|
2182
2219
|
}
|
|
2183
2220
|
};
|
|
2184
2221
|
function errorReply(error) {
|
|
2185
|
-
return [`**
|
|
2222
|
+
return [`**huddle60: \`${error.code}\`**`, "", sanitize(error.message)].join("\n");
|
|
2186
2223
|
}
|
|
2187
2224
|
function firstLine(text) {
|
|
2188
2225
|
return text.split("\n")[0] ?? "";
|
|
@@ -2195,13 +2232,13 @@ function truncate(text, max) {
|
|
|
2195
2232
|
var LANES2 = ["fast", "standard", "strict"];
|
|
2196
2233
|
var MAX_DRAFTS = 50;
|
|
2197
2234
|
var LABEL_COLORS = {
|
|
2198
|
-
[LABELS.request]: { color: "5319e7", description: "
|
|
2199
|
-
[LABELS.planDraft]: { color: "c5def5", description: "
|
|
2200
|
-
[LABELS.planCreated]: { color: "0e8a16", description: "
|
|
2201
|
-
[LABELS.planPartial]: { color: "d93f0b", description: "
|
|
2202
|
-
[LABELS.fromPlan]: { color: "bfdadc", description: "
|
|
2203
|
-
[LABELS.stale]: { color: "fbca04", description: "
|
|
2204
|
-
[LABELS.exempt]: { color: "ededed", description: "
|
|
2235
|
+
[LABELS.request]: { color: "5319e7", description: "huddle60: product request from a PO" },
|
|
2236
|
+
[LABELS.planDraft]: { color: "c5def5", description: "huddle60: plan proposed, not created" },
|
|
2237
|
+
[LABELS.planCreated]: { color: "0e8a16", description: "huddle60: Issues created from the plan" },
|
|
2238
|
+
[LABELS.planPartial]: { color: "d93f0b", description: "huddle60: Issue creation incomplete" },
|
|
2239
|
+
[LABELS.fromPlan]: { color: "bfdadc", description: "huddle60: created from a confirmed plan" },
|
|
2240
|
+
[LABELS.stale]: { color: "fbca04", description: "huddle60: content changed after decisions" },
|
|
2241
|
+
[LABELS.exempt]: { color: "ededed", description: "huddle60: PR exempt (maintainer only)" },
|
|
2205
2242
|
[LABELS.epic]: { color: "3e4b9e", description: "Epic" },
|
|
2206
2243
|
[LABELS.story]: { color: "1d76db", description: "User story" },
|
|
2207
2244
|
[LABELS.task]: { color: "0052cc", description: "Technical task" }
|
|
@@ -2501,13 +2538,13 @@ ${sanitize(draft.request)}
|
|
|
2501
2538
|
let command;
|
|
2502
2539
|
switch (input.action) {
|
|
2503
2540
|
case "approve":
|
|
2504
|
-
command = `/
|
|
2541
|
+
command = `/huddle60 approve ${target} hash=${snapshot.contentHash}`;
|
|
2505
2542
|
break;
|
|
2506
2543
|
case "ready": {
|
|
2507
2544
|
const lane = input.lane;
|
|
2508
2545
|
if (!LANES2.includes(lane))
|
|
2509
2546
|
throw new AiDlcError("E_SYNTAX", `Lane must be one of ${LANES2.join(", ")}.`);
|
|
2510
|
-
command = `/
|
|
2547
|
+
command = `/huddle60 ready ${target} hash=${snapshot.contentHash} lane=${lane}`;
|
|
2511
2548
|
break;
|
|
2512
2549
|
}
|
|
2513
2550
|
case "accept": {
|
|
@@ -2519,7 +2556,7 @@ ${sanitize(draft.request)}
|
|
|
2519
2556
|
"No successful acceptance build on the default branch. Merge the PR and wait for the build."
|
|
2520
2557
|
);
|
|
2521
2558
|
}
|
|
2522
|
-
command = `/
|
|
2559
|
+
command = `/huddle60 accept ${target} run=${build.id} commit=${build.headSha}`;
|
|
2523
2560
|
break;
|
|
2524
2561
|
}
|
|
2525
2562
|
default:
|
|
@@ -2535,7 +2572,11 @@ ${sanitize(draft.request)}
|
|
|
2535
2572
|
);
|
|
2536
2573
|
if (!found) return { status: "pending", body: null, ok: false };
|
|
2537
2574
|
const body = found.body.replace(/<!--[\s\S]*?-->/gu, "").trim();
|
|
2538
|
-
return {
|
|
2575
|
+
return {
|
|
2576
|
+
status: "done",
|
|
2577
|
+
body,
|
|
2578
|
+
ok: !body.startsWith("**huddle60: `E_") && !body.startsWith("**AI-DLC: `E_")
|
|
2579
|
+
};
|
|
2539
2580
|
}
|
|
2540
2581
|
async builds() {
|
|
2541
2582
|
const policy = await this.workflow.policy();
|
|
@@ -2582,19 +2623,15 @@ import { fileURLToPath } from "node:url";
|
|
|
2582
2623
|
var systemClock = { now: () => /* @__PURE__ */ new Date() };
|
|
2583
2624
|
|
|
2584
2625
|
// src/app/server.ts
|
|
2626
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2585
2627
|
import { readFile } from "node:fs/promises";
|
|
2586
2628
|
import { join } from "node:path";
|
|
2587
2629
|
import { z as z3 } from "zod";
|
|
2588
|
-
|
|
2589
|
-
// src/server/http.ts
|
|
2590
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2591
2630
|
function safeEqual(a, b) {
|
|
2592
2631
|
const left = Buffer.from(a);
|
|
2593
2632
|
const right = Buffer.from(b);
|
|
2594
2633
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
2595
2634
|
}
|
|
2596
|
-
|
|
2597
|
-
// src/app/server.ts
|
|
2598
2635
|
var TOKEN_HEADER = "x-ai-dlc-token";
|
|
2599
2636
|
var MAX_BODY = 256 * 1024;
|
|
2600
2637
|
var STATIC_FILES = {
|
|
@@ -2792,7 +2829,10 @@ async function startLocalApp(options) {
|
|
|
2792
2829
|
const logger = createJsonLogger({ component: "huddle60-ui" });
|
|
2793
2830
|
const octokit = octokitFromToken(resolveGitHubToken());
|
|
2794
2831
|
const github = await connectRepository(octokit, options.owner, options.repo);
|
|
2795
|
-
|
|
2832
|
+
let policySource = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
|
|
2833
|
+
if (!policySource) {
|
|
2834
|
+
policySource = await github.readFile(LEGACY_POLICY_PATH, await github.getDefaultBranch());
|
|
2835
|
+
}
|
|
2796
2836
|
if (!policySource)
|
|
2797
2837
|
throw new AiDlcError(
|
|
2798
2838
|
"E_POLICY_INVALID",
|
|
@@ -2889,7 +2929,10 @@ async function context(repoArg, issueArg, draft) {
|
|
|
2889
2929
|
const [owner, repo] = detectRepo(repoArg);
|
|
2890
2930
|
const octokit = octokitFromToken(token());
|
|
2891
2931
|
const github = await connectRepository(octokit, owner, repo);
|
|
2892
|
-
|
|
2932
|
+
let policySource = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
|
|
2933
|
+
if (!policySource) {
|
|
2934
|
+
policySource = await github.readFile(LEGACY_POLICY_PATH, await github.getDefaultBranch());
|
|
2935
|
+
}
|
|
2893
2936
|
if (!policySource) throw new Error(`Missing ${POLICY_PATH} in ${owner}/${repo}.`);
|
|
2894
2937
|
const policy = parsePolicy(policySource);
|
|
2895
2938
|
const evidence = new GitBranchEvidenceStore(octokit, owner, repo, policy.evidence.branch);
|
|
@@ -2902,31 +2945,128 @@ async function context(repoArg, issueArg, draft) {
|
|
|
2902
2945
|
});
|
|
2903
2946
|
return renderAgentContext({ github, evidence, workflow }, policy, issueNumber, draft);
|
|
2904
2947
|
}
|
|
2948
|
+
function resolveTemplatesDir() {
|
|
2949
|
+
const distRelative = fileURLToPath2(new URL("../templates", import.meta.url));
|
|
2950
|
+
if (existsSync(distRelative)) return distRelative;
|
|
2951
|
+
const srcRelative = fileURLToPath2(new URL("../../templates", import.meta.url));
|
|
2952
|
+
if (existsSync(srcRelative)) return srcRelative;
|
|
2953
|
+
throw new Error("Cannot find templates directory.");
|
|
2954
|
+
}
|
|
2955
|
+
async function scaffoldTemplates(destDir, viewer) {
|
|
2956
|
+
const templatesDir = resolveTemplatesDir();
|
|
2957
|
+
const created = [];
|
|
2958
|
+
const skipped = [];
|
|
2959
|
+
async function walk(currentSrc, currentDest) {
|
|
2960
|
+
const entries = await readdir(currentSrc, { withFileTypes: true });
|
|
2961
|
+
for (const entry of entries) {
|
|
2962
|
+
const srcPath = join2(currentSrc, entry.name);
|
|
2963
|
+
const destPath = join2(currentDest, entry.name);
|
|
2964
|
+
if (entry.isDirectory()) {
|
|
2965
|
+
await walk(srcPath, destPath);
|
|
2966
|
+
} else if (entry.isFile()) {
|
|
2967
|
+
const rel = relative(destDir, destPath);
|
|
2968
|
+
if (existsSync(destPath)) {
|
|
2969
|
+
skipped.push(rel);
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
2973
|
+
let content = await readFile2(srcPath, "utf8");
|
|
2974
|
+
if (entry.name === "policy.yml") {
|
|
2975
|
+
content = content.replace(
|
|
2976
|
+
/roles:[\s\S]*?maintainer:\s*\[\]/u,
|
|
2977
|
+
`roles:
|
|
2978
|
+
po:
|
|
2979
|
+
- id: ${viewer.id}
|
|
2980
|
+
login: ${viewer.login}
|
|
2981
|
+
dev:
|
|
2982
|
+
- id: ${viewer.id}
|
|
2983
|
+
login: ${viewer.login}
|
|
2984
|
+
maintainer:
|
|
2985
|
+
- id: ${viewer.id}
|
|
2986
|
+
login: ${viewer.login}`
|
|
2987
|
+
).replace(
|
|
2988
|
+
/project:\s*\n\s*owner:\s*your-org\s*\n\s*number:\s*1/u,
|
|
2989
|
+
`# project:
|
|
2990
|
+
# owner: your-org
|
|
2991
|
+
# number: 1`
|
|
2992
|
+
);
|
|
2993
|
+
} else if (entry.name === "huddle60.yml" || entry.name === "ai-dlc.yml") {
|
|
2994
|
+
content = content.replace("your-org/huddle60@v0", "tadev999/huddle60@main");
|
|
2995
|
+
}
|
|
2996
|
+
await writeFile(destPath, content, "utf8");
|
|
2997
|
+
created.push(rel);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
const githubSrc = join2(templatesDir, ".github");
|
|
3002
|
+
if (existsSync(githubSrc)) {
|
|
3003
|
+
await walk(githubSrc, join2(destDir, ".github"));
|
|
3004
|
+
}
|
|
3005
|
+
const agentsSrc = join2(templatesDir, "AGENTS.md");
|
|
3006
|
+
const agentsDest = join2(destDir, "AGENTS.md");
|
|
3007
|
+
if (existsSync(agentsSrc)) {
|
|
3008
|
+
if (existsSync(agentsDest)) {
|
|
3009
|
+
skipped.push("AGENTS.md");
|
|
3010
|
+
} else {
|
|
3011
|
+
const content = await readFile2(agentsSrc, "utf8");
|
|
3012
|
+
await writeFile(agentsDest, content, "utf8");
|
|
3013
|
+
created.push("AGENTS.md");
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return { created, skipped };
|
|
3017
|
+
}
|
|
2905
3018
|
async function initEvidence(repoArg) {
|
|
2906
3019
|
const [owner, repo] = detectRepo(repoArg);
|
|
2907
3020
|
const octokit = octokitFromToken(token());
|
|
2908
3021
|
const github = await connectRepository(octokit, owner, repo);
|
|
2909
|
-
|
|
2910
|
-
|
|
3022
|
+
let policySource = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
|
|
3023
|
+
if (!policySource) {
|
|
3024
|
+
policySource = await github.readFile(LEGACY_POLICY_PATH, await github.getDefaultBranch());
|
|
3025
|
+
}
|
|
3026
|
+
const branch = policySource ? parsePolicy(policySource).evidence.branch : "huddle60-evidence";
|
|
2911
3027
|
const result = await new GitBranchEvidenceStore(octokit, owner, repo, branch).initialize();
|
|
2912
|
-
return result === "created" ?
|
|
3028
|
+
return result === "created" ? `\u2713 Created branch ${branch}. Protect it with a ruleset that blocks force-push and deletion.` : `\u2713 Branch ${branch} already exists.`;
|
|
2913
3029
|
}
|
|
2914
3030
|
async function init(repoArg) {
|
|
2915
3031
|
const [owner, repo] = detectRepo(repoArg);
|
|
2916
3032
|
const github = await connectRepository(octokitFromToken(token()), owner, repo);
|
|
2917
|
-
const
|
|
3033
|
+
const viewer = await github.viewer();
|
|
3034
|
+
const createdLabels = [];
|
|
2918
3035
|
for (const [name, meta] of Object.entries(LABEL_COLORS)) {
|
|
2919
|
-
if (await github.ensureLabel({ name, ...meta }) === "created")
|
|
3036
|
+
if (await github.ensureLabel({ name, ...meta }) === "created") createdLabels.push(name);
|
|
2920
3037
|
}
|
|
2921
3038
|
const lines = [
|
|
2922
|
-
|
|
3039
|
+
`\u2713 Authenticated as ${viewer.login} (id: ${viewer.id}).`,
|
|
3040
|
+
createdLabels.length > 0 ? `\u2713 Created labels: ${createdLabels.join(", ")}` : "\u2713 Labels already exist.",
|
|
2923
3041
|
await initEvidence(repoArg)
|
|
2924
3042
|
];
|
|
2925
|
-
|
|
2926
|
-
|
|
3043
|
+
try {
|
|
3044
|
+
const { created, skipped } = await scaffoldTemplates(process.cwd(), viewer);
|
|
3045
|
+
if (created.length > 0) {
|
|
3046
|
+
lines.push(
|
|
3047
|
+
`\u2713 Scaffolding completed (${created.length} files created):`,
|
|
3048
|
+
...created.map((f) => ` + ${f}`)
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
if (skipped.length > 0) {
|
|
3052
|
+
lines.push(`\u2139 Kept existing files (${skipped.length}): ${skipped.join(", ")}`);
|
|
3053
|
+
}
|
|
2927
3054
|
lines.push(
|
|
2928
|
-
|
|
3055
|
+
"",
|
|
3056
|
+
`Setup complete! Next steps:`,
|
|
3057
|
+
` 1. Commit and push templates to default branch:`,
|
|
3058
|
+
` git add .github AGENTS.md && git commit -m "chore: setup huddle60" && git push`,
|
|
3059
|
+
` 2. Start the local app:`,
|
|
3060
|
+
` npx huddle60 ui`
|
|
2929
3061
|
);
|
|
3062
|
+
} catch {
|
|
3063
|
+
const policy = await github.readFile(POLICY_PATH, await github.getDefaultBranch());
|
|
3064
|
+
if (!policy) {
|
|
3065
|
+
lines.push(
|
|
3066
|
+
`Next: commit templates/.github/** (policy with user id ${viewer.id}) to the default branch.`
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
2930
3070
|
return lines.join("\n");
|
|
2931
3071
|
}
|
|
2932
3072
|
async function ui(repoArg, portArg, open) {
|
|
@@ -2942,7 +3082,7 @@ async function validatePolicy(file) {
|
|
|
2942
3082
|
}
|
|
2943
3083
|
async function planPreview(request) {
|
|
2944
3084
|
if (!request)
|
|
2945
|
-
throw new Error('Pass the request text, e.g.
|
|
3085
|
+
throw new Error('Pass the request text, e.g. huddle60 plan-preview "Login with Google".');
|
|
2946
3086
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
2947
3087
|
if (!apiKey) throw new Error("Set GEMINI_API_KEY to call Gemini.");
|
|
2948
3088
|
const model = new GeminiModel({
|