loki-mode 9.55.0 → 9.57.0
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/.github/actions/issue-to-pr/action.yml +165 -29
- package/.github/workflows/loki-issue-to-pr.yml +137 -24
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/proof-check.sh +9 -0
- package/autonomy/lib/proof-generator.py +12 -3
- package/autonomy/lib/workspace_diff.py +18 -7
- package/autonomy/loki +133 -3
- package/autonomy/run.sh +385 -22
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_keys.py +12 -5
- package/dashboard/build_supervisor.py +25 -1
- package/dashboard/server.py +46 -24
- package/dashboard/static/index.html +57 -57
- package/docs/VERIFIED-COMPLETION-PLAN.md +7 -5
- package/docs/dev/architecture-reference.md +99 -0
- package/docs/dev/project-structure.md +134 -0
- package/docs/dev/release-checklist.md +157 -0
- package/docs/dev/research-foundation.md +29 -0
- package/docs/dev/tmp-cleanup.md +157 -0
- package/docs/environment-variables.md +1 -0
- package/docs/v10/BACKLOG.md +33 -0
- package/docs/v10/BOARD.md +97 -27
- package/docs/v10/DECISIONS.md +43 -0
- package/docs/v10/OPERATING-MODEL.md +47 -0
- package/docs/v10/PROGRESS.md +893 -0
- package/loki-ts/dist/loki.js +326 -325
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +1 -1
- package/providers/model_catalog.json +12 -5
- package/providers/models.sh +1 -1
- package/references/core-workflow.md +1 -1
- package/web-app/dist/assets/{AdminPage-CZrBkzRF.js → AdminPage-Oyx-w2Ch.js} +1 -1
- package/web-app/dist/assets/{Avatar-gQIp6FBI.js → Avatar-D60m65JB.js} +1 -1
- package/web-app/dist/assets/{Badge-D2_fBTFY.js → Badge-CqfZNaem.js} +1 -1
- package/web-app/dist/assets/{Button-D5qDd2ef.js → Button-BqjzE4fz.js} +1 -1
- package/web-app/dist/assets/CockpitPage-Czk7o_ou.js +47 -0
- package/web-app/dist/assets/ComparePage-BEfTrTb1.js +1 -0
- package/web-app/dist/assets/{ErrorBoundary-BRABI-BT.js → ErrorBoundary-BrLk5nkO.js} +1 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-9vEpi5ir.js +1 -0
- package/web-app/dist/assets/{GitHubIssuesPanel-BC5GzKUN.js → GitHubIssuesPanel-D6izZx0w.js} +1 -1
- package/web-app/dist/assets/GitHubPRsPanel-BTkG2FgA.js +17 -0
- package/web-app/dist/assets/{HomePage-D_CDMoen.js → HomePage-nsr4kpN1.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BBNA_N9t.js → LoginPage-BVblQrr9.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CnFRZCMx.js → MagicPage-CmnSyLq7.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CPfLMRlh.js → MetricsPage-BVfwU81j.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-lA0XSGTz.js → NotFoundPage-AYQ6uqqI.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CNAPbr9z.js → ProjectPage-DMnbw9X9.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CMtm8goN.js → ProjectsPage-CNVTAF2N.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-BhIjVMYE.js → SettingsPage-D1_qasFD.js} +3 -3
- package/web-app/dist/assets/{ShowcasePage-BndtGeYo.js → ShowcasePage-2dBCjLXn.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-euXI-KXE.js → SystemSettingsPage-Djo6IE4u.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-ClUKecun.js → TeamsPage-CY8p-yxD.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-BWHWVk0L.js → TemplatesPage-B6iFgH6t.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cv3N3Szf.js → TerminalOutput-B3yzZe-h.js} +1 -1
- package/web-app/dist/assets/{activity-dEhz-bXR.js → activity-CNfAjtRv.js} +1 -1
- package/web-app/dist/assets/{bell-B3XUMq1t.js → bell-CMETsHPH.js} +1 -1
- package/web-app/dist/assets/{bot-X-ssNlwR.js → bot-C9kXvk1i.js} +1 -1
- package/web-app/dist/assets/{check-C_9Nx7kb.js → check-BRK-zLB5.js} +1 -1
- package/web-app/dist/assets/{chevron-left-tvNswo3A.js → chevron-left-CxI5uD4g.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CSB88nEs.js → circle-alert-CVFRPWpv.js} +1 -1
- package/web-app/dist/assets/{clock-C8p2e4NI.js → clock-CLYV8a_x.js} +1 -1
- package/web-app/dist/assets/{cloud-CKp2ZvhG.js → cloud-CiISiIOa.js} +1 -1
- package/web-app/dist/assets/{code-xml-dY70LCHD.js → code-xml-DHsgqwV-.js} +1 -1
- package/web-app/dist/assets/{database-DObn5fmn.js → database-DbvUqtz3.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DSu6n3K3.js → dollar-sign-Ct0lRPHw.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DpCnS73z.js → file-code-corner-D1sjFEh7.js} +1 -1
- package/web-app/dist/assets/{file-plus-D3IPKCf1.js → file-plus-DnmX18Fi.js} +1 -1
- package/web-app/dist/assets/{globe-C9QXkgH_.js → globe-bSpDNgoA.js} +1 -1
- package/web-app/dist/assets/{hammer-D_FtWe1y.js → hammer-Bj41Kvlq.js} +1 -1
- package/web-app/dist/assets/{index-CI54rara.css → index-BfQ0f3-S.css} +1 -1
- package/web-app/dist/assets/{index-Cz7q1bvC.js → index-CxajNJVu.js} +3 -3
- package/web-app/dist/assets/{layers-DsW7jnJX.js → layers-BNtBwYER.js} +1 -1
- package/web-app/dist/assets/{loader-circle-B3HspjKR.js → loader-circle-YY_VZofG.js} +1 -1
- package/web-app/dist/assets/{lock-DSI29pct.js → lock-rpjEPVoC.js} +1 -1
- package/web-app/dist/assets/{package-CLNxtA2x.js → package-desXU6XA.js} +1 -1
- package/web-app/dist/assets/{plus-DyI4WC4_.js → plus-7AoOU-I9.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D2dv0xA0.js → refresh-cw-oNj-Xn4B.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CBvihA-a.js → rotate-ccw-DeiFjfVO.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DvoOvAl_.js → scroll-text-Q9AzWiNV.js} +1 -1
- package/web-app/dist/assets/{server-DenWvSUt.js → server-DKgVwfek.js} +1 -1
- package/web-app/dist/assets/{shield-alert-40cX_FvG.js → shield-alert-G6x-bS-I.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-BrjZ6QuV.js → thumbs-up-Qhk0yvw0.js} +1 -1
- package/web-app/dist/assets/{trash-2-C6evwkEH.js → trash-2-D23gvznl.js} +1 -1
- package/web-app/dist/assets/{trending-down-CfbZis3O.js → trending-down-CSQClPrr.js} +1 -1
- package/web-app/dist/assets/{trending-up-DWuEK3WC.js → trending-up-sjMSd1wc.js} +1 -1
- package/web-app/dist/assets/{upload-B9DFBcPG.js → upload-Dchovhld.js} +1 -1
- package/web-app/dist/assets/{usePolling-BryVC-T1.js → usePolling-DwLceXwF.js} +1 -1
- package/web-app/dist/assets/{user-DMRXwZiw.js → user-BqZM8CmD.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +52 -17
- package/web-app/dist/assets/CockpitPage-DVMk2RUK.js +0 -47
- package/web-app/dist/assets/ComparePage-NMLyE1xL.js +0 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-D2LNKqUx.js +0 -1
- package/web-app/dist/assets/GitHubPRsPanel-BSlmoVTS.js +0 -17
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
name: 'Loki Issue to PR'
|
|
2
|
-
description: 'Hand Loki Mode a GitHub issue; it
|
|
2
|
+
description: 'Hand Loki Mode a GitHub issue; it produces a patch and a PR body with a verifiable evidence receipt. Open the PR from a separate job that runs no agent.'
|
|
3
3
|
author: 'asklokesh'
|
|
4
4
|
|
|
5
5
|
branding:
|
|
6
6
|
icon: 'git-pull-request'
|
|
7
7
|
color: 'purple'
|
|
8
8
|
|
|
9
|
+
# Rule of Two: the issue text is untrusted, so the job that runs this action
|
|
10
|
+
# must not hold a write token. Run it with `permissions: contents: read,
|
|
11
|
+
# issues: read` and `actions/checkout` with `persist-credentials: false`, upload
|
|
12
|
+
# `outputs.patch`, and push and open the PR from a second job that runs no
|
|
13
|
+
# agent. .github/workflows/loki-issue-to-pr.yml in asklokesh/loki-mode is the
|
|
14
|
+
# reference. Earlier versions pushed and opened the PR from the same step that
|
|
15
|
+
# ran the agent; this one does not.
|
|
16
|
+
#
|
|
17
|
+
# Token co-location within THIS job: the fetch step below holds GH_TOKEN and
|
|
18
|
+
# runs no agent; the agent step runs `loki start` on the pre-fetched PRD file
|
|
19
|
+
# and is given no token (GH_TOKEN/GITHUB_TOKEN/GH_ENTERPRISE_TOKEN/
|
|
20
|
+
# GITHUB_ENTERPRISE_TOKEN all explicitly blanked, overriding anything the
|
|
21
|
+
# caller's `uses:` step set at a level this composite's steps would otherwise
|
|
22
|
+
# inherit). This narrows exposure to the same-step case (a prompt-injected
|
|
23
|
+
# agent invoking `gh`/git with the token directly) but does NOT provide
|
|
24
|
+
# process isolation: fetch and agent steps still share one runner VM and one
|
|
25
|
+
# Actions Runner.Worker process holding the job token in memory, reachable via
|
|
26
|
+
# /proc/<pid>/environ or ptrace with the passwordless sudo hosted runners
|
|
27
|
+
# grant (see the March 2025 tj-actions/changed-files compromise, which used
|
|
28
|
+
# exactly this). The real isolation boundary is the caller's job split: keep
|
|
29
|
+
# this job's permissions read-only (contents: read, issues: read) and push
|
|
30
|
+
# from a separate job/runner, as the reference workflow does.
|
|
31
|
+
|
|
9
32
|
inputs:
|
|
10
33
|
issue:
|
|
11
34
|
description: 'Issue reference. Defaults to the issue that triggered this workflow.'
|
|
@@ -29,13 +52,29 @@ inputs:
|
|
|
29
52
|
default: 'latest'
|
|
30
53
|
|
|
31
54
|
outputs:
|
|
55
|
+
patch:
|
|
56
|
+
description: 'Path to the patch Loki committed on top of the checked-out commit (empty file when nothing changed).'
|
|
57
|
+
value: ${{ steps.run.outputs.patch }}
|
|
58
|
+
pr_body:
|
|
59
|
+
description: 'Path to a PR body carrying the evidence receipt.'
|
|
60
|
+
value: ${{ steps.run.outputs.pr_body }}
|
|
32
61
|
pr_url:
|
|
33
|
-
description: '
|
|
34
|
-
value:
|
|
62
|
+
description: 'Always empty. This action no longer opens the pull request; open it from a job that runs no agent.'
|
|
63
|
+
value: ''
|
|
35
64
|
|
|
36
65
|
runs:
|
|
37
66
|
using: 'composite'
|
|
38
67
|
steps:
|
|
68
|
+
- name: Refuse a checkout that left a push token behind
|
|
69
|
+
shell: bash
|
|
70
|
+
run: |
|
|
71
|
+
# actions/checkout persists the job token in git config unless told
|
|
72
|
+
# not to. The agent could push with it, whatever this action does.
|
|
73
|
+
if git config --get-regexp '^http\..*\.extraheader$' >/dev/null 2>&1; then
|
|
74
|
+
echo "::error::actions/checkout left a token in git config that the agent could push with. Set 'persist-credentials: false' on the checkout step of this job."
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
39
78
|
- name: Install Loki Mode
|
|
40
79
|
shell: bash
|
|
41
80
|
run: npm install -g loki-mode@${{ inputs.version }}
|
|
@@ -46,7 +85,7 @@ runs:
|
|
|
46
85
|
run: |
|
|
47
86
|
# Prefer the explicit input; otherwise use the triggering issue. Failing
|
|
48
87
|
# CLOSED here is deliberate: a run with no issue would otherwise start
|
|
49
|
-
# from the repository's own state and produce an unrelated
|
|
88
|
+
# from the repository's own state and produce an unrelated change.
|
|
50
89
|
REF="${{ inputs.issue }}"
|
|
51
90
|
if [ -z "$REF" ]; then
|
|
52
91
|
NUM="${{ github.event.issue.number }}"
|
|
@@ -59,45 +98,142 @@ runs:
|
|
|
59
98
|
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
|
60
99
|
echo "Resolved issue reference: $REF"
|
|
61
100
|
|
|
62
|
-
- name:
|
|
63
|
-
id:
|
|
101
|
+
- name: Fetch the issue (holds the token, runs no agent)
|
|
102
|
+
id: fetch
|
|
64
103
|
shell: bash
|
|
65
104
|
env:
|
|
66
|
-
#
|
|
67
|
-
#
|
|
105
|
+
# Used only to read the issue in this step. Never exported to the
|
|
106
|
+
# agent step below.
|
|
68
107
|
GH_TOKEN: ${{ github.token }}
|
|
108
|
+
ISSUE_REF: ${{ steps.ref.outputs.ref }}
|
|
109
|
+
run: |
|
|
110
|
+
set -euo pipefail
|
|
111
|
+
|
|
112
|
+
case "$ISSUE_REF" in
|
|
113
|
+
*/*'#'*) : ;;
|
|
114
|
+
*) echo "::error::Malformed issue reference: $ISSUE_REF (expected owner/repo#number)"; exit 1 ;;
|
|
115
|
+
esac
|
|
116
|
+
REPO="${ISSUE_REF%%#*}"
|
|
117
|
+
NUMBER="${ISSUE_REF##*#}"
|
|
118
|
+
case "$NUMBER" in
|
|
119
|
+
''|*[!0-9]*) echo "::error::Issue number must be digits, got '$NUMBER' from '$ISSUE_REF'"; exit 1 ;;
|
|
120
|
+
esac
|
|
121
|
+
|
|
122
|
+
# Same fields as fetch_github_issue() in autonomy/issue-providers.sh:
|
|
123
|
+
# number,title,body,labels,author,createdAt,url. No comments -- the
|
|
124
|
+
# PRD the agent works from carries the same content the CLI's own
|
|
125
|
+
# issue-mode path would have handed it, nothing wider.
|
|
126
|
+
ISSUE_JSON="$(gh issue view "$NUMBER" --repo "$REPO" \
|
|
127
|
+
--json number,title,body,labels,author,createdAt,url)"
|
|
128
|
+
|
|
129
|
+
OUT="$RUNNER_TEMP/loki-out"
|
|
130
|
+
mkdir -p "$OUT"
|
|
131
|
+
PRD_FILE="$OUT/prd-issue-${NUMBER}.md"
|
|
132
|
+
|
|
133
|
+
REPO="$REPO" ISSUE_JSON="$ISSUE_JSON" PRD_FILE="$PRD_FILE" python3 -c "
|
|
134
|
+
import json, os
|
|
135
|
+
|
|
136
|
+
data = json.loads(os.environ['ISSUE_JSON'])
|
|
137
|
+
repo = os.environ['REPO']
|
|
138
|
+
title = data.get('title', '')
|
|
139
|
+
body = data.get('body') or ''
|
|
140
|
+
labels = ', '.join(l['name'] for l in data.get('labels', []))
|
|
141
|
+
author = (data.get('author') or {}).get('login', '')
|
|
142
|
+
url = data.get('url', '')
|
|
143
|
+
created_at = data.get('createdAt', '')
|
|
144
|
+
number = data.get('number', '')
|
|
145
|
+
|
|
146
|
+
lines = [
|
|
147
|
+
'# PRD: ' + title,
|
|
148
|
+
'',
|
|
149
|
+
'**Source:** GitHub Issue [#' + str(number) + '](' + url + ')',
|
|
150
|
+
'**Author:** @' + author,
|
|
151
|
+
'**Created:** ' + created_at,
|
|
152
|
+
'**Repository:** ' + repo,
|
|
153
|
+
]
|
|
154
|
+
if labels:
|
|
155
|
+
lines.append('**Labels:** ' + labels)
|
|
156
|
+
lines += [
|
|
157
|
+
'',
|
|
158
|
+
'---',
|
|
159
|
+
'',
|
|
160
|
+
'## Overview',
|
|
161
|
+
'',
|
|
162
|
+
body,
|
|
163
|
+
'',
|
|
164
|
+
'---',
|
|
165
|
+
'',
|
|
166
|
+
'## Acceptance Criteria',
|
|
167
|
+
'',
|
|
168
|
+
'Based on the issue description, implement the following:',
|
|
169
|
+
'',
|
|
170
|
+
'1. Address all requirements specified in the issue body above',
|
|
171
|
+
'2. Ensure backward compatibility (unless explicitly breaking changes are requested)',
|
|
172
|
+
'3. Add appropriate tests for new functionality',
|
|
173
|
+
'4. Update documentation as needed',
|
|
174
|
+
'',
|
|
175
|
+
'---',
|
|
176
|
+
'',
|
|
177
|
+
'## References',
|
|
178
|
+
'',
|
|
179
|
+
'- Original Issue: ' + url,
|
|
180
|
+
'',
|
|
181
|
+
]
|
|
182
|
+
with open(os.environ['PRD_FILE'], 'w') as f:
|
|
183
|
+
f.write(chr(10).join(lines))
|
|
184
|
+
"
|
|
185
|
+
echo "prd=$PRD_FILE" >> "$GITHUB_OUTPUT"
|
|
186
|
+
echo "issue_number=$NUMBER" >> "$GITHUB_OUTPUT"
|
|
187
|
+
|
|
188
|
+
- name: Resolve the issue to a patch (no token in this step's environment)
|
|
189
|
+
id: run
|
|
190
|
+
shell: bash
|
|
191
|
+
env:
|
|
69
192
|
LOKI_PROVIDER: ${{ inputs.provider }}
|
|
70
193
|
LOKI_BUDGET_LIMIT: ${{ inputs.budget_limit }}
|
|
71
194
|
LOKI_MAX_ITERATIONS: ${{ inputs.max_iterations }}
|
|
72
|
-
# The
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
195
|
+
# The agent step never pushes or opens a PR (Rule of Two).
|
|
196
|
+
LOKI_DELEGATE_PR: '0'
|
|
197
|
+
ISSUE_REF: ${{ steps.ref.outputs.ref }}
|
|
198
|
+
PRD_FILE: ${{ steps.fetch.outputs.prd }}
|
|
199
|
+
# Explicitly blanked, not merely omitted: this overrides any value the
|
|
200
|
+
# caller's `uses:` step set, which a composite action's steps would
|
|
201
|
+
# otherwise inherit. These are the same four run.sh withholds
|
|
202
|
+
# (_loki_withhold_github_tokens).
|
|
203
|
+
GH_TOKEN: ''
|
|
204
|
+
GITHUB_TOKEN: ''
|
|
205
|
+
GH_ENTERPRISE_TOKEN: ''
|
|
206
|
+
GITHUB_ENTERPRISE_TOKEN: ''
|
|
76
207
|
run: |
|
|
77
208
|
set -uo pipefail
|
|
78
209
|
|
|
79
|
-
# git identity:
|
|
210
|
+
# git identity: Loki commits the session's work on its own branch.
|
|
80
211
|
git config user.name "${GITHUB_ACTOR:-loki-mode}"
|
|
81
212
|
git config user.email "${GITHUB_ACTOR:-loki-mode}@users.noreply.github.com"
|
|
82
213
|
|
|
83
|
-
|
|
84
|
-
RC=
|
|
85
|
-
|
|
86
|
-
# Report the PR if one was opened. Read it from the state Loki writes
|
|
87
|
-
# rather than re-deriving it, so this reports what ACTUALLY happened.
|
|
88
|
-
PR_URL=""
|
|
89
|
-
if [ -f .loki/state/pr-url.txt ]; then
|
|
90
|
-
PR_URL="$(cat .loki/state/pr-url.txt 2>/dev/null || true)"
|
|
91
|
-
fi
|
|
92
|
-
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
|
|
214
|
+
START_SHA="$(git rev-parse HEAD)"
|
|
215
|
+
RC=0
|
|
216
|
+
loki start "$PRD_FILE" || RC=$?
|
|
93
217
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
218
|
+
OUT="$RUNNER_TEMP/loki-out"
|
|
219
|
+
mkdir -p "$OUT"
|
|
220
|
+
git diff --binary "$START_SHA" HEAD -- . ':(exclude).loki' > "$OUT/loki.patch"
|
|
221
|
+
{
|
|
222
|
+
echo "Resolves ${ISSUE_REF}."
|
|
223
|
+
echo ""
|
|
224
|
+
ID="$(cat .loki/state/last-proof-id.txt 2>/dev/null || true)"
|
|
225
|
+
LIB="$(npm root -g)/loki-mode/autonomy/lib/proof-pr.sh"
|
|
226
|
+
if [ -n "$ID" ] && [ -f ".loki/proofs/$ID/proof.json" ] && [ -f "$LIB" ]; then
|
|
227
|
+
# shellcheck source=/dev/null
|
|
228
|
+
. "$LIB" && render_evidence_receipt_md ".loki/proofs/$ID/proof.json" "" ""
|
|
229
|
+
else
|
|
230
|
+
echo "Evidence Receipt: unavailable for this run."
|
|
231
|
+
fi
|
|
232
|
+
} > "$OUT/pr-body.md"
|
|
233
|
+
echo "patch=$OUT/loki.patch" >> "$GITHUB_OUTPUT"
|
|
234
|
+
echo "pr_body=$OUT/pr-body.md" >> "$GITHUB_OUTPUT"
|
|
99
235
|
|
|
100
|
-
|
|
236
|
+
echo "::warning::This action does not open the pull request (Rule of Two). Upload outputs.patch and open the PR from a job that holds the write token and runs no agent; see .github/workflows/loki-issue-to-pr.yml in asklokesh/loki-mode."
|
|
101
237
|
if [ -d .loki/proofs ]; then
|
|
102
238
|
echo "Evidence receipts under .loki/proofs/"
|
|
103
239
|
fi
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
# Hand Loki an issue, get a pull request.
|
|
2
2
|
#
|
|
3
|
-
# Label an issue `loki` (or comment `/loki`
|
|
4
|
-
# evidence receipt. Copy this file
|
|
3
|
+
# Label an issue `loki` (or comment `/loki` as a repository owner, member or
|
|
4
|
+
# collaborator) and this opens a PR with an evidence receipt. Copy this file
|
|
5
|
+
# into .github/workflows/ of any repository.
|
|
5
6
|
#
|
|
6
7
|
# REQUIRED: set ANTHROPIC_API_KEY in repository secrets. Without it the run
|
|
7
8
|
# cannot call a model and fails fast rather than burning minutes.
|
|
9
|
+
#
|
|
10
|
+
# Rule of Two: issue text is untrusted, so the agent that reads it never holds
|
|
11
|
+
# a write token. The work is split in two jobs on two fresh runners:
|
|
12
|
+
# agent reads the issue and runs Loki with a read-only token. Its only
|
|
13
|
+
# output is an artifact: a patch and a PR body.
|
|
14
|
+
# publish holds the write token and runs no agent. It applies the patch as
|
|
15
|
+
# data, pushes a branch and opens the PR.
|
|
16
|
+
# A patch that touches .github/workflows/ cannot be pushed with this token
|
|
17
|
+
# (GitHub requires the `workflows` permission), so the publish job fails
|
|
18
|
+
# visibly rather than letting an agent rewrite CI.
|
|
8
19
|
name: Loki - Issue to PR
|
|
9
20
|
|
|
10
21
|
on:
|
|
@@ -24,24 +35,29 @@ concurrency:
|
|
|
24
35
|
group: loki-issue-${{ github.event.issue.number || inputs.issue }}
|
|
25
36
|
cancel-in-progress: false
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
pull-requests: write # open the PR
|
|
30
|
-
issues: read
|
|
38
|
+
# Nothing by default; each job asks for exactly what it needs.
|
|
39
|
+
permissions: {}
|
|
31
40
|
|
|
32
41
|
jobs:
|
|
33
|
-
|
|
34
|
-
# Gate on an explicit signal so the agent never fires on unrelated
|
|
42
|
+
agent:
|
|
43
|
+
# Gate on an explicit signal so the agent never fires on unrelated
|
|
44
|
+
# activity. A label needs triage access; a `/loki` comment must come from
|
|
45
|
+
# someone with write-level association, not any GitHub user.
|
|
35
46
|
if: >-
|
|
36
47
|
(github.event_name == 'issues' && github.event.label.name == 'loki') ||
|
|
37
|
-
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/loki')
|
|
48
|
+
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/loki') &&
|
|
49
|
+
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
|
|
38
50
|
github.event_name == 'workflow_dispatch'
|
|
39
51
|
runs-on: ubuntu-latest
|
|
40
52
|
timeout-minutes: 45
|
|
53
|
+
permissions:
|
|
54
|
+
contents: read # checkout
|
|
55
|
+
issues: read # Loki reads the issue
|
|
41
56
|
steps:
|
|
42
57
|
- uses: actions/checkout@v4
|
|
43
58
|
with:
|
|
44
59
|
fetch-depth: 0 # the agent branches and diffs; a shallow clone breaks both
|
|
60
|
+
persist-credentials: false # leave no token in .git/config for the agent
|
|
45
61
|
|
|
46
62
|
- uses: actions/setup-node@v4
|
|
47
63
|
with:
|
|
@@ -56,32 +72,129 @@ jobs:
|
|
|
56
72
|
exit 1
|
|
57
73
|
fi
|
|
58
74
|
|
|
59
|
-
- name:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# half-landed release. Bump this line deliberately.
|
|
64
|
-
uses: asklokesh/loki-mode/.github/actions/issue-to-pr@v9.49.4
|
|
75
|
+
- name: Install Loki Mode
|
|
76
|
+
run: npm install -g loki-mode
|
|
77
|
+
|
|
78
|
+
- name: Resolve the issue (no write access)
|
|
65
79
|
env:
|
|
66
80
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
81
|
+
# Read-only in this job (contents: read, issues: read). Loki uses it
|
|
82
|
+
# to fetch the issue and withholds it from the agent session.
|
|
83
|
+
GH_TOKEN: ${{ github.token }}
|
|
84
|
+
ISSUE: ${{ github.event.issue.number || inputs.issue }}
|
|
85
|
+
LOKI_BUDGET_LIMIT: '10.00'
|
|
86
|
+
LOKI_MAX_ITERATIONS: '20'
|
|
87
|
+
# Never push or open a PR from here; the publish job does that.
|
|
88
|
+
LOKI_DELEGATE_PR: '0'
|
|
89
|
+
run: |
|
|
90
|
+
set -uo pipefail
|
|
91
|
+
case "$ISSUE" in
|
|
92
|
+
''|*[!0-9]*) echo "::error::Issue number must be digits, got '$ISSUE'."; exit 1 ;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
# git identity: Loki commits the session's work on its own branch.
|
|
96
|
+
git config user.name "${GITHUB_ACTOR:-loki-mode}"
|
|
97
|
+
git config user.email "${GITHUB_ACTOR:-loki-mode}@users.noreply.github.com"
|
|
98
|
+
|
|
99
|
+
RC=0
|
|
100
|
+
loki start "${GITHUB_REPOSITORY}#${ISSUE}" || RC=$?
|
|
101
|
+
|
|
102
|
+
OUT="$RUNNER_TEMP/loki-out"
|
|
103
|
+
mkdir -p "$OUT"
|
|
104
|
+
# The deliverable is what the session committed on top of the commit
|
|
105
|
+
# this run started from. .loki/ is run state, never part of the change.
|
|
106
|
+
git diff --binary "$GITHUB_SHA" HEAD -- . ':(exclude).loki' > "$OUT/loki.patch"
|
|
107
|
+
|
|
108
|
+
# PR body: the evidence receipt this run wrote, when there is one.
|
|
109
|
+
{
|
|
110
|
+
echo "Resolves #${ISSUE}."
|
|
111
|
+
echo ""
|
|
112
|
+
ID="$(cat .loki/state/last-proof-id.txt 2>/dev/null || true)"
|
|
113
|
+
LIB="$(npm root -g)/loki-mode/autonomy/lib/proof-pr.sh"
|
|
114
|
+
if [ -n "$ID" ] && [ -f ".loki/proofs/$ID/proof.json" ] && [ -f "$LIB" ]; then
|
|
115
|
+
# shellcheck source=/dev/null
|
|
116
|
+
. "$LIB" && render_evidence_receipt_md ".loki/proofs/$ID/proof.json" "" ""
|
|
117
|
+
else
|
|
118
|
+
echo "Evidence Receipt: unavailable for this run."
|
|
119
|
+
fi
|
|
120
|
+
} > "$OUT/pr-body.md"
|
|
121
|
+
|
|
122
|
+
if [ -s "$OUT/loki.patch" ]; then
|
|
123
|
+
echo "Loki produced a change; the publish job opens the pull request."
|
|
124
|
+
else
|
|
125
|
+
echo "::warning::Loki produced no committed change, so no pull request will be opened."
|
|
126
|
+
fi
|
|
127
|
+
exit "$RC"
|
|
128
|
+
|
|
129
|
+
- name: Hand the patch to the publish job
|
|
130
|
+
uses: actions/upload-artifact@v4
|
|
67
131
|
with:
|
|
68
|
-
|
|
69
|
-
|
|
132
|
+
name: loki-patch
|
|
133
|
+
path: ${{ runner.temp }}/loki-out/
|
|
134
|
+
if-no-files-found: error
|
|
135
|
+
retention-days: 7
|
|
70
136
|
|
|
71
|
-
|
|
72
|
-
|
|
137
|
+
publish:
|
|
138
|
+
# Runs only after the agent job succeeded. Everything it reads from that
|
|
139
|
+
# job is attacker-controllable data: no job outputs, no `${{ }}` of agent
|
|
140
|
+
# values in a script, nothing from the artifact is executed.
|
|
141
|
+
needs: agent
|
|
142
|
+
runs-on: ubuntu-latest
|
|
143
|
+
timeout-minutes: 10
|
|
144
|
+
permissions:
|
|
145
|
+
contents: write # push the branch
|
|
146
|
+
pull-requests: write # open the PR
|
|
147
|
+
issues: write # report back on the issue
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/checkout@v4
|
|
150
|
+
with:
|
|
151
|
+
ref: ${{ github.sha }} # the commit the agent started from
|
|
152
|
+
persist-credentials: false
|
|
153
|
+
|
|
154
|
+
- uses: actions/download-artifact@v4
|
|
155
|
+
with:
|
|
156
|
+
name: loki-patch
|
|
157
|
+
path: ${{ runner.temp }}/loki-out
|
|
158
|
+
|
|
159
|
+
- name: Open the pull request
|
|
73
160
|
env:
|
|
74
161
|
GH_TOKEN: ${{ github.token }}
|
|
75
|
-
PR_URL: ${{ steps.loki.outputs.pr_url }}
|
|
76
162
|
ISSUE: ${{ github.event.issue.number || inputs.issue }}
|
|
163
|
+
BASE: ${{ github.ref_name }}
|
|
77
164
|
run: |
|
|
78
|
-
|
|
79
|
-
|
|
165
|
+
set -euo pipefail
|
|
166
|
+
case "$ISSUE" in
|
|
167
|
+
''|*[!0-9]*) echo "::error::Issue number must be digits, got '$ISSUE'."; exit 1 ;;
|
|
168
|
+
esac
|
|
169
|
+
|
|
170
|
+
PATCH="$RUNNER_TEMP/loki-out/loki.patch"
|
|
171
|
+
BODY="$RUNNER_TEMP/loki-out/pr-body.md"
|
|
172
|
+
if [ ! -s "$PATCH" ]; then
|
|
173
|
+
echo "The agent produced no change; nothing to open."
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
[ -s "$BODY" ] || echo "Resolves #${ISSUE}." > "$BODY"
|
|
177
|
+
|
|
178
|
+
BRANCH="loki/issue-${ISSUE}-${GITHUB_RUN_ID}"
|
|
179
|
+
git config user.name "github-actions[bot]"
|
|
180
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
181
|
+
git switch -c "$BRANCH"
|
|
182
|
+
git apply --index "$PATCH"
|
|
183
|
+
git commit -q -m "Loki: resolve #${ISSUE}"
|
|
184
|
+
gh auth setup-git
|
|
185
|
+
git push origin "HEAD:refs/heads/${BRANCH}"
|
|
186
|
+
|
|
187
|
+
PR_URL="$(gh pr create --base "$BASE" --head "$BRANCH" \
|
|
188
|
+
--title "Loki: resolve #${ISSUE}" --body-file "$BODY")"
|
|
189
|
+
echo "Pull request: $PR_URL"
|
|
190
|
+
|
|
191
|
+
# Build the comment in a file: a blank line inside an inline YAML
|
|
192
|
+
# string terminates the block scalar and breaks the workflow parse.
|
|
80
193
|
{
|
|
81
194
|
echo "Loki opened a pull request: $PR_URL"
|
|
82
195
|
echo ""
|
|
83
196
|
echo "The PR body carries an evidence receipt stating what was verified,"
|
|
84
197
|
echo "what was skipped, and what is degraded. Check it rather than taking"
|
|
85
198
|
echo "the result on trust."
|
|
86
|
-
} > /
|
|
87
|
-
gh issue comment "$ISSUE" --body-file /
|
|
199
|
+
} > "$RUNNER_TEMP/loki-issue-comment.md"
|
|
200
|
+
gh issue comment "$ISSUE" --body-file "$RUNNER_TEMP/loki-issue-comment.md" || true
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v9.
|
|
6
|
+
# Loki Mode v9.57.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
470
470
|
|
|
471
471
|
---
|
|
472
472
|
|
|
473
|
-
**v9.
|
|
473
|
+
**v9.57.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
1
|
+
9.57.0
|
|
@@ -40,6 +40,15 @@ _PROOF_CHECK_SH=1
|
|
|
40
40
|
# Best-effort timeout wrapper so a hung network call cannot stall the caller.
|
|
41
41
|
# Mirrors the run.sh _loki_net idiom. Never fatal.
|
|
42
42
|
_proof_check_net() {
|
|
43
|
+
# run.sh withholds GitHub tokens from agent sessions (Rule of Two); this is
|
|
44
|
+
# one of Loki's own trusted calls, so it takes them back for the command.
|
|
45
|
+
if declare -f _loki_with_github_tokens >/dev/null 2>&1; then
|
|
46
|
+
_loki_with_github_tokens _proof_check_net_timed "$@"
|
|
47
|
+
else
|
|
48
|
+
_proof_check_net_timed "$@"
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
_proof_check_net_timed() {
|
|
43
52
|
if command -v timeout >/dev/null 2>&1; then
|
|
44
53
|
timeout 30 "$@"
|
|
45
54
|
else
|
|
@@ -1152,16 +1152,25 @@ def _collect_journey(loki_dir):
|
|
|
1152
1152
|
},
|
|
1153
1153
|
}
|
|
1154
1154
|
|
|
1155
|
-
# Time to first useful result. Prefer the first
|
|
1156
|
-
#
|
|
1155
|
+
# Time to first useful result. Prefer the first code artifact when one
|
|
1156
|
+
# exists. Before that, issue mode can truthfully report its acceptance-
|
|
1157
1157
|
# bound proposed plan, explicitly labelled as NOT a verified patch.
|
|
1158
|
+
#
|
|
1159
|
+
# first_result_verified_patch is honesty-critical: it must be True only
|
|
1160
|
+
# when a verification step actually ran on the first result, never merely
|
|
1161
|
+
# because a code change happened. first-artifact.json (run.sh) is written
|
|
1162
|
+
# from `git status --porcelain` the instant the tree goes dirty -- a
|
|
1163
|
+
# change signal, not a verification signal -- so this branch is False,
|
|
1164
|
+
# same as the plan branch below. No producer in this codebase sets this
|
|
1165
|
+
# True today; it stays here, honestly False, for the day a verified-patch
|
|
1166
|
+
# signal exists to flip it.
|
|
1158
1167
|
fa = _read_json(os.path.join(state, "first-artifact.json"), default=None)
|
|
1159
1168
|
if isinstance(fa, dict):
|
|
1160
1169
|
v = fa.get("seconds_to_first_artifact")
|
|
1161
1170
|
if isinstance(v, (int, float)) and v >= 0:
|
|
1162
1171
|
out["time_to_first_result_sec"] = int(v)
|
|
1163
1172
|
out["first_result_kind"] = "code_change"
|
|
1164
|
-
out["first_result_verified_patch"] =
|
|
1173
|
+
out["first_result_verified_patch"] = False
|
|
1165
1174
|
else:
|
|
1166
1175
|
fr = _read_json(os.path.join(state, "first-useful-result.json"), default=None)
|
|
1167
1176
|
if isinstance(fr, dict):
|
|
@@ -109,13 +109,16 @@ def _preexisting_untracked(repo_dir):
|
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
def _covered(key, paths):
|
|
112
|
-
"""True when ``key`` or one of its parent directories
|
|
113
|
-
|
|
112
|
+
"""True when ``key`` or one of its parent directories is in ``paths``,
|
|
113
|
+
either as a directory entry ("a/", "a/b/") or a bare-path entry ("a",
|
|
114
|
+
"a/b"): an ignored directory, or a preexisting file later replaced by a
|
|
115
|
+
directory of the same name, covers everything below it."""
|
|
114
116
|
if key in paths:
|
|
115
117
|
return True
|
|
116
118
|
cut = key.find("/")
|
|
117
119
|
while cut != -1:
|
|
118
|
-
|
|
120
|
+
parent = key[:cut]
|
|
121
|
+
if parent in paths or (parent + "/") in paths:
|
|
119
122
|
return True
|
|
120
123
|
cut = key.find("/", cut + 1)
|
|
121
124
|
return False
|
|
@@ -209,7 +212,7 @@ def collect_workspace_diff(repo_dir, base, include_diffs=False):
|
|
|
209
212
|
# not land in the receipt.
|
|
210
213
|
recorded = hashes.get(key)
|
|
211
214
|
if recorded and _content_hash(os.path.join(repo_dir, path)) != recorded:
|
|
212
|
-
files.append({"path":
|
|
215
|
+
files.append({"path": key, "insertions": 0, "deletions": 0,
|
|
213
216
|
"status": "preexisting_modified"})
|
|
214
217
|
continue
|
|
215
218
|
stat = _git(
|
|
@@ -219,9 +222,17 @@ def collect_workspace_diff(repo_dir, base, include_diffs=False):
|
|
|
219
222
|
)
|
|
220
223
|
parsed = _parse_numstat(stat)
|
|
221
224
|
entry = parsed[0] if parsed else {
|
|
222
|
-
"path":
|
|
225
|
+
"path": key, "insertions": 0, "deletions": 0, "status": "untracked"
|
|
223
226
|
}
|
|
224
|
-
|
|
227
|
+
# Tracked entries come from `git diff --numstat`, which git always
|
|
228
|
+
# reports repo-top-relative regardless of -C. Untracked/preexisting
|
|
229
|
+
# paths come from `git ls-files --others`, which git reports relative
|
|
230
|
+
# to repo_dir (the caller's TARGET_DIR) instead, so without `prefix`
|
|
231
|
+
# they would stay TARGET_DIR-relative while tracked entries are
|
|
232
|
+
# repo-relative -- two path bases in the same receipt. `key` (prefix +
|
|
233
|
+
# path) normalizes both to repo-top-relative to match the tracked
|
|
234
|
+
# convention.
|
|
235
|
+
entry["path"] = key
|
|
225
236
|
entry["status"] = "untracked_binary" if entry["status"] == "binary" else "untracked"
|
|
226
237
|
files.append(entry)
|
|
227
238
|
if diffs is not None:
|
|
@@ -231,7 +242,7 @@ def collect_workspace_diff(repo_dir, base, include_diffs=False):
|
|
|
231
242
|
allowed=(0, 1),
|
|
232
243
|
)
|
|
233
244
|
if patch:
|
|
234
|
-
diffs.append({"path":
|
|
245
|
+
diffs.append({"path": key, "patch": patch})
|
|
235
246
|
|
|
236
247
|
files.sort(key=lambda item: item["path"])
|
|
237
248
|
return {
|