@loopover/miner 0.1.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/DEPLOYMENT.md +156 -0
- package/Dockerfile +38 -0
- package/README.md +269 -0
- package/bin/gittensory-miner-mcp.d.ts +64 -0
- package/bin/gittensory-miner-mcp.js +279 -0
- package/bin/gittensory-miner.js +196 -0
- package/docs/coding-agent-driver.md +140 -0
- package/docs/config-precedence.md +96 -0
- package/docs/cross-repo-discovery-phase1.md +55 -0
- package/docs/discovery-index-contract.md +60 -0
- package/docs/discovery-plane-operator-guide.md +101 -0
- package/docs/env-reference.md +30 -0
- package/docs/fleet-run-manifest.md +50 -0
- package/docs/miner-goal-spec.md +76 -0
- package/docs/miner-selfimprove-calibration.md +105 -0
- package/docs/observability.md +60 -0
- package/docs/operations-runbook.md +209 -0
- package/docs/repo-agnostic-capability-audit.md +129 -0
- package/docs/unattended-scheduling.md +107 -0
- package/expected-engine.version +1 -0
- package/lib/ams-policy.d.ts +20 -0
- package/lib/ams-policy.js +68 -0
- package/lib/attempt-cli.d.ts +99 -0
- package/lib/attempt-cli.js +523 -0
- package/lib/attempt-input-builder.d.ts +24 -0
- package/lib/attempt-input-builder.js +91 -0
- package/lib/attempt-log.d.ts +37 -0
- package/lib/attempt-log.js +177 -0
- package/lib/attempt-runner.d.ts +59 -0
- package/lib/attempt-runner.js +164 -0
- package/lib/attempt-worktree.d.ts +31 -0
- package/lib/attempt-worktree.js +94 -0
- package/lib/calibration-cli.d.ts +1 -0
- package/lib/calibration-cli.js +92 -0
- package/lib/calibration-run.d.ts +161 -0
- package/lib/calibration-run.js +232 -0
- package/lib/calibration-types.d.ts +46 -0
- package/lib/calibration-types.js +74 -0
- package/lib/calibration.d.ts +24 -0
- package/lib/calibration.js +99 -0
- package/lib/ci-poller.d.ts +33 -0
- package/lib/ci-poller.js +234 -0
- package/lib/claim-adjudication.d.ts +21 -0
- package/lib/claim-adjudication.js +36 -0
- package/lib/claim-conflict-resolver.d.ts +29 -0
- package/lib/claim-conflict-resolver.js +101 -0
- package/lib/claim-ledger-cli.d.ts +59 -0
- package/lib/claim-ledger-cli.js +320 -0
- package/lib/claim-ledger-expiry.d.ts +20 -0
- package/lib/claim-ledger-expiry.js +41 -0
- package/lib/claim-ledger.d.ts +63 -0
- package/lib/claim-ledger.js +303 -0
- package/lib/cli-error.d.ts +3 -0
- package/lib/cli-error.js +27 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +72 -0
- package/lib/coding-agent-construction.d.ts +17 -0
- package/lib/coding-agent-construction.js +103 -0
- package/lib/coding-agent-house-rules.d.ts +24 -0
- package/lib/coding-agent-house-rules.js +64 -0
- package/lib/coding-task-spec.d.ts +44 -0
- package/lib/coding-task-spec.js +183 -0
- package/lib/config-precedence.d.ts +25 -0
- package/lib/config-precedence.js +38 -0
- package/lib/deny-check.d.ts +11 -0
- package/lib/deny-check.js +76 -0
- package/lib/deny-hook-synthesis.d.ts +93 -0
- package/lib/deny-hook-synthesis.js +399 -0
- package/lib/deny-hooks.d.ts +28 -0
- package/lib/deny-hooks.js +155 -0
- package/lib/deployment-docs-audit.d.ts +41 -0
- package/lib/deployment-docs-audit.js +115 -0
- package/lib/discover-cli.d.ts +94 -0
- package/lib/discover-cli.js +284 -0
- package/lib/discovery-throttle.d.ts +8 -0
- package/lib/discovery-throttle.js +33 -0
- package/lib/env-file-indirection.d.ts +4 -0
- package/lib/env-file-indirection.js +45 -0
- package/lib/event-ledger-cli.d.ts +82 -0
- package/lib/event-ledger-cli.js +266 -0
- package/lib/event-ledger.d.ts +37 -0
- package/lib/event-ledger.js +210 -0
- package/lib/execute-local-write.d.ts +14 -0
- package/lib/execute-local-write.js +50 -0
- package/lib/feasibility-cli.d.ts +25 -0
- package/lib/feasibility-cli.js +80 -0
- package/lib/forge-config.d.ts +17 -0
- package/lib/forge-config.js +37 -0
- package/lib/governor-action-mode.d.ts +26 -0
- package/lib/governor-action-mode.js +47 -0
- package/lib/governor-chokepoint-persisted.d.ts +18 -0
- package/lib/governor-chokepoint-persisted.js +46 -0
- package/lib/governor-chokepoint.d.ts +14 -0
- package/lib/governor-chokepoint.js +52 -0
- package/lib/governor-kill-switch.d.ts +26 -0
- package/lib/governor-kill-switch.js +47 -0
- package/lib/governor-ledger-cli.d.ts +32 -0
- package/lib/governor-ledger-cli.js +155 -0
- package/lib/governor-ledger.d.ts +46 -0
- package/lib/governor-ledger.js +203 -0
- package/lib/governor-open-pr.d.ts +21 -0
- package/lib/governor-open-pr.js +27 -0
- package/lib/governor-pause-cli.d.ts +23 -0
- package/lib/governor-pause-cli.js +171 -0
- package/lib/governor-run-halt.d.ts +37 -0
- package/lib/governor-run-halt.js +58 -0
- package/lib/governor-state.d.ts +63 -0
- package/lib/governor-state.js +363 -0
- package/lib/governor-write-rate-limit.d.ts +30 -0
- package/lib/governor-write-rate-limit.js +64 -0
- package/lib/harness-submission-trigger.d.ts +69 -0
- package/lib/harness-submission-trigger.js +138 -0
- package/lib/http-retry.d.ts +12 -0
- package/lib/http-retry.js +52 -0
- package/lib/laptop-init.d.ts +53 -0
- package/lib/laptop-init.js +332 -0
- package/lib/live-issue-snapshot.d.ts +16 -0
- package/lib/live-issue-snapshot.js +117 -0
- package/lib/local-store.d.ts +18 -0
- package/lib/local-store.js +62 -0
- package/lib/logger.d.ts +58 -0
- package/lib/logger.js +167 -0
- package/lib/loop-cli.d.ts +66 -0
- package/lib/loop-cli.js +540 -0
- package/lib/loop-closure.d.ts +35 -0
- package/lib/loop-closure.js +66 -0
- package/lib/loop-reentry.d.ts +51 -0
- package/lib/loop-reentry.js +124 -0
- package/lib/manage-poll.d.ts +79 -0
- package/lib/manage-poll.js +254 -0
- package/lib/manage-status.d.ts +70 -0
- package/lib/manage-status.js +241 -0
- package/lib/metrics-cli.d.ts +9 -0
- package/lib/metrics-cli.js +50 -0
- package/lib/migrate-cli.d.ts +24 -0
- package/lib/migrate-cli.js +111 -0
- package/lib/miner-goal-spec.d.ts +12 -0
- package/lib/miner-goal-spec.js +67 -0
- package/lib/opportunity-fanout.d.ts +86 -0
- package/lib/opportunity-fanout.js +597 -0
- package/lib/opportunity-ranker.d.ts +36 -0
- package/lib/opportunity-ranker.js +118 -0
- package/lib/orb-export.d.ts +54 -0
- package/lib/orb-export.js +206 -0
- package/lib/plan-store-cli.d.ts +37 -0
- package/lib/plan-store-cli.js +151 -0
- package/lib/plan-store.d.ts +51 -0
- package/lib/plan-store.js +231 -0
- package/lib/policy-doc-cache.d.ts +25 -0
- package/lib/policy-doc-cache.js +79 -0
- package/lib/policy-verdict-cache.d.ts +35 -0
- package/lib/policy-verdict-cache.js +106 -0
- package/lib/portfolio-dashboard.d.ts +30 -0
- package/lib/portfolio-dashboard.js +105 -0
- package/lib/portfolio-discovery.d.ts +29 -0
- package/lib/portfolio-discovery.js +100 -0
- package/lib/portfolio-queue-cli.d.ts +87 -0
- package/lib/portfolio-queue-cli.js +509 -0
- package/lib/portfolio-queue-expiry.d.ts +20 -0
- package/lib/portfolio-queue-expiry.js +51 -0
- package/lib/portfolio-queue-manager.d.ts +50 -0
- package/lib/portfolio-queue-manager.js +134 -0
- package/lib/portfolio-queue.d.ts +62 -0
- package/lib/portfolio-queue.js +351 -0
- package/lib/pr-disposition-poller.d.ts +26 -0
- package/lib/pr-disposition-poller.js +163 -0
- package/lib/pr-number-parse.d.ts +4 -0
- package/lib/pr-number-parse.js +23 -0
- package/lib/pr-outcome.d.ts +41 -0
- package/lib/pr-outcome.js +90 -0
- package/lib/prediction-ledger.d.ts +47 -0
- package/lib/prediction-ledger.js +222 -0
- package/lib/pretooluse-hook.d.ts +31 -0
- package/lib/pretooluse-hook.js +93 -0
- package/lib/process-lifecycle.d.ts +31 -0
- package/lib/process-lifecycle.js +106 -0
- package/lib/purge-cli.d.ts +44 -0
- package/lib/purge-cli.js +198 -0
- package/lib/rejection-signal.d.ts +6 -0
- package/lib/rejection-signal.js +101 -0
- package/lib/rejection-state-machine.d.ts +37 -0
- package/lib/rejection-state-machine.js +81 -0
- package/lib/rejection-templates.d.ts +12 -0
- package/lib/rejection-templates.js +71 -0
- package/lib/replay-objective-anchor.d.ts +79 -0
- package/lib/replay-objective-anchor.js +179 -0
- package/lib/replay-snapshot.d.ts +41 -0
- package/lib/replay-snapshot.js +288 -0
- package/lib/replay-task-generation.d.ts +126 -0
- package/lib/replay-task-generation.js +223 -0
- package/lib/repo-clone.d.ts +19 -0
- package/lib/repo-clone.js +102 -0
- package/lib/run-state-cli.d.ts +27 -0
- package/lib/run-state-cli.js +154 -0
- package/lib/run-state.d.ts +37 -0
- package/lib/run-state.js +150 -0
- package/lib/schema-version.d.ts +17 -0
- package/lib/schema-version.js +71 -0
- package/lib/self-review-context.d.ts +28 -0
- package/lib/self-review-context.js +339 -0
- package/lib/slop-assessment.d.ts +3 -0
- package/lib/slop-assessment.js +20 -0
- package/lib/stack-detection.d.ts +41 -0
- package/lib/stack-detection.js +248 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +441 -0
- package/lib/store-maintenance.d.ts +31 -0
- package/lib/store-maintenance.js +183 -0
- package/lib/submission-freshness-check.d.ts +32 -0
- package/lib/submission-freshness-check.js +93 -0
- package/lib/update-check.d.ts +36 -0
- package/lib/update-check.js +161 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.js +10 -0
- package/lib/worktree-allocator.d.ts +39 -0
- package/lib/worktree-allocator.js +262 -0
- package/package.json +50 -0
- package/schema/miner-goal-spec.schema.json +111 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Observing your miner
|
|
2
|
+
|
|
3
|
+
How to point Grafana at redacted miner reporting exports to see attempt and prediction history without exposing the
|
|
4
|
+
miner's live local ledgers. This covers the **miner-specific** observability wiring only; for general self-host
|
|
5
|
+
operations, see your ops runbook.
|
|
6
|
+
|
|
7
|
+
## What's observable
|
|
8
|
+
|
|
9
|
+
The miner writes append-only SQLite ledgers under `GITTENSORY_MINER_CONFIG_DIR` (default
|
|
10
|
+
`~/.config/gittensory-miner` on a laptop, or `/data/miner` in the fleet Docker image — see
|
|
11
|
+
[`DEPLOYMENT.md`](../DEPLOYMENT.md)):
|
|
12
|
+
|
|
13
|
+
- **`attempt-log.sqlite3`** — the driver-level attempt event trace (event type, action class, mode, reason,
|
|
14
|
+
timestamps), table `attempt_log_events`.
|
|
15
|
+
- **`prediction-ledger.sqlite3`** — recorded predicted-gate verdicts for later scoring.
|
|
16
|
+
|
|
17
|
+
Those live files can contain free-form payloads, repo/target identifiers, readiness scores, and blocker/warning
|
|
18
|
+
codes. Keep `GITTENSORY_MINER_CONFIG_DIR` private to the miner. Grafana should read only sanitized reporting
|
|
19
|
+
exports that operators create from those ledgers.
|
|
20
|
+
|
|
21
|
+
## Point Grafana at reporting exports
|
|
22
|
+
|
|
23
|
+
The repo ships datasource provisioning at
|
|
24
|
+
[`grafana/provisioning/datasources/ams-ledgers.yml`](../../../grafana/provisioning/datasources/ams-ledgers.yml)
|
|
25
|
+
— two **read-only** `frser-sqlite-datasource` entries: `AMS Attempt Log` (uid `ams-attempt-log`) and
|
|
26
|
+
`AMS Prediction Ledger` (uid `ams-prediction-ledger`). Their default paths live under Grafana's reporting mount,
|
|
27
|
+
not under the miner config directory.
|
|
28
|
+
|
|
29
|
+
1. **Install the SQLite plugin** in Grafana — the same one the maintainer `LoopoverDB` datasource uses:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
GF_INSTALL_PLUGINS=frser-sqlite-datasource
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. **Run the AMS reporting exporter**, a dedicated compose profile (only useful when a miner also runs on this
|
|
36
|
+
same host — an engine-only deployment has nothing for it to read):
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
docker compose --profile ams-observability up -d
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Set `GITTENSORY_MINER_CONFIG_DIR` in your `.env` (see [`.env.example`](../../../.env.example)) to the same
|
|
43
|
+
directory your miner uses. The `ams-reporting-exporter` container mounts it **read-only**, runs
|
|
44
|
+
[`scripts/export-ams-reporting-db.sh`](../../../scripts/export-ams-reporting-db.sh) on an interval
|
|
45
|
+
(`GITTENSORY_AMS_REPORTING_EXPORT_INTERVAL_SECONDS`, default 30s), and writes the redacted snapshots into the
|
|
46
|
+
same `reporting` volume Grafana already reads — Grafana itself never mounts the live ledgers. The exported
|
|
47
|
+
schema drops `attempt_log_events.reason`/`.payload_json` (the free-form fields) entirely; every other column,
|
|
48
|
+
including the `predictions` table's `blocker_codes_json`/`warning_codes_json` (fixed, engine-defined codes —
|
|
49
|
+
never free text), passes through unchanged.
|
|
50
|
+
|
|
51
|
+
3. **Restart Grafana.** The two datasources appear under **Connections → Data sources**, already provisioned
|
|
52
|
+
(non-editable) so they survive restarts.
|
|
53
|
+
|
|
54
|
+
## Load a dashboard
|
|
55
|
+
|
|
56
|
+
Dashboards live in [`grafana/dashboards/`](../../../grafana/dashboards/) and are auto-provisioned from that
|
|
57
|
+
directory. To visualize AMS activity, add a dashboard JSON there — or import one at runtime via the Grafana UI
|
|
58
|
+
(**Dashboards → Import**) — and point its panels at the `AMS Attempt Log` / `AMS Prediction Ledger` datasources
|
|
59
|
+
above. Panels should query only the redacted reporting schema (e.g. `SELECT * FROM attempt_log_events`), never a
|
|
60
|
+
`payload_json`/`reason` column — the exporter drops both, so a panel referencing them returns no such column.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# gittensory-miner — operational runbook
|
|
2
|
+
|
|
3
|
+
Operator-facing runbook for **local SQLite state**: what the concurrency guarantees actually mean, how to recover from corruption, what to do when two miner processes collide on the same files, and how schema upgrades migrate your on-disk ledgers after a package update.
|
|
4
|
+
|
|
5
|
+
> **Scope:** AMS local stores only. For laptop/fleet deployment layout see [`../DEPLOYMENT.md`](../DEPLOYMENT.md). For Grafana setup see [#5190](https://github.com/JSONbored/gittensory/issues/5190). For the optional hosted discovery plane see [`discovery-plane-operator-guide.md`](discovery-plane-operator-guide.md). This runbook does **not** cover the self-hosted **review stack** (Orb/API/LoopoverDB).
|
|
6
|
+
|
|
7
|
+
## Local state at a glance
|
|
8
|
+
|
|
9
|
+
Every miner keeps **independent SQLite files** under one state directory (default `~/.config/gittensory-miner/`, override with `GITTENSORY_MINER_CONFIG_DIR`). Each store has its own file, table, and optional per-store env override — see the table in [`../README.md`](../README.md#local-storage) and [`env-reference.md`](env-reference.md).
|
|
10
|
+
|
|
11
|
+
Common files you will touch in incidents:
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
|------|---------|
|
|
15
|
+
| `laptop-state.sqlite3` | Bootstrap metadata (`gittensory-miner init`) |
|
|
16
|
+
| `claim-ledger.sqlite3` | Soft issue claims on this machine |
|
|
17
|
+
| `event-ledger.sqlite3` | Append-only manage-loop audit trail |
|
|
18
|
+
| `portfolio-queue.sqlite3` | Per-repo portfolio queue |
|
|
19
|
+
| `run-state.sqlite3` | Discover/plan/prepare phase markers |
|
|
20
|
+
| `attempt-log.sqlite3` | Per-attempt coding-agent driver events |
|
|
21
|
+
| `prediction-ledger.sqlite3` | Predicted gate verdicts for self-improve |
|
|
22
|
+
| `plan-store.sqlite3` | Persisted MCP plan DAGs |
|
|
23
|
+
| `governor-ledger.sqlite3` | Governor allow/deny/throttle decisions |
|
|
24
|
+
|
|
25
|
+
Files are created with **`0700` directories / `0600` database files** on first open.
|
|
26
|
+
|
|
27
|
+
## SQLite concurrency — what `busy_timeout` guarantees
|
|
28
|
+
|
|
29
|
+
Every store opened through `local-store.js` sets:
|
|
30
|
+
|
|
31
|
+
```sql
|
|
32
|
+
PRAGMA busy_timeout = 5000;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(default **5000 ms**; overridable via `openLocalStoreDb(path, { busyTimeoutMs })` in tests only — production stores use the default.)
|
|
36
|
+
|
|
37
|
+
### What this means for operators
|
|
38
|
+
|
|
39
|
+
| Situation | Expected behavior |
|
|
40
|
+
|-----------|-------------------|
|
|
41
|
+
| Two **short-lived** writers on the **same file** (e.g. CLI command finishing while `loop` is idle, or Grafana reading while the miner appends) | SQLite waits up to **5 seconds** for the lock, then proceeds or surfaces `database is locked` |
|
|
42
|
+
| Append-only ledgers (`event-ledger`, `attempt-log`, …) | Writes use **`BEGIN IMMEDIATE`** (or equivalent single-statement atomicity) so sequence allocation cannot interleave |
|
|
43
|
+
| Claim / queue stores | **`INSERT … ON CONFLICT`** and **`UPDATE … RETURNING`** patterns avoid read-then-write races **within one file** |
|
|
44
|
+
| Two **long-running `gittensory-miner loop` daemons** on the **same `GITTENSORY_MINER_CONFIG_DIR`** | **Unsupported.** `busy_timeout` reduces transient lock errors; it does **not** make multi-process loop workers safe on one volume |
|
|
45
|
+
|
|
46
|
+
**Invariant:** one active loop (or one intentional writer set) per state directory. Horizontal scale = **isolated state dirs** (separate compose projects, separate `GITTENSORY_MINER_CONFIG_DIR`, or the k8s StatefulSet pattern in [`../DEPLOYMENT.md`](../DEPLOYMENT.md)).
|
|
47
|
+
|
|
48
|
+
### Quick health check
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
gittensory-miner doctor --json
|
|
52
|
+
gittensory-miner status --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`doctor` includes `laptop-state-sqlite` (file exists + readable) and `state-dir-writable`. It performs **no network I/O**.
|
|
56
|
+
|
|
57
|
+
## Scenario: two miners collided
|
|
58
|
+
|
|
59
|
+
**Symptoms**
|
|
60
|
+
|
|
61
|
+
- `database is locked` / `SQLITE_BUSY` in logs or stderr
|
|
62
|
+
- Duplicate or out-of-order event sequences after an unclean shutdown
|
|
63
|
+
- Two systemd units, two `docker compose --scale miner=N` replicas, or a manual `loop` plus a supervised `loop` sharing one config dir
|
|
64
|
+
- Claims or queue rows flipping unexpectedly
|
|
65
|
+
|
|
66
|
+
**Diagnosis**
|
|
67
|
+
|
|
68
|
+
1. List processes using the state dir:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
STATE_DIR="$(gittensory-miner status --json | jq -r .stateDir)"
|
|
72
|
+
ls -la "$STATE_DIR"
|
|
73
|
+
# Linux: lsof +D "$STATE_DIR" 2>/dev/null || fuser -v "$STATE_DIR"/*.sqlite3 2>/dev/null
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
2. Confirm only **one** long-lived miner should own that directory.
|
|
77
|
+
|
|
78
|
+
3. Inspect soft claims and queue without mutating:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
gittensory-miner claim list --json
|
|
82
|
+
gittensory-miner queue list --json
|
|
83
|
+
gittensory-miner ledger list --json | tail -20
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Remediation**
|
|
87
|
+
|
|
88
|
+
1. **Stop all but one** miner process targeting that state dir (`systemctl stop`, `docker compose down`, kill stray `loop`).
|
|
89
|
+
2. If you need **N parallel workers**, give each an isolated state path — do **not** share one volume:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
# Example: two isolated compose projects
|
|
93
|
+
docker compose -p miner-a -f docker-compose.miner.yml up -d
|
|
94
|
+
docker compose -p miner-b -f docker-compose.miner.yml up -d
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or set distinct `GITTENSORY_MINER_CONFIG_DIR` per worker.
|
|
98
|
+
|
|
99
|
+
3. Re-run `gittensory-miner doctor`. If locks persist with a single process, see **Ledger corrupted** below.
|
|
100
|
+
|
|
101
|
+
4. **Claims are local bookkeeping only.** Two miners on different machines claiming the same GitHub issue is a **fleet coordination** problem (duplicate-cluster adjudication in the engine), not something SQLite resolves — split state dirs and use operational claim hygiene.
|
|
102
|
+
|
|
103
|
+
## Scenario: ledger corrupted
|
|
104
|
+
|
|
105
|
+
**Symptoms**
|
|
106
|
+
|
|
107
|
+
- Command throws `corrupted_*_row` (`corrupted_attempt_log_row`, `corrupted_governor_row`, `corrupted_plan_row`, `corrupted_prediction_row`, …)
|
|
108
|
+
- `gittensory-miner doctor` reports `laptop-state-sqlite` not readable
|
|
109
|
+
- `sqlite3` reports `database disk image is malformed`
|
|
110
|
+
- Partial writes after disk full, forced kill during a migration transaction, or copying a live `.sqlite3` while the miner is writing
|
|
111
|
+
|
|
112
|
+
**Diagnosis**
|
|
113
|
+
|
|
114
|
+
1. Identify which file fails (error message or env override path).
|
|
115
|
+
2. Read-only probe:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
DB="$STATE_DIR/event-ledger.sqlite3" # example
|
|
119
|
+
sqlite3 "$DB" "PRAGMA integrity_check;"
|
|
120
|
+
sqlite3 "$DB" "PRAGMA user_version;"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
3. Check filesystem: disk space, permissions (`0600` file, `0700` parent), backup tools copying mid-write.
|
|
124
|
+
|
|
125
|
+
**Remediation**
|
|
126
|
+
|
|
127
|
+
1. **Stop the miner** before any file surgery.
|
|
128
|
+
2. **Backup the whole state directory** (even damaged files help post-mortems):
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
cp -a "$STATE_DIR" "${STATE_DIR}.bak.$(date +%Y%m%d%H%M%S)"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
3. Choose a recovery tier:
|
|
135
|
+
|
|
136
|
+
| Tier | When | Action |
|
|
137
|
+
|------|------|--------|
|
|
138
|
+
| **A — single store reset** | One ledger is corrupt; others healthy; you accept losing that store's history | Remove only the bad `*.sqlite3` (and any `-wal`/`-shm` siblings). Next command recreates an empty store. |
|
|
139
|
+
| **B — restore from backup** | You have a recent quiesced backup | Stop miner → restore the known-good file → restart. |
|
|
140
|
+
| **C — full re-init** | Multiple files suspect or state is disposable | Archive dir → `gittensory-miner init` → reconfigure env/goals. Rebuild claims/plans from GitHub metadata as needed. |
|
|
141
|
+
|
|
142
|
+
4. **Never copy a live SQLite file** from a running miner as backup — stop first, or use SQLite's `.backup` command:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
sqlite3 "$DB" ".backup '${DB}.safe-copy'"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
5. After recovery, run `gittensory-miner doctor --json` and spot-check read-only listings (`claim list`, `ledger list`).
|
|
149
|
+
|
|
150
|
+
Append-only stores **do not repair individual bad rows** in place — corrupted payload JSON is rejected on read by design so bad data cannot silently propagate.
|
|
151
|
+
|
|
152
|
+
## Scenario: migrate ledgers after a package upgrade
|
|
153
|
+
|
|
154
|
+
**How upgrades work**
|
|
155
|
+
|
|
156
|
+
Stores use the lightweight **`schema-version.js`** convention ([#4832](https://github.com/JSONbored/gittensory/issues/4832)):
|
|
157
|
+
|
|
158
|
+
- Bootstrap `CREATE TABLE IF NOT EXISTS …` is schema **version 1** (`BASELINE_SCHEMA_VERSION`).
|
|
159
|
+
- Each store may register post-baseline migrations; `applySchemaMigrations` runs pending steps on **every open**.
|
|
160
|
+
- Version is stamped in SQLite **`PRAGMA user_version`**.
|
|
161
|
+
- Migrations run **once**, in order, inside a transaction; a failed migration rolls back and retries on next open.
|
|
162
|
+
- **Downgrade is not supported** — older miner versions may not read files written by newer migrations.
|
|
163
|
+
|
|
164
|
+
**Operator checklist**
|
|
165
|
+
|
|
166
|
+
1. **Before upgrading** the `@loopover/miner` package (npm, image tag, or git pull):
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
gittensory-miner doctor --json > /tmp/miner-pre-upgrade-doctor.json
|
|
170
|
+
STATE_DIR="$(gittensory-miner status --json | jq -r .stateDir)"
|
|
171
|
+
tar -czf "/tmp/gittensory-miner-state-$(date +%Y%m%d).tar.gz" -C "$(dirname "$STATE_DIR")" "$(basename "$STATE_DIR")"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
2. **Stop** supervised loops (`systemctl stop gittensory-miner.service`, `docker compose stop miner`, etc.).
|
|
175
|
+
|
|
176
|
+
3. **Install** the new version (`npm install -g @loopover/miner@latest`, rebuild image, …). The CLI prints a one-line npm upgrade nudge when behind registry latest — informational only.
|
|
177
|
+
|
|
178
|
+
4. **Migrate**, before starting any miner process, so every existing store is brought up to date in one
|
|
179
|
+
deliberate pass instead of relying on whichever command happens to open a given store first:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
gittensory-miner migrate --json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
(Pending migrations still apply automatically on first open regardless — e.g. `portfolio-queue` adds
|
|
186
|
+
`leased_at` when upgrading from pre-#4827 files — `migrate` is the proactive, explicit alternative to
|
|
187
|
+
waiting for that implicit path. A store file that hasn't been created yet is reported as skipped, not
|
|
188
|
+
created; `migrate` never bootstraps fresh state.)
|
|
189
|
+
|
|
190
|
+
5. **Start** one miner process, then **verify**:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
gittensory-miner doctor --json
|
|
194
|
+
gittensory-miner status --json
|
|
195
|
+
gittensory-miner migrate --json # re-run: every store should now report "up-to-date"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
6. If a migration throws on startup, **do not delete files immediately** — restore the pre-upgrade tarball, pin the previous package version, and file an issue with the failing `user_version` and store filename.
|
|
199
|
+
|
|
200
|
+
**Rolling fleet upgrades:** upgrade and restart **one worker/state dir at a time** so isolated workers never share a directory mid-migration.
|
|
201
|
+
|
|
202
|
+
## Related docs
|
|
203
|
+
|
|
204
|
+
- [`../DEPLOYMENT.md`](../DEPLOYMENT.md) — laptop vs fleet, volumes, systemd, scaling rules
|
|
205
|
+
- [`../README.md`](../README.md#local-storage) — store inventory
|
|
206
|
+
- [`env-reference.md`](env-reference.md) — per-store path overrides
|
|
207
|
+
- [`coding-agent-driver.md`](coding-agent-driver.md) — attempt log semantics
|
|
208
|
+
- [#5190](https://github.com/JSONbored/gittensory/issues/5190) — Grafana + SQLite ledgers (observability doc)
|
|
209
|
+
- [`discovery-plane-operator-guide.md`](discovery-plane-operator-guide.md) — optional hosted plane (distinct from local ledger ops)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Repo-agnostic capability audit — miner discovery / claim / scoring
|
|
2
|
+
|
|
3
|
+
Audit of the `packages/gittensory-miner` discovery, claim, and scoring code for hardcoded or
|
|
4
|
+
implicitly gittensory-specific assumptions that would need to become per-tenant configuration before
|
|
5
|
+
the loop can run against an arbitrary repo. This is the checklist deliverable for **#4780**; the
|
|
6
|
+
follow-up **#4784** executes it (turning each open item below into config, gittensory's own values
|
|
7
|
+
surviving only as the default). Audit-and-document only — no code changes here.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
The miner's discovery/claim/scoring code is **already largely repo-agnostic**. Discovery queries are
|
|
12
|
+
caller-supplied, label preferences are generic per-tenant config (`.gittensory-miner.yml` uses plain
|
|
13
|
+
`bug`/`enhancement`, not `gittensor:*`), ranking/feasibility are delegated to
|
|
14
|
+
`@loopover/engine` with config-overridable inputs, and runtime knobs live in
|
|
15
|
+
`.gittensory-ams.yml`. The remaining assumptions fall into three buckets:
|
|
16
|
+
|
|
17
|
+
1. **GitHub as the only forge** — the fan-out hardcodes GitHub's REST paths, headers, API version, and
|
|
18
|
+
search-qualifier syntax. This is the single largest gap for a non-GitHub tenant.
|
|
19
|
+
2. **An existing forge-host override that isn't reachable from the CLI** — `opportunity-fanout` already
|
|
20
|
+
accepts `apiBaseUrl` (GitHub Enterprise), but `discover-cli` never parses or threads it.
|
|
21
|
+
3. **Engine-delegated gittensory *defaults*** — label taxonomy and the miner goal spec default to
|
|
22
|
+
gittensory's conventions in `@loopover/engine`; they are overridable, but the miner uses
|
|
23
|
+
the gittensory defaults out of the box unless a per-tenant config is supplied.
|
|
24
|
+
|
|
25
|
+
Everything else audited is already parameterized (see the last section) and needs no #4784 work.
|
|
26
|
+
|
|
27
|
+
## Findings by file / module
|
|
28
|
+
|
|
29
|
+
### `lib/opportunity-fanout.js` — discovery fan-out (biggest gap)
|
|
30
|
+
|
|
31
|
+
| Line | Assumption | Category | Should become |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| 4 | `const defaultApiBaseUrl = "https://api.github.com"` | (6) endpoint | Per-tenant forge base URL (default github.com). Override path exists (see below) but the default is GitHub. |
|
|
34
|
+
| 7 | `const githubApiVersion = "2022-11-28"` | (2) forge protocol | Per-forge API version; **no override today**. |
|
|
35
|
+
| 69–71 | `accept: "application/vnd.github+json"`, `"x-github-api-version": githubApiVersion` | (2) forge protocol | Per-forge request headers (a forge adapter). |
|
|
36
|
+
| 83 | `` `/repos/${owner}/${repo}${suffix}` `` | (5) query construction | Per-forge repo path template. |
|
|
37
|
+
| 202 | `` `${trimmed} state:open type:issue` `` (search qualifiers) | (5) query construction | Per-forge search dialect (GitHub search syntax is assumed). |
|
|
38
|
+
| 241 | `"/search/issues"` | (5) query construction | Per-forge search endpoint. |
|
|
39
|
+
| 70 | `"user-agent": "gittensory-miner"` | (7) branding | Cosmetic; low priority — a configurable UA string. |
|
|
40
|
+
|
|
41
|
+
Note: the label extractor (`labelNames`, ~168) and issue projection are already generic — they read
|
|
42
|
+
whatever label strings the forge returns, with no `gittensor:*` filter. No change needed there.
|
|
43
|
+
|
|
44
|
+
### `lib/discover-cli.js` — `discover` command wiring
|
|
45
|
+
|
|
46
|
+
| Line | Assumption | Category | Should become |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| 289–290 (fanout) vs `runDiscover` | `opportunity-fanout` accepts `options.apiBaseUrl`, but `runDiscover`'s arg parser only handles `--search` / targets / `--json` — it never parses or passes `apiBaseUrl`, so the existing GitHub-Enterprise override is **unreachable from the CLI**. | (6) endpoint | Thread a forge base URL from `.gittensory-miner.yml` / a `--api-base-url` flag into the fan-out call. |
|
|
49
|
+
| 102 | `process.env.GITHUB_TOKEN` | (6) auth | Per-forge credential env var (default `GITHUB_TOKEN`). |
|
|
50
|
+
|
|
51
|
+
### `lib/opportunity-ranker.js` — candidate ranking (delegated)
|
|
52
|
+
|
|
53
|
+
| Line | Assumption | Category | Should become |
|
|
54
|
+
| --- | --- | --- | --- |
|
|
55
|
+
| 1–5 | imports `DEFAULT_MINER_GOAL_SPEC`, `parseMinerGoalSpecContent`, `rankMetadataOpportunities` from `@loopover/engine` | (3) scoring rubric | Ranking is engine-delegated and config-driven via `parseMinerGoalSpecContent`, but falls back to `DEFAULT_MINER_GOAL_SPEC` (gittensory's rubric) when no per-tenant goal spec is provided. #4784 should ensure a tenant goal spec is surfaced/required rather than silently defaulting. |
|
|
56
|
+
|
|
57
|
+
### `lib/feasibility-cli.js` — feasibility verdict (delegated)
|
|
58
|
+
|
|
59
|
+
| Line | Assumption | Category | Should become |
|
|
60
|
+
| --- | --- | --- | --- |
|
|
61
|
+
| 4 | delegates to engine `buildFeasibilityVerdict` | (3) scoring rubric | The status vocabularies (`CLAIM_STATUSES`, `DUPLICATE_CLUSTER_RISKS`, `ISSUE_STATUSES`) are generic; the *verdict logic* lives in the engine. No miner-side hardcode, but the engine rubric is a shared dependency to confirm is tenant-neutral. |
|
|
62
|
+
|
|
63
|
+
### `@loopover/engine` (shared dependency the miner scores through)
|
|
64
|
+
|
|
65
|
+
| Location | Assumption | Category | Should become |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `src/settings/pr-type-label.ts:26–28` | `DEFAULT_TYPE_LABELS = { bug: "gittensor:bug", feature: "gittensor:feature", priority: "gittensor:priority" }` | (1) label names | Already overridable per repo (`#label-modularity`), **default gittensory**. The miner inherits these defaults; #4784 should pass the tenant's label names through. |
|
|
68
|
+
|
|
69
|
+
## Already parameterized — no #4784 change needed
|
|
70
|
+
|
|
71
|
+
These were checked and are already tenant-agnostic / config-driven; call them out so #4784 doesn't
|
|
72
|
+
redo them:
|
|
73
|
+
|
|
74
|
+
- **Discovery query** — caller-supplied via `discover --search <query>` or explicit `owner/repo`
|
|
75
|
+
targets; no hardcoded gittensory label query (`discover-cli.js`).
|
|
76
|
+
- **Label preferences** — `.gittensory-miner.yml` `preferredLabels` / `blockedLabels` are generic
|
|
77
|
+
(`bug`, `enhancement`, `wontfix`, `duplicate`), not `gittensor:*`.
|
|
78
|
+
- **Path scope, feasibility gate, self-plagiarism threshold, concurrency** — all per-tenant in
|
|
79
|
+
`.gittensory-miner.yml` (`wantedPaths`, `blockedPaths`, `feasibilityGate`, `selfPlagiarism`,
|
|
80
|
+
`maxConcurrentClaims`).
|
|
81
|
+
- **Runtime caps** — `.gittensory-ams.yml` (`submissionMode`, `capLimits`, `convergenceThresholds`,
|
|
82
|
+
`maxIterations`) are generic runtime knobs.
|
|
83
|
+
- **Local stores** — `portfolio-queue`, `claim-ledger`, `event-ledger`, etc. are generic SQLite
|
|
84
|
+
bookkeeping with env-configurable DB paths; no gittensory-specific schema.
|
|
85
|
+
|
|
86
|
+
## Prioritized checklist for #4784
|
|
87
|
+
|
|
88
|
+
- [x] **High — forge abstraction (`opportunity-fanout.js`):** move the GitHub API version (7),
|
|
89
|
+
headers (69–71), repo path (83), search endpoint (241), and search-qualifier dialect (202) behind a
|
|
90
|
+
per-tenant forge adapter; keep GitHub as the default.
|
|
91
|
+
- [x] **High — thread `apiBaseUrl` to the CLI (`discover-cli.js`):** surface the already-supported
|
|
92
|
+
`opportunity-fanout` `apiBaseUrl` override via config / a flag so a non-`api.github.com` host is
|
|
93
|
+
reachable.
|
|
94
|
+
- [x] **Medium — credential env var (`discover-cli.js:102`):** make the token env var name
|
|
95
|
+
configurable (default `GITHUB_TOKEN`).
|
|
96
|
+
- [x] **Medium — pass tenant label taxonomy + goal spec through (`opportunity-ranker.js`, engine
|
|
97
|
+
`DEFAULT_TYPE_LABELS`):** ensure the miner supplies the tenant's labels / goal spec instead of
|
|
98
|
+
silently falling back to the gittensory defaults.
|
|
99
|
+
- [x] **Low — configurable user-agent (`opportunity-fanout.js:70`).**
|
|
100
|
+
|
|
101
|
+
## Resolution (#4784)
|
|
102
|
+
|
|
103
|
+
All five checklist items are resolved (or, where noted, explicitly deferred with a reason); gittensory's
|
|
104
|
+
own github.com conventions survive only as defaults, and the existing gittensory discovery path is
|
|
105
|
+
unchanged (`resolveForgeConfig()` with no overrides is byte-identical to the pre-#4784 hardcoded
|
|
106
|
+
behavior).
|
|
107
|
+
|
|
108
|
+
- **Forge abstraction — resolved.** [`lib/forge-config.js`](../lib/forge-config.js) is the per-tenant
|
|
109
|
+
forge adapter: `DEFAULT_FORGE_CONFIG` holds every github.com value (base URL, API version + version
|
|
110
|
+
header name, `accept` header, user-agent, repo path prefix, search endpoint, search qualifiers,
|
|
111
|
+
token env var) and `resolveForgeConfig(overrides)` fills any missing field from that default.
|
|
112
|
+
`opportunity-fanout.js` now reads these from the resolved forge instead of module constants, so the
|
|
113
|
+
API version, request headers, repo path, search endpoint, and search-qualifier dialect are all
|
|
114
|
+
per-tenant.
|
|
115
|
+
- **`apiBaseUrl` reachable from the CLI — resolved.** `discover` accepts `--api-base-url <url>` (and
|
|
116
|
+
`runDiscover({ apiBaseUrl })`), threading the forge host that the fan-out already supported but that
|
|
117
|
+
the CLI never surfaced. A programmatic caller can also pass the rest of the forge knobs via
|
|
118
|
+
`runDiscover({ forge })`.
|
|
119
|
+
- **Credential env var — resolved.** `discover --token-env <VAR>` (and `runDiscover({ tokenEnv })`)
|
|
120
|
+
reads a non-`GITHUB_TOKEN` variable, defaulting to `GITHUB_TOKEN`.
|
|
121
|
+
- **Tenant goal spec through — resolved.** `runDiscover` forwards `goalSpecsByRepo` /
|
|
122
|
+
`goalSpecContentByRepo` to the ranker and surfaces `usedDefaultGoalSpec` in both the JSON and the
|
|
123
|
+
human-readable summary, so the fall-back to gittensory's built-in rubric is explicit rather than
|
|
124
|
+
silent. The **discovery** label taxonomy is the goal spec's generic `preferredLabels` /
|
|
125
|
+
`blockedLabels` (already per-tenant). The engine's `DEFAULT_TYPE_LABELS` (`gittensor:*`) is a
|
|
126
|
+
**review-stack** default (overridable per repo via the focus manifest) that the miner discovery
|
|
127
|
+
ranker never consults, so it is **deferred**: changing it belongs to the review path, not #4784's
|
|
128
|
+
discovery/claim scope.
|
|
129
|
+
- **Configurable user-agent — resolved.** `forge.userAgent` (default `loopover-miner`).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# gittensory-miner — unattended scheduling & failure alerting
|
|
2
|
+
|
|
3
|
+
Operational guidance for running the miner's scheduled commands — `manage poll` and `discover` —
|
|
4
|
+
unattended on a timer (cron or systemd), and for alerting when a run fails. These are the two commands
|
|
5
|
+
most likely to run on a schedule; everything they need is local and they make no interactive prompts.
|
|
6
|
+
|
|
7
|
+
> **Scope:** scheduling + failure alerting for `manage poll` / `discover`. For local-state recovery see
|
|
8
|
+
> [`operations-runbook.md`](operations-runbook.md); for deployment layout see
|
|
9
|
+
> [`../DEPLOYMENT.md`](../DEPLOYMENT.md).
|
|
10
|
+
|
|
11
|
+
## The exit-code contract (what to alert on)
|
|
12
|
+
|
|
13
|
+
Both commands follow the same convention, so any scheduler can detect a failed run from the exit code:
|
|
14
|
+
|
|
15
|
+
| Exit code | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `0` | Success — the command completed. |
|
|
18
|
+
| `2` | Failure — invalid arguments, or the run hit an error (network / API / local state). **Alert on this.** |
|
|
19
|
+
|
|
20
|
+
For scheduled runs, two flags matter:
|
|
21
|
+
|
|
22
|
+
- `--no-update-check` (or `GITTENSORY_MINER_NO_UPDATE_CHECK=1`) — skip the npm-registry version nudge so
|
|
23
|
+
an unattended run never depends on / prints it.
|
|
24
|
+
- `--json` — machine-parseable stdout, so an alert handler can attach the structured output.
|
|
25
|
+
|
|
26
|
+
## cron
|
|
27
|
+
|
|
28
|
+
```cron
|
|
29
|
+
# crontab env applies to every job below.
|
|
30
|
+
MAILTO=you@example.com
|
|
31
|
+
GITTENSORY_MINER_NO_UPDATE_CHECK=1
|
|
32
|
+
|
|
33
|
+
# Poll a tracked PR every 10 minutes. The `||` branch fires on any non-zero exit: it logs the failing
|
|
34
|
+
# code to syslog AND re-raises it with `exit "$status"`, so the failure stays visible to exit-status
|
|
35
|
+
# monitoring instead of being masked by logger's own success.
|
|
36
|
+
*/10 * * * * /usr/local/bin/gittensory-miner manage poll acme/widgets 42 --json || { status=$?; logger -t gittensory-miner "manage poll failed (exit $status)"; exit "$status"; }
|
|
37
|
+
|
|
38
|
+
# Discover + enqueue candidate work hourly.
|
|
39
|
+
0 * * * * /usr/local/bin/gittensory-miner discover --search "label:good-first-issue" --json || { status=$?; logger -t gittensory-miner "discover failed (exit $status)"; exit "$status"; }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Two cron facts to get right here:
|
|
43
|
+
|
|
44
|
+
- **`MAILTO` mails *output*, not exit status.** cron emails whatever a job writes to stdout/stderr to
|
|
45
|
+
`MAILTO` — it does not send a message "because" the exit code was non-zero. A job that fails *silently*
|
|
46
|
+
(non-zero exit, no output) produces no mail, so don't rely on `MAILTO` alone as the failure signal.
|
|
47
|
+
- **A bare `|| logger …` hides the failure.** `logger` succeeds (exit 0), so `cmd || logger …` makes the
|
|
48
|
+
whole cron job exit 0 — any exit-status-based monitoring then sees success. Capture the code first
|
|
49
|
+
(`status=$?`) and re-raise it (`exit "$status"`) as shown, so the real failing code survives.
|
|
50
|
+
|
|
51
|
+
## systemd (service + timer)
|
|
52
|
+
|
|
53
|
+
A `oneshot` service plus a timer is the more observable option: `systemctl status` / `journalctl`
|
|
54
|
+
capture each run, and `OnFailure=` is a first-class alerting hook.
|
|
55
|
+
|
|
56
|
+
`gittensory-miner-discover.service`:
|
|
57
|
+
```ini
|
|
58
|
+
[Unit]
|
|
59
|
+
Description=gittensory-miner discover
|
|
60
|
+
OnFailure=gittensory-miner-alert@%n.service
|
|
61
|
+
|
|
62
|
+
[Service]
|
|
63
|
+
Type=oneshot
|
|
64
|
+
Environment=GITTENSORY_MINER_NO_UPDATE_CHECK=1
|
|
65
|
+
# A non-zero exit (2) marks the unit failed and triggers OnFailure=.
|
|
66
|
+
ExecStart=/usr/local/bin/gittensory-miner discover --search "label:good-first-issue" --json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`gittensory-miner-discover.timer`:
|
|
70
|
+
```ini
|
|
71
|
+
[Unit]
|
|
72
|
+
Description=Run gittensory-miner discover hourly
|
|
73
|
+
|
|
74
|
+
[Timer]
|
|
75
|
+
OnCalendar=hourly
|
|
76
|
+
Persistent=true
|
|
77
|
+
|
|
78
|
+
[Install]
|
|
79
|
+
WantedBy=timers.target
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Enable with `systemctl enable --now gittensory-miner-discover.timer`.
|
|
83
|
+
|
|
84
|
+
## Alerting on failure
|
|
85
|
+
|
|
86
|
+
Every option keys on the same exit-code contract (`2` = failure):
|
|
87
|
+
|
|
88
|
+
- **cron:** append `|| { status=$?; <alert-command>; exit "$status"; }` (as in the cron example above) —
|
|
89
|
+
capture `$?` before the alert command runs and re-raise it, so the failure isn't masked. Substitute
|
|
90
|
+
`logger` with a webhook `curl`, a PagerDuty/Slack CLI, etc. (`MAILTO` still mails any output, but is not
|
|
91
|
+
a reliable signal for a silent failure — see the cron note above.)
|
|
92
|
+
- **systemd:** `OnFailure=gittensory-miner-alert@%n.service` runs a templated alert unit on any non-zero
|
|
93
|
+
exit. A minimal alert unit:
|
|
94
|
+
```ini
|
|
95
|
+
# gittensory-miner-alert@.service
|
|
96
|
+
[Service]
|
|
97
|
+
Type=oneshot
|
|
98
|
+
ExecStart=/usr/local/bin/notify-failure "gittensory-miner unit %i failed"
|
|
99
|
+
```
|
|
100
|
+
- **wrapper script:** for any scheduler, wrap the command and preserve its exit code:
|
|
101
|
+
```sh
|
|
102
|
+
#!/bin/sh
|
|
103
|
+
gittensory-miner "$@" || { status=$?; notify-failure "gittensory-miner $* exited $status"; exit "$status"; }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Keep `--json` on scheduled runs so the alert handler can forward the structured output; the
|
|
107
|
+
human-readable form is for interactive use.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AmsPolicySpec } from "@loopover/engine";
|
|
2
|
+
export function resolveAmsPolicyConfigPath(env?: Record<string, string | undefined>): string;
|
|
3
|
+
|
|
4
|
+
export type AmsPolicySource = "local" | "default";
|
|
5
|
+
|
|
6
|
+
export type ResolvedAmsPolicy = {
|
|
7
|
+
spec: AmsPolicySpec;
|
|
8
|
+
source: AmsPolicySource;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function resolveAmsPolicy(
|
|
13
|
+
repoFullName: string,
|
|
14
|
+
options?: {
|
|
15
|
+
fetchImpl?: unknown;
|
|
16
|
+
readFileSync?: (path: string, encoding: "utf8") => string;
|
|
17
|
+
existsSync?: (path: string) => boolean;
|
|
18
|
+
env?: Record<string, string | undefined>;
|
|
19
|
+
},
|
|
20
|
+
): Promise<ResolvedAmsPolicy>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { DEFAULT_AMS_POLICY_SPEC, parseAmsPolicySpecContent } from "@loopover/engine";
|
|
3
|
+
import { resolveLocalStoreDbPath } from "./local-store.js";
|
|
4
|
+
|
|
5
|
+
// Resolver for the operator-local `.gittensory-ams.yml` (#5132, Wave 3.5 follow-up). AmsPolicySpec
|
|
6
|
+
// (ams-policy-spec.ts, engine package) is the type/parser surface; this module is the actual local
|
|
7
|
+
// read+resolve caller.
|
|
8
|
+
//
|
|
9
|
+
// This is deliberately NOT the same resolution shape as self-review-context.js/rejection-signal.js, which
|
|
10
|
+
// read from the target repo: AmsPolicySpec's fields are the OPERATOR's own execution-risk policy, so an
|
|
11
|
+
// untrusted target repo must never get final say over them.
|
|
12
|
+
|
|
13
|
+
const AMS_POLICY_FILENAME = ".gittensory-ams.yml";
|
|
14
|
+
|
|
15
|
+
/** Resolve the operator's local AMS policy file path: explicit env var > `GITTENSORY_MINER_CONFIG_DIR` >
|
|
16
|
+
* `XDG_CONFIG_HOME`/`~/.config`, mirroring every other local-store path in this package. */
|
|
17
|
+
export function resolveAmsPolicyConfigPath(env = process.env) {
|
|
18
|
+
return resolveLocalStoreDbPath(AMS_POLICY_FILENAME, "GITTENSORY_MINER_AMS_POLICY_PATH", env);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeOptions(options = {}) {
|
|
22
|
+
return {
|
|
23
|
+
readFileSync: options.readFileSync ?? readFileSync,
|
|
24
|
+
existsSync: options.existsSync ?? existsSync,
|
|
25
|
+
env: options.env ?? process.env,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read the operator's own local `.gittensory-ams.yml`, if one exists. Never throws: an unreadable file is
|
|
30
|
+
* treated the same as an absent one, falling through to the next resolution layer. */
|
|
31
|
+
function readLocalAmsPolicyContent(resolved) {
|
|
32
|
+
const path = resolveAmsPolicyConfigPath(resolved.env);
|
|
33
|
+
if (!resolved.existsSync(path)) return null;
|
|
34
|
+
try {
|
|
35
|
+
return resolved.readFileSync(path, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the real, effective AMS execution policy for one attempt: the operator's own local
|
|
43
|
+
* `.gittensory-ams.yml` when present (source: "local"), else the engine's safe defaults (source: "default").
|
|
44
|
+
* Never throws -- an unreadable/malformed local file degrades through the tolerant parser to the safe
|
|
45
|
+
* defaults, same discipline as every other tolerant parser in this pipeline.
|
|
46
|
+
*
|
|
47
|
+
* `repoFullName` is accepted for API compatibility with callers that resolve policy per target repo, but the
|
|
48
|
+
* resolver intentionally does not fetch or trust target-repository AMS policy content.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} repoFullName
|
|
51
|
+
* @param {{
|
|
52
|
+
* readFileSync?: (path: string, encoding: "utf8") => string, existsSync?: (path: string) => boolean,
|
|
53
|
+
* env?: Record<string, string | undefined>,
|
|
54
|
+
* }} [options]
|
|
55
|
+
* @returns {Promise<{ spec: import("@loopover/engine").AmsPolicySpec, source: "local"|"default", warnings: string[] }>}
|
|
56
|
+
*/
|
|
57
|
+
export async function resolveAmsPolicy(repoFullName, options = {}) {
|
|
58
|
+
void repoFullName;
|
|
59
|
+
const resolved = normalizeOptions(options);
|
|
60
|
+
|
|
61
|
+
const localContent = readLocalAmsPolicyContent(resolved);
|
|
62
|
+
if (localContent !== null) {
|
|
63
|
+
const parsed = parseAmsPolicySpecContent(localContent);
|
|
64
|
+
return { spec: parsed.spec, source: "local", warnings: parsed.warnings };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { spec: DEFAULT_AMS_POLICY_SPEC, source: "default", warnings: [] };
|
|
68
|
+
}
|