pi-harness-runtime 1.1.393 → 1.1.395

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 (221) hide show
  1. package/AGENTS.md +125 -3
  2. package/harness/console-guard.ts +73 -0
  3. package/harness/glm-quota-logger.ts +17 -0
  4. package/harness/langchain/planner-logger.ts +15 -0
  5. package/harness/service-diagnostics.ts +32 -1
  6. package/index.ts +195 -4
  7. package/package.json +1 -1
  8. package/packages/a2a-adapter/package.json +1 -1
  9. package/packages/agent-policy/package.json +1 -1
  10. package/packages/architecture-generator/package.json +1 -1
  11. package/packages/auth/package.json +1 -1
  12. package/packages/autonomous-refactor/package.json +1 -1
  13. package/packages/autonomous-runtime/package.json +1 -1
  14. package/packages/cache-strategy/package.json +1 -1
  15. package/packages/capability-registry/package.json +1 -1
  16. package/packages/checkpoint/package.json +1 -1
  17. package/packages/cli-plugin-sdk/package.json +1 -1
  18. package/packages/clipboard/package.json +1 -1
  19. package/packages/clipboard-plugin/package.json +1 -1
  20. package/packages/code-generation/package.json +1 -1
  21. package/packages/code-review/package.json +1 -1
  22. package/packages/codex-adapter/package.json +1 -1
  23. package/packages/codex-watcher/package.json +1 -1
  24. package/packages/config-capture/package.json +1 -1
  25. package/packages/connection-runtime/package.json +1 -1
  26. package/packages/context-compiler/package.json +1 -1
  27. package/packages/context-discovery/package.json +1 -1
  28. package/packages/context-manager/package.json +1 -1
  29. package/packages/contracts/dist/workspace-env-format.d.ts +63 -0
  30. package/packages/contracts/dist/workspace-env-format.js +133 -5
  31. package/packages/contracts/dist/workspace-env.js +114 -6
  32. package/packages/contracts/package.json +1 -1
  33. package/packages/contracts/src/workspace-env-format.ts +171 -5
  34. package/packages/contracts/src/workspace-env.ts +120 -4
  35. package/packages/contracts/test/workspace-env.test.ts +129 -0
  36. package/packages/cookie-sanitizer/package.json +1 -1
  37. package/packages/cost-optimizer/package.json +1 -1
  38. package/packages/dependency-analyzer/package.json +1 -1
  39. package/packages/django-plugin/package.json +1 -1
  40. package/packages/doc-generator/package.json +1 -1
  41. package/packages/durable-review/package.json +1 -1
  42. package/packages/e2e-tester/package.json +1 -1
  43. package/packages/evaluation-engine/package.json +1 -1
  44. package/packages/evaluation-runner/package.json +1 -1
  45. package/packages/event-bus/package.json +1 -1
  46. package/packages/event-sink/package.json +1 -1
  47. package/packages/event-store/package.json +1 -1
  48. package/packages/experience-replay/package.json +1 -1
  49. package/packages/feedback-collector/package.json +1 -1
  50. package/packages/file-copy-helper/package.json +1 -1
  51. package/packages/framework-detector/package.json +1 -1
  52. package/packages/framework-plugin-sdk/package.json +1 -1
  53. package/packages/frappe-plugin/package.json +1 -1
  54. package/packages/generic-web-plugin/package.json +1 -1
  55. package/packages/health-monitor/package.json +1 -1
  56. package/packages/intent-analyzer/package.json +1 -1
  57. package/packages/jev-judge/dist/continuation-controller.d.ts +47 -0
  58. package/packages/jev-judge/dist/continuation-controller.d.ts.map +1 -1
  59. package/packages/jev-judge/dist/continuation-controller.js +142 -16
  60. package/packages/jev-judge/dist/continuation-controller.js.map +1 -1
  61. package/packages/jev-judge/dist/index.d.ts +6 -0
  62. package/packages/jev-judge/dist/index.d.ts.map +1 -1
  63. package/packages/jev-judge/dist/index.js +6 -0
  64. package/packages/jev-judge/dist/index.js.map +1 -1
  65. package/packages/jev-judge/dist/validation-ledger.d.ts +129 -0
  66. package/packages/jev-judge/dist/validation-ledger.d.ts.map +1 -0
  67. package/packages/jev-judge/dist/validation-ledger.js +234 -0
  68. package/packages/jev-judge/dist/validation-ledger.js.map +1 -0
  69. package/packages/jev-judge/dist/validation-plan.d.ts +176 -0
  70. package/packages/jev-judge/dist/validation-plan.d.ts.map +1 -0
  71. package/packages/jev-judge/dist/validation-plan.js +293 -0
  72. package/packages/jev-judge/dist/validation-plan.js.map +1 -0
  73. package/packages/jev-judge/dist/validation-tie-breaker.d.ts +48 -0
  74. package/packages/jev-judge/dist/validation-tie-breaker.d.ts.map +1 -0
  75. package/packages/jev-judge/dist/validation-tie-breaker.js +83 -0
  76. package/packages/jev-judge/dist/validation-tie-breaker.js.map +1 -0
  77. package/packages/jev-judge/package.json +1 -1
  78. package/packages/jev-judge/src/continuation-controller.ts +179 -20
  79. package/packages/jev-judge/src/index.ts +41 -0
  80. package/packages/jev-judge/src/validation-ledger.ts +290 -0
  81. package/packages/jev-judge/src/validation-plan.ts +406 -0
  82. package/packages/jev-judge/src/validation-tie-breaker.ts +123 -0
  83. package/packages/jev-judge/tests/continuation-controller.test.ts +167 -0
  84. package/packages/jev-judge/tests/validation-ledger.test.ts +181 -0
  85. package/packages/jev-judge/tests/validation-plan.test.ts +170 -0
  86. package/packages/jev-judge/tests/validation-tie-breaker.test.ts +88 -0
  87. package/packages/knowledge-graph/package.json +1 -1
  88. package/packages/knowledge-retrieval/package.json +1 -1
  89. package/packages/laravel-plugin/package.json +1 -1
  90. package/packages/learning-engine/package.json +1 -1
  91. package/packages/logging/README.md +37 -0
  92. package/packages/logging/dist/buffer.d.ts +33 -0
  93. package/packages/logging/dist/buffer.d.ts.map +1 -0
  94. package/packages/logging/dist/buffer.js +60 -0
  95. package/packages/logging/dist/buffer.js.map +1 -0
  96. package/packages/logging/dist/config.d.ts +40 -0
  97. package/packages/logging/dist/config.d.ts.map +1 -0
  98. package/packages/logging/dist/config.js +114 -0
  99. package/packages/logging/dist/config.js.map +1 -0
  100. package/packages/logging/dist/index.d.ts +32 -0
  101. package/packages/logging/dist/index.d.ts.map +1 -0
  102. package/packages/logging/dist/index.js +31 -0
  103. package/packages/logging/dist/index.js.map +1 -0
  104. package/packages/logging/dist/redact.d.ts +43 -0
  105. package/packages/logging/dist/redact.d.ts.map +1 -0
  106. package/packages/logging/dist/redact.js +220 -0
  107. package/packages/logging/dist/redact.js.map +1 -0
  108. package/packages/logging/dist/sampler.d.ts +37 -0
  109. package/packages/logging/dist/sampler.d.ts.map +1 -0
  110. package/packages/logging/dist/sampler.js +78 -0
  111. package/packages/logging/dist/sampler.js.map +1 -0
  112. package/packages/logging/dist/shipper.d.ts +82 -0
  113. package/packages/logging/dist/shipper.d.ts.map +1 -0
  114. package/packages/logging/dist/shipper.js +238 -0
  115. package/packages/logging/dist/shipper.js.map +1 -0
  116. package/packages/logging/dist/singleton.d.ts +38 -0
  117. package/packages/logging/dist/singleton.d.ts.map +1 -0
  118. package/packages/logging/dist/singleton.js +95 -0
  119. package/packages/logging/dist/singleton.js.map +1 -0
  120. package/packages/logging/dist/spool.d.ts +39 -0
  121. package/packages/logging/dist/spool.d.ts.map +1 -0
  122. package/packages/logging/dist/spool.js +110 -0
  123. package/packages/logging/dist/spool.js.map +1 -0
  124. package/packages/logging/dist/transports/r2.d.ts +31 -0
  125. package/packages/logging/dist/transports/r2.d.ts.map +1 -0
  126. package/packages/logging/dist/transports/r2.js +60 -0
  127. package/packages/logging/dist/transports/r2.js.map +1 -0
  128. package/packages/logging/dist/transports/webhook.d.ts +22 -0
  129. package/packages/logging/dist/transports/webhook.d.ts.map +1 -0
  130. package/packages/logging/dist/transports/webhook.js +37 -0
  131. package/packages/logging/dist/transports/webhook.js.map +1 -0
  132. package/packages/logging/dist/types.d.ts +73 -0
  133. package/packages/logging/dist/types.d.ts.map +1 -0
  134. package/packages/logging/dist/types.js +19 -0
  135. package/packages/logging/dist/types.js.map +1 -0
  136. package/packages/logging/package.json +37 -0
  137. package/packages/logging/src/buffer.ts +77 -0
  138. package/packages/logging/src/config.ts +163 -0
  139. package/packages/logging/src/index.ts +87 -0
  140. package/packages/logging/src/redact.ts +271 -0
  141. package/packages/logging/src/sampler.ts +97 -0
  142. package/packages/logging/src/shipper.ts +281 -0
  143. package/packages/logging/src/singleton.ts +142 -0
  144. package/packages/logging/src/spool.ts +139 -0
  145. package/packages/logging/src/transports/r2.ts +86 -0
  146. package/packages/logging/src/transports/webhook.ts +54 -0
  147. package/packages/logging/src/types.ts +85 -0
  148. package/packages/logging/test/buffer.test.ts +44 -0
  149. package/packages/logging/test/config.test.ts +97 -0
  150. package/packages/logging/test/integration.test.ts +135 -0
  151. package/packages/logging/test/redact.test.ts +109 -0
  152. package/packages/logging/test/sampler.test.ts +57 -0
  153. package/packages/logging/test/shipper.test.ts +152 -0
  154. package/packages/logging/test/spool.test.ts +93 -0
  155. package/packages/mcp-adapter/package.json +1 -1
  156. package/packages/memory-engine/package.json +1 -1
  157. package/packages/milestone-manager/package.json +1 -1
  158. package/packages/model-registry/package.json +1 -1
  159. package/packages/nextjs-plugin/package.json +1 -1
  160. package/packages/notification/package.json +1 -1
  161. package/packages/observability/package.json +1 -1
  162. package/packages/okf-indexer/package.json +1 -1
  163. package/packages/paperclip-integration/package.json +1 -1
  164. package/packages/performance-optimizer/package.json +1 -1
  165. package/packages/permission-mode/package.json +1 -1
  166. package/packages/privilege-broker/package.json +1 -1
  167. package/packages/project-analyzer/package.json +1 -1
  168. package/packages/project-bootstrap/package.json +1 -1
  169. package/packages/projection-engine/package.json +1 -1
  170. package/packages/prompt-compiler/package.json +1 -1
  171. package/packages/prompt-versioning/package.json +1 -1
  172. package/packages/provider-adapter-sdk/package.json +1 -1
  173. package/packages/provider-router/package.json +1 -1
  174. package/packages/provider-selector/package.json +1 -1
  175. package/packages/providers/package.json +1 -1
  176. package/packages/qdrant-skills/package.json +1 -1
  177. package/packages/quota-manager/package.json +1 -1
  178. package/packages/rate-limiter/package.json +1 -1
  179. package/packages/react-vite-plugin/package.json +1 -1
  180. package/packages/relay/dist/client.d.ts +25 -1
  181. package/packages/relay/dist/client.js +71 -23
  182. package/packages/relay/dist/index.d.ts +1 -1
  183. package/packages/relay/package.json +1 -1
  184. package/packages/relay/src/client.ts +87 -23
  185. package/packages/relay/src/index.ts +1 -1
  186. package/packages/relay/test/client.test.ts +134 -0
  187. package/packages/release-manager/package.json +1 -1
  188. package/packages/requirement-compiler/package.json +1 -1
  189. package/packages/runtime/package.json +1 -1
  190. package/packages/scheduler/package.json +1 -1
  191. package/packages/scheduler-adapter/package.json +1 -1
  192. package/packages/session/package.json +1 -1
  193. package/packages/session-api/package.json +1 -1
  194. package/packages/session-export/package.json +1 -1
  195. package/packages/shared-context/package.json +1 -1
  196. package/packages/skill-mcp-client/package.json +1 -1
  197. package/packages/skill-registry/package.json +1 -1
  198. package/packages/skills/package.json +1 -1
  199. package/packages/sprint-planner/package.json +1 -1
  200. package/packages/subscription-engine/package.json +1 -1
  201. package/packages/task-compiler/package.json +1 -1
  202. package/packages/telegram/package.json +1 -1
  203. package/packages/tencentdb-memory/package.json +1 -1
  204. package/packages/tencentdb-sync/package.json +1 -1
  205. package/packages/test-data-generator/package.json +1 -1
  206. package/packages/test-generator/package.json +1 -1
  207. package/packages/todo-bd-sync/package.json +1 -1
  208. package/packages/token-estimation/package.json +1 -1
  209. package/packages/token-optimizer/package.json +1 -1
  210. package/packages/trajectory/package.json +1 -1
  211. package/packages/tui/package.json +1 -1
  212. package/packages/types/package.json +1 -1
  213. package/packages/voice-api/package.json +1 -1
  214. package/packages/webmcp-tester/package.json +1 -1
  215. package/packages/wiki/package.json +1 -1
  216. package/packages/workflow-events/package.json +1 -1
  217. package/packages/workspace-scanner/package.json +1 -1
  218. package/packages/worktree/package.json +1 -1
  219. package/packages/write-review/package.json +1 -1
  220. package/scripts/unified-webhook-server.ts +102 -0
  221. package/scripts/validation-run.ts +191 -0
package/AGENTS.md CHANGED
@@ -36,6 +36,56 @@ bd list --status pending
36
36
 
37
37
  **Why**: Agent often builds/commits without checking todos. This causes incomplete tasks to be forgotten.
38
38
 
39
+ ## Staged Validation & Test-Cost Discipline (CRITICAL)
40
+
41
+ A task with a cheap tier and an expensive tier MUST use the cheap tier while the code is still changing. Repeating an expensive regression after every fix is the most common way an autonomous run burns an entire night.
42
+
43
+ 1. **Declare the tiers.** Put the plan in the todo metadata (`metadata.validationPlan`):
44
+
45
+ ```json
46
+ {
47
+ "metadata": {
48
+ "validationPlan": {
49
+ "fast": { "command": "bun test tests/fast", "expectedDurationMs": 18000, "datasetSize": 100 },
50
+ "full": { "command": "bun test tests/full", "expectedDurationMs": 2400000, "datasetSize": 10000 },
51
+ "budgetMinutes": 90,
52
+ "finalTierRequired": true
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ The harness never guesses a tier from a command string. Without a declared plan the current behaviour is unchanged.
59
+
60
+ 2. **Iterate on the fast tier.** After every code edit, rerun the fast tier plus the focused unit/type checks the edit touches.
61
+
62
+ 3. **Run the full tier once.** Run the full tier as the final acceptance check, when the implementation and the fast tier are stable.
63
+
64
+ 4. **Do not repeat the full tier blindly.** Never rerun the full tier after an edit that cannot affect the behavior it covers. If it genuinely must be repeated, say why in one line.
65
+
66
+ 5. **Make the cost visible.** Report each validation run compactly: `[TEST] fast: bun test tests/fast - passed (18s, 100 records)`. A test that is not reported cannot be budgeted.
67
+
68
+ 6. **No false completion.** A todo whose `finalTierRequired` plan is set must not be marked completed while that tier is pending or failed. Pause instead and report what passed, what is pending, and why.
69
+
70
+ 7. **Respect the time budget.** `budgetMinutes` is a soft budget. When it is reached, stop at a safe boundary, report completed and skipped checks, and leave the work open. Never kill a destructive or non-interruptible test mid-run.
71
+
72
+ 8. **The fast tier must be representative.** A small fixture keeps the shape of the data (optional fields, relations, uniqueness, boundary values, aggregates under test), not just the row count. Use deterministic data with cleanup so repeated runs do not accumulate rows.
73
+
74
+ 9. **Record every tier run in the ledger.** The harness never runs the tests, so the run is recorded by whoever knows the result:
75
+
76
+ ```bash
77
+ bun scripts/validation-run.ts --item 12 --tier fast --status passed --duration 18s --dataset 100
78
+ bun scripts/validation-run.ts --item 12 --tier full --status passed --duration 41m -- "bun test tests/full"
79
+ ```
80
+
81
+ The ledger is plain NDJSON under `~/.pi-harness-runtime/logs/validation-runs.jsonl` (inspectable with `tail` or `jq`) and every record is mirrored into the central logging pipeline. The harness reads it to know which tier already passed, so an unreported run looks like a run that never happened.
82
+
83
+ **What the harness does with the plan:**
84
+
85
+ - The continuation message carries the plan, the recorded tier state and the remaining budget, so the agent never re-runs a tier that already passed.
86
+ - When `budgetMinutes` is spent, the item pauses at a safe boundary: nothing is dispatched, the work stays open, and the reason is reported.
87
+ - When a todo with `finalTierRequired` is marked complete while that tier is pending or failed, the completion report is suppressed and the item is named instead. A todo may not be reported as done on the strength of an unreported run.
88
+
39
89
  ## Versioning Convention
40
90
 
41
91
  **Only patch-level updates (`0.10.x`)** — no minor/major bumps until the app is fully functional.
@@ -502,6 +552,9 @@ bun run build && bun run start # Server deployment
502
552
  mode (`*_ENV`, `*_ENVIRONMENT`, `*_MODE`, `*_STAGE`, e.g. `AMOS_ENV`), then
503
553
  any other environment shaped key - and only then an environment shaped key
504
554
  of the process env → development/production/test
555
+ - The facts about the repository (only when NO variable names the environment
556
+ at all) → a published source repository without a runtime of its own is a
557
+ development checkout; a `develop`/`development` branch is the fallback
505
558
  - System RAM → Low RAM warning (<16GB total or <4GB available)
506
559
  - Running processes → dev/build server status
507
560
 
@@ -513,8 +566,26 @@ A value that is not provably harmless is masked (`<set>`): only the
513
566
  shown. A key that only looks like a branch (`BRANCH=production`) never
514
567
  decides the mode.
515
568
 
569
+ **Session variables of a desktop are not environment selectors.**
570
+ `GNOME_SHELL_SESSION_MODE`, `XDG_*`, `DESKTOP_*`, `DBUS_*`, ... name the
571
+ desktop session, never the environment of a workspace. Some of them end in
572
+ `_MODE` or `_ENV`, so they are excluded from the candidates by NAMESPACE,
573
+ not by value: their value ("ubuntu", "wayland") says nothing about the mode.
574
+
575
+ **The repository tier (last resort, never production).** It runs only when
576
+ no variable names the environment at all. A published package (a
577
+ `package.json` with a name, a version and declared content - `files`,
578
+ `bin` or `publishConfig`) without a deployment configuration (no Dockerfile,
579
+ compose file, Procfile, fly.toml, render.yaml, railway.json, vercel.json,
580
+ netlify.toml, serverless.yml, app.yaml, pm2 ecosystem file, no deploy
581
+ workflow) and without a running server is SOURCE CODE that the user runs on
582
+ their own machine → `DEVELOPMENT (source-only repository)`. A branch named
583
+ `develop` or `development` is the fallback → `DEVELOPMENT (git branch
584
+ develop)`. A production branch is NEVER a development checkout, and this
585
+ tier never claims production.
586
+
516
587
  **This info is shown in UI and delivered to agent context:**
517
- ```
588
+ ```text
518
589
  Workspace: ~/pi-harness-runtime | OS: Linux | mode: DEVELOPMENT (from AMOS_ENV in .env) | dev server: running | dev script: available | build script: available | RAM: 10/16
519
590
  ```
520
591
 
@@ -522,7 +593,8 @@ The mode words are exactly what the detector emits: `DEVELOPMENT`, `PRODUCTION`,
522
593
  `TEST` or `UNKNOWN` (with its reason, e.g.
523
594
  `UNKNOWN (no NODE_ENV or environment variable found)` or
524
595
  `UNKNOWN (AMOS_ENV in .env names no known mode)`). A known mode also
525
- names its source: `DEVELOPMENT (from AMOS_ENV in .env)`.
596
+ names its source: `DEVELOPMENT (from AMOS_ENV in .env)` or
597
+ `DEVELOPMENT (source-only repository)`.
526
598
  A dev/build script is a CAPABILITY, not a state: `dev script: available` means
527
599
  the script exists, not that a server runs. The server state is separate and
528
600
  never claims more than it knows: `running`, `not detected` (a check ran and
@@ -545,12 +617,15 @@ command to use.
545
617
  | `Dev server running` | Reuse it, don't restart |
546
618
 
547
619
  **Decision flow:**
548
- ```
620
+ ```text
549
621
  Check process.env.NODE_ENV first (it decides, also when it names no known mode)
550
622
  Check the .env and .env.* files of the workspace second
551
623
  (NODE_ENV, then an environment shaped key whose value names a mode,
552
624
  then any other environment shaped key)
553
625
  Only then an environment shaped key of the process env (e.g. AMOS_ENV)
626
+ Only then the facts about the repository
627
+ (a published source repository without a runtime of its own,
628
+ or a develop/development branch)
554
629
  Presence of dev script = capability, not proof of mode
555
630
  UNKNOWN requires investigation, not auto-build
556
631
  Production detection ≠ deployment authorization
@@ -561,3 +636,50 @@ Production detection ≠ deployment authorization
561
636
  ```
562
637
  ~/.pi-harness-runtime/workspace-env.sqlite
563
638
  ```
639
+
640
+ ## Centralized Application Logging
641
+
642
+ All application logs can be shipped to **Cloudflare R2** (durable archive) and to the
643
+ **unified webhook server** (live path). The pipeline lives in `packages/logging`
644
+ (`@pi-harness/logging`); the harness uses the process-wide singleton from it.
645
+
646
+ **Delivery order** (first success wins, the rest are skipped):
647
+
648
+ ```text
649
+ producer -> redact -> sample -> buffer -> R2 -> webhook -> local spool (~/.pi-harness-runtime/logs/spool)
650
+ ```
651
+
652
+ - The local spool is the fallback: if every transport fails the batch is written to
653
+ disk as NDJSON and re-shipped on the next successful flush. Nothing is lost, only delayed.
654
+ - The local log files (`~/.pi-harness-runtime/logs/*.jsonl`, `harness/*.log`) stay the
655
+ source of truth. The centralized pipeline is a copy for the archive.
656
+
657
+ **Configuration** (environment first, then `~/.pi-harness-runtime/keys/`):
658
+
659
+ | Variable | Meaning | Default |
660
+ |----------|---------|---------|
661
+ | `PI_LOG_SINK` | `off` \| `auto` \| `r2` \| `webhook` \| `r2+webhook` | `auto` |
662
+ | `PI_LOG_LEVEL` | Minimum level that is shipped | `info` |
663
+ | `PI_LOG_SAMPLE_RATE` | Share of `debug` records kept (0..1) | `0.1` |
664
+ | `PI_LOG_RATE_LIMIT` | Max records per source+event per minute | `60` |
665
+ | `PI_LOG_BATCH_SIZE` | Records per batch | `100` |
666
+ | `PI_LOG_BUFFER_SIZE` | Records held in memory | `1000` |
667
+ | `PI_LOG_FLUSH_INTERVAL_MS` | Automatic flush interval | `15000` |
668
+ | `PI_LOG_WEBHOOK_URL` | Ingest URL of the webhook server | unset |
669
+ | `PI_LOG_WEBHOOK_TOKEN` | Bearer token for the ingest endpoint | unset |
670
+ | `PI_LOG_R2_BUCKET` / `PI_LOG_R2_ENDPOINT` / `PI_LOG_R2_ACCESS_KEY_ID` / `PI_LOG_R2_SECRET_ACCESS_KEY` | R2 credentials | key files |
671
+ | `PI_LOG_SPOOL_DIR` | Local spool directory | `~/.pi-harness-runtime/logs/spool` |
672
+
673
+ R2 credentials are read from the key files `r2-bucket-name.txt`, `r2-endpoint.txt`,
674
+ `r2-access-key-id.txt`, `r2-secret-access-key.txt` and `r2-account-id.txt`.
675
+
676
+ **Rules for producers:**
677
+
678
+ - Emit through the singleton: `centralInfo/centralWarn/centralError(source, event, message?, fields?)`.
679
+ The `source` identifies the component, the `event` is a dotted name.
680
+ - Redaction is automatic (key names, credential patterns, truncation), but producers must
681
+ not pass user content: prompts, raw model output and message bodies are dropped by the
682
+ redactor, so shipping them is pointless.
683
+ - Never put a credential in a log message. The redactor is a safety net, not a licence.
684
+ - The webhook server exposes `POST /api/logs` for the ingest path. Set `LOG_INGEST_TOKEN`
685
+ to require a bearer token; the server re-redacts every batch and relays it to R2.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Console guard - keep a TUI's terminal clean.
3
+ *
4
+ * A TUI owns the terminal: a stray `console.log` from a library lands in the
5
+ * prompt input field ("[RelayClient] Connected ..."). While the TUI runs, the
6
+ * harness redirects the console into its log file.
7
+ *
8
+ * The guard is deliberately dumb: it never writes to the terminal and a
9
+ * failing sink can never break the caller.
10
+ */
11
+
12
+ export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
13
+
14
+ export interface ConsoleGuardOptions {
15
+ /** Receives every console call. Exceptions are swallowed. */
16
+ sink: (level: ConsoleLevel, message: string) => void;
17
+ }
18
+
19
+ const LEVELS: ConsoleLevel[] = ["log", "info", "warn", "error", "debug"];
20
+
21
+ /** Flatten the console arguments into a single line. */
22
+ export function formatConsoleArgs(args: unknown[]): string {
23
+ return args
24
+ .map((arg) => {
25
+ if (typeof arg === "string") return arg;
26
+ // The stack matters: pi's fatal path prints the error object.
27
+ if (arg instanceof Error) return arg.stack ?? arg.message;
28
+ try {
29
+ const json = JSON.stringify(arg);
30
+ return json === undefined ? String(arg) : json;
31
+ } catch {
32
+ return String(arg);
33
+ }
34
+ })
35
+ .join(" ");
36
+ }
37
+
38
+ /**
39
+ * Replace the console methods with a sink.
40
+ *
41
+ * @returns a restore function that puts the original console back.
42
+ */
43
+ export function installConsoleGuard(options: ConsoleGuardOptions): () => void {
44
+ // SAFETY: the console methods are own, writable properties of the global
45
+ // console object; TypeScript cannot express that index signature.
46
+ const target = globalThis.console as unknown as Record<string, unknown>;
47
+ const originals = new Map<string, unknown>();
48
+
49
+ for (const level of LEVELS) {
50
+ originals.set(level, target[level]);
51
+ target[level] = (...args: unknown[]): void => {
52
+ try {
53
+ options.sink(level, formatConsoleArgs(args));
54
+ } catch {
55
+ // A failing sink must never break the caller.
56
+ }
57
+ };
58
+ }
59
+
60
+ return () => {
61
+ for (const level of LEVELS) {
62
+ // The presence test must not depend on the value: a level that was
63
+ // absent before must be removed again.
64
+ if (!originals.has(level)) continue;
65
+ const original = originals.get(level);
66
+ if (original === undefined) {
67
+ delete target[level];
68
+ } else {
69
+ target[level] = original;
70
+ }
71
+ }
72
+ };
73
+ }
@@ -15,6 +15,8 @@
15
15
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { homedir } from "node:os";
18
+ // Centralized pipeline (R2 / webhook) - a copy of the local log.
19
+ import { centralError, centralInfo, centralWarn } from "../packages/logging/dist/index.js";
18
20
 
19
21
  export type LogLevel = "INFO" | "WARN" | "ERROR";
20
22
 
@@ -55,6 +57,21 @@ function writeLog(
55
57
  // Non-fatal - just log to console
56
58
  console.error(`[GLMQuotaLogger] Failed to write log: ${message}`);
57
59
  }
60
+
61
+ // Ship a copy to the centralized pipeline. The job id is an identifier,
62
+ // not content, so it is safe to include.
63
+ try {
64
+ const fields = { jobId: jobId ?? "system" };
65
+ if (level === "ERROR") {
66
+ centralError("harness.glm-quota", "quota", message, fields);
67
+ } else if (level === "WARN") {
68
+ centralWarn("harness.glm-quota", "quota", message, fields);
69
+ } else {
70
+ centralInfo("harness.glm-quota", "quota", message, fields);
71
+ }
72
+ } catch {
73
+ // Logging must never break the quota flow
74
+ }
58
75
  }
59
76
 
60
77
  // --- Public API ---
@@ -14,6 +14,8 @@
14
14
  import { mkdirSync, appendFileSync, existsSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { homedir } from "node:os";
17
+ // Centralized pipeline (R2 / webhook) - a copy of the local log.
18
+ import { centralInfo } from "../../packages/logging/dist/index.js";
17
19
 
18
20
  export interface PlannerDecision {
19
21
  timestamp: string;
@@ -179,6 +181,19 @@ export async function logPlannerDecision(decision: PlannerLogEntry): Promise<voi
179
181
  appendFileSync(LOG_FILE, fileLog);
180
182
  console.log(fileLog);
181
183
 
184
+ // Ship a copy to the centralized pipeline. Only the decision itself is
185
+ // shipped - the request and the raw planner output stay local.
186
+ try {
187
+ centralInfo("harness.planner", "decision", undefined, {
188
+ provider: decision.provider,
189
+ escalate: decision.escalate,
190
+ escalateReason: decision.escalateReason,
191
+ userInputType: decision.userInput?.type ?? null,
192
+ });
193
+ } catch {
194
+ // Logging must never break the planner flow
195
+ }
196
+
182
197
  // Send to Telegram (async, non-blocking for main flow)
183
198
  const telegramMsg = formatTelegramMessage(decision);
184
199
  // Fire-and-forget: don't await, let it run in background
@@ -8,6 +8,9 @@
8
8
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
+ // Centralized pipeline (R2 / webhook). The local JSONL file stays the
12
+ // source of truth; the shipper is a copy for the archive.
13
+ import { centralError, centralInfo, centralWarn } from "../packages/logging/dist/index.js";
11
14
 
12
15
  export type ServiceLevel = "info" | "warn" | "error" | "debug";
13
16
  export type ServiceName = "jev" | "honcho" | "qdrant";
@@ -41,16 +44,44 @@ function ensureLogDir(): void {
41
44
  * Sensitive fields (keys, tokens, full content) are never logged.
42
45
  */
43
46
  export function logServiceEvent(event: ServiceEvent): void {
47
+ let safeEvent: Record<string, unknown> = { ...event };
44
48
  try {
45
49
  ensureLogDir();
46
50
 
47
51
  // Redact sensitive fields
48
- const safeEvent = redactSensitive(event);
52
+ safeEvent = redactSensitive(event);
49
53
 
50
54
  appendFileSync(LOG_FILE, JSON.stringify(safeEvent) + "\n");
51
55
  } catch {
52
56
  // Silently fail if logging fails - don't crash the service
53
57
  }
58
+
59
+ shipServiceEvent(safeEvent);
60
+ }
61
+
62
+ /**
63
+ * Send a service event to the centralized pipeline.
64
+ *
65
+ * The event was already redacted above; the shipper redacts again, which
66
+ * is cheap and keeps the guarantee local to one place.
67
+ */
68
+ function shipServiceEvent(event: Record<string, unknown>): void {
69
+ try {
70
+ const level = typeof event.level === "string" ? event.level : "info";
71
+ const fields: Record<string, unknown> = { ...event };
72
+ delete fields.level;
73
+ delete fields.ts;
74
+
75
+ if (level === "error") {
76
+ centralError("harness.service-diagnostics", String(event.operation ?? "event"), undefined, fields);
77
+ } else if (level === "warn") {
78
+ centralWarn("harness.service-diagnostics", String(event.operation ?? "event"), undefined, fields);
79
+ } else {
80
+ centralInfo("harness.service-diagnostics", String(event.operation ?? "event"), undefined, fields);
81
+ }
82
+ } catch {
83
+ // Never let shipping break the service
84
+ }
54
85
  }
55
86
 
56
87
  /**
package/index.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  import { renderDetailLines } from "./harness/startup-lines.ts";
31
31
  import { describeInboundAccess, getAllowedUserIds } from "./harness/telegram-auth.ts";
32
32
  import { buildTelegramCallbackUpdate, buildTelegramMessageUpdate } from "./harness/telegram-update.ts";
33
+ import { installConsoleGuard } from "./harness/console-guard.ts";
33
34
  import {
34
35
  jevMissingKeyReport,
35
36
  lineConfigurationReport,
@@ -99,6 +100,10 @@ import {
99
100
  hasPaperclipConfig,
100
101
  PaperclipWatcher,
101
102
  } from "./packages/paperclip-integration/dist/index.js";
103
+ // Centralized application logging: R2 (durable archive) + webhook (live),
104
+ // with a local spool as the fallback. Optional - without credentials the
105
+ // logs simply stay local.
106
+ import { centralInfo, initCentralLogging } from "./packages/logging/dist/index.js";
102
107
 
103
108
  // --- Jev Auto-Continue: Agent autonomous decision-making --------------------
104
109
  // Lazy import - only loads when packages/jev-judge exists and API key available
@@ -345,6 +350,7 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
345
350
  if (ctx.hasUI && ctx.ui) {
346
351
  _setAutoContinueStatus = ctx.ui.setStatus.bind(ctx.ui, "harness-auto-continue");
347
352
  }
353
+
348
354
  });
349
355
 
350
356
  // Clear status and cancel background work on session shutdown
@@ -640,12 +646,29 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
640
646
 
641
647
  // Get task context from local todo controller (not bd)
642
648
  const snapshot = await (todoContinuationController?.getSnapshot() ?? Promise.resolve({ items: [] }));
649
+ // An item whose final tier is still pending is NOT done, even when
650
+ // it is already marked completed. The judge must see it as
651
+ // remaining work, otherwise the session ends on a false
652
+ // completion.
653
+ const pendingFinalTier = validationLedger
654
+ ? snapshot.items.filter((item) => validationLedger?.isFinalTierPending(item))
655
+ : [];
656
+ const pendingFinalTierIds = new Set(pendingFinalTier.map((item) => item.id));
657
+ if (pendingFinalTier.length > 0) {
658
+ logToFile(
659
+ "[auto-continue]",
660
+ "Final tier still pending for:",
661
+ pendingFinalTier.map((item) => `${item.id}:${item.subject}`).join(", "),
662
+ );
663
+ }
643
664
  const taskContext = {
644
665
  completed: snapshot.items
645
- .filter((item: { status?: string; subject?: string }) => item.status === "completed")
666
+ .filter((item: { id?: number; status?: string; subject?: string }) =>
667
+ item.status === "completed" && !(item.id !== undefined && pendingFinalTierIds.has(item.id)))
646
668
  .map((item: { status?: string; subject?: string }) => item.subject ?? ""),
647
669
  remaining: snapshot.items
648
- .filter((item: { status?: string; subject?: string }) => item.status !== "completed")
670
+ .filter((item: { id?: number; status?: string; subject?: string }) =>
671
+ item.status !== "completed" || (item.id !== undefined && pendingFinalTierIds.has(item.id)))
649
672
  .map((item: { status?: string; subject?: string }) => item.subject ?? ""),
650
673
  total: snapshot.items.length,
651
674
  };
@@ -727,11 +750,35 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
727
750
 
728
751
  // --- todo continuation: Retain one controller per session ----------
729
752
  let todoContinuationController: import("./packages/jev-judge/dist/continuation-controller.js").TodoContinuationController | null = null;
753
+ // Ledger of the validation runs: it is the only mechanical source for
754
+ // "did the final tier pass?" and therefore guards the completion report.
755
+ let validationLedger: import("./packages/jev-judge/dist/validation-ledger.js").ValidationLedger | null = null;
730
756
 
731
757
  // Create controller on session_start with proper dispatch callback
732
758
  pi.on("session_start", async (_event, ctx) => {
733
759
  try {
734
760
  const { TodoContinuationController } = await import("./packages/jev-judge/dist/continuation-controller.js");
761
+ const { ValidationLedger } = await import("./packages/jev-judge/dist/validation-ledger.js");
762
+ // The ledger records which validation tier ran. The controller
763
+ // uses it to tell the agent what already passed and whether the
764
+ // final tier is still pending.
765
+ validationLedger = new ValidationLedger({
766
+ onRecord: (record) => {
767
+ centralInfo(
768
+ "validation",
769
+ "tier.run",
770
+ `${record.tier} tier ${record.status}`,
771
+ {
772
+ itemId: record.itemId,
773
+ tier: record.tier,
774
+ status: record.status,
775
+ durationMs: record.durationMs,
776
+ datasetSize: record.datasetSize,
777
+ command: record.command,
778
+ },
779
+ );
780
+ },
781
+ });
735
782
  // SAFETY: ctx is ExtensionContext with sessionManager, isIdle, hasPendingMessages
736
783
  // dispatch uses pi.sendUserMessage for actual message delivery
737
784
  todoContinuationController = new TodoContinuationController(
@@ -740,6 +787,7 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
740
787
  dispatch: async (content: string) => {
741
788
  await pi.sendUserMessage(content, { deliverAs: "steer" });
742
789
  },
790
+ validationLedger,
743
791
  }
744
792
  );
745
793
  logToFile("[auto-continue]", "Todo continuation controller initialized");
@@ -986,6 +1034,26 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
986
1034
  const isNoPendingTasks = reason === "No pending tasks";
987
1035
 
988
1036
  if (isNoPendingTasks) {
1037
+ // A todo whose final tier is still pending must not be
1038
+ // reported as complete. The item may already be marked
1039
+ // completed by the agent - the ledger is the check that
1040
+ // catches it.
1041
+ const snapshot = await todoContinuationController.getSnapshot();
1042
+ const pendingFinalTier = validationLedger
1043
+ ? snapshot.items.filter((item) => validationLedger?.isFinalTierPending(item))
1044
+ : [];
1045
+ if (pendingFinalTier.length > 0) {
1046
+ const details = pendingFinalTier
1047
+ .map((item) => `${item.id}:${item.subject}`)
1048
+ .join(", ");
1049
+ logToFile(
1050
+ "[auto-continue]",
1051
+ "Completion suppressed - final tier still pending for:",
1052
+ details,
1053
+ );
1054
+ updateAutoContinueStatus("ERROR", "final tier pending");
1055
+ return;
1056
+ }
989
1057
  // Check if there's a pending question - suppress completion if so
990
1058
  if (pqController.hasPendingQuestion()) {
991
1059
  logToFile("[auto-continue]", "Suppressing completion - pending question exists");
@@ -1003,7 +1071,6 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1003
1071
  _completedNotifiedFor = completionKey;
1004
1072
 
1005
1073
  // Get full todo list for notification
1006
- const snapshot = await todoContinuationController.getSnapshot();
1007
1074
  const todoList = snapshot.items
1008
1075
  .map((item) => {
1009
1076
  const status = item.status === "completed" ? "[DONE]" : "[PENDING]";
@@ -2044,6 +2111,14 @@ async function initRelayStream(): Promise<void> {
2044
2111
  ]);
2045
2112
  }
2046
2113
  },
2114
+ },
2115
+ {
2116
+ // A TUI owns the terminal: the relay diagnostics belong in the log
2117
+ // file. A console write would land in the prompt input field
2118
+ // ("[RelayClient] Connected ...").
2119
+ log: (message) => logToFile("[RelayStream]", `log: ${message}`),
2120
+ warn: (message) => logToFile("[RelayStream]", `warn: ${message}`),
2121
+ error: (message) => logToFile("[RelayStream]", `error: ${message}`),
2047
2122
  }
2048
2123
  );
2049
2124
 
@@ -2055,6 +2130,50 @@ async function initRelayStream(): Promise<void> {
2055
2130
  }
2056
2131
  }
2057
2132
 
2133
+ /**
2134
+ * Console guard - the TUI owns the terminal.
2135
+ *
2136
+ * A library that writes to the console puts its text into the prompt input
2137
+ * field ("[RelayClient] Connected ..."). In TUI mode every console call goes
2138
+ * to the harness log instead. An error is also surfaced as a harness message,
2139
+ * so a crash is never invisible.
2140
+ */
2141
+ function initConsoleGuard(pi: ExtensionAPI): void {
2142
+ let restore: (() => void) | null = null;
2143
+ // A reload emits session_start BEFORE the previous session_shutdown:
2144
+ // the guard must stay installed for the new session. The console is only
2145
+ // given back when every started session has been shut down.
2146
+ let starts = 0;
2147
+ let shutdowns = 0;
2148
+ const recentErrors = new Map<string, number>();
2149
+
2150
+ pi.on("session_start", (_event, ctx) => {
2151
+ starts++;
2152
+ if (ctx.mode !== "tui" || restore) return;
2153
+ restore = installConsoleGuard({
2154
+ sink: (level, message) => {
2155
+ logToFile("[Console]", `${level}: ${message}`);
2156
+ if (level !== "error") return;
2157
+ // One message per 30s: a crash must be visible, a loop must not flood.
2158
+ const now = Date.now();
2159
+ if (now - (recentErrors.get(message) ?? 0) < 30_000) return;
2160
+ recentErrors.set(message, now);
2161
+ const text = message.length > 200 ? `${message.slice(0, 200)}...` : message;
2162
+ addHarnessMessage("Console", text, "err");
2163
+ },
2164
+ });
2165
+ logToFile("[Console]", "Console redirected to the log file (TUI mode)");
2166
+ });
2167
+
2168
+ pi.on("session_shutdown", () => {
2169
+ shutdowns++;
2170
+ // Only the last session may take the console back.
2171
+ if (!restore || shutdowns < starts) return;
2172
+ restore();
2173
+ restore = null;
2174
+ });
2175
+ }
2176
+
2058
2177
  // --- honcho-memory: Honcho MCP for peer/user memory via https://mcp.honcho.dev ------
2059
2178
  // Registers Honcho MCP server and implements automatic memory lifecycle:
2060
2179
  // - Ingest sanitized messages on completed turns
@@ -2765,7 +2884,7 @@ function processCompletionFile(filePath: string, pi: ExtensionAPI): void {
2765
2884
  // --- Debug logging (file only, no console override) ---------------
2766
2885
  // Logs written to file only. Real console output preserved for pi's TUI.
2767
2886
  import { homedir } from "node:os";
2768
- import { existsSync, mkdirSync, readFileSync, readdirSync, symlinkSync, appendFileSync, realpathSync, statSync } from "node:fs";
2887
+ import { existsSync, mkdirSync, readFileSync, readdirSync, symlinkSync, appendFileSync, realpathSync, statSync, chmodSync, openSync, closeSync, renameSync } from "node:fs";
2769
2888
  import { truncateToWidth } from "@earendil-works/pi-tui";
2770
2889
  import { join, dirname } from "node:path";
2771
2890
 
@@ -2775,6 +2894,11 @@ const HARNESS_ROOT_DIR = join(homedir(), ".pi-harness-runtime");
2775
2894
  /** Startup messages collected during init — shown on session_start via ctx.ui.notify */
2776
2895
  const startupMessages: string[] = [];
2777
2896
  const HARNESS_LOG_FILE = "/tmp/pi-harness.log";
2897
+ // The log can contain urls and session ids: owner only, no rotation while it
2898
+ // is small, and a bounded size so a chatty dependency cannot fill the disk.
2899
+ const HARNESS_LOG_MAX_BYTES = 5 * 1024 * 1024;
2900
+ let harnessLogFileSecured = false;
2901
+ let harnessLogWrites = 0;
2778
2902
 
2779
2903
  /** Log a harness message to file (not stderr - bypasses Pi's cursor tracking) */
2780
2904
  function logToFile(...parts: string[]): void {
@@ -2782,10 +2906,56 @@ function logToFile(...parts: string[]): void {
2782
2906
  // A timestamp makes the log usable for latency measurements.
2783
2907
  const stamp = new Date().toISOString();
2784
2908
  try {
2909
+ if (!harnessLogFileSecured) {
2910
+ try {
2911
+ // Create the file with the owner-only mode BEFORE the first
2912
+ // write: there must be no world-readable window.
2913
+ const fd = openSync(HARNESS_LOG_FILE, "a", 0o600);
2914
+ closeSync(fd);
2915
+ chmodSync(HARNESS_LOG_FILE, 0o600);
2916
+ // Only a successful attempt stops the retries.
2917
+ harnessLogFileSecured = true;
2918
+ } catch {
2919
+ // Ignore permission errors (e.g. a mounted /tmp) and retry
2920
+ // on the next write.
2921
+ }
2922
+ }
2923
+
2924
+ // Keep the log bounded: rotate it when it grew too large.
2925
+ harnessLogWrites++;
2926
+ if (harnessLogWrites % 200 === 0) {
2927
+ try {
2928
+ if (statSync(HARNESS_LOG_FILE).size > HARNESS_LOG_MAX_BYTES) {
2929
+ renameSync(HARNESS_LOG_FILE, `${HARNESS_LOG_FILE}.1`);
2930
+ chmodSync(`${HARNESS_LOG_FILE}.1`, 0o600);
2931
+ // Secure the fresh file right away: the write that
2932
+ // follows must not land in a 0644 file.
2933
+ try {
2934
+ const fd = openSync(HARNESS_LOG_FILE, "a", 0o600);
2935
+ closeSync(fd);
2936
+ chmodSync(HARNESS_LOG_FILE, 0o600);
2937
+ harnessLogFileSecured = true;
2938
+ } catch {
2939
+ harnessLogFileSecured = false;
2940
+ }
2941
+ }
2942
+ } catch {
2943
+ // Ignore rotation errors
2944
+ }
2945
+ }
2946
+
2785
2947
  appendFileSync(HARNESS_LOG_FILE, `[${stamp}] ${msg}\n`);
2786
2948
  } catch {
2787
2949
  // Ignore file write errors
2788
2950
  }
2951
+
2952
+ // Mirror the line into the centralized pipeline. The sampler keeps the
2953
+ // volume bounded; the local file above stays the source of truth.
2954
+ try {
2955
+ centralInfo("harness", "log", msg);
2956
+ } catch {
2957
+ // Logging must never break the caller.
2958
+ }
2789
2959
  }
2790
2960
 
2791
2961
  /** Log a harness startup message to file and track for UI */
@@ -3233,6 +3403,22 @@ export default async function (pi: ExtensionAPI) {
3233
3403
  const mirrorStore = new MirrorStore();
3234
3404
  ensureHarnessDir();
3235
3405
 
3406
+ // --- central-logging: ship structured logs to R2 / the webhook server ---
3407
+ // The pipeline is opt-in by configuration, never by code: without R2
3408
+ // credentials and without a webhook URL nothing leaves the machine.
3409
+ try {
3410
+ const logConfig = initCentralLogging();
3411
+ if (logConfig?.enabled) {
3412
+ logToFile(
3413
+ "[CentralLogging]",
3414
+ "enabled -",
3415
+ logConfig.notes.length > 0 ? logConfig.notes.join("; ") : "no transport configured",
3416
+ );
3417
+ }
3418
+ } catch (e) {
3419
+ logToFile("[CentralLogging]", "init failed:", String(e));
3420
+ }
3421
+
3236
3422
  // Collect async init promises so we can wait for them before showing startup notification
3237
3423
  const asyncInitPromises: Promise<unknown>[] = [];
3238
3424
 
@@ -3294,6 +3480,11 @@ export default async function (pi: ExtensionAPI) {
3294
3480
  // Telegram is not configured at all - that is the fresh-install case.
3295
3481
  reportRelayConfiguration();
3296
3482
 
3483
+ // --- console-guard: keep the TUI prompt input field clean ----
3484
+ // A library that writes to the console would put its text into the
3485
+ // prompt input field. The guard sends it to the harness log instead.
3486
+ initConsoleGuard(pi);
3487
+
3297
3488
  // --- line-voice-queue: Watch for LINE voice commands ----
3298
3489
  void initLineVoiceQueue(pi);
3299
3490