@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.
Files changed (218) hide show
  1. package/DEPLOYMENT.md +156 -0
  2. package/Dockerfile +38 -0
  3. package/README.md +269 -0
  4. package/bin/gittensory-miner-mcp.d.ts +64 -0
  5. package/bin/gittensory-miner-mcp.js +279 -0
  6. package/bin/gittensory-miner.js +196 -0
  7. package/docs/coding-agent-driver.md +140 -0
  8. package/docs/config-precedence.md +96 -0
  9. package/docs/cross-repo-discovery-phase1.md +55 -0
  10. package/docs/discovery-index-contract.md +60 -0
  11. package/docs/discovery-plane-operator-guide.md +101 -0
  12. package/docs/env-reference.md +30 -0
  13. package/docs/fleet-run-manifest.md +50 -0
  14. package/docs/miner-goal-spec.md +76 -0
  15. package/docs/miner-selfimprove-calibration.md +105 -0
  16. package/docs/observability.md +60 -0
  17. package/docs/operations-runbook.md +209 -0
  18. package/docs/repo-agnostic-capability-audit.md +129 -0
  19. package/docs/unattended-scheduling.md +107 -0
  20. package/expected-engine.version +1 -0
  21. package/lib/ams-policy.d.ts +20 -0
  22. package/lib/ams-policy.js +68 -0
  23. package/lib/attempt-cli.d.ts +99 -0
  24. package/lib/attempt-cli.js +523 -0
  25. package/lib/attempt-input-builder.d.ts +24 -0
  26. package/lib/attempt-input-builder.js +91 -0
  27. package/lib/attempt-log.d.ts +37 -0
  28. package/lib/attempt-log.js +177 -0
  29. package/lib/attempt-runner.d.ts +59 -0
  30. package/lib/attempt-runner.js +164 -0
  31. package/lib/attempt-worktree.d.ts +31 -0
  32. package/lib/attempt-worktree.js +94 -0
  33. package/lib/calibration-cli.d.ts +1 -0
  34. package/lib/calibration-cli.js +92 -0
  35. package/lib/calibration-run.d.ts +161 -0
  36. package/lib/calibration-run.js +232 -0
  37. package/lib/calibration-types.d.ts +46 -0
  38. package/lib/calibration-types.js +74 -0
  39. package/lib/calibration.d.ts +24 -0
  40. package/lib/calibration.js +99 -0
  41. package/lib/ci-poller.d.ts +33 -0
  42. package/lib/ci-poller.js +234 -0
  43. package/lib/claim-adjudication.d.ts +21 -0
  44. package/lib/claim-adjudication.js +36 -0
  45. package/lib/claim-conflict-resolver.d.ts +29 -0
  46. package/lib/claim-conflict-resolver.js +101 -0
  47. package/lib/claim-ledger-cli.d.ts +59 -0
  48. package/lib/claim-ledger-cli.js +320 -0
  49. package/lib/claim-ledger-expiry.d.ts +20 -0
  50. package/lib/claim-ledger-expiry.js +41 -0
  51. package/lib/claim-ledger.d.ts +63 -0
  52. package/lib/claim-ledger.js +303 -0
  53. package/lib/cli-error.d.ts +3 -0
  54. package/lib/cli-error.js +27 -0
  55. package/lib/cli.d.ts +3 -0
  56. package/lib/cli.js +72 -0
  57. package/lib/coding-agent-construction.d.ts +17 -0
  58. package/lib/coding-agent-construction.js +103 -0
  59. package/lib/coding-agent-house-rules.d.ts +24 -0
  60. package/lib/coding-agent-house-rules.js +64 -0
  61. package/lib/coding-task-spec.d.ts +44 -0
  62. package/lib/coding-task-spec.js +183 -0
  63. package/lib/config-precedence.d.ts +25 -0
  64. package/lib/config-precedence.js +38 -0
  65. package/lib/deny-check.d.ts +11 -0
  66. package/lib/deny-check.js +76 -0
  67. package/lib/deny-hook-synthesis.d.ts +93 -0
  68. package/lib/deny-hook-synthesis.js +399 -0
  69. package/lib/deny-hooks.d.ts +28 -0
  70. package/lib/deny-hooks.js +155 -0
  71. package/lib/deployment-docs-audit.d.ts +41 -0
  72. package/lib/deployment-docs-audit.js +115 -0
  73. package/lib/discover-cli.d.ts +94 -0
  74. package/lib/discover-cli.js +284 -0
  75. package/lib/discovery-throttle.d.ts +8 -0
  76. package/lib/discovery-throttle.js +33 -0
  77. package/lib/env-file-indirection.d.ts +4 -0
  78. package/lib/env-file-indirection.js +45 -0
  79. package/lib/event-ledger-cli.d.ts +82 -0
  80. package/lib/event-ledger-cli.js +266 -0
  81. package/lib/event-ledger.d.ts +37 -0
  82. package/lib/event-ledger.js +210 -0
  83. package/lib/execute-local-write.d.ts +14 -0
  84. package/lib/execute-local-write.js +50 -0
  85. package/lib/feasibility-cli.d.ts +25 -0
  86. package/lib/feasibility-cli.js +80 -0
  87. package/lib/forge-config.d.ts +17 -0
  88. package/lib/forge-config.js +37 -0
  89. package/lib/governor-action-mode.d.ts +26 -0
  90. package/lib/governor-action-mode.js +47 -0
  91. package/lib/governor-chokepoint-persisted.d.ts +18 -0
  92. package/lib/governor-chokepoint-persisted.js +46 -0
  93. package/lib/governor-chokepoint.d.ts +14 -0
  94. package/lib/governor-chokepoint.js +52 -0
  95. package/lib/governor-kill-switch.d.ts +26 -0
  96. package/lib/governor-kill-switch.js +47 -0
  97. package/lib/governor-ledger-cli.d.ts +32 -0
  98. package/lib/governor-ledger-cli.js +155 -0
  99. package/lib/governor-ledger.d.ts +46 -0
  100. package/lib/governor-ledger.js +203 -0
  101. package/lib/governor-open-pr.d.ts +21 -0
  102. package/lib/governor-open-pr.js +27 -0
  103. package/lib/governor-pause-cli.d.ts +23 -0
  104. package/lib/governor-pause-cli.js +171 -0
  105. package/lib/governor-run-halt.d.ts +37 -0
  106. package/lib/governor-run-halt.js +58 -0
  107. package/lib/governor-state.d.ts +63 -0
  108. package/lib/governor-state.js +363 -0
  109. package/lib/governor-write-rate-limit.d.ts +30 -0
  110. package/lib/governor-write-rate-limit.js +64 -0
  111. package/lib/harness-submission-trigger.d.ts +69 -0
  112. package/lib/harness-submission-trigger.js +138 -0
  113. package/lib/http-retry.d.ts +12 -0
  114. package/lib/http-retry.js +52 -0
  115. package/lib/laptop-init.d.ts +53 -0
  116. package/lib/laptop-init.js +332 -0
  117. package/lib/live-issue-snapshot.d.ts +16 -0
  118. package/lib/live-issue-snapshot.js +117 -0
  119. package/lib/local-store.d.ts +18 -0
  120. package/lib/local-store.js +62 -0
  121. package/lib/logger.d.ts +58 -0
  122. package/lib/logger.js +167 -0
  123. package/lib/loop-cli.d.ts +66 -0
  124. package/lib/loop-cli.js +540 -0
  125. package/lib/loop-closure.d.ts +35 -0
  126. package/lib/loop-closure.js +66 -0
  127. package/lib/loop-reentry.d.ts +51 -0
  128. package/lib/loop-reentry.js +124 -0
  129. package/lib/manage-poll.d.ts +79 -0
  130. package/lib/manage-poll.js +254 -0
  131. package/lib/manage-status.d.ts +70 -0
  132. package/lib/manage-status.js +241 -0
  133. package/lib/metrics-cli.d.ts +9 -0
  134. package/lib/metrics-cli.js +50 -0
  135. package/lib/migrate-cli.d.ts +24 -0
  136. package/lib/migrate-cli.js +111 -0
  137. package/lib/miner-goal-spec.d.ts +12 -0
  138. package/lib/miner-goal-spec.js +67 -0
  139. package/lib/opportunity-fanout.d.ts +86 -0
  140. package/lib/opportunity-fanout.js +597 -0
  141. package/lib/opportunity-ranker.d.ts +36 -0
  142. package/lib/opportunity-ranker.js +118 -0
  143. package/lib/orb-export.d.ts +54 -0
  144. package/lib/orb-export.js +206 -0
  145. package/lib/plan-store-cli.d.ts +37 -0
  146. package/lib/plan-store-cli.js +151 -0
  147. package/lib/plan-store.d.ts +51 -0
  148. package/lib/plan-store.js +231 -0
  149. package/lib/policy-doc-cache.d.ts +25 -0
  150. package/lib/policy-doc-cache.js +79 -0
  151. package/lib/policy-verdict-cache.d.ts +35 -0
  152. package/lib/policy-verdict-cache.js +106 -0
  153. package/lib/portfolio-dashboard.d.ts +30 -0
  154. package/lib/portfolio-dashboard.js +105 -0
  155. package/lib/portfolio-discovery.d.ts +29 -0
  156. package/lib/portfolio-discovery.js +100 -0
  157. package/lib/portfolio-queue-cli.d.ts +87 -0
  158. package/lib/portfolio-queue-cli.js +509 -0
  159. package/lib/portfolio-queue-expiry.d.ts +20 -0
  160. package/lib/portfolio-queue-expiry.js +51 -0
  161. package/lib/portfolio-queue-manager.d.ts +50 -0
  162. package/lib/portfolio-queue-manager.js +134 -0
  163. package/lib/portfolio-queue.d.ts +62 -0
  164. package/lib/portfolio-queue.js +351 -0
  165. package/lib/pr-disposition-poller.d.ts +26 -0
  166. package/lib/pr-disposition-poller.js +163 -0
  167. package/lib/pr-number-parse.d.ts +4 -0
  168. package/lib/pr-number-parse.js +23 -0
  169. package/lib/pr-outcome.d.ts +41 -0
  170. package/lib/pr-outcome.js +90 -0
  171. package/lib/prediction-ledger.d.ts +47 -0
  172. package/lib/prediction-ledger.js +222 -0
  173. package/lib/pretooluse-hook.d.ts +31 -0
  174. package/lib/pretooluse-hook.js +93 -0
  175. package/lib/process-lifecycle.d.ts +31 -0
  176. package/lib/process-lifecycle.js +106 -0
  177. package/lib/purge-cli.d.ts +44 -0
  178. package/lib/purge-cli.js +198 -0
  179. package/lib/rejection-signal.d.ts +6 -0
  180. package/lib/rejection-signal.js +101 -0
  181. package/lib/rejection-state-machine.d.ts +37 -0
  182. package/lib/rejection-state-machine.js +81 -0
  183. package/lib/rejection-templates.d.ts +12 -0
  184. package/lib/rejection-templates.js +71 -0
  185. package/lib/replay-objective-anchor.d.ts +79 -0
  186. package/lib/replay-objective-anchor.js +179 -0
  187. package/lib/replay-snapshot.d.ts +41 -0
  188. package/lib/replay-snapshot.js +288 -0
  189. package/lib/replay-task-generation.d.ts +126 -0
  190. package/lib/replay-task-generation.js +223 -0
  191. package/lib/repo-clone.d.ts +19 -0
  192. package/lib/repo-clone.js +102 -0
  193. package/lib/run-state-cli.d.ts +27 -0
  194. package/lib/run-state-cli.js +154 -0
  195. package/lib/run-state.d.ts +37 -0
  196. package/lib/run-state.js +150 -0
  197. package/lib/schema-version.d.ts +17 -0
  198. package/lib/schema-version.js +71 -0
  199. package/lib/self-review-context.d.ts +28 -0
  200. package/lib/self-review-context.js +339 -0
  201. package/lib/slop-assessment.d.ts +3 -0
  202. package/lib/slop-assessment.js +20 -0
  203. package/lib/stack-detection.d.ts +41 -0
  204. package/lib/stack-detection.js +248 -0
  205. package/lib/status.d.ts +64 -0
  206. package/lib/status.js +441 -0
  207. package/lib/store-maintenance.d.ts +31 -0
  208. package/lib/store-maintenance.js +183 -0
  209. package/lib/submission-freshness-check.d.ts +32 -0
  210. package/lib/submission-freshness-check.js +93 -0
  211. package/lib/update-check.d.ts +36 -0
  212. package/lib/update-check.js +161 -0
  213. package/lib/version.d.ts +3 -0
  214. package/lib/version.js +10 -0
  215. package/lib/worktree-allocator.d.ts +39 -0
  216. package/lib/worktree-allocator.js +262 -0
  217. package/package.json +50 -0
  218. package/schema/miner-goal-spec.schema.json +111 -0
@@ -0,0 +1,91 @@
1
+ import { isGlobalMinerKillSwitch, isGlobalMinerLiveModeOptIn } from "@loopover/engine";
2
+
3
+ // Pure composers for runMinerAttempt's real input (#5132, Wave 3.5 -- the final assembly). Everything here is
4
+ // a plain in/out transform over already-fetched/already-computed real data (coding-task-spec, #5239;
5
+ // self-review-context, #5145; worktree preparation, #5237/#5252; AmsPolicySpec, #5249) -- no fetching, no IO,
6
+ // same discipline as coding-task-spec.js's own composers.
7
+ //
8
+ // KNOWN, DOCUMENTED GAPS (not fabricated -- explicitly left as real, narrow follow-ups):
9
+ // - governor.convergenceInput is a first-attempt-shaped literal ({ attempts: 0, consecutiveFailures: 0,
10
+ // reenqueues: 0, reachedDone: false }), not a real per-issue query. attempt-log.js's schema has no
11
+ // repo+issue index (attemptId embeds a timestamp, so it's not a stable group key), and reenqueue counts
12
+ // aren't tracked ANYWHERE yet (non-convergence.ts's own header says that belongs on the portfolio-queue
13
+ // table once it grows attempt-history columns -- a real, separate schema change, not something to fake
14
+ // here). This literal is only ever an UNDER-estimate (reads "fresh, no prior failures" even on a real
15
+ // Nth attempt), which fails toward LETTING an attempt through, not blocking one -- documented, not silent.
16
+ // - governor.reputationHistory/selfPlagiarismCandidate/selfPlagiarismRecentSubmissions are omitted, which
17
+ // chokepoint.ts's own design treats as "skip that stage entirely" -- an honest absence, not a fabricated
18
+ // "clean" verdict.
19
+
20
+ /**
21
+ * Assemble the real Governor chokepoint context for one attempt. rateLimitBuckets/rateLimitBackoffAttempts/
22
+ * capUsage are deliberately omitted -- evaluateGovernorChokepointGatePersisted (#5134) auto-loads them from
23
+ * the persisted governor-state store when absent.
24
+ *
25
+ * `repoPaused` (#5392) is the caller's own resolved `MinerGoalSpec.killSwitch.paused` for the target repo
26
+ * (miner-goal-spec.js's resolveMinerGoalSpec) -- this composer stays pure and just threads whatever the
27
+ * caller already resolved through; passing nothing keeps the prior fails-open-on-that-axis-only behavior.
28
+ *
29
+ * @param {Record<string, string | undefined>} env
30
+ * @param {import("@loopover/engine").AmsPolicySpec} amsPolicySpec
31
+ * @param {boolean} [repoPaused]
32
+ * @returns {import("./attempt-runner.js").AttemptGovernorContext}
33
+ */
34
+ export function buildAttemptGovernorContext(env, amsPolicySpec, repoPaused) {
35
+ return {
36
+ killSwitchGlobal: isGlobalMinerKillSwitch(env),
37
+ killSwitchRepoPaused: repoPaused,
38
+ liveModeGlobalOptIn: isGlobalMinerLiveModeOptIn(env),
39
+ capLimits: amsPolicySpec.capLimits,
40
+ convergenceInput: { attempts: 0, consecutiveFailures: 0, reenqueues: 0, reachedDone: false },
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Assemble the real IterateLoopInput for one attempt from every already-computed real dependency. Pure --
46
+ * throws nothing itself (callers are expected to have already validated `codingTaskSpec.ready`).
47
+ *
48
+ * @param {{
49
+ * codingTaskSpec: Extract<import("./coding-task-spec.js").CodingTaskSpecResult, { ready: true }>,
50
+ * reviewContext: import("@loopover/engine").SelfReviewContext,
51
+ * worktreePath: string,
52
+ * attemptId: string,
53
+ * mode: import("@loopover/engine").CodingAgentExecutionMode,
54
+ * repoFullName: string,
55
+ * minerLogin: string,
56
+ * rejectionSignaled: boolean,
57
+ * amsPolicySpec: import("@loopover/engine").AmsPolicySpec,
58
+ * branchRef?: string,
59
+ * }} input
60
+ * @returns {import("@loopover/engine").IterateLoopInput}
61
+ */
62
+ export function buildAttemptLoopInput(input) {
63
+ return {
64
+ attemptId: input.attemptId,
65
+ workingDirectory: input.worktreePath,
66
+ acceptanceCriteriaPath: input.codingTaskSpec.acceptanceCriteriaPath,
67
+ instructions: input.codingTaskSpec.instructions,
68
+ mode: input.mode,
69
+ maxIterations: input.amsPolicySpec.maxIterations,
70
+ maxTurnsPerIteration: input.amsPolicySpec.maxTurnsPerIteration,
71
+ // Real mid-attempt budget (#5395): the SAME Governor cap ceilings that already bound cross-cycle spend
72
+ // (loop-cli.js's after-the-fact governorState.saveCapUsage) now also bound this ONE attempt in progress,
73
+ // via the engine's real accumulateAttemptUsage/evaluateAttemptBudget -- a runaway attempt can no longer
74
+ // burn through the entire cross-cycle budget before anything reacts. No maxTokens: no driver reports a
75
+ // real per-iteration token count today, so that axis has no real ceiling to set (never fabricated).
76
+ budget: {
77
+ maxTurns: input.amsPolicySpec.capLimits.turns,
78
+ maxWallClockMs: input.amsPolicySpec.capLimits.elapsedMs,
79
+ maxCostUsd: input.amsPolicySpec.capLimits.budget,
80
+ },
81
+ repoFullName: input.repoFullName,
82
+ contributorLogin: input.minerLogin,
83
+ title: input.codingTaskSpec.title,
84
+ body: input.codingTaskSpec.body,
85
+ labels: input.codingTaskSpec.labels,
86
+ linkedIssues: input.codingTaskSpec.linkedIssues,
87
+ branchRef: input.branchRef,
88
+ reviewContext: input.reviewContext,
89
+ rejectionSignaled: input.rejectionSignaled,
90
+ };
91
+ }
@@ -0,0 +1,37 @@
1
+ import type { AttemptLogEvent } from "@loopover/engine";
2
+
3
+ export type AttemptLogEntry = {
4
+ id: number;
5
+ seq: number;
6
+ eventType: string;
7
+ attemptId: string;
8
+ actionClass: string;
9
+ mode: string;
10
+ reason: string;
11
+ payload: Record<string, unknown>;
12
+ createdAt: string;
13
+ };
14
+
15
+ export type ReadAttemptLogEventsFilter = {
16
+ attemptId?: string | null;
17
+ };
18
+
19
+ export type AttemptLog = {
20
+ dbPath: string;
21
+ appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry;
22
+ readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[];
23
+ exportAttemptLogJsonl(attemptId: string): string;
24
+ close(): void;
25
+ };
26
+
27
+ export function resolveAttemptLogDbPath(env?: Record<string, string | undefined>): string;
28
+
29
+ export function initAttemptLog(dbPath?: string): AttemptLog;
30
+
31
+ export function appendAttemptLogEvent(event: AttemptLogEvent): AttemptLogEntry;
32
+
33
+ export function readAttemptLogEvents(filter?: ReadAttemptLogEventsFilter): AttemptLogEntry[];
34
+
35
+ export function exportAttemptLogJsonl(attemptId: string): string;
36
+
37
+ export function closeDefaultAttemptLog(): void;
@@ -0,0 +1,177 @@
1
+ import { formatAttemptLogJsonl, normalizeAttemptLogEvent } from "@loopover/engine";
2
+ import { normalizeLocalStoreDbPath, openLocalStoreDb, resolveLocalStoreDbPath } from "./local-store.js";
3
+
4
+ // Append-only driver attempt log (#4294): a structured, attempt-scoped event trace for every CodingAgentDriver run
5
+ // (started, tool/edit, succeeded/failed/aborted). IMMUTABILITY INVARIANT: INSERT + SELECT only — rows are never
6
+ // rewritten or removed after append.
7
+ //
8
+ // Why a sibling store instead of extending event-ledger.js: event-ledger is the general miner-loop audit trail
9
+ // (discovered_issue, plan_built, pr_prepared, …) keyed by repo scope with a growing free-form type vocabulary.
10
+ // Attempt events are keyed by attempt_id, validated against the engine's fixed ATTEMPT_LOG_EVENT_TYPES, and are
11
+ // exported per attempt as JSONL — mixing both into one table would couple unrelated lifecycles and complicate the
12
+ // per-attempt dump path. This module mirrors governor-ledger.js: engine holds pure normalization, miner holds SQLite.
13
+
14
+ const defaultDbFileName = "attempt-log.sqlite3";
15
+ let defaultAttemptLog = null;
16
+
17
+ export function resolveAttemptLogDbPath(env = process.env) {
18
+ return resolveLocalStoreDbPath(defaultDbFileName, "GITTENSORY_MINER_ATTEMPT_LOG_DB", env);
19
+ }
20
+
21
+ function normalizeDbPath(dbPath) {
22
+ return normalizeLocalStoreDbPath(dbPath, resolveAttemptLogDbPath(), "invalid_attempt_log_db_path");
23
+ }
24
+
25
+ /** Read-filter attempt scope: omitted/nullish → unscoped (all events); otherwise a non-empty attempt id. */
26
+ function normalizeReadAttemptIdFilter(attemptId) {
27
+ if (attemptId === undefined || attemptId === null) return undefined;
28
+ if (typeof attemptId !== "string") throw new Error("invalid_attempt_id");
29
+ const trimmed = attemptId.trim();
30
+ if (!trimmed) throw new Error("invalid_attempt_id");
31
+ return trimmed;
32
+ }
33
+
34
+ /** Export requires an explicit attempt id — JSONL dumps are always per attempt. */
35
+ function normalizeRequiredAttemptId(attemptId) {
36
+ const normalized = normalizeReadAttemptIdFilter(attemptId);
37
+ if (normalized === undefined) throw new Error("invalid_attempt_id");
38
+ return normalized;
39
+ }
40
+
41
+ function rowToEntry(row) {
42
+ let payload;
43
+ try {
44
+ payload = JSON.parse(row.payload_json);
45
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
46
+ throw new Error("corrupted_attempt_log_row");
47
+ }
48
+ } catch {
49
+ throw new Error("corrupted_attempt_log_row");
50
+ }
51
+ return {
52
+ id: row.id,
53
+ seq: row.seq,
54
+ eventType: row.event_type,
55
+ attemptId: row.attempt_id,
56
+ actionClass: row.action_class,
57
+ mode: row.mode,
58
+ reason: row.reason,
59
+ payload,
60
+ createdAt: row.created_at,
61
+ };
62
+ }
63
+
64
+ function rowToNormalized(row) {
65
+ return {
66
+ eventType: row.event_type,
67
+ attemptId: row.attempt_id,
68
+ actionClass: row.action_class,
69
+ mode: row.mode,
70
+ reason: row.reason,
71
+ payloadJson: row.payload_json,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Opens the append-only attempt log, creating the table on first use. `seq` is a monotonically increasing counter
77
+ * maintained by this module (next = current MAX(seq) + 1) with a UNIQUE(seq) constraint. Rows read back in seq ASC
78
+ * order. (#4294)
79
+ */
80
+ export function initAttemptLog(dbPath = resolveAttemptLogDbPath()) {
81
+ const resolvedPath = normalizeDbPath(dbPath);
82
+ const db = openLocalStoreDb(resolvedPath);
83
+ db.exec(`
84
+ CREATE TABLE IF NOT EXISTS attempt_log_events (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ seq INTEGER NOT NULL UNIQUE,
87
+ attempt_id TEXT NOT NULL,
88
+ event_type TEXT NOT NULL,
89
+ action_class TEXT NOT NULL,
90
+ mode TEXT NOT NULL,
91
+ reason TEXT NOT NULL,
92
+ payload_json TEXT NOT NULL,
93
+ created_at TEXT NOT NULL
94
+ )
95
+ `);
96
+ db.exec(
97
+ "CREATE INDEX IF NOT EXISTS idx_attempt_log_attempt ON attempt_log_events (attempt_id, seq)",
98
+ );
99
+
100
+ const nextSeqStatement = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS nextSeq FROM attempt_log_events");
101
+ const appendStatement = db.prepare(`
102
+ INSERT INTO attempt_log_events (
103
+ seq, attempt_id, event_type, action_class, mode, reason, payload_json, created_at
104
+ )
105
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
106
+ `);
107
+ const getByIdStatement = db.prepare("SELECT * FROM attempt_log_events WHERE id = ?");
108
+ const readAllStatement = db.prepare("SELECT * FROM attempt_log_events ORDER BY seq ASC");
109
+ const readByAttemptStatement = db.prepare(
110
+ "SELECT * FROM attempt_log_events WHERE attempt_id = ? ORDER BY seq ASC",
111
+ );
112
+
113
+ return {
114
+ dbPath: resolvedPath,
115
+ appendAttemptLogEvent(event) {
116
+ const normalized = normalizeAttemptLogEvent(event);
117
+ const createdAt = new Date().toISOString();
118
+ db.exec("BEGIN IMMEDIATE");
119
+ try {
120
+ const { nextSeq } = nextSeqStatement.get();
121
+ const result = appendStatement.run(
122
+ nextSeq,
123
+ normalized.attemptId,
124
+ normalized.eventType,
125
+ normalized.actionClass,
126
+ normalized.mode,
127
+ normalized.reason,
128
+ normalized.payloadJson,
129
+ createdAt,
130
+ );
131
+ const entry = rowToEntry(getByIdStatement.get(Number(result.lastInsertRowid)));
132
+ db.exec("COMMIT");
133
+ return entry;
134
+ } catch (error) {
135
+ db.exec("ROLLBACK");
136
+ throw error;
137
+ }
138
+ },
139
+ readAttemptLogEvents(filter = {}) {
140
+ const attemptId = normalizeReadAttemptIdFilter(filter.attemptId);
141
+ const rows =
142
+ attemptId === undefined ? readAllStatement.all() : readByAttemptStatement.all(attemptId);
143
+ return rows.map(rowToEntry);
144
+ },
145
+ exportAttemptLogJsonl(attemptId) {
146
+ const scopedAttemptId = normalizeRequiredAttemptId(attemptId);
147
+ const rows = readByAttemptStatement.all(scopedAttemptId);
148
+ return formatAttemptLogJsonl(rows.map(rowToNormalized));
149
+ },
150
+ close() {
151
+ db.close();
152
+ },
153
+ };
154
+ }
155
+
156
+ function getDefaultAttemptLog() {
157
+ defaultAttemptLog ??= initAttemptLog();
158
+ return defaultAttemptLog;
159
+ }
160
+
161
+ export function appendAttemptLogEvent(event) {
162
+ return getDefaultAttemptLog().appendAttemptLogEvent(event);
163
+ }
164
+
165
+ export function readAttemptLogEvents(filter) {
166
+ return getDefaultAttemptLog().readAttemptLogEvents(filter);
167
+ }
168
+
169
+ export function exportAttemptLogJsonl(attemptId) {
170
+ return getDefaultAttemptLog().exportAttemptLogJsonl(attemptId);
171
+ }
172
+
173
+ export function closeDefaultAttemptLog() {
174
+ if (!defaultAttemptLog) return;
175
+ defaultAttemptLog.close();
176
+ defaultAttemptLog = null;
177
+ }
@@ -0,0 +1,59 @@
1
+ import type {
2
+ CodingAgentDriver,
3
+ GovernorDecision,
4
+ IterateLoopInput,
5
+ IterateLoopResult,
6
+ LocalWriteActionSpec,
7
+ } from "@loopover/engine";
8
+ import type { HarnessSubmissionDecision, HarnessSubmissionEventLedger } from "./harness-submission-trigger.js";
9
+ import type { SubmissionFreshnessClaimLedger, LiveIssueSnapshot, FreshnessAbortReason } from "./submission-freshness-check.js";
10
+ import type { GovernorChokepointInputPersisted } from "./governor-chokepoint-persisted.js";
11
+ import type { GovernorState } from "./governor-state.js";
12
+
13
+ export const ATTEMPT_OUTCOMES: readonly ["abandon", "stale", "blocked", "governed", "submitted"];
14
+
15
+ // rateLimitBuckets/rateLimitBackoffAttempts/capUsage are optional here (via GovernorChokepointInputPersisted,
16
+ // not the engine's own GovernorChokepointInput) so a caller can omit them and let evaluateGovernorChokepointGatePersisted
17
+ // (#5134) auto-supply real persisted state -- forcing them required at this layer would make every caller
18
+ // hand-thread honest-but-stale zero defaults on every invocation, silently defeating that persistence.
19
+ export type AttemptGovernorContext = Omit<GovernorChokepointInputPersisted, "actionClass" | "repoFullName" | "nowMs" | "wouldBeAction">;
20
+
21
+ export type AttemptInput = {
22
+ loopInput: IterateLoopInput;
23
+ issueNumber: number;
24
+ minerLogin: string;
25
+ base: string;
26
+ killSwitchScope: "global" | "repo" | "none";
27
+ slopThreshold: "clean" | "low" | "elevated" | "high";
28
+ submissionMode: "observe" | "enforce";
29
+ maxConsecutiveGateBlocks?: number;
30
+ draft?: boolean;
31
+ governor: AttemptGovernorContext;
32
+ };
33
+
34
+ export type AttemptDeps = {
35
+ driver: CodingAgentDriver;
36
+ runSlopAssessment: (input: unknown) => unknown;
37
+ appendAttemptLogEvent: (event: unknown) => void;
38
+ claimLedger: SubmissionFreshnessClaimLedger;
39
+ fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<LiveIssueSnapshot | null>;
40
+ eventLedger: HarnessSubmissionEventLedger;
41
+ /** Injected governor-ledger append (mirrors evaluateGovernorChokepointGate's own `options.append`); omitted
42
+ * falls back to that function's own default (the real default governor ledger). */
43
+ governorLedgerAppend?: (event: unknown) => unknown;
44
+ /** Injected governor-state store (#5134); omitted falls back to evaluateGovernorChokepointGatePersisted's
45
+ * own default (opens + closes the real default governor-state store for this one call). */
46
+ governorState?: GovernorState;
47
+ sessionStartMs?: number;
48
+ nowMs: number;
49
+ executeLocalWrite: (spec: LocalWriteActionSpec) => Promise<unknown>;
50
+ };
51
+
52
+ export type AttemptResult =
53
+ | { outcome: "abandon"; loopResult: IterateLoopResult }
54
+ | { outcome: "stale"; reason: FreshnessAbortReason; loopResult: IterateLoopResult }
55
+ | { outcome: "blocked"; decision: HarnessSubmissionDecision; loopResult: IterateLoopResult }
56
+ | { outcome: "governed"; decision: GovernorDecision; loopResult: IterateLoopResult }
57
+ | { outcome: "submitted"; spec: LocalWriteActionSpec; execResult: unknown; loopResult: IterateLoopResult };
58
+
59
+ export function runMinerAttempt(input: AttemptInput, deps: AttemptDeps): Promise<AttemptResult>;
@@ -0,0 +1,164 @@
1
+ import { buildOpenPrSpec } from "@loopover/engine";
2
+ import { runIterateLoop } from "@loopover/engine";
3
+ import { checkSubmissionFreshness } from "./submission-freshness-check.js";
4
+ import { evaluateGovernorChokepointGatePersisted } from "./governor-chokepoint-persisted.js";
5
+ import { prepareOpenPrSubmission } from "./harness-submission-trigger.js";
6
+
7
+ // The real driving-loop entrypoint (#2337): the missing link between #2333's iterate-loop orchestrator and an
8
+ // actual, executed open_pr write. Composes, in order: runIterateLoop (create -> score -> self-review -> decide,
9
+ // #2333) -> on handoff, checkSubmissionFreshness (#3007) -> prepareOpenPrSubmission (#2336/#2337) -> the
10
+ // Governor chokepoint (#2340, which itself composes kill-switch, dry-run, rate-limit, budget caps, non-
11
+ // convergence, self-reputation-throttle, and self-plagiarism -- see chokepoint.ts's own module doc comment for
12
+ // the exact precedence ladder) -> on allowed:true, builds the REAL open_pr command via the now-shared
13
+ // buildOpenPrSpec (@loopover/engine, moved from root src/mcp/local-write-tools.ts) and executes it.
14
+ //
15
+ // WORKTREE LIFECYCLE IS NOT THIS MODULE'S JOB: runIterateLoop already takes a plain `workingDirectory` string
16
+ // (packages/gittensory-engine/src/miner/iterate-loop.ts's own IterateLoopInput), deliberately agnostic about
17
+ // where it came from. Allocating one is the caller's job, via the already-built slot allocator
18
+ // (worktree-allocator.js, #4297) -- this module composes the create/review/gate/submit sequence #2337 is
19
+ // actually about, not worktree allocation policy, which is a separate, already-solved concern.
20
+ //
21
+ // KNOWN, DELIBERATE GAP (not silently papered over -- was an injected-but-unwired seam before this module
22
+ // existed, and remains so here):
23
+ // - `deps.runSlopAssessment` has no production implementation anywhere in this package. The real slop scorer
24
+ // (src/signals/slop.ts, 518 lines, 5 sibling src/signals/** dependencies) is far larger and more
25
+ // interconnected than local-write-tools.ts was, so extracting it is separate, substantial scope -- this
26
+ // function requires a real one be injected rather than silently stubbing a result that would either always
27
+ // pass (unsafe) or always fail (useless).
28
+ //
29
+ // `input.governor`'s cross-attempt state (rate-limit buckets, backoff attempts, budget-cap usage) DOES now
30
+ // persist across separate process invocations (#5134, governor-state.js), via
31
+ // evaluateGovernorChokepointGatePersisted -- callers no longer need to hand-thread honest empty/zero defaults
32
+ // on every invocation; `capUsage` is loaded from that same store but its post-attempt save stays the caller's
33
+ // job (see governor-chokepoint-persisted.js's own header for why: nothing computes "the next capUsage" from a
34
+ // verdict, only the attempt's real outcome does). Reputation/self-plagiarism state also has real persistence
35
+ // primitives (governor-state.js) but isn't auto-loaded here yet -- `input.governor.reputationHistory`/
36
+ // `selfPlagiarismCandidate`/`selfPlagiarismRecentSubmissions` are still caller-supplied optional fields on
37
+ // GovernorChokepointInput, same as before.
38
+
39
+ /** True once the loop reaches handoff AND every downstream gate (freshness, submission, governor) allows. */
40
+ export const ATTEMPT_OUTCOMES = Object.freeze(["abandon", "stale", "blocked", "governed", "submitted"]);
41
+
42
+ function assertFn(value, name) {
43
+ if (typeof value !== "function") throw new Error(`invalid_${name}`);
44
+ }
45
+
46
+ function assertDeps(deps) {
47
+ if (!deps || typeof deps !== "object") throw new Error("invalid_attempt_deps");
48
+ assertFn(deps.runSlopAssessment, "run_slop_assessment");
49
+ assertFn(deps.appendAttemptLogEvent, "append_attempt_log_event");
50
+ assertFn(deps.fetchLiveIssueSnapshot, "fetch_live_issue_snapshot");
51
+ assertFn(deps.executeLocalWrite, "execute_local_write");
52
+ if (!deps.driver || typeof deps.driver.run !== "function") throw new Error("invalid_driver");
53
+ if (!deps.claimLedger || typeof deps.claimLedger.listClaims !== "function") throw new Error("invalid_claim_ledger");
54
+ if (!deps.eventLedger || typeof deps.eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger");
55
+ if (typeof deps.nowMs !== "number" || !Number.isFinite(deps.nowMs)) throw new Error("invalid_now_ms");
56
+ }
57
+
58
+ function assertInput(input) {
59
+ if (!input || typeof input !== "object") throw new Error("invalid_attempt_input");
60
+ if (!input.loopInput || typeof input.loopInput !== "object") throw new Error("invalid_loop_input");
61
+ if (!Number.isInteger(input.issueNumber) || input.issueNumber < 1) throw new Error("invalid_issue_number");
62
+ if (typeof input.minerLogin !== "string" || !input.minerLogin.trim()) throw new Error("invalid_miner_login");
63
+ if (typeof input.base !== "string" || !input.base.trim()) throw new Error("invalid_base");
64
+ if (!["global", "repo", "none"].includes(input.killSwitchScope)) throw new Error("invalid_kill_switch_scope");
65
+ if (!["clean", "low", "elevated", "high"].includes(input.slopThreshold)) throw new Error("invalid_slop_threshold");
66
+ if (!["observe", "enforce"].includes(input.submissionMode)) throw new Error("invalid_submission_mode");
67
+ if (!input.governor || typeof input.governor !== "object") throw new Error("invalid_governor_context");
68
+ }
69
+
70
+ /**
71
+ * Run one full attempt end to end: iterate-loop -> (on handoff) freshness -> submission-gate -> Governor
72
+ * chokepoint -> (on allowed:true) build + execute the real open_pr command. Fails closed (throws) on malformed
73
+ * input/deps, mirroring every sibling module in this pipeline.
74
+ *
75
+ * @param {{
76
+ * loopInput: import("@loopover/engine").IterateLoopInput,
77
+ * issueNumber: number,
78
+ * minerLogin: string,
79
+ * base: string,
80
+ * killSwitchScope: "global"|"repo"|"none",
81
+ * slopThreshold: "clean"|"low"|"elevated"|"high",
82
+ * submissionMode: "observe"|"enforce",
83
+ * maxConsecutiveGateBlocks?: number,
84
+ * draft?: boolean,
85
+ * governor: Omit<import("@loopover/engine").GovernorChokepointInput, "actionClass"|"repoFullName"|"nowMs"|"wouldBeAction">,
86
+ * }} input
87
+ * @param {{
88
+ * driver: import("@loopover/engine").CodingAgentDriver,
89
+ * runSlopAssessment: Function,
90
+ * appendAttemptLogEvent: Function,
91
+ * claimLedger: object,
92
+ * fetchLiveIssueSnapshot: Function,
93
+ * eventLedger: object,
94
+ * governorLedgerAppend?: Function,
95
+ * governorState?: import("./governor-state.js").GovernorState,
96
+ * sessionStartMs?: number,
97
+ * nowMs: number,
98
+ * executeLocalWrite: (spec: import("@loopover/engine").LocalWriteActionSpec) => Promise<unknown>,
99
+ * }} deps
100
+ */
101
+ export async function runMinerAttempt(input, deps) {
102
+ assertInput(input);
103
+ assertDeps(deps);
104
+
105
+ const loopResult = await runIterateLoop(input.loopInput, {
106
+ driver: deps.driver,
107
+ runSlopAssessment: deps.runSlopAssessment,
108
+ appendAttemptLogEvent: deps.appendAttemptLogEvent,
109
+ });
110
+
111
+ if (loopResult.outcome === "abandon") {
112
+ return { outcome: "abandon", loopResult };
113
+ }
114
+
115
+ const handoffPacket = loopResult.handoffPacket;
116
+
117
+ const freshness = await checkSubmissionFreshness(
118
+ { repoFullName: input.loopInput.repoFullName, issueNumber: input.issueNumber, minerLogin: input.minerLogin },
119
+ { claimLedger: deps.claimLedger, fetchLiveIssueSnapshot: deps.fetchLiveIssueSnapshot, eventLedger: deps.eventLedger },
120
+ );
121
+ if (!freshness.fresh) {
122
+ return { outcome: "stale", reason: freshness.reason, loopResult };
123
+ }
124
+
125
+ const submission = await prepareOpenPrSubmission(
126
+ {
127
+ killSwitchScope: input.killSwitchScope,
128
+ repoFullName: input.loopInput.repoFullName,
129
+ handoffPacket,
130
+ slopThreshold: input.slopThreshold,
131
+ mode: input.submissionMode,
132
+ maxConsecutiveGateBlocks: input.maxConsecutiveGateBlocks,
133
+ base: input.base,
134
+ title: input.loopInput.title,
135
+ body: input.loopInput.body ?? "",
136
+ draft: input.draft,
137
+ },
138
+ { eventLedger: deps.eventLedger, sessionStartMs: deps.sessionStartMs },
139
+ );
140
+ if (!submission.ready) {
141
+ return { outcome: "blocked", decision: submission.decision, loopResult };
142
+ }
143
+
144
+ const governed = evaluateGovernorChokepointGatePersisted(
145
+ {
146
+ actionClass: "open_pr",
147
+ repoFullName: input.loopInput.repoFullName,
148
+ nowMs: deps.nowMs,
149
+ wouldBeAction: submission.openPrInput,
150
+ ...input.governor,
151
+ },
152
+ {
153
+ ...(deps.governorLedgerAppend ? { append: deps.governorLedgerAppend } : {}),
154
+ ...(deps.governorState ? { governorState: deps.governorState } : {}),
155
+ },
156
+ );
157
+ if (!governed.decision.allowed) {
158
+ return { outcome: "governed", decision: governed.decision, loopResult };
159
+ }
160
+
161
+ const spec = buildOpenPrSpec(submission.openPrInput);
162
+ const execResult = await deps.executeLocalWrite(spec);
163
+ return { outcome: "submitted", spec, execResult, loopResult };
164
+ }
@@ -0,0 +1,31 @@
1
+ import type { WorktreeExecFn } from "@loopover/engine";
2
+ import type { RunGitFn } from "./repo-clone.js";
3
+
4
+ export function createRealWorktreeExec(timeoutMs?: number): WorktreeExecFn;
5
+
6
+ export type PrepareAttemptWorktreeOptions = {
7
+ baseBranch?: string;
8
+ cloneBaseDir?: string;
9
+ env?: Record<string, string | undefined>;
10
+ exec?: WorktreeExecFn;
11
+ timeoutMs?: number;
12
+ remoteUrl?: string;
13
+ runGit?: RunGitFn;
14
+ };
15
+
16
+ export type PrepareAttemptWorktreeResult =
17
+ | { ok: true; worktreePath: string; branchName: string; repoPath: string }
18
+ | { ok: false; repoPath?: string; error: string };
19
+
20
+ export function prepareAttemptWorktree(
21
+ repoFullName: string,
22
+ attemptId: string,
23
+ options?: PrepareAttemptWorktreeOptions,
24
+ ): Promise<PrepareAttemptWorktreeResult>;
25
+
26
+ export function cleanupAttemptWorktree(
27
+ repoPath: string,
28
+ worktreePath: string,
29
+ attemptOk: boolean,
30
+ options?: { exec?: WorktreeExecFn; timeoutMs?: number },
31
+ ): Promise<{ ok: boolean; removed: boolean; error?: string }>;
@@ -0,0 +1,94 @@
1
+ import { spawn } from "node:child_process";
2
+ import { addWorktree, removeWorktree, shouldRetainWorktree } from "@loopover/engine";
3
+ import { ensureRepoCloned } from "./repo-clone.js";
4
+
5
+ // Real attempt-worktree preparation (#5132, Wave 3.5 follow-up). Composes ensureRepoCloned (repo-clone.js,
6
+ // the missing base-clone-management step) with @loopover/engine's already-built, already-tested
7
+ // addWorktree/removeWorktree primitives -- which existed but were never called from this package, so
8
+ // `workingDirectory` handed to runIterateLoop was always just an empty directory with no real git repo in
9
+ // it. This is the caller that finally exercises them for real.
10
+
11
+ const DEFAULT_TIMEOUT_MS = 120_000;
12
+
13
+ /**
14
+ * Real child_process-backed implementation of the engine's WorktreeExecFn contract. Resolves (never
15
+ * rejects) on error/timeout, mirroring coding-agent-construction.js's createRealCliSubprocessSpawn -- a
16
+ * failed `git worktree add`'s stderr is the diagnosable signal, not something to lose to an unhandled
17
+ * rejection.
18
+ *
19
+ * @returns {import("@loopover/engine").WorktreeExecFn}
20
+ */
21
+ export function createRealWorktreeExec(timeoutMs = DEFAULT_TIMEOUT_MS) {
22
+ return (cmd, args, opts) =>
23
+ new Promise((resolve) => {
24
+ const child = spawn(cmd, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] });
25
+ let stdout = "";
26
+ let stderr = "";
27
+ const timer = setTimeout(() => {
28
+ child.kill("SIGKILL");
29
+ resolve({ code: null, stdout, stderr: `${stderr}\ntimed_out_after_${timeoutMs}ms`.trim() });
30
+ }, timeoutMs);
31
+ child.stdout?.on("data", (chunk) => {
32
+ stdout += chunk.toString("utf8");
33
+ });
34
+ child.stderr?.on("data", (chunk) => {
35
+ stderr += chunk.toString("utf8");
36
+ });
37
+ child.on("error", (err) => {
38
+ clearTimeout(timer);
39
+ resolve({ code: null, stdout, stderr: err.message });
40
+ });
41
+ child.on("close", (code) => {
42
+ clearTimeout(timer);
43
+ resolve({ code, stdout, stderr });
44
+ });
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Prepare a real, isolated git worktree for one attempt: ensure the target repo's base clone exists and is
50
+ * current, then create a fresh `git worktree` off it on a deterministically-named branch. Fails closed
51
+ * (`ok: false`) on any step's failure rather than handing back a half-prepared directory.
52
+ *
53
+ * @param {string} repoFullName
54
+ * @param {string} attemptId
55
+ * @param {{
56
+ * baseBranch?: string, cloneBaseDir?: string, env?: Record<string, string | undefined>,
57
+ * exec?: import("@loopover/engine").WorktreeExecFn, timeoutMs?: number,
58
+ * remoteUrl?: string, runGit?: import("./repo-clone.js").RunGitFn,
59
+ * }} [options]
60
+ * @returns {Promise<{ ok: boolean, worktreePath?: string, branchName?: string, repoPath?: string, error?: string }>}
61
+ */
62
+ export async function prepareAttemptWorktree(repoFullName, attemptId, options = {}) {
63
+ const cloneResult = await ensureRepoCloned(repoFullName, {
64
+ baseBranch: options.baseBranch,
65
+ cloneBaseDir: options.cloneBaseDir,
66
+ env: options.env,
67
+ timeoutMs: options.timeoutMs,
68
+ remoteUrl: options.remoteUrl,
69
+ runGit: options.runGit,
70
+ });
71
+ if (!cloneResult.ok) return { ok: false, error: cloneResult.error ?? "ensure_repo_cloned_failed" };
72
+
73
+ const exec = options.exec ?? createRealWorktreeExec(options.timeoutMs);
74
+ const baseBranch = typeof options.baseBranch === "string" && options.baseBranch.trim() ? options.baseBranch.trim() : "main";
75
+ const added = await addWorktree({ exec, repoPath: cloneResult.repoPath, baseBranch, attemptId });
76
+ if (!added.ok) return { ok: false, repoPath: cloneResult.repoPath, error: added.error ?? "git_worktree_add_failed" };
77
+
78
+ return { ok: true, worktreePath: added.plan.worktreePath, branchName: added.plan.branchName, repoPath: cloneResult.repoPath };
79
+ }
80
+
81
+ /**
82
+ * Tear down an attempt's worktree once the attempt concludes, per the engine's own retention policy: a
83
+ * failed attempt's worktree is RETAINED for post-mortem inspection, a succeeded one is removed.
84
+ *
85
+ * @param {string} repoPath
86
+ * @param {string} worktreePath
87
+ * @param {boolean} attemptOk
88
+ * @param {{ exec?: import("@loopover/engine").WorktreeExecFn, timeoutMs?: number }} [options]
89
+ * @returns {Promise<{ ok: boolean, removed: boolean, error?: string }>}
90
+ */
91
+ export async function cleanupAttemptWorktree(repoPath, worktreePath, attemptOk, options = {}) {
92
+ const exec = options.exec ?? createRealWorktreeExec(options.timeoutMs);
93
+ return removeWorktree({ exec, repoPath, worktreePath, retain: shouldRetainWorktree(attemptOk) });
94
+ }
@@ -0,0 +1 @@
1
+ export function runCalibrationCli(args?: string[], env?: Record<string, string | undefined>): number;