loki-mode 11.17.58 → 11.17.60

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.
Files changed (84) hide show
  1. package/README.md +13 -4
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/autonomy/lib/engine10-push.sh +99 -5
  5. package/autonomy/lib/proof_redact.py +26 -2
  6. package/autonomy/loki +384 -9
  7. package/autonomy/run.sh +11 -1
  8. package/autonomy/telemetry.sh +2 -2
  9. package/bin/loki +105 -0
  10. package/completions/_loki +13 -0
  11. package/completions/loki.bash +15 -1
  12. package/dashboard/__init__.py +1 -1
  13. package/docs/CLI-REFERENCE.md +3 -1
  14. package/docs/COMPARE-OPENCODE.md +18 -6
  15. package/docs/INSTALLATION.md +6 -2
  16. package/docs/QUICKSTART.md +7 -0
  17. package/docs/v10/CLI-MODERN.md +5 -3
  18. package/docs/v10/FAILURE-CLASSES.md +13 -0
  19. package/loki-ts/dist/cockpit.js +10 -10
  20. package/loki-ts/dist/loki.js +1243 -1231
  21. package/mcp/__init__.py +1 -1
  22. package/package.json +2 -2
  23. package/packages/control-plane/dist/server.js +23 -8
  24. package/packages/control-plane/ui/dist/assets/{index-CxZWgwie.css → index-DeSVrWgd.css} +1 -1
  25. package/packages/control-plane/ui/dist/assets/{index-u8cd6TRQ.js → index-Dww-8G06.js} +10 -10
  26. package/packages/control-plane/ui/dist/index.html +2 -2
  27. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  28. package/web-app/dist/assets/{AdminPage-BKyeQyvY.js → AdminPage-DUuYBy7U.js} +1 -1
  29. package/web-app/dist/assets/{Avatar-Dc4GsZ99.js → Avatar-apqPQq7o.js} +1 -1
  30. package/web-app/dist/assets/{Badge-CmDNBkNC.js → Badge-BCKC5rw5.js} +1 -1
  31. package/web-app/dist/assets/{Button-_HvRTrtz.js → Button-Ii2qz5ro.js} +1 -1
  32. package/web-app/dist/assets/{CockpitPage-CivGuFnR.js → CockpitPage-DCRCbvnY.js} +1 -1
  33. package/web-app/dist/assets/{ComparePage-WPTJgORO.js → ComparePage-BoEYx63C.js} +1 -1
  34. package/web-app/dist/assets/{ErrorBoundary-CLEe9Zqz.js → ErrorBoundary-XXVx8_4-.js} +1 -1
  35. package/web-app/dist/assets/{EvidenceReceiptPanel-DAHnqKD9.js → EvidenceReceiptPanel-k40RtZB_.js} +1 -1
  36. package/web-app/dist/assets/{GitHubIssuesPanel-B5b-um7U.js → GitHubIssuesPanel-BgPkc-75.js} +1 -1
  37. package/web-app/dist/assets/{GitHubPRsPanel-MhdImRpI.js → GitHubPRsPanel-CwhorB4G.js} +1 -1
  38. package/web-app/dist/assets/{HomePage-CvvJdIZn.js → HomePage-CIZPEgSM.js} +2 -2
  39. package/web-app/dist/assets/{LoginPage-Byj8MMzz.js → LoginPage-DZSoNZ3y.js} +1 -1
  40. package/web-app/dist/assets/{MagicPage-C-5_-KNn.js → MagicPage-eThBQUgO.js} +1 -1
  41. package/web-app/dist/assets/{MetricsPage-Vt6R8gT2.js → MetricsPage-BWtdAivK.js} +1 -1
  42. package/web-app/dist/assets/{NotFoundPage-3ZuTdABy.js → NotFoundPage-CNeOANG9.js} +1 -1
  43. package/web-app/dist/assets/{ProjectPage-DT4gY_or.js → ProjectPage-CdFR-RTO.js} +3 -3
  44. package/web-app/dist/assets/{ProjectsPage-DHxhds01.js → ProjectsPage-BPdXEovl.js} +1 -1
  45. package/web-app/dist/assets/{SettingsPage-DIOVf1vJ.js → SettingsPage-B3vsyBHf.js} +1 -1
  46. package/web-app/dist/assets/{ShowcasePage-JEvMj4vs.js → ShowcasePage-DhtEGTId.js} +1 -1
  47. package/web-app/dist/assets/{SystemSettingsPage-C2zCt5HQ.js → SystemSettingsPage-CCRMF5oP.js} +1 -1
  48. package/web-app/dist/assets/{TeamsPage-C5RgWC4V.js → TeamsPage-B5jpVrT8.js} +1 -1
  49. package/web-app/dist/assets/{TemplatesPage-Dp2yg177.js → TemplatesPage-B0NOrY9y.js} +1 -1
  50. package/web-app/dist/assets/{TerminalOutput-Ci63VyQZ.js → TerminalOutput-Y7S9_kCF.js} +1 -1
  51. package/web-app/dist/assets/{activity-BIP9izqH.js → activity-DDGuxXV8.js} +1 -1
  52. package/web-app/dist/assets/{bell-DLap5m5M.js → bell-v1a0v0aO.js} +1 -1
  53. package/web-app/dist/assets/{bot-YX6dHd86.js → bot-Dh9apAJT.js} +1 -1
  54. package/web-app/dist/assets/{check-CooDPKFF.js → check-BIqFwS-y.js} +1 -1
  55. package/web-app/dist/assets/{chevron-left-BcmSPVT2.js → chevron-left-EBM7yWz9.js} +1 -1
  56. package/web-app/dist/assets/{circle-alert-CSmTAxJL.js → circle-alert-Czhu4uRg.js} +1 -1
  57. package/web-app/dist/assets/{clock-DFweV2mu.js → clock-BdJ55LJy.js} +1 -1
  58. package/web-app/dist/assets/{cloud-BtPtouD8.js → cloud-CC4nJD4m.js} +1 -1
  59. package/web-app/dist/assets/{code-xml-V628GOYR.js → code-xml-2TmM3ZXT.js} +1 -1
  60. package/web-app/dist/assets/{database-ChLXHopT.js → database-0TieR4KZ.js} +1 -1
  61. package/web-app/dist/assets/{dollar-sign-DIZWYLK1.js → dollar-sign-pTh1osY0.js} +1 -1
  62. package/web-app/dist/assets/{file-code-corner-Jy00ateq.js → file-code-corner-DQ8FfkCJ.js} +1 -1
  63. package/web-app/dist/assets/{file-plus-fiEcUbz1.js → file-plus-CYgNMECJ.js} +1 -1
  64. package/web-app/dist/assets/{globe-DKpU9VEw.js → globe-KQA_9R6O.js} +1 -1
  65. package/web-app/dist/assets/{hammer-Bhw-veCn.js → hammer-CZMQWpu3.js} +1 -1
  66. package/web-app/dist/assets/{index-BzFRyGry.js → index-BRCtE1U8.js} +3 -3
  67. package/web-app/dist/assets/{layers-CQWnlC67.js → layers-CPYePIbK.js} +1 -1
  68. package/web-app/dist/assets/{loader-circle-CZle8WBp.js → loader-circle-DmvGxkYe.js} +1 -1
  69. package/web-app/dist/assets/{lock-C2GowAkd.js → lock-BTKaeWpE.js} +1 -1
  70. package/web-app/dist/assets/{package-Q2oBgqlC.js → package-Blxc3ohw.js} +1 -1
  71. package/web-app/dist/assets/{plus-BH1scfMy.js → plus-DxtJyvK_.js} +1 -1
  72. package/web-app/dist/assets/{refresh-cw-B8tSKF5G.js → refresh-cw-hA4AfO_9.js} +1 -1
  73. package/web-app/dist/assets/{rotate-ccw-C4WOu54e.js → rotate-ccw-CEqVnL9N.js} +1 -1
  74. package/web-app/dist/assets/{scroll-text-Bub_kemX.js → scroll-text-tJPLGtKd.js} +1 -1
  75. package/web-app/dist/assets/{server-D1OdElLz.js → server-TuWGPUjH.js} +1 -1
  76. package/web-app/dist/assets/{shield-alert-xnpxbh6i.js → shield-alert-C8d0Z4Ax.js} +1 -1
  77. package/web-app/dist/assets/{thumbs-up-DGmIPcAx.js → thumbs-up-C6hv5hUr.js} +1 -1
  78. package/web-app/dist/assets/{trash-2-4iYf66Di.js → trash-2-Dk9XPU3k.js} +1 -1
  79. package/web-app/dist/assets/{trending-down-Ce1pcJ6W.js → trending-down-BJp7dvps.js} +1 -1
  80. package/web-app/dist/assets/{trending-up-S9Uk6w_R.js → trending-up-Rh6ULiPp.js} +1 -1
  81. package/web-app/dist/assets/{upload-Bx2aCcjx.js → upload-CRP_Npdp.js} +1 -1
  82. package/web-app/dist/assets/{usePolling-D2WAlKAr.js → usePolling-UXubXtkC.js} +1 -1
  83. package/web-app/dist/assets/{user-DyuaLsHp.js → user-AiyHiPpF.js} +1 -1
  84. package/web-app/dist/index.html +1 -1
package/README.md CHANGED
@@ -26,7 +26,14 @@ The npm package installs the Bun runtime the engine needs as an optional depende
26
26
  Want to see a result before you install or spend anything?
27
27
 
28
28
  ```bash
29
- npx loki-mode tour # prints a real receipt from a past build: no key, no spend, no network
29
+ npx loki-mode start # no git repo: a real receipt, no key, no spend
30
+ npx loki-mode tour # the same receipt when you are already inside a repo
31
+ ```
32
+
33
+ `npx` downloads the package the first time, so that step needs the network. The receipt itself replays offline and checks with no provider and no key.
34
+
35
+ ```bash
36
+ npx loki-mode wow # a provider is ready: one tiny real task in a temp repo, its own receipt
30
37
  ```
31
38
 
32
39
  ## First run
@@ -57,7 +64,7 @@ Every run starts from a delivery contract derived from your task, writes checks
57
64
  loki verify # newest run; or: loki verify <run-id>
58
65
  ```
59
66
 
60
- `loki verify` re-hashes the receipt, checks its signature and checks it against the run's event log. It prints a verdict and exits 0 (VERIFIED), 1 (TAMPERED), 2 (UNCHECKED), 3 (UNSIGNED, refused unless `--allow-unsigned`), 4 (run outcome not verified) or 66 (no runs); `[ $rc -eq 0 ]` is the only safe test for "verified". Full table: [docs/exit-codes.md](docs/exit-codes.md). On another machine, export the public key with `loki keys export` and pass it with `loki verify --pubkey <file>`. A receipt claims only what its checks ran, and lists what they did not; signing proves who produced it, not that the code is bug-free. Details: [docs/SIGNED-RECEIPTS.md](docs/SIGNED-RECEIPTS.md).
67
+ `loki verify` re-hashes the receipt, checks its signature and checks it against the run's event log. It prints a verdict and exits 0 (VERIFIED), 1 (TAMPERED), 2 (UNCHECKED), 3 (UNSIGNED, refused unless `--allow-unsigned`), 4 (run outcome not verified) or 66 (no runs); `[ $rc -eq 0 ]` is the only safe test for "verified". `loki start` also requires fail-to-pass for a declared behavior change: a Wall test must fail on the old code and pass after, or the verdict stays PARTIAL (`LOKI_MUTATION_STRICT=0` opts out). Full table: [docs/exit-codes.md](docs/exit-codes.md). On another machine, export the public key with `loki keys export` and pass it with `loki verify --pubkey <file>`. A receipt claims only what its checks ran, and lists what they did not; signing proves who produced it, not that the code is bug-free. Details: [docs/SIGNED-RECEIPTS.md](docs/SIGNED-RECEIPTS.md).
61
68
 
62
69
  For a multi-repo change set, `loki start --workspace <name> "<brief>"` seals one signed `composite.json` over every child receipt, and `loki verify <run-dir>/composite.json` re-checks it offline ([docs/WORKSPACES.md](docs/WORKSPACES.md)). A composite never reads higher than its weakest child.
63
70
 
@@ -91,7 +98,7 @@ Time: 4m12s (intake 11s, plan 20s, implement 3m10s, verify 31s)
91
98
 
92
99
  | Command | What it does |
93
100
  |---------|--------------|
94
- | `loki start "<task>"` | The primary path: build a task, PRD file or issue, verify it, seal a receipt, open a PR (`--no-pr`, `--deep`, `--provider`, `--max-cost`). `loki "<task>"` and `loki owner/repo#N` are shorthand |
101
+ | `loki start "<task>"` | The command you run: same provider model as a raw session, then the harness re-runs the checks and seals a receipt (`--no-pr`, `--provider`, `--max-cost`, `--haiku` to opt the first implement onto haiku). `loki "<task>"` and `loki owner/repo#N` are shorthand. No task and no git repo shows a receipt and spends nothing |
95
102
  | `loki edit "<change>"` | One targeted change in one pass, no PR, plan or council (`--verify`, `--receipt`, `--commit`, `--dry-run`) |
96
103
  | `loki quick "<task>"` | One small task with a verify loop, no PR |
97
104
  | `loki chat` | Interactive alternative: Plan and Build modes, slash commands, `/run` starts a `loki start` run, `--attach` joins one |
@@ -106,7 +113,9 @@ Time: 4m12s (intake 11s, plan 20s, implement 3m10s, verify 31s)
106
113
  | `loki doctor [--fix] [--json]` | Check prerequisites, providers and login; `--fix` applies safe fixes |
107
114
  | `loki control serve` | Start the Control Plane (see below) |
108
115
  | `loki dashboard` | Open the Control Plane UI, starting it if needed |
109
- | `loki tour` | Show a sample receipt offline |
116
+ | `loki tour` | Show a sample receipt offline. `loki start` does this when you are not in a git repo and did not pass a task |
117
+ | `loki wow` | Same live mini-task as a normal start, in a temp repo. Offline sample when no provider is ready |
118
+ | `loki factory "<task>"` | Same as `loki start`. The policy line is printed by start |
110
119
  | `loki --help` | Everything else; `loki help <command>` for one command |
111
120
 
112
121
  Core verbs: [docs/CLI-CORE-VERBS.md](docs/CLI-CORE-VERBS.md). Every command (generated): [docs/CLI-REFERENCE.md](docs/CLI-REFERENCE.md). Engine guide, provider table, model routes, custom agents and chat: [docs/v10/GUIDE.md](docs/v10/GUIDE.md). Honest comparison with opencode: [docs/COMPARE-OPENCODE.md](docs/COMPARE-OPENCODE.md).
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v11.17.58
6
+ # Loki Mode v11.17.60
7
7
 
8
8
  **Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
9
9
 
@@ -200,7 +200,7 @@ This protocol governs **skill module** loading -- task-scoped instruction files
200
200
  <!-- generated:commands -->
201
201
  Commands (generated from `loki-ts/src/cli/registry.ts`; full reference in `docs/CLI-REFERENCE.md`):
202
202
 
203
- `version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `backup`, `restore`, `proof`, `wiki`, `control`, `intent`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `cancel`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`, `steer`
203
+ `version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `backup`, `restore`, `proof`, `wiki`, `control`, `intent`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `cancel`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `wow`, `factory`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`, `steer`
204
204
  <!-- /generated -->
205
205
 
206
206
  ---
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
483
483
 
484
484
  ---
485
485
 
486
- **v11.17.58 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
486
+ **v11.17.60 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.17.58
1
+ 11.17.60
@@ -8,6 +8,16 @@
8
8
  # engine10-push.sh issue-comment [--update] <issue-ref> <body-file>
9
9
  # engine10-push.sh status <sha> <pending|success|failure|error> <description>
10
10
  #
11
+ # Fork mode: when gh reports the authenticated user cannot push to the pinned
12
+ # repo (permissions.push false), push-pr pushes to that user's fork (created
13
+ # if missing, verified as a fork of the pinned repo owned by that user) and
14
+ # opens the PR upstream from <user>:<branch>. A draft run in fork mode pushes
15
+ # to the fork only and exits 4: Loki does not open draft PRs on a repository
16
+ # the user cannot push to. issue-comment (status and backstop comments) is a
17
+ # no-op there for the same reason; a receipt comment on the user's own PR is
18
+ # still posted. LOKI_PR_FORK=0 turns fork mode off (the direct push runs and
19
+ # fails as before). No token is needed beyond the user's gh/git login.
20
+ #
11
21
  # Holds GitHub credentials, runs no LLM, reads no untrusted text. Inputs come
12
22
  # only from argv and the origin pinned by P0 before any provider ran
13
23
  # (_LOKI_ORIGIN_PINNED=1 _LOKI_PINNED_ORIGIN=<url>). The target repository is
@@ -128,6 +138,37 @@ _e10_find_pr() {
128
138
  --jq "[.[] | select(.isCrossRepository == false and .state == \"OPEN\" and (.headRepositoryOwner.login | ascii_downcase) == \"$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')\")] | .[0].url // empty"
129
139
  }
130
140
 
141
+ # Fork mode probe. Prints true, false, or nothing (unknown: gh failed or did not say). Only a literal false means no push
142
+ # access; anything else keeps the direct path (fail open, so a gh hiccup never reroutes a push the user could make).
143
+ _e10_can_push() { _e10_gh api "repos/$_e10_repo" --jq '.permissions.push' 2>/dev/null | head -n 1; }
144
+ _e10_fork_off() { [ "${LOKI_PR_FORK:-}" = "0" ]; }
145
+ # Creates or finds the gh user's fork of the pinned repo; prints OWNER/REPO only after gh confirms it is a fork of
146
+ # that repo owned by that user. A fork can take a few seconds to appear after the create call.
147
+ _e10_fork() {
148
+ local me="$1" fork="" info i
149
+ fork="$(_e10_gh api -X POST "repos/$_e10_repo/forks" --jq .full_name 2>/dev/null | head -n 1)" || fork=""
150
+ [[ "$fork" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || return 1
151
+ for i in 1 2 3 4 5 6; do
152
+ info="$(_e10_gh api "repos/$fork" --jq '[(.fork|tostring), (.parent.full_name // ""), .owner.login] | join(" ")' 2>/dev/null)" || info=""
153
+ if [ -n "$info" ]; then
154
+ [ "$(printf '%s' "$info" | tr '[:upper:]' '[:lower:]')" = "$(printf 'true %s %s' "$_e10_repo" "$me" | tr '[:upper:]' '[:lower:]')" ] || return 1
155
+ printf '%s\n' "$fork"
156
+ return 0
157
+ fi
158
+ [ "$i" -lt 6 ] && sleep 2
159
+ done
160
+ return 3 # the fork exists by name but GitHub has not served it yet
161
+ }
162
+ # Fork-mode twin of _e10_find_pr: the open cross-repo PR whose head is <me>:<branch> (--head matches the branch name
163
+ # only; the jq owner filter is what pins it to <me>).
164
+ _e10_find_fork_pr() {
165
+ local me="$1"
166
+ case "$me" in '' | *[!A-Za-z0-9_.-]*) return 1 ;; esac
167
+ _e10_gh pr list --repo "$_e10_repo" --head "$2" --state open \
168
+ --json url,isCrossRepository,headRepositoryOwner,headRefName,state \
169
+ --jq "[.[] | select(.isCrossRepository == true and .state == \"OPEN\" and (.headRepositoryOwner.login | ascii_downcase) == \"$(printf '%s' "$me" | tr '[:upper:]' '[:lower:]')\")] | .[0].url // empty"
170
+ }
171
+
131
172
  mode="${1:-}"
132
173
  shift || true
133
174
  case "$mode" in
@@ -157,6 +198,33 @@ case "$mode" in
157
198
  printf 'local://%s#%s\n' "$_LOKI_PINNED_ORIGIN" "$branch"
158
199
  exit 0
159
200
  fi
201
+ if ! _e10_fork_off && [ "$(_e10_can_push)" = "false" ]; then
202
+ # A stacked parent published in fork mode lives on the fork, not upstream, so an upstream base would not exist.
203
+ [ "${#base[@]}" -eq 0 ] || die "no push access to $_e10_repo: a stacked PR (--base) needs push access there; rerun without LOKI_PR_BASE"
204
+ _e10_me="$(_e10_gh api user --jq .login 2>/dev/null)" || _e10_me=""
205
+ [[ "$_e10_me" =~ ^[A-Za-z0-9_.-]+$ ]] || die "no push access to $_e10_repo and the gh user could not be resolved for a fork; run gh auth login"
206
+ log_info "no push access to $_e10_repo: creating or reusing the fork under $_e10_me (LOKI_PR_FORK=0 turns this off)"
207
+ _e10_fork_repo="$(_e10_fork "$_e10_me")" || { [ "$?" -eq 3 ] && die "no push access to $_e10_repo: the fork under $_e10_me is still being created on GitHub; rerun to push once it exists"
208
+ die "no push access to $_e10_repo and its fork under $_e10_me could not be created or verified"; }
209
+ log_info "no push access to $_e10_repo: pushing to the fork $_e10_fork_repo"
210
+ _loki_trusted_push _loki_with_github_tokens "$dir" "$branch" "$_e10_fork_repo" || die "push to fork $_e10_fork_repo refused or failed (rc=$?)"
211
+ if [ "${#draft[@]}" -gt 0 ]; then
212
+ printf 'fork-only: pushed %s to %s; no draft PR is opened on %s, which you cannot push to. Open one yourself: gh pr create --repo %s --head %s:%s\n' \
213
+ "$branch" "$_e10_fork_repo" "$_e10_repo" "$_e10_repo" "$_e10_me" "$branch" >&2
214
+ exit 4
215
+ fi
216
+ url="$(_e10_find_fork_pr "$_e10_me" "$branch")" || die "gh pr list failed"
217
+ _e10_pr_state=reused
218
+ if [ -z "$url" ]; then
219
+ _e10_pr_state=created
220
+ url="$(_e10_gh pr create --repo "$_e10_repo" --head "$_e10_me:$branch" --title "$title" --body-file "$body" "${base[@]}")" \
221
+ || die "gh pr create failed"
222
+ url="$(printf '%s\n' "$url" | tail -n 1)"
223
+ fi
224
+ printf '%s\n' "$_e10_pr_state"
225
+ printf '%s\n' "$url"
226
+ exit 0
227
+ fi
160
228
  # RECOVERY: a resumed publish skips the push when the remote branch is already at the local commit.
161
229
  _e10_want="$(_e10_git -C "$dir" rev-parse -q --verify "refs/heads/$branch" 2>/dev/null)" || _e10_want=""
162
230
  _e10_have=""
@@ -207,6 +275,7 @@ case "$mode" in
207
275
  # --update: the status projector. One comment carrying the marker below is edited in place (created when absent),
208
276
  # so an issue shows live run state without a new comment per event. Only a comment written by the authenticated
209
277
  # gh user counts, and the body file must carry the marker (it is built by the supervisor, never from task text).
278
+ # The PR receipt comment (<!-- loki-receipt -->) uses the same upsert: a PR number is an issue number to the API.
210
279
  _e10_upd=""
211
280
  [ "${1:-}" != "--update" ] || { _e10_upd=1; shift; }
212
281
  [ "$#" -eq 2 ] || die "usage: issue-comment [--update] <issue-ref> <body-file>"
@@ -214,17 +283,42 @@ case "$mode" in
214
283
  case "$num" in '' | *[!0-9]*) die "cannot extract issue number from: $1" ;; esac
215
284
  [ -f "$2" ] || die "body file not found: $2"
216
285
  [ -z "$_e10_local" ] || { log_info "local origin: no issue to comment on (no-op)"; exit 0; }
286
+ # Status and backstop comments are for a repo the user maintains; on one they cannot push to they would land on
287
+ # someone else's issue. The receipt comment rides the user's own PR and is still posted.
288
+ # The receipt body starts with its marker (receipt_surface.ts); only that first line exempts a comment, so issue
289
+ # text quoting the marker inside a status body cannot. Unlike push-pr this fails closed: a status or backstop
290
+ # comment is posted only when gh confirms push access, since a wrong guess is a public comment on someone's issue.
291
+ if [ "$(head -n 1 -- "$2")" != '<!-- loki-receipt -->' ] && [ "$(_e10_can_push)" != "true" ]; then
292
+ log_info "push access to $_e10_repo not confirmed: not commenting on its issue $num"
293
+ exit 0
294
+ fi
217
295
  if [ -n "$_e10_upd" ]; then
218
- _e10_marker='<!-- loki-status -->'
219
- grep -qF -- "$_e10_marker" "$2" || die "status body must carry $_e10_marker"
296
+ _e10_marker=""
297
+ for _e10_m in '<!-- loki-status -->' '<!-- loki-receipt -->'; do
298
+ if grep -qF -- "$_e10_m" "$2"; then _e10_marker="$_e10_m"; break; fi
299
+ done
300
+ [ -n "$_e10_marker" ] || die "status body must carry <!-- loki-status --> or <!-- loki-receipt -->"
220
301
  _e10_me="$(_e10_gh api user --jq .login 2>/dev/null)" || _e10_me=""
221
302
  _e10_cid=""
303
+ # A receipt upsert never posts on a failed lookup: that would stack a duplicate and still report success.
304
+ if [ -z "$_e10_me" ] && [ "$_e10_marker" = '<!-- loki-receipt -->' ]; then die "cannot resolve the gh user for the receipt comment lookup"; fi
222
305
  if [ -n "$_e10_me" ]; then
223
- _e10_cid="$(_e10_gh api "repos/$_e10_repo/issues/$num/comments" --paginate \
224
- --jq ".[] | select(.user.login == \"$_e10_me\") | select(.body | contains(\"$_e10_marker\")) | .id" 2>/dev/null | head -n 1)" || _e10_cid=""
306
+ if _e10_ids="$(_e10_gh api "repos/$_e10_repo/issues/$num/comments" --paginate \
307
+ --jq ".[] | select(.user.login == \"$_e10_me\") | select(.body | contains(\"$_e10_marker\")) | .id" 2>/dev/null)"; then
308
+ _e10_cid="$(printf '%s\n' "$_e10_ids" | head -n 1)"
309
+ elif [ "$_e10_marker" = '<!-- loki-receipt -->' ]; then
310
+ die "receipt comment lookup failed"
311
+ fi
225
312
  fi
226
313
  case "$_e10_cid" in
227
- '') _e10_gh issue comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh issue comment failed" ;;
314
+ '')
315
+ # A PR number is not an issue to `gh issue comment`; the receipt marker only ever rides a PR.
316
+ if [ "$_e10_marker" = '<!-- loki-receipt -->' ]; then
317
+ _e10_gh pr comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh pr comment failed"
318
+ else
319
+ _e10_gh issue comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh issue comment failed"
320
+ fi
321
+ ;;
228
322
  *[!0-9]*) die "unexpected comment id" ;;
229
323
  *) _e10_gh api -X PATCH "repos/$_e10_repo/issues/comments/$_e10_cid" -F "body=@$2" >/dev/null || die "gh api comment update failed" ;;
230
324
  esac
@@ -194,8 +194,31 @@ def _env_assign_sub(m):
194
194
  # The password class is [^\s@]+ (stops only at the closing @) so passwords that
195
195
  # contain "/" (e.g. postgres://user:p/ss@host) are still fully captured.
196
196
  _URI_CREDENTIAL = re.compile(
197
- r"\b([A-Za-z][A-Za-z0-9+.\-]*://[^\s:/@]*:)([^\s@]+)(@)"
197
+ r"\b([A-Za-z][A-Za-z0-9+.\-]{0,63}://[^\s:/@]*:)([^\s@]+)(@)" # bounded scheme: linear (TEN-SECURITY-26)
198
198
  )
199
+ _TOKEN = re.compile(r"\S+")
200
+
201
+
202
+ def _redact_uri_credentials(s):
203
+ """Apply _URI_CREDENTIAL to the part of each whitespace-delimited token up to
204
+ its last "@". No match ends past that "@" (the password class stops at "@"),
205
+ and scanning an "@"-free tail from every scheme start is quadratic even with
206
+ the bounded scheme ("a://" * 16000 took 5.6s). TEN-SECURITY-26 follow-up.
207
+ Returns (new_string, count)."""
208
+ total = 0
209
+
210
+ def tok(m):
211
+ nonlocal total
212
+ t = m.group(0)
213
+ cut = t.rfind("@") + 1
214
+ if not cut:
215
+ return t
216
+ head, n = _URI_CREDENTIAL.subn(r"\1[REDACTED]\3", t[:cut])
217
+ total += n
218
+ return head + t[cut:]
219
+
220
+ return _TOKEN.sub(tok, s), total
221
+
199
222
 
200
223
  # Absolute user home paths -> ~ . Applied to every string (paths, diffs,
201
224
  # brief, summaries). Windows form handled separately to keep the backslash
@@ -289,7 +312,8 @@ def _redact_value(s):
289
312
  # Connection-string / URI credentials: scheme://user:PASSWORD@ -> redact
290
313
  # the password. Runs before the generic env-assign so the "scheme://user:"
291
314
  # prefix is consumed and the assign rule does not double-process it.
292
- s, n = _URI_CREDENTIAL.subn(r"\1[REDACTED]\3", s)
315
+ # The helper applies _URI_CREDENTIAL per token; keep the two in sync.
316
+ s, n = _redact_uri_credentials(s)
293
317
  total += n
294
318
 
295
319
  # Secret assignments (bare, JSON-quoted, YAML-quoted): keep key, redact value.