@kody-ade/kody-engine 0.2.7 → 0.2.9
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/bin/kody2.js +320 -50
- package/dist/executables/build/profile.json +16 -1
- package/dist/executables/orchestrator/profile.json +55 -0
- package/dist/executables/orchestrator/prompts/orchestrator.md +56 -0
- package/dist/executables/plan/profile.json +49 -0
- package/dist/executables/plan/prompts/plan.md +42 -0
- package/package.json +1 -1
package/dist/bin/kody2.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.9",
|
|
7
7
|
description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -108,6 +108,7 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
108
108
|
},
|
|
109
109
|
issueContext: parseIssueContext(raw.issueContext),
|
|
110
110
|
testRequirements: parseTestRequirements(raw.testRequirements),
|
|
111
|
+
defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : void 0,
|
|
111
112
|
release: parseReleaseConfig(raw.release)
|
|
112
113
|
};
|
|
113
114
|
}
|
|
@@ -1962,10 +1963,201 @@ var loadCoverageRules = async (ctx) => {
|
|
|
1962
1963
|
ctx.data.coverageRules = ctx.config.testRequirements ?? [];
|
|
1963
1964
|
};
|
|
1964
1965
|
|
|
1966
|
+
// src/state.ts
|
|
1967
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1968
|
+
var STATE_BEGIN = "<!-- kody2:state:v1:begin -->";
|
|
1969
|
+
var STATE_END = "<!-- kody2:state:v1:end -->";
|
|
1970
|
+
var HISTORY_MAX_ENTRIES = 20;
|
|
1971
|
+
var API_TIMEOUT_MS2 = 3e4;
|
|
1972
|
+
function emptyState() {
|
|
1973
|
+
return {
|
|
1974
|
+
schemaVersion: 1,
|
|
1975
|
+
core: {
|
|
1976
|
+
phase: "idle",
|
|
1977
|
+
status: "pending",
|
|
1978
|
+
currentExecutable: null,
|
|
1979
|
+
lastOutcome: null,
|
|
1980
|
+
attempts: {}
|
|
1981
|
+
},
|
|
1982
|
+
executables: {},
|
|
1983
|
+
history: []
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
function ghToken3() {
|
|
1987
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
1988
|
+
}
|
|
1989
|
+
function gh3(args, input, cwd) {
|
|
1990
|
+
const token = ghToken3();
|
|
1991
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
1992
|
+
return execFileSync10("gh", args, {
|
|
1993
|
+
encoding: "utf-8",
|
|
1994
|
+
timeout: API_TIMEOUT_MS2,
|
|
1995
|
+
cwd,
|
|
1996
|
+
env,
|
|
1997
|
+
input,
|
|
1998
|
+
stdio: input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
|
|
1999
|
+
}).trim();
|
|
2000
|
+
}
|
|
2001
|
+
function findStateComment(target, number, cwd) {
|
|
2002
|
+
const apiPath = target === "issue" ? `repos/{owner}/{repo}/issues/${number}/comments` : `repos/{owner}/{repo}/issues/${number}/comments`;
|
|
2003
|
+
try {
|
|
2004
|
+
const raw = gh3(["api", "--paginate", apiPath], void 0, cwd);
|
|
2005
|
+
const list = JSON.parse(raw);
|
|
2006
|
+
for (const c of list) {
|
|
2007
|
+
if (c.body?.includes(STATE_BEGIN)) {
|
|
2008
|
+
return { id: String(c.id), body: c.body };
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
function parseStateComment(body) {
|
|
2016
|
+
const beginIdx = body.indexOf(STATE_BEGIN);
|
|
2017
|
+
const endIdx = body.indexOf(STATE_END, beginIdx + 1);
|
|
2018
|
+
if (beginIdx < 0 || endIdx < 0) return emptyState();
|
|
2019
|
+
const between = body.slice(beginIdx + STATE_BEGIN.length, endIdx);
|
|
2020
|
+
const fenceMatch = between.match(/```json\s*([\s\S]*?)\s*```/);
|
|
2021
|
+
if (!fenceMatch) return emptyState();
|
|
2022
|
+
try {
|
|
2023
|
+
const parsed = JSON.parse(fenceMatch[1]);
|
|
2024
|
+
if (parsed?.schemaVersion !== 1) return emptyState();
|
|
2025
|
+
return {
|
|
2026
|
+
schemaVersion: 1,
|
|
2027
|
+
core: { ...emptyState().core, ...parsed.core },
|
|
2028
|
+
executables: parsed.executables ?? {},
|
|
2029
|
+
history: Array.isArray(parsed.history) ? parsed.history : []
|
|
2030
|
+
};
|
|
2031
|
+
} catch {
|
|
2032
|
+
return emptyState();
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
function reduce(state, executable, action) {
|
|
2036
|
+
if (!action) return state;
|
|
2037
|
+
const newAttempts = { ...state.core.attempts, [executable]: (state.core.attempts[executable] ?? 0) + 1 };
|
|
2038
|
+
const newExecutables = {
|
|
2039
|
+
...state.executables,
|
|
2040
|
+
[executable]: { ...state.executables[executable] ?? { lastAction: null }, lastAction: action }
|
|
2041
|
+
};
|
|
2042
|
+
const newHistory = [
|
|
2043
|
+
...state.history,
|
|
2044
|
+
{ timestamp: action.timestamp, executable, action: action.type, note: noteFromAction(action) }
|
|
2045
|
+
].slice(-HISTORY_MAX_ENTRIES);
|
|
2046
|
+
return {
|
|
2047
|
+
schemaVersion: 1,
|
|
2048
|
+
core: {
|
|
2049
|
+
...state.core,
|
|
2050
|
+
attempts: newAttempts,
|
|
2051
|
+
lastOutcome: action,
|
|
2052
|
+
currentExecutable: executable,
|
|
2053
|
+
status: statusFromAction(action),
|
|
2054
|
+
phase: phaseFromAction(executable, action)
|
|
2055
|
+
},
|
|
2056
|
+
executables: newExecutables,
|
|
2057
|
+
history: newHistory
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function statusFromAction(action) {
|
|
2061
|
+
if (/FAILED$|ERROR$|MISSING$|REJECTED$/i.test(action.type)) return "failed";
|
|
2062
|
+
if (/COMPLETED$|SHIPPED$|MERGED$|SUCCESS$/i.test(action.type)) return "succeeded";
|
|
2063
|
+
return "running";
|
|
2064
|
+
}
|
|
2065
|
+
function phaseFromAction(executable, action) {
|
|
2066
|
+
if (/FAILED$|ERROR$|REJECTED$/i.test(action.type)) return "failed";
|
|
2067
|
+
if (executable === "build") return statusFromAction(action) === "succeeded" ? "implementing" : "implementing";
|
|
2068
|
+
if (executable === "review") return "reviewing";
|
|
2069
|
+
if (executable === "release") return "shipped";
|
|
2070
|
+
return "idle";
|
|
2071
|
+
}
|
|
2072
|
+
function noteFromAction(action) {
|
|
2073
|
+
const p = action.payload;
|
|
2074
|
+
if (typeof p?.prUrl === "string") return p.prUrl;
|
|
2075
|
+
if (typeof p?.reason === "string") return p.reason.slice(0, 120);
|
|
2076
|
+
if (typeof p?.commitMessage === "string") return p.commitMessage.slice(0, 120);
|
|
2077
|
+
return void 0;
|
|
2078
|
+
}
|
|
2079
|
+
function renderStateComment(state) {
|
|
2080
|
+
const lines = [];
|
|
2081
|
+
lines.push(STATE_BEGIN);
|
|
2082
|
+
lines.push("");
|
|
2083
|
+
lines.push("```json");
|
|
2084
|
+
lines.push(JSON.stringify(
|
|
2085
|
+
{ schemaVersion: state.schemaVersion, core: state.core, executables: state.executables, history: state.history },
|
|
2086
|
+
null,
|
|
2087
|
+
2
|
|
2088
|
+
));
|
|
2089
|
+
lines.push("```");
|
|
2090
|
+
lines.push("");
|
|
2091
|
+
lines.push(STATE_END);
|
|
2092
|
+
lines.push("");
|
|
2093
|
+
lines.push("## kody2 task state");
|
|
2094
|
+
lines.push("");
|
|
2095
|
+
lines.push(`- **Phase:** \`${state.core.phase}\` **Status:** \`${state.core.status}\``);
|
|
2096
|
+
if (state.core.currentExecutable) {
|
|
2097
|
+
lines.push(`- **Last executable:** \`${state.core.currentExecutable}\``);
|
|
2098
|
+
}
|
|
2099
|
+
if (state.core.lastOutcome) {
|
|
2100
|
+
lines.push(`- **Last action:** \`${state.core.lastOutcome.type}\``);
|
|
2101
|
+
}
|
|
2102
|
+
const attempts = Object.entries(state.core.attempts).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
2103
|
+
if (attempts) lines.push(`- **Attempts:** ${attempts}`);
|
|
2104
|
+
if (state.core.prUrl) lines.push(`- **PR:** ${state.core.prUrl}`);
|
|
2105
|
+
if (state.core.runUrl) lines.push(`- **Run:** ${state.core.runUrl}`);
|
|
2106
|
+
lines.push("");
|
|
2107
|
+
if (state.history.length > 0) {
|
|
2108
|
+
lines.push("### Recent history");
|
|
2109
|
+
lines.push("");
|
|
2110
|
+
const recent = state.history.slice(-10).reverse();
|
|
2111
|
+
for (const h of recent) {
|
|
2112
|
+
const note = h.note ? ` \u2014 ${h.note}` : "";
|
|
2113
|
+
lines.push(`- \`${h.timestamp}\` **${h.executable}** \u2192 \`${h.action}\`${note}`);
|
|
2114
|
+
}
|
|
2115
|
+
lines.push("");
|
|
2116
|
+
}
|
|
2117
|
+
return lines.join("\n");
|
|
2118
|
+
}
|
|
2119
|
+
function readTaskState(target, number, cwd) {
|
|
2120
|
+
const existing = findStateComment(target, number, cwd);
|
|
2121
|
+
return existing ? parseStateComment(existing.body) : emptyState();
|
|
2122
|
+
}
|
|
2123
|
+
function writeTaskState(target, number, state, cwd) {
|
|
2124
|
+
const body = renderStateComment(state);
|
|
2125
|
+
const existing = findStateComment(target, number, cwd);
|
|
2126
|
+
try {
|
|
2127
|
+
if (existing) {
|
|
2128
|
+
gh3(
|
|
2129
|
+
["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"],
|
|
2130
|
+
body,
|
|
2131
|
+
cwd
|
|
2132
|
+
);
|
|
2133
|
+
} else {
|
|
2134
|
+
const sub = target === "issue" ? "issue" : "pr";
|
|
2135
|
+
gh3([sub, "comment", String(number), "--body-file", "-"], body, cwd);
|
|
2136
|
+
}
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
process.stderr.write(
|
|
2139
|
+
`[kody2 state] failed to write state on ${target} #${number}: ${err instanceof Error ? err.message : String(err)}
|
|
2140
|
+
`
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/scripts/loadTaskState.ts
|
|
2146
|
+
var loadTaskState = async (ctx) => {
|
|
2147
|
+
const target = ctx.data.commentTargetType;
|
|
2148
|
+
const number = ctx.data.commentTargetNumber;
|
|
2149
|
+
if (!target || !number) {
|
|
2150
|
+
ctx.data.taskState = emptyState();
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
ctx.data.taskState = readTaskState(target, number, ctx.cwd);
|
|
2154
|
+
};
|
|
2155
|
+
|
|
1965
2156
|
// src/scripts/parseAgentResult.ts
|
|
1966
|
-
var parseAgentResult2 = async (ctx,
|
|
2157
|
+
var parseAgentResult2 = async (ctx, profile, agentResult) => {
|
|
1967
2158
|
if (!agentResult) {
|
|
1968
2159
|
ctx.data.agentDone = false;
|
|
2160
|
+
ctx.data.action = makeAction("AGENT_NOT_RUN", { reason: "no agent result" });
|
|
1969
2161
|
return;
|
|
1970
2162
|
}
|
|
1971
2163
|
const parsed = parseAgentResult(agentResult.finalText);
|
|
@@ -1975,7 +2167,21 @@ var parseAgentResult2 = async (ctx, _profile, agentResult) => {
|
|
|
1975
2167
|
ctx.data.agentFailureReason = parsed.failureReason;
|
|
1976
2168
|
ctx.data.agentOutcome = agentResult.outcome;
|
|
1977
2169
|
ctx.data.agentError = agentResult.error;
|
|
2170
|
+
const modeSeg = (ctx.args.mode ?? profile.name).replace(/-/g, "_").toUpperCase();
|
|
2171
|
+
if (parsed.done) {
|
|
2172
|
+
ctx.data.action = makeAction(`${modeSeg}_COMPLETED`, {
|
|
2173
|
+
commitMessage: parsed.commitMessage,
|
|
2174
|
+
prSummary: parsed.prSummary
|
|
2175
|
+
});
|
|
2176
|
+
} else {
|
|
2177
|
+
ctx.data.action = makeAction(`${modeSeg}_FAILED`, {
|
|
2178
|
+
reason: parsed.failureReason || agentResult.error || "unknown failure"
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
1978
2181
|
};
|
|
2182
|
+
function makeAction(type, payload) {
|
|
2183
|
+
return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2184
|
+
}
|
|
1979
2185
|
|
|
1980
2186
|
// src/scripts/postIssueComment.ts
|
|
1981
2187
|
var postIssueComment2 = async (ctx) => {
|
|
@@ -2079,7 +2285,7 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
|
|
|
2079
2285
|
};
|
|
2080
2286
|
|
|
2081
2287
|
// src/scripts/releaseFlow.ts
|
|
2082
|
-
import { execFileSync as
|
|
2288
|
+
import { execFileSync as execFileSync11, spawnSync } from "child_process";
|
|
2083
2289
|
import * as fs11 from "fs";
|
|
2084
2290
|
import * as path10 from "path";
|
|
2085
2291
|
function bumpVersion(current, bump) {
|
|
@@ -2109,7 +2315,7 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
2109
2315
|
const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
|
|
2110
2316
|
let log = "";
|
|
2111
2317
|
try {
|
|
2112
|
-
log =
|
|
2318
|
+
log = execFileSync11("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
|
|
2113
2319
|
cwd,
|
|
2114
2320
|
encoding: "utf-8",
|
|
2115
2321
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2166,7 +2372,7 @@ ${entry}${prior.slice(idx + 1)}`);
|
|
|
2166
2372
|
}
|
|
2167
2373
|
}
|
|
2168
2374
|
function git3(args, cwd, timeout = 6e4) {
|
|
2169
|
-
return
|
|
2375
|
+
return execFileSync11("git", args, {
|
|
2170
2376
|
encoding: "utf-8",
|
|
2171
2377
|
timeout,
|
|
2172
2378
|
cwd,
|
|
@@ -2381,7 +2587,7 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
2381
2587
|
}
|
|
2382
2588
|
|
|
2383
2589
|
// src/scripts/resolveFlow.ts
|
|
2384
|
-
import { execFileSync as
|
|
2590
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2385
2591
|
var CONFLICT_DIFF_MAX_BYTES = 4e4;
|
|
2386
2592
|
var resolveFlow = async (ctx) => {
|
|
2387
2593
|
const prNumber = ctx.args.pr;
|
|
@@ -2433,7 +2639,7 @@ var resolveFlow = async (ctx) => {
|
|
|
2433
2639
|
};
|
|
2434
2640
|
function getConflictedFiles(cwd) {
|
|
2435
2641
|
try {
|
|
2436
|
-
const out =
|
|
2642
|
+
const out = execFileSync12("git", ["diff", "--name-only", "--diff-filter=U"], {
|
|
2437
2643
|
encoding: "utf-8",
|
|
2438
2644
|
cwd,
|
|
2439
2645
|
env: { ...process.env, HUSKY: "0" }
|
|
@@ -2448,7 +2654,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
|
|
|
2448
2654
|
let total = 0;
|
|
2449
2655
|
for (const f of files) {
|
|
2450
2656
|
try {
|
|
2451
|
-
const content =
|
|
2657
|
+
const content = execFileSync12("cat", [f], { encoding: "utf-8", cwd }).toString();
|
|
2452
2658
|
const snippet = `### ${f}
|
|
2453
2659
|
|
|
2454
2660
|
\`\`\`
|
|
@@ -2528,6 +2734,35 @@ function tryPost(issueNumber, body, cwd) {
|
|
|
2528
2734
|
}
|
|
2529
2735
|
}
|
|
2530
2736
|
|
|
2737
|
+
// src/scripts/saveTaskState.ts
|
|
2738
|
+
var saveTaskState = async (ctx, profile) => {
|
|
2739
|
+
const target = ctx.data.commentTargetType;
|
|
2740
|
+
const number = ctx.data.commentTargetNumber;
|
|
2741
|
+
const state = ctx.data.taskState;
|
|
2742
|
+
if (!target || !number || !state) return;
|
|
2743
|
+
const executable = profile.name;
|
|
2744
|
+
const action = ctx.data.action ?? synthesizeAction(ctx);
|
|
2745
|
+
if (ctx.output.prUrl && !state.core.prUrl) state.core.prUrl = ctx.output.prUrl;
|
|
2746
|
+
if (typeof ctx.data.runUrl === "string") state.core.runUrl = ctx.data.runUrl;
|
|
2747
|
+
const next = reduce(state, executable, action);
|
|
2748
|
+
if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
|
|
2749
|
+
if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
|
|
2750
|
+
writeTaskState(target, number, next, ctx.cwd);
|
|
2751
|
+
ctx.data.taskStateRendered = renderStateComment(next);
|
|
2752
|
+
};
|
|
2753
|
+
function synthesizeAction(ctx) {
|
|
2754
|
+
const ok = ctx.output.exitCode === 0;
|
|
2755
|
+
return {
|
|
2756
|
+
type: ok ? "RUN_COMPLETED" : "RUN_FAILED",
|
|
2757
|
+
payload: {
|
|
2758
|
+
exitCode: ctx.output.exitCode,
|
|
2759
|
+
reason: ctx.output.reason,
|
|
2760
|
+
prUrl: ctx.output.prUrl
|
|
2761
|
+
},
|
|
2762
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2531
2766
|
// src/verify.ts
|
|
2532
2767
|
import { spawn as spawn2 } from "child_process";
|
|
2533
2768
|
var TAIL_CHARS = 4e3;
|
|
@@ -2730,6 +2965,7 @@ var preflightScripts = {
|
|
|
2730
2965
|
initFlow,
|
|
2731
2966
|
releaseFlow,
|
|
2732
2967
|
watchStalePrsFlow,
|
|
2968
|
+
loadTaskState,
|
|
2733
2969
|
loadConventions,
|
|
2734
2970
|
loadCoverageRules,
|
|
2735
2971
|
composePrompt
|
|
@@ -2742,7 +2978,8 @@ var postflightScripts = {
|
|
|
2742
2978
|
ensurePr: ensurePr2,
|
|
2743
2979
|
postIssueComment: postIssueComment2,
|
|
2744
2980
|
postReviewResult,
|
|
2745
|
-
writeRunSummary
|
|
2981
|
+
writeRunSummary,
|
|
2982
|
+
saveTaskState
|
|
2746
2983
|
};
|
|
2747
2984
|
var allScriptNames = /* @__PURE__ */ new Set([
|
|
2748
2985
|
...Object.keys(preflightScripts),
|
|
@@ -2750,7 +2987,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
|
|
|
2750
2987
|
]);
|
|
2751
2988
|
|
|
2752
2989
|
// src/tools.ts
|
|
2753
|
-
import { execFileSync as
|
|
2990
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
2754
2991
|
function verifyCliTools(tools, cwd) {
|
|
2755
2992
|
const out = [];
|
|
2756
2993
|
for (const t of tools) out.push(verifyOne(t, cwd));
|
|
@@ -2783,7 +3020,7 @@ function verifyOne(tool, cwd) {
|
|
|
2783
3020
|
}
|
|
2784
3021
|
function runShell2(cmd, cwd, timeoutMs = 3e4) {
|
|
2785
3022
|
try {
|
|
2786
|
-
|
|
3023
|
+
execFileSync13("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
|
|
2787
3024
|
return true;
|
|
2788
3025
|
} catch {
|
|
2789
3026
|
return false;
|
|
@@ -2981,17 +3218,19 @@ function finish(out) {
|
|
|
2981
3218
|
}
|
|
2982
3219
|
|
|
2983
3220
|
// src/kody2-cli.ts
|
|
2984
|
-
import { execFileSync as
|
|
3221
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
2985
3222
|
import * as fs15 from "fs";
|
|
2986
3223
|
import * as path12 from "path";
|
|
2987
3224
|
|
|
2988
3225
|
// src/dispatch.ts
|
|
2989
3226
|
import * as fs14 from "fs";
|
|
2990
|
-
function autoDispatch(
|
|
2991
|
-
|
|
3227
|
+
function autoDispatch(opts) {
|
|
3228
|
+
const explicit = opts?.explicit;
|
|
3229
|
+
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
2992
3230
|
return {
|
|
2993
|
-
|
|
2994
|
-
|
|
3231
|
+
executable: "build",
|
|
3232
|
+
cliArgs: { mode: "run", issue: explicit.issueNumber },
|
|
3233
|
+
target: explicit.issueNumber
|
|
2995
3234
|
};
|
|
2996
3235
|
}
|
|
2997
3236
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
@@ -3005,30 +3244,60 @@ function autoDispatch(explicit) {
|
|
|
3005
3244
|
}
|
|
3006
3245
|
if (eventName === "workflow_dispatch") {
|
|
3007
3246
|
const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
|
|
3008
|
-
if (!Number.isNaN(n) && n > 0)
|
|
3247
|
+
if (!Number.isNaN(n) && n > 0) {
|
|
3248
|
+
return { executable: "build", cliArgs: { mode: "run", issue: n }, target: n };
|
|
3249
|
+
}
|
|
3009
3250
|
return null;
|
|
3010
3251
|
}
|
|
3011
|
-
if (eventName
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
const feedbackText = extractFeedback(afterTag);
|
|
3021
|
-
return { mode: "fix", target: issueNum, feedback: feedbackText };
|
|
3252
|
+
if (eventName !== "issue_comment") return null;
|
|
3253
|
+
const body = String(event.comment?.body ?? "").toLowerCase();
|
|
3254
|
+
const targetNum = Number(event.issue?.number ?? 0);
|
|
3255
|
+
const isPr = !!event.issue?.pull_request;
|
|
3256
|
+
if (!targetNum) return null;
|
|
3257
|
+
const afterTag = extractAfterTag(body);
|
|
3258
|
+
if (isPr) {
|
|
3259
|
+
if (/\bfix-ci\b/.test(afterTag)) {
|
|
3260
|
+
return { executable: "build", cliArgs: { mode: "fix-ci", pr: targetNum }, target: targetNum };
|
|
3022
3261
|
}
|
|
3023
|
-
|
|
3262
|
+
if (/\bresolve\b/.test(afterTag)) {
|
|
3263
|
+
return { executable: "build", cliArgs: { mode: "resolve", pr: targetNum }, target: targetNum };
|
|
3264
|
+
}
|
|
3265
|
+
const feedback = extractFeedback(afterTag);
|
|
3266
|
+
return {
|
|
3267
|
+
executable: "build",
|
|
3268
|
+
cliArgs: { mode: "fix", pr: targetNum, ...feedback ? { feedback } : {} },
|
|
3269
|
+
target: targetNum
|
|
3270
|
+
};
|
|
3024
3271
|
}
|
|
3025
|
-
|
|
3272
|
+
const sub = extractSubcommand(afterTag);
|
|
3273
|
+
const defaultExec = opts?.config?.defaultExecutable ?? "build";
|
|
3274
|
+
if (!sub) {
|
|
3275
|
+
return asDispatch(defaultExec, targetNum);
|
|
3276
|
+
}
|
|
3277
|
+
if (sub === "build") {
|
|
3278
|
+
return { executable: "build", cliArgs: { mode: "run", issue: targetNum }, target: targetNum };
|
|
3279
|
+
}
|
|
3280
|
+
if (sub === "orchestrate" || sub === "orchestrator") {
|
|
3281
|
+
return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
|
|
3282
|
+
}
|
|
3283
|
+
return asDispatch(sub, targetNum);
|
|
3284
|
+
}
|
|
3285
|
+
function asDispatch(executable, target) {
|
|
3286
|
+
if (executable === "build") {
|
|
3287
|
+
return { executable, cliArgs: { mode: "run", issue: target }, target };
|
|
3288
|
+
}
|
|
3289
|
+
return { executable, cliArgs: { issue: target }, target };
|
|
3026
3290
|
}
|
|
3027
3291
|
function extractAfterTag(body) {
|
|
3028
3292
|
const idx = body.indexOf("@kody2");
|
|
3029
3293
|
if (idx === -1) return body;
|
|
3030
3294
|
return body.slice(idx + "@kody2".length).trim();
|
|
3031
3295
|
}
|
|
3296
|
+
function extractSubcommand(afterTag) {
|
|
3297
|
+
const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
|
|
3298
|
+
if (!match) return null;
|
|
3299
|
+
return match[1];
|
|
3300
|
+
}
|
|
3032
3301
|
function extractFeedback(afterTag) {
|
|
3033
3302
|
const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
|
|
3034
3303
|
return cleaned.length > 0 ? cleaned : void 0;
|
|
@@ -3126,7 +3395,7 @@ function detectPackageManager2(cwd) {
|
|
|
3126
3395
|
}
|
|
3127
3396
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
3128
3397
|
try {
|
|
3129
|
-
|
|
3398
|
+
execFileSync14(cmd, args, {
|
|
3130
3399
|
cwd,
|
|
3131
3400
|
stdio: stream ? "inherit" : "pipe",
|
|
3132
3401
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
|
|
@@ -3139,7 +3408,7 @@ function shellOut(cmd, args, cwd, stream = true) {
|
|
|
3139
3408
|
}
|
|
3140
3409
|
function isOnPath(bin) {
|
|
3141
3410
|
try {
|
|
3142
|
-
|
|
3411
|
+
execFileSync14("which", [bin], { stdio: "pipe" });
|
|
3143
3412
|
return true;
|
|
3144
3413
|
} catch {
|
|
3145
3414
|
return false;
|
|
@@ -3173,7 +3442,7 @@ function installLitellmIfNeeded(cwd) {
|
|
|
3173
3442
|
} catch {
|
|
3174
3443
|
}
|
|
3175
3444
|
try {
|
|
3176
|
-
|
|
3445
|
+
execFileSync14("python3", ["-c", "import litellm"], { stdio: "pipe" });
|
|
3177
3446
|
process.stdout.write("\u2192 kody2: litellm already installed\n");
|
|
3178
3447
|
return 0;
|
|
3179
3448
|
} catch {
|
|
@@ -3183,16 +3452,16 @@ function installLitellmIfNeeded(cwd) {
|
|
|
3183
3452
|
}
|
|
3184
3453
|
function configureGitIdentity(cwd) {
|
|
3185
3454
|
try {
|
|
3186
|
-
const name =
|
|
3455
|
+
const name = execFileSync14("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
3187
3456
|
if (name) return;
|
|
3188
3457
|
} catch {
|
|
3189
3458
|
}
|
|
3190
3459
|
try {
|
|
3191
|
-
|
|
3460
|
+
execFileSync14("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
|
|
3192
3461
|
} catch {
|
|
3193
3462
|
}
|
|
3194
3463
|
try {
|
|
3195
|
-
|
|
3464
|
+
execFileSync14("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
|
|
3196
3465
|
} catch {
|
|
3197
3466
|
}
|
|
3198
3467
|
}
|
|
@@ -3227,7 +3496,13 @@ async function runCi(argv) {
|
|
|
3227
3496
|
return 0;
|
|
3228
3497
|
}
|
|
3229
3498
|
const args = parseCiArgs(argv);
|
|
3230
|
-
const
|
|
3499
|
+
const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
|
|
3500
|
+
let earlyConfig;
|
|
3501
|
+
try {
|
|
3502
|
+
earlyConfig = loadConfig(cwd);
|
|
3503
|
+
} catch {
|
|
3504
|
+
}
|
|
3505
|
+
const autoFallback = !args.issueNumber ? autoDispatch({ config: earlyConfig }) : null;
|
|
3231
3506
|
if (!args.issueNumber && !autoFallback) {
|
|
3232
3507
|
} else {
|
|
3233
3508
|
args.errors = args.errors.filter((e) => !e.includes("--issue"));
|
|
@@ -3239,14 +3514,13 @@ async function runCi(argv) {
|
|
|
3239
3514
|
${CI_HELP}`);
|
|
3240
3515
|
return 64;
|
|
3241
3516
|
}
|
|
3242
|
-
const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
|
|
3243
3517
|
const dispatch = autoFallback ?? {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3518
|
+
executable: "build",
|
|
3519
|
+
cliArgs: { mode: "run", issue: args.issueNumber },
|
|
3520
|
+
target: args.issueNumber
|
|
3247
3521
|
};
|
|
3248
3522
|
const issueNumber = dispatch.target;
|
|
3249
|
-
process.stdout.write(`\u2192 kody2 preflight (cwd=${cwd},
|
|
3523
|
+
process.stdout.write(`\u2192 kody2 preflight (cwd=${cwd}, executable=${dispatch.executable}, target=${issueNumber})
|
|
3250
3524
|
`);
|
|
3251
3525
|
try {
|
|
3252
3526
|
const n = unpackAllSecrets();
|
|
@@ -3283,17 +3557,13 @@ ${CI_HELP}`);
|
|
|
3283
3557
|
postFailureTail(issueNumber, cwd, `preflight crashed: ${msg}`);
|
|
3284
3558
|
return 99;
|
|
3285
3559
|
}
|
|
3286
|
-
process.stdout.write(`\u2192 kody2: preflight done, handing off to kody2 ${dispatch.
|
|
3560
|
+
process.stdout.write(`\u2192 kody2: preflight done, handing off to kody2 ${dispatch.executable}
|
|
3287
3561
|
|
|
3288
3562
|
`);
|
|
3289
3563
|
try {
|
|
3290
|
-
const config = loadConfig(cwd);
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3293
|
-
else cliArgs.pr = issueNumber;
|
|
3294
|
-
if (dispatch.feedback) cliArgs.feedback = dispatch.feedback;
|
|
3295
|
-
const result = await runExecutable("build", {
|
|
3296
|
-
cliArgs,
|
|
3564
|
+
const config = earlyConfig ?? loadConfig(cwd);
|
|
3565
|
+
const result = await runExecutable(dispatch.executable, {
|
|
3566
|
+
cliArgs: dispatch.cliArgs,
|
|
3297
3567
|
cwd,
|
|
3298
3568
|
config,
|
|
3299
3569
|
verbose: args.verbose,
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
{ "script": "fixFlow", "runWhen": { "args.mode": "fix" } },
|
|
68
68
|
{ "script": "fixCiFlow", "runWhen": { "args.mode": "fix-ci" } },
|
|
69
69
|
{ "script": "resolveFlow", "runWhen": { "args.mode": "resolve" } },
|
|
70
|
+
{ "script": "loadTaskState" },
|
|
70
71
|
{ "script": "loadConventions" },
|
|
71
72
|
{ "script": "loadCoverageRules" },
|
|
72
73
|
{ "script": "composePrompt" }
|
|
@@ -78,7 +79,21 @@
|
|
|
78
79
|
{ "script": "commitAndPush" },
|
|
79
80
|
{ "script": "ensurePr" },
|
|
80
81
|
{ "script": "postIssueComment" },
|
|
81
|
-
{ "script": "writeRunSummary" }
|
|
82
|
+
{ "script": "writeRunSummary" },
|
|
83
|
+
{ "script": "saveTaskState" }
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"output": {
|
|
87
|
+
"actionTypes": [
|
|
88
|
+
"RUN_COMPLETED",
|
|
89
|
+
"RUN_FAILED",
|
|
90
|
+
"FIX_COMPLETED",
|
|
91
|
+
"FIX_FAILED",
|
|
92
|
+
"FIX_CI_COMPLETED",
|
|
93
|
+
"FIX_CI_FAILED",
|
|
94
|
+
"RESOLVE_COMPLETED",
|
|
95
|
+
"RESOLVE_FAILED",
|
|
96
|
+
"AGENT_NOT_RUN"
|
|
82
97
|
]
|
|
83
98
|
}
|
|
84
99
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orchestrator",
|
|
3
|
+
"describe": "Drive a chain of kody2 executables by posting @kody2 subcommand comments and polling the task-state comment. Atomic — one orchestrator run fires one flow.",
|
|
4
|
+
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "issue",
|
|
8
|
+
"flag": "--issue",
|
|
9
|
+
"type": "int",
|
|
10
|
+
"required": true,
|
|
11
|
+
"describe": "GitHub issue number to drive."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "flow",
|
|
15
|
+
"flag": "--flow",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": false,
|
|
18
|
+
"describe": "Named flow to run (e.g. 'plan-then-build'). Defaults to plan-then-build."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
"claudeCode": {
|
|
23
|
+
"model": "inherit",
|
|
24
|
+
"permissionMode": "default",
|
|
25
|
+
"maxTurns": null,
|
|
26
|
+
"systemPromptAppend": null,
|
|
27
|
+
"tools": ["Bash", "Read"],
|
|
28
|
+
"hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
|
|
29
|
+
"skills": [],
|
|
30
|
+
"commands": [],
|
|
31
|
+
"subagents": [],
|
|
32
|
+
"plugins": [],
|
|
33
|
+
"mcpServers": []
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"cliTools": [],
|
|
37
|
+
|
|
38
|
+
"scripts": {
|
|
39
|
+
"preflight": [
|
|
40
|
+
{ "script": "runFlow" },
|
|
41
|
+
{ "script": "loadTaskState" },
|
|
42
|
+
{ "script": "composePrompt" }
|
|
43
|
+
],
|
|
44
|
+
"postflight": [
|
|
45
|
+
{ "script": "parseAgentResult" },
|
|
46
|
+
{ "script": "postIssueComment" },
|
|
47
|
+
{ "script": "writeRunSummary" },
|
|
48
|
+
{ "script": "saveTaskState" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"output": {
|
|
53
|
+
"actionTypes": ["ORCHESTRATION_COMPLETED", "ORCHESTRATION_FAILED"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
You are the **kody2 orchestrator** for issue #{{issue.number}} on {{repoOwner}}/{{repoName}}.
|
|
2
|
+
|
|
3
|
+
Your job: drive a 2-step flow **plan → build** by posting `@kody2 <subcommand>` comments on the issue and watching the state-comment for completion signals. You do NOT edit files. You do NOT run git. You use `gh` (via Bash) only to post comments and read the state-comment.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Issue #{{issue.number}}: {{issue.title}}
|
|
8
|
+
|
|
9
|
+
{{issue.body}}
|
|
10
|
+
|
|
11
|
+
# Required flow (plan-then-build)
|
|
12
|
+
|
|
13
|
+
1. **Kick off plan.** Post an issue comment with EXACTLY this body:
|
|
14
|
+
```
|
|
15
|
+
@kody2 plan
|
|
16
|
+
```
|
|
17
|
+
Use: `gh issue comment {{issue.number}} --body "@kody2 plan"` (in the cwd).
|
|
18
|
+
2. **Wait for plan to complete.** Poll the issue's state-comment every ~30s. The state-comment is the one whose body starts with `<!-- kody2:state:v1:begin -->`. Fetch it with:
|
|
19
|
+
```
|
|
20
|
+
gh api repos/{{repoOwner}}/{{repoName}}/issues/{{issue.number}}/comments --paginate --jq '.[] | select(.body | contains("kody2:state:v1:begin")) | .body'
|
|
21
|
+
```
|
|
22
|
+
Parse the JSON block inside the sentinels. Look for `core.lastOutcome.type == "PLAN_COMPLETED"`.
|
|
23
|
+
If `core.lastOutcome.type == "PLAN_FAILED"` OR if 10 minutes pass without completion → abort with:
|
|
24
|
+
```
|
|
25
|
+
FAILED: plan did not complete (<reason from state or "timeout">)
|
|
26
|
+
```
|
|
27
|
+
3. **Kick off build.** Post:
|
|
28
|
+
```
|
|
29
|
+
@kody2 build
|
|
30
|
+
```
|
|
31
|
+
Same `gh issue comment` command.
|
|
32
|
+
4. **Wait for build to complete.** Same poll technique. Look for `core.lastOutcome.type == "RUN_COMPLETED"` (build's success marker) or `RUN_FAILED`. If `RUN_FAILED` or 30 minutes pass → abort with `FAILED: build did not complete (...)`.
|
|
33
|
+
5. **Emit final summary.**
|
|
34
|
+
|
|
35
|
+
# Required final output
|
|
36
|
+
|
|
37
|
+
On success:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
DONE
|
|
41
|
+
COMMIT_MSG: chore(orchestrator): plan-then-build for #{{issue.number}}
|
|
42
|
+
PR_SUMMARY:
|
|
43
|
+
- Posted `@kody2 plan` and observed PLAN_COMPLETED.
|
|
44
|
+
- Posted `@kody2 build` and observed RUN_COMPLETED.
|
|
45
|
+
- Final PR: <prUrl from state>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
On failure, a single line: `FAILED: <concrete reason>`.
|
|
49
|
+
|
|
50
|
+
# Rules
|
|
51
|
+
|
|
52
|
+
- NEVER edit files. Read-only flow.
|
|
53
|
+
- NEVER run git. Only `gh` via Bash for comment posting and state polling.
|
|
54
|
+
- Between polls, sleep ~30 seconds. Do NOT poll faster than once every 30 seconds.
|
|
55
|
+
- Hard cap: 40 turns total across the whole flow. If you're approaching the cap, fail early with `FAILED: turn budget exhausted`.
|
|
56
|
+
- If you post an `@kody2` comment and the state-comment does NOT update within the poll window, the child executable likely didn't run — check the GitHub Actions runs tab URL via `gh run list --limit 5 --json conclusion,status,url` to diagnose, then fail with a concrete reason.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plan",
|
|
3
|
+
"describe": "Research an issue and produce a concrete implementation plan as a comment. Read-only — no branches, no commits.",
|
|
4
|
+
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "issue",
|
|
8
|
+
"flag": "--issue",
|
|
9
|
+
"type": "int",
|
|
10
|
+
"required": true,
|
|
11
|
+
"describe": "GitHub issue number to plan."
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
|
|
15
|
+
"claudeCode": {
|
|
16
|
+
"model": "inherit",
|
|
17
|
+
"permissionMode": "default",
|
|
18
|
+
"maxTurns": null,
|
|
19
|
+
"systemPromptAppend": null,
|
|
20
|
+
"tools": ["Read", "Grep", "Glob", "Bash"],
|
|
21
|
+
"hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
|
|
22
|
+
"skills": [],
|
|
23
|
+
"commands": [],
|
|
24
|
+
"subagents": [],
|
|
25
|
+
"plugins": [],
|
|
26
|
+
"mcpServers": []
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"cliTools": [],
|
|
30
|
+
|
|
31
|
+
"scripts": {
|
|
32
|
+
"preflight": [
|
|
33
|
+
{ "script": "runFlow" },
|
|
34
|
+
{ "script": "loadTaskState" },
|
|
35
|
+
{ "script": "loadConventions" },
|
|
36
|
+
{ "script": "composePrompt" }
|
|
37
|
+
],
|
|
38
|
+
"postflight": [
|
|
39
|
+
{ "script": "parseAgentResult" },
|
|
40
|
+
{ "script": "postIssueComment" },
|
|
41
|
+
{ "script": "writeRunSummary" },
|
|
42
|
+
{ "script": "saveTaskState" }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"output": {
|
|
47
|
+
"actionTypes": ["PLAN_COMPLETED", "PLAN_FAILED"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
You are a senior engineer producing an **implementation plan** for the GitHub issue below. You will NOT write code. You will NOT run git or gh commands. You will NOT modify files. Your only outputs are:
|
|
2
|
+
|
|
3
|
+
1. Use Read / Grep / Glob / Bash (read-only) to study the codebase as much as needed.
|
|
4
|
+
2. Emit a final message with the plan wrapped in the required markers (see "Required output").
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Repo
|
|
9
|
+
- {{repoOwner}}/{{repoName}}, default branch: {{defaultBranch}}
|
|
10
|
+
|
|
11
|
+
# Issue #{{issue.number}}: {{issue.title}}
|
|
12
|
+
|
|
13
|
+
{{issue.body}}
|
|
14
|
+
|
|
15
|
+
Recent comments (most recent first, truncated):
|
|
16
|
+
{{issue.commentsFormatted}}
|
|
17
|
+
|
|
18
|
+
{{conventionsBlock}}
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Required output
|
|
23
|
+
|
|
24
|
+
Your FINAL message must be exactly this shape (no extra text before or after):
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
DONE
|
|
28
|
+
COMMIT_MSG: plan: <very short title>
|
|
29
|
+
PR_SUMMARY:
|
|
30
|
+
<A concrete implementation plan in markdown. Include:
|
|
31
|
+
- Files to change (with paths), and the change in each.
|
|
32
|
+
- New files to create, with their purpose and rough shape.
|
|
33
|
+
- Any ambiguities that need the human to resolve first.
|
|
34
|
+
- Verification checklist (typecheck / tests / lint expectations).
|
|
35
|
+
Keep to ~60 lines or less. No filler. No marketing language.>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
# Rules
|
|
39
|
+
- Read-only. Do NOT modify any file.
|
|
40
|
+
- Do NOT run git or gh commands.
|
|
41
|
+
- No speculative scope — plan only what the issue asks for.
|
|
42
|
+
- If the issue is ambiguous and you cannot make progress without input, output `FAILED: <what's unclear>` instead of a plan.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|