@kody-ade/kody-engine 0.4.86 → 0.4.87
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/kody.js
CHANGED
|
@@ -877,7 +877,7 @@ var init_loadPriorArt = __esm({
|
|
|
877
877
|
// package.json
|
|
878
878
|
var package_default = {
|
|
879
879
|
name: "@kody-ade/kody-engine",
|
|
880
|
-
version: "0.4.
|
|
880
|
+
version: "0.4.87",
|
|
881
881
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
882
882
|
license: "MIT",
|
|
883
883
|
type: "module",
|
|
@@ -3307,6 +3307,10 @@ var CONVENTIONAL_PREFIXES = [
|
|
|
3307
3307
|
"build:",
|
|
3308
3308
|
"revert:"
|
|
3309
3309
|
];
|
|
3310
|
+
var PUSH_RETRY_DELAYS_MS = [2e3, 4e3, 8e3];
|
|
3311
|
+
function sleepSync(ms) {
|
|
3312
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
3313
|
+
}
|
|
3310
3314
|
function git(args, cwd) {
|
|
3311
3315
|
try {
|
|
3312
3316
|
return execFileSync5("git", args, {
|
|
@@ -3424,25 +3428,28 @@ function commitAndPush(branch, agentMessage, cwd) {
|
|
|
3424
3428
|
throw err;
|
|
3425
3429
|
}
|
|
3426
3430
|
const sha = git(["rev-parse", "HEAD"], cwd).slice(0, 7);
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
return { committed: true, pushed: true, sha, message };
|
|
3430
|
-
} catch (firstErr) {
|
|
3431
|
+
let pushError = "push failed (no error detail)";
|
|
3432
|
+
for (let attempt = 0; attempt <= PUSH_RETRY_DELAYS_MS.length; attempt++) {
|
|
3431
3433
|
try {
|
|
3432
|
-
git(["push", "
|
|
3434
|
+
git(["push", "-u", "origin", branch], cwd);
|
|
3433
3435
|
return { committed: true, pushed: true, sha, message };
|
|
3434
|
-
} catch (
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3436
|
+
} catch (firstErr) {
|
|
3437
|
+
try {
|
|
3438
|
+
git(["push", "--force-with-lease", "-u", "origin", branch], cwd);
|
|
3439
|
+
return { committed: true, pushed: true, sha, message };
|
|
3440
|
+
} catch (secondErr) {
|
|
3441
|
+
const tail = (secondErr instanceof Error ? secondErr.message : String(secondErr)).slice(-400);
|
|
3442
|
+
const initial = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
3443
|
+
pushError = `push failed: ${initial.slice(-200)} | force-with-lease failed: ${tail}`;
|
|
3444
|
+
const delay = PUSH_RETRY_DELAYS_MS[attempt];
|
|
3445
|
+
if (delay === void 0) break;
|
|
3446
|
+
process.stderr.write(`[kody:commit] push failed (attempt ${attempt + 1}); retrying in ${delay}ms
|
|
3447
|
+
`);
|
|
3448
|
+
sleepSync(delay);
|
|
3449
|
+
}
|
|
3444
3450
|
}
|
|
3445
3451
|
}
|
|
3452
|
+
return { committed: true, pushed: false, sha, message, pushError };
|
|
3446
3453
|
}
|
|
3447
3454
|
function hasCommitsAhead(branch, defaultBranch2, cwd) {
|
|
3448
3455
|
try {
|
|
@@ -5190,6 +5197,7 @@ init_issue();
|
|
|
5190
5197
|
function goalLabel(goalId) {
|
|
5191
5198
|
return `goal:${goalId}`;
|
|
5192
5199
|
}
|
|
5200
|
+
var QA_GATE_LABEL = "kody:qa-gate";
|
|
5193
5201
|
|
|
5194
5202
|
// src/goal/operations.ts
|
|
5195
5203
|
function fail(err) {
|
|
@@ -5206,7 +5214,7 @@ function listGoalIssues(goalId, cwd) {
|
|
|
5206
5214
|
"api",
|
|
5207
5215
|
`repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
|
|
5208
5216
|
"--jq",
|
|
5209
|
-
|
|
5217
|
+
`[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase), isQaGate: ([.labels[].name] | any(. == "${QA_GATE_LABEL}"))}]`
|
|
5210
5218
|
],
|
|
5211
5219
|
{ cwd }
|
|
5212
5220
|
);
|
|
@@ -5242,7 +5250,7 @@ function pairIssuesWithPrs(issues, openPrs) {
|
|
|
5242
5250
|
const pr = prByIssue.get(i.number);
|
|
5243
5251
|
let prState = "absent";
|
|
5244
5252
|
if (pr) prState = pr.isDraft ? "draft" : "ready";
|
|
5245
|
-
return { number: i.number, state: i.state, prState };
|
|
5253
|
+
return { number: i.number, state: i.state, prState, isQaGate: i.isQaGate };
|
|
5246
5254
|
});
|
|
5247
5255
|
}
|
|
5248
5256
|
function claimPrForIssue(pr, prByIssue) {
|
|
@@ -5302,14 +5310,6 @@ function closePr(prNumber, comment, cwd) {
|
|
|
5302
5310
|
return fail(err);
|
|
5303
5311
|
}
|
|
5304
5312
|
}
|
|
5305
|
-
function mergePrSquash(prNumber, cwd) {
|
|
5306
|
-
try {
|
|
5307
|
-
gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
|
|
5308
|
-
return { ok: true };
|
|
5309
|
-
} catch (err) {
|
|
5310
|
-
return fail(err);
|
|
5311
|
-
}
|
|
5312
|
-
}
|
|
5313
5313
|
function editPrBase(prNumber, baseBranch, cwd) {
|
|
5314
5314
|
try {
|
|
5315
5315
|
gh(
|
|
@@ -5336,17 +5336,19 @@ function derivePhase(snap) {
|
|
|
5336
5336
|
if (snap.lifecycleState === "abandoned") return "abandoned";
|
|
5337
5337
|
if (snap.lifecycleState === "closed" || snap.lifecycleState === "done") return "terminal";
|
|
5338
5338
|
if (snap.lifecycleState === "awaiting-merge") return "awaiting-merge";
|
|
5339
|
-
const
|
|
5339
|
+
const tasks = snap.childTasks.filter((t) => !t.isQaGate);
|
|
5340
|
+
const qaGateOpen = snap.childTasks.some((t) => t.isQaGate && t.state === "OPEN");
|
|
5341
|
+
const hasInFlight = tasks.some((t) => t.state === "OPEN" && t.prState === "draft");
|
|
5340
5342
|
if (hasInFlight) return "in-flight";
|
|
5341
|
-
if (
|
|
5342
|
-
const
|
|
5343
|
-
if (allDone) return "all-done";
|
|
5344
|
-
const dispatchable = snap.childTasks.some((t) => t.state === "OPEN" && t.prState === "absent");
|
|
5343
|
+
if (tasks.length === 0) return "idle";
|
|
5344
|
+
const dispatchable = tasks.some((t) => t.state === "OPEN" && t.prState === "absent");
|
|
5345
5345
|
if (dispatchable) return "ready-to-dispatch";
|
|
5346
|
+
const allDone = tasks.every((t) => t.state === "CLOSED" || t.prState === "ready");
|
|
5347
|
+
if (allDone) return qaGateOpen ? "idle" : "all-done";
|
|
5346
5348
|
return "idle";
|
|
5347
5349
|
}
|
|
5348
5350
|
function pickNextDispatchable(snap) {
|
|
5349
|
-
return snap.childTasks.filter((t) => t.state === "OPEN" && t.prState === "absent").sort((a, b) => a.number - b.number)[0];
|
|
5351
|
+
return snap.childTasks.filter((t) => !t.isQaGate && t.state === "OPEN" && t.prState === "absent").sort((a, b) => a.number - b.number)[0];
|
|
5350
5352
|
}
|
|
5351
5353
|
|
|
5352
5354
|
// src/scripts/deriveGoalPhase.ts
|
|
@@ -6929,11 +6931,11 @@ function collectExpectedTests(raw) {
|
|
|
6929
6931
|
var finalizeGoal = async (ctx) => {
|
|
6930
6932
|
const goal = ctx.data.goal;
|
|
6931
6933
|
if (!goal) return;
|
|
6932
|
-
process.stdout.write(`[goal-tick] all task(s) done \u2014
|
|
6934
|
+
process.stdout.write(`[goal-tick] all task(s) done \u2014 preparing deliverable PR for goal ${goal.id}
|
|
6933
6935
|
`);
|
|
6934
6936
|
const leaf = goal.leafPr;
|
|
6935
6937
|
if (!leaf) {
|
|
6936
|
-
process.stdout.write(`[goal-tick] no leaf PR \u2014 marking goal done without
|
|
6938
|
+
process.stdout.write(`[goal-tick] no leaf PR \u2014 marking goal done without a deliverable PR
|
|
6937
6939
|
`);
|
|
6938
6940
|
goal.state = "done";
|
|
6939
6941
|
return;
|
|
@@ -6951,7 +6953,7 @@ var finalizeGoal = async (ctx) => {
|
|
|
6951
6953
|
}
|
|
6952
6954
|
}
|
|
6953
6955
|
if (leaf.isDraft) {
|
|
6954
|
-
process.stdout.write(`[goal-tick] promoting draft leaf PR #${leaf.number} \u2192 ready
|
|
6956
|
+
process.stdout.write(`[goal-tick] promoting draft leaf PR #${leaf.number} \u2192 ready-for-review
|
|
6955
6957
|
`);
|
|
6956
6958
|
const ready = markPrReady(leaf.number, ctx.cwd);
|
|
6957
6959
|
if (!ready.ok) {
|
|
@@ -6961,22 +6963,16 @@ var finalizeGoal = async (ctx) => {
|
|
|
6961
6963
|
}
|
|
6962
6964
|
}
|
|
6963
6965
|
process.stdout.write(
|
|
6964
|
-
`[goal-tick]
|
|
6966
|
+
`[goal-tick] leaf PR #${leaf.number} is the deliverable (cumulative goal diff vs ${goal.defaultBranch}) \u2014 left open for human merge
|
|
6965
6967
|
`
|
|
6966
6968
|
);
|
|
6967
|
-
const merged = mergePrSquash(leaf.number, ctx.cwd);
|
|
6968
|
-
if (!merged.ok) {
|
|
6969
|
-
process.stderr.write(`[goal-tick] finalizeGoal: mergePrSquash #${leaf.number} failed: ${merged.error}
|
|
6970
|
-
`);
|
|
6971
|
-
return;
|
|
6972
|
-
}
|
|
6973
6969
|
const others = (goal.openTaskPrs ?? []).filter((p) => p.number !== leaf.number);
|
|
6974
6970
|
for (const pr of others) {
|
|
6975
|
-
process.stdout.write(`[goal-tick] closing intermediate stacked PR #${pr.number} (
|
|
6971
|
+
process.stdout.write(`[goal-tick] closing intermediate stacked PR #${pr.number} (carried by deliverable leaf)
|
|
6976
6972
|
`);
|
|
6977
6973
|
const closed = closePr(
|
|
6978
6974
|
pr.number,
|
|
6979
|
-
`_Stacked-PR finalize: this PR's content is
|
|
6975
|
+
`_Stacked-PR finalize: this PR's content is carried by the goal's single deliverable PR #${leaf.number} (open against \`${goal.defaultBranch}\`, awaiting human merge)._`,
|
|
6980
6976
|
ctx.cwd
|
|
6981
6977
|
);
|
|
6982
6978
|
if (!closed.ok) {
|
|
@@ -6986,12 +6982,12 @@ var finalizeGoal = async (ctx) => {
|
|
|
6986
6982
|
}
|
|
6987
6983
|
const openIssues = (goal.childTasks ?? []).filter((t) => t.state === "OPEN");
|
|
6988
6984
|
for (const t of openIssues) {
|
|
6989
|
-
process.stdout.write(`[goal-tick] closing task issue #${t.number} (goal finalized)
|
|
6985
|
+
process.stdout.write(`[goal-tick] closing task issue #${t.number} (goal finalized \u2014 carried by PR #${leaf.number})
|
|
6990
6986
|
`);
|
|
6991
6987
|
const closed = closeIssue(
|
|
6992
6988
|
t.number,
|
|
6993
6989
|
{
|
|
6994
|
-
comment: `_Goal \`${goal.id}\` finalized \u2014
|
|
6990
|
+
comment: `_Goal \`${goal.id}\` finalized \u2014 its single deliverable PR #${leaf.number} (open against \`${goal.defaultBranch}\`) carries this task's changes. Merge that PR to ship._`,
|
|
6995
6991
|
reason: "completed"
|
|
6996
6992
|
},
|
|
6997
6993
|
ctx.cwd
|
|
@@ -8357,23 +8353,6 @@ QA_REPORT_POSTED=${created.url} (verdict: ${verdict})
|
|
|
8357
8353
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
8358
8354
|
};
|
|
8359
8355
|
|
|
8360
|
-
// src/scripts/parkGoalForMerge.ts
|
|
8361
|
-
var parkGoalForMerge = async (ctx) => {
|
|
8362
|
-
const goal = ctx.data.goal;
|
|
8363
|
-
if (!goal) return;
|
|
8364
|
-
const approved = goal.raw?.mergeApproved === true;
|
|
8365
|
-
if (approved) {
|
|
8366
|
-
process.stdout.write(`[goal-tick] goal ${goal.id}: merge approved \u2014 running finalize (one-shot)
|
|
8367
|
-
`);
|
|
8368
|
-
if (goal.raw) goal.raw.mergeApproved = false;
|
|
8369
|
-
return;
|
|
8370
|
-
}
|
|
8371
|
-
process.stdout.write(`[goal-tick] all task(s) done \u2014 parking goal ${goal.id} for manual merge (no auto-merge)
|
|
8372
|
-
`);
|
|
8373
|
-
goal.state = "awaiting-merge";
|
|
8374
|
-
goal.phase = "awaiting-merge";
|
|
8375
|
-
};
|
|
8376
|
-
|
|
8377
8356
|
// src/scripts/parseAgentResult.ts
|
|
8378
8357
|
init_prompt();
|
|
8379
8358
|
var parseAgentResult2 = async (ctx, profile, agentResult) => {
|
|
@@ -10578,7 +10557,6 @@ var preflightScripts = {
|
|
|
10578
10557
|
handleAbandonedGoal,
|
|
10579
10558
|
deriveGoalPhase,
|
|
10580
10559
|
dispatchNextTask,
|
|
10581
|
-
parkGoalForMerge,
|
|
10582
10560
|
finalizeGoal,
|
|
10583
10561
|
saveGoalState
|
|
10584
10562
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goal-tick",
|
|
3
3
|
"role": "primitive",
|
|
4
|
-
"describe": "One deterministic tick for one goal in the stacked-PR model: read .kody/goals/<id>/state.json, dispatch @kody on the next ready task (stacked on the leaf PR), or — when all tasks are done —
|
|
4
|
+
"describe": "One deterministic tick for one goal in the stacked-PR model: read .kody/goals/<id>/state.json, dispatch @kody on the next ready task (stacked on the leaf PR), or — when all tasks are done — finalize by preparing the leaf as a single review-ready PR (retarget to default branch, mark ready, close intermediate PRs/issues) and leave it OPEN for a human to merge. The engine never auto-merges. No agent.",
|
|
5
5
|
"kind": "oneshot",
|
|
6
6
|
"inputs": [
|
|
7
7
|
{
|
|
@@ -51,10 +51,6 @@
|
|
|
51
51
|
"script": "deriveGoalPhase",
|
|
52
52
|
"runWhen": { "data.goal.state": "active" }
|
|
53
53
|
},
|
|
54
|
-
{
|
|
55
|
-
"script": "parkGoalForMerge",
|
|
56
|
-
"runWhen": { "data.goal.phase": "all-done" }
|
|
57
|
-
},
|
|
58
54
|
{
|
|
59
55
|
"script": "finalizeGoal",
|
|
60
56
|
"runWhen": { "data.goal.phase": "all-done" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.87",
|
|
4
4
|
"description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Goal Manager
|
|
2
|
+
|
|
3
|
+
Autonomous manager for **manager-driven goals**. One worker, every goal:
|
|
4
|
+
each tick it picks up every goal flagged `managed`, breaks it into tasks,
|
|
5
|
+
lets the deterministic `goal-tick` execute them, verifies the
|
|
6
|
+
end-to-end user journey with `qa-engineer`, recovers stalled tasks, and
|
|
7
|
+
stops a goal only when its single deliverable PR is open and the journey
|
|
8
|
+
passes. It never edits code and never merges anything.
|
|
9
|
+
|
|
10
|
+
> Drop this file at `.kody/workers/goal-manager.md` in a consumer repo.
|
|
11
|
+
> The `worker-scheduler` cron invokes `worker-tick` on it automatically.
|
|
12
|
+
|
|
13
|
+
## Worker
|
|
14
|
+
|
|
15
|
+
You coordinate goals **only** through `gh`. You do not write code, open
|
|
16
|
+
PRs, or merge. Everything below is per-goal; you handle all goals in one
|
|
17
|
+
tick.
|
|
18
|
+
|
|
19
|
+
### 1. Discover managed goals
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
ls .kody/goals/*/state.json 2>/dev/null
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For each, read it (`cat`). A goal is **in scope** when BOTH:
|
|
26
|
+
|
|
27
|
+
- `.state == "active"`, and
|
|
28
|
+
- `.managed == true` (the dashboard's "Let Kody manage this goal" toggle
|
|
29
|
+
writes this; absent/false → ignore the goal entirely).
|
|
30
|
+
|
|
31
|
+
For an in-scope goal, the goal **id** is the directory name
|
|
32
|
+
(`.kody/goals/<id>/state.json`). Its definition (intent + the ordered
|
|
33
|
+
end-to-end user journey that defines "done") is, in priority order:
|
|
34
|
+
|
|
35
|
+
1. `.journey` / `.description` / `.intent` / `.title` fields in
|
|
36
|
+
`state.json` if present, else
|
|
37
|
+
2. the goal's GitHub Discussion body — if `state.json` has a
|
|
38
|
+
`discussionNumber`, fetch it:
|
|
39
|
+
`gh api graphql -f query='{repository(owner:"OWNER",name:"REPO"){discussion(number:N){body title}}}'`
|
|
40
|
+
(resolve OWNER/REPO from `gh repo view --json owner,name`).
|
|
41
|
+
|
|
42
|
+
The **journey** is the acceptance test. If you cannot find any journey
|
|
43
|
+
text, do NOT guess — escalate the goal (see §5) with reason
|
|
44
|
+
`no-journey-defined` and skip it.
|
|
45
|
+
|
|
46
|
+
### 2. Per-goal cursor
|
|
47
|
+
|
|
48
|
+
Keep all per-goal progress under `data.goals[<id>]`. Each entry:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
{ "cursor": "...", "tasks": [<issue numbers you created>],
|
|
52
|
+
"gate": <qa-gate issue number|null>, "qaRound": <int>,
|
|
53
|
+
"stall": { "<issueNumber>": { "ticks": <int>, "attempts": <int> } },
|
|
54
|
+
"lastQaTick": "<ISO>" }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`cursor` ∈ `new | building | qa | finalizing | done | escalated`.
|
|
58
|
+
Missing entry ⇒ treat as `new`.
|
|
59
|
+
|
|
60
|
+
### 3. Decompose (`cursor: new`)
|
|
61
|
+
|
|
62
|
+
One-time setup for the goal:
|
|
63
|
+
|
|
64
|
+
1. Ensure labels exist (idempotent — ignore "already exists"):
|
|
65
|
+
`gh label create "goal:<id>" --color 5319e7 2>/dev/null || true`
|
|
66
|
+
`gh label create "kody:qa-gate" --color b60205 2>/dev/null || true`
|
|
67
|
+
`gh label create "kody:needs-human" --color d93f0b 2>/dev/null || true`
|
|
68
|
+
2. Break the goal into the **smallest correct set of sequential tasks**.
|
|
69
|
+
For each, create an issue:
|
|
70
|
+
`gh issue create --title "<task>" --label "goal:<id>" --body "<precise spec, acceptance criteria>"`
|
|
71
|
+
Record the returned issue numbers in `tasks`. Order matters — the
|
|
72
|
+
stacked-PR model builds them lowest-number first.
|
|
73
|
+
3. Create the **QA gate** issue (exactly one per goal):
|
|
74
|
+
`gh issue create --title "QA gate: <id> — end-to-end journey" --label "goal:<id>" --label "kody:qa-gate" --body "<the full ordered journey, step 1 → success state>"`
|
|
75
|
+
Record its number in `gate`.
|
|
76
|
+
4. Set `cursor: building`. **Do not** dispatch anything yourself —
|
|
77
|
+
`goal-tick` (its own cron) sees the new `goal:<id>` issues and drives
|
|
78
|
+
them. The open `kody:qa-gate` issue keeps the goal short of
|
|
79
|
+
`all-done`, so nothing finalizes until you close it in §4.
|
|
80
|
+
|
|
81
|
+
### 4. Drive + recover (`cursor: building`)
|
|
82
|
+
|
|
83
|
+
Each tick, for the goal:
|
|
84
|
+
|
|
85
|
+
- Enumerate tasks:
|
|
86
|
+
`gh issue list --label "goal:<id>" --state all --json number,state,title`
|
|
87
|
+
and the PRs:
|
|
88
|
+
`gh pr list --state open --json number,headRefName,body,isDraft`.
|
|
89
|
+
A task is **done** when its issue is CLOSED or it has a ready
|
|
90
|
+
(non-draft) open PR (`Closes #<n>` in body, or head ref `^<n>-`).
|
|
91
|
+
- **Stall detection** (per non-gate, non-done task):
|
|
92
|
+
- Increment `stall[n].ticks`.
|
|
93
|
+
- If the task has **no open PR** and `stall[n].ticks >= 3`: it was
|
|
94
|
+
never picked up — re-nudge: `gh issue comment <n> --body "@kody"`,
|
|
95
|
+
`stall[n].attempts += 1`, reset `stall[n].ticks = 0`.
|
|
96
|
+
- If the task has a **draft PR with no new commits** for
|
|
97
|
+
`stall[n].ticks >= 4`: comment `@kody continue` on the PR, bump
|
|
98
|
+
attempts, reset ticks.
|
|
99
|
+
- When `stall[n].attempts >= 3` for any task → escalate (§5) and set
|
|
100
|
+
`cursor: escalated`.
|
|
101
|
+
- Clear `stall[n]` once the task is done.
|
|
102
|
+
- When **every non-gate task is done** → `cursor: qa`.
|
|
103
|
+
|
|
104
|
+
### 4b. Verify the journey (`cursor: qa`)
|
|
105
|
+
|
|
106
|
+
- Throttle: skip if `lastQaTick` is < 20 min ago (avoid stacking QA
|
|
107
|
+
runs); otherwise:
|
|
108
|
+
- Trigger QA against the journey by commenting on the **gate issue**:
|
|
109
|
+
`gh issue comment <gate> --body "@kody qa-engineer --goal <id> --scope \"<the full journey text>\""`
|
|
110
|
+
Set `lastQaTick` = now, `qaRound += 1`.
|
|
111
|
+
- `qa-engineer` browses the running app and files **each failure as a
|
|
112
|
+
new `goal:<id>` task issue** automatically. So on the **next** tick:
|
|
113
|
+
- If new `goal:<id>` task issues appeared after this `qaRound` (issues
|
|
114
|
+
you didn't create / not in `tasks`) → add them to `tasks`, set
|
|
115
|
+
`cursor: building` (the loop fixes them, then QA re-runs).
|
|
116
|
+
- If no new task issues and the latest `qa-engineer` report comment on
|
|
117
|
+
the gate issue is a **PASS** (read it:
|
|
118
|
+
`gh issue view <gate> --json comments`) → the journey works:
|
|
119
|
+
close the gate `gh issue close <gate> --comment "QA passed — journey verified."`,
|
|
120
|
+
set `cursor: finalizing`.
|
|
121
|
+
- If the report is ambiguous/unreachable and `qaRound >= 4` →
|
|
122
|
+
escalate (§5).
|
|
123
|
+
|
|
124
|
+
### 4c. Finish (`cursor: finalizing`)
|
|
125
|
+
|
|
126
|
+
With the gate closed, `goal-tick` observes `all-done`, prepares the
|
|
127
|
+
**single open deliverable PR** (cumulative diff vs the default branch,
|
|
128
|
+
review-ready), closes the task issues, and sets `state: "done"` in
|
|
129
|
+
`state.json`. You do **not** merge — a human merges that PR.
|
|
130
|
+
|
|
131
|
+
- When `state.json.state == "done"` (or the deliverable PR is open and
|
|
132
|
+
task issues are closed): post one summary comment on the gate issue or
|
|
133
|
+
the discussion linking the deliverable PR, set `cursor: done`.
|
|
134
|
+
|
|
135
|
+
### 5. Escalation
|
|
136
|
+
|
|
137
|
+
Escalate by: commenting the reason on the goal's most relevant artifact
|
|
138
|
+
(gate issue if it exists, else the highest-numbered task issue), adding
|
|
139
|
+
the `kody:needs-human` label to that issue, and setting
|
|
140
|
+
`cursor: escalated`. On later ticks, re-check an escalated goal: if a
|
|
141
|
+
human removed `kody:needs-human` or the blocking condition cleared,
|
|
142
|
+
resume at the appropriate cursor.
|
|
143
|
+
|
|
144
|
+
### 6. `cursor: done | escalated`
|
|
145
|
+
|
|
146
|
+
No action. Keep the entry so you don't reprocess it. (These are per-goal;
|
|
147
|
+
the **worker** is evergreen — never emit worker-level `done: true`.)
|
|
148
|
+
|
|
149
|
+
## Allowed Commands
|
|
150
|
+
|
|
151
|
+
`gh` only: `gh issue ...`, `gh pr ...`, `gh label ...`, `gh api ...`,
|
|
152
|
+
`gh repo view`. Read-only shell (`ls`, `cat`) on the checked-out tree to
|
|
153
|
+
read `.kody/goals/*/state.json`.
|
|
154
|
+
|
|
155
|
+
## Restrictions
|
|
156
|
+
|
|
157
|
+
- Never edit, create, or delete files in the working tree (no code, no
|
|
158
|
+
`state.json` writes — the gate is GitHub-side via the qa-gate issue).
|
|
159
|
+
- Never run `git`. Never merge or close a PR. Never mark a PR ready.
|
|
160
|
+
- Never `@kody` a `kody:qa-gate` issue except the one `qa-engineer`
|
|
161
|
+
trigger in §4b.
|
|
162
|
+
- One QA trigger per `qaRound`; honour the 20-min throttle.
|
|
163
|
+
- Do not dispatch implementation tasks yourself — only create the issues;
|
|
164
|
+
`goal-tick` dispatches. Your only `@kody` comments are stall re-nudges
|
|
165
|
+
(§4) and the `qa-engineer` trigger (§4b).
|
|
166
|
+
|
|
167
|
+
## State
|
|
168
|
+
|
|
169
|
+
`cursor` (worker-level): always `"managing"` (evergreen). All real
|
|
170
|
+
progress lives in `data.goals[<id>]` as described in §2. `done`: always
|
|
171
|
+
`false`.
|
|
172
|
+
|
|
173
|
+
**Cadence guard.** If `forceRun` is false and your last tick was < 4 min
|
|
174
|
+
ago (compare a `data.lastTick` ISO you maintain), emit the prior state
|
|
175
|
+
unchanged and exit. Always update `data.lastTick` when you do run.
|