@loops-adk/core 0.2.0 → 0.3.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.
@@ -4,9 +4,9 @@ import { isLimitError, waitMsFor } from './chunk-Y2SD7GBL.js';
4
4
  import { LoopError } from './chunk-I3STY7U6.js';
5
5
  import { readFileSync, mkdtempSync, existsSync, writeFileSync, appendFileSync, readdirSync, mkdirSync, rmSync } from 'fs';
6
6
  import { execa } from 'execa';
7
+ import { createHash, randomBytes } from 'crypto';
7
8
  import { tmpdir, homedir } from 'os';
8
9
  import { join, dirname } from 'path';
9
- import { randomBytes } from 'crypto';
10
10
 
11
11
  // src/core/describe.ts
12
12
  var META = /* @__PURE__ */ new WeakMap();
@@ -54,7 +54,11 @@ function renderPlan(meta, indent = "") {
54
54
  const out = [];
55
55
  switch (meta.kind) {
56
56
  case "loop": {
57
- const max = typeof meta.max === "number" ? ` (max ${meta.max})` : "";
57
+ const caps = [];
58
+ if (typeof meta.max === "number") caps.push(`max ${meta.max}`);
59
+ if (typeof meta.noProgress === "number")
60
+ caps.push(`stall after ${meta.noProgress} flat`);
61
+ const max = caps.length ? ` (${caps.join("; ")})` : "";
58
62
  out.push(`${indent}loop${nm}${max}`);
59
63
  const start = meta.start;
60
64
  const gate = meta.gate;
@@ -812,6 +816,32 @@ async function isDirty(opts) {
812
816
  const r = await git(["status", "--porcelain"], opts);
813
817
  return r.stdout.trim().length > 0;
814
818
  }
819
+ async function workspaceFingerprint(opts) {
820
+ try {
821
+ if (!await isRepo(opts)) return void 0;
822
+ const hash = createHash("sha256");
823
+ const feed = (label, value) => {
824
+ hash.update(label);
825
+ hash.update("");
826
+ hash.update(value);
827
+ hash.update("");
828
+ };
829
+ feed("head", await headSha(opts) ?? "");
830
+ feed("status", (await git(["status", "--porcelain"], opts)).stdout);
831
+ feed("unstaged", (await git(["diff"], opts)).stdout);
832
+ feed("staged", (await git(["diff", "--cached"], opts)).stdout);
833
+ const untracked = (await git(["ls-files", "--others", "--exclude-standard"], opts)).stdout.split("\n").filter(Boolean);
834
+ for (let i = 0; i < untracked.length; i += 500) {
835
+ const chunk = untracked.slice(i, i + 500);
836
+ feed(`untracked-paths:${i}`, chunk.join("\n"));
837
+ const r = await git(["hash-object", "--", ...chunk], opts);
838
+ if (r.exitCode === 0) feed(`untracked-content:${i}`, r.stdout);
839
+ }
840
+ return hash.digest("hex");
841
+ } catch {
842
+ return void 0;
843
+ }
844
+ }
815
845
  async function commit(input, opts) {
816
846
  if (!input.allowEmpty && !await hasStagedChanges(opts)) return void 0;
817
847
  const message = input.body ? `${input.subject}
@@ -1780,6 +1810,102 @@ function fnJob(label, fn) {
1780
1810
  return setMeta(job, { kind: "fn", name: label });
1781
1811
  }
1782
1812
 
1813
+ // src/core/progress.ts
1814
+ function resolveNoProgress(input) {
1815
+ if (input == null) return void 0;
1816
+ const cfg = typeof input === "number" ? { window: input } : input;
1817
+ return {
1818
+ ...cfg,
1819
+ window: cfg.window ?? 3,
1820
+ minConfidenceDelta: cfg.minConfidenceDelta ?? 0.02
1821
+ };
1822
+ }
1823
+ var ProgressTracker = class {
1824
+ window;
1825
+ minConfidenceDelta;
1826
+ /** Every state this run has reached, namespaced by channel. */
1827
+ seen = /* @__PURE__ */ new Set();
1828
+ /** Confidence high-water mark — the best score at the last progress point. */
1829
+ best;
1830
+ /** The current run of consecutive no-progress iterations. */
1831
+ stalledRun = [];
1832
+ lastEvidence = [];
1833
+ lastReason = "gate not met";
1834
+ indeterminate = 0;
1835
+ sampled = 0;
1836
+ constructor(cfg) {
1837
+ this.window = cfg.window;
1838
+ this.minConfidenceDelta = cfg.minConfidenceDelta;
1839
+ }
1840
+ /**
1841
+ * Record one iteration. Returns a `StallReport` when this sample fills the
1842
+ * window, else undefined.
1843
+ */
1844
+ record(sample) {
1845
+ this.sampled += 1;
1846
+ if (sample.reason) this.lastReason = sample.reason;
1847
+ const flat = [];
1848
+ let progressed = false;
1849
+ let channels = 0;
1850
+ if (sample.fingerprint !== void 0) {
1851
+ channels += 1;
1852
+ const key = `fp:${sample.fingerprint}`;
1853
+ if (this.seen.has(key)) {
1854
+ flat.push("workspace: no state this run has not already visited");
1855
+ } else {
1856
+ this.seen.add(key);
1857
+ progressed = true;
1858
+ }
1859
+ }
1860
+ if (sample.signal !== void 0) {
1861
+ channels += 1;
1862
+ const key = `sig:${sample.signal}`;
1863
+ if (this.seen.has(key)) {
1864
+ flat.push(`signal: "${sample.signal}" already seen this run`);
1865
+ } else {
1866
+ this.seen.add(key);
1867
+ progressed = true;
1868
+ }
1869
+ }
1870
+ if (sample.confidence !== void 0) {
1871
+ channels += 1;
1872
+ if (this.best === void 0 || sample.confidence >= this.best + this.minConfidenceDelta) {
1873
+ this.best = Math.max(this.best ?? -Infinity, sample.confidence);
1874
+ progressed = true;
1875
+ } else {
1876
+ flat.push(
1877
+ `confidence ${sample.confidence.toFixed(2)} did not improve on ${this.best.toFixed(2)} (needs +${this.minConfidenceDelta})`
1878
+ );
1879
+ }
1880
+ }
1881
+ if (channels === 0) {
1882
+ this.indeterminate += 1;
1883
+ return void 0;
1884
+ }
1885
+ if (progressed) {
1886
+ this.stalledRun = [];
1887
+ return void 0;
1888
+ }
1889
+ this.stalledRun.push(sample.iteration);
1890
+ this.lastEvidence = flat;
1891
+ if (this.stalledRun.length < this.window) return void 0;
1892
+ return {
1893
+ window: this.window,
1894
+ iterations: [...this.stalledRun],
1895
+ reason: this.lastReason,
1896
+ evidence: [...this.lastEvidence]
1897
+ };
1898
+ }
1899
+ /**
1900
+ * True when the detector has seen a full window of samples and none carried
1901
+ * any evidence channel — detection is configured but cannot fire. The loop
1902
+ * uses this to warn once instead of failing silently-inert.
1903
+ */
1904
+ isInert() {
1905
+ return this.indeterminate >= this.window && this.indeterminate === this.sampled;
1906
+ }
1907
+ };
1908
+
1783
1909
  // src/core/context.ts
1784
1910
  function childContext(parent, over) {
1785
1911
  return {
@@ -1842,6 +1968,7 @@ function loop(config) {
1842
1968
  const until = config.until ? toCondition(config.until) : void 0;
1843
1969
  const stopOn = config.stopOn ? toCondition(config.stopOn) : void 0;
1844
1970
  const onError = config.retry?.onError ?? "continue";
1971
+ const noProgress = resolveNoProgress(config.noProgress);
1845
1972
  const job = async (parent) => {
1846
1973
  const path = [...parent.path, config.name];
1847
1974
  const depth = parent.depth + 1;
@@ -1928,6 +2055,8 @@ function loop(config) {
1928
2055
  let last;
1929
2056
  let consecutiveErrors = 0;
1930
2057
  let consecutiveReviewFails = 0;
2058
+ const tracker = noProgress ? new ProgressTracker(noProgress) : void 0;
2059
+ let warnedInert = false;
1931
2060
  while (true) {
1932
2061
  await yieldToLoop();
1933
2062
  if (parent.signal.aborted)
@@ -1948,6 +2077,7 @@ function loop(config) {
1948
2077
  }
1949
2078
  iteration += 1;
1950
2079
  const ctx = ctxAt(iteration, last);
2080
+ let turnReview;
1951
2081
  parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
1952
2082
  let bodyThrew = false;
1953
2083
  try {
@@ -2133,6 +2263,7 @@ function loop(config) {
2133
2263
  }
2134
2264
  consecutiveReviewFails += 1;
2135
2265
  lastReview = reviewOutcome;
2266
+ turnReview = reviewOutcome;
2136
2267
  parent.log(
2137
2268
  `review did not pass (${reviewOutcome.summary ?? reviewOutcome.status}); re-entering ${config.name}`,
2138
2269
  "warn"
@@ -2148,6 +2279,62 @@ function loop(config) {
2148
2279
  );
2149
2280
  }
2150
2281
  }
2282
+ if (tracker) {
2283
+ let fingerprint;
2284
+ if (noProgress.workspace !== false) {
2285
+ fingerprint = await workspaceFingerprint({
2286
+ cwd: ctx.workspace.dir,
2287
+ signal: parent.signal
2288
+ });
2289
+ }
2290
+ let signalValue;
2291
+ if (noProgress.signal) {
2292
+ try {
2293
+ const v = await noProgress.signal(ctx, last);
2294
+ signalValue = v == null ? void 0 : String(v);
2295
+ } catch (e) {
2296
+ throw LoopError.from(e, {
2297
+ code: "VALIDATION",
2298
+ phase: "body",
2299
+ path,
2300
+ iteration
2301
+ });
2302
+ }
2303
+ }
2304
+ const report = tracker.record({
2305
+ iteration,
2306
+ fingerprint,
2307
+ signal: signalValue,
2308
+ confidence: turnReview?.confidence ?? conv.confidence ?? last.confidence,
2309
+ reason: turnReview ? turnReview.summary ?? "review rejected" : conv.reason
2310
+ });
2311
+ if (!warnedInert && tracker.isInert()) {
2312
+ warnedInert = true;
2313
+ parent.log(
2314
+ `noProgress is set on ${config.name} but no evidence channel exists (no git workspace, no gate confidence, no custom signal); stall detection is inert`,
2315
+ "warn"
2316
+ );
2317
+ }
2318
+ if (report) {
2319
+ parent.emit({
2320
+ kind: "loop:stall",
2321
+ ts: ts(),
2322
+ path,
2323
+ iteration,
2324
+ report
2325
+ });
2326
+ return finish(
2327
+ {
2328
+ status: "exhausted",
2329
+ summary: `stalled after ${report.iterations.length} iterations with no observable progress: ${report.reason}`,
2330
+ confidence: last.confidence,
2331
+ data: last.data,
2332
+ stall: report
2333
+ },
2334
+ iteration
2335
+ );
2336
+ }
2337
+ }
2151
2338
  if (config.delayMs) await delay(config.delayMs, parent.signal);
2152
2339
  }
2153
2340
  } catch (e) {
@@ -2169,6 +2356,7 @@ function loop(config) {
2169
2356
  kind: "loop",
2170
2357
  name: config.name,
2171
2358
  max: config.max,
2359
+ noProgress: noProgress?.window,
2172
2360
  start: describeConditions(config.start),
2173
2361
  gate: describeConditions(config.until),
2174
2362
  stopOn: describeConditions(config.stopOn),
@@ -2685,6 +2873,8 @@ function formatEvent(event) {
2685
2873
  return `${at} tool ${event.name} ${event.phase}`;
2686
2874
  case "engine:usage":
2687
2875
  return `${at} ${event.model}: ${event.usage.inputTokens}/${event.usage.outputTokens} tok`;
2876
+ case "loop:stall":
2877
+ return `${at}\u23F9 stalled after ${event.report.iterations.length} no-progress iterations: ${event.report.reason}`;
2688
2878
  case "limit:wait":
2689
2879
  return `${at}\u23F8 limit ${event.code}: waiting ${Math.round(event.waitMs / 1e3)}s`;
2690
2880
  case "limit:pause":
@@ -2901,6 +3091,6 @@ function exitCodeFor(outcome) {
2901
3091
  }
2902
3092
  }
2903
3093
 
2904
- export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentContract, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, feedbackBlock, fnJob, forgeChecks, formatEvent, fromFile, gateJob, graphPositionBlock, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, isRequiredFeedbackSeverity, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, normalizeFeedbackSeverity, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, reviewContext, reviewPanel, revisionFromOutcome, revisionRequest, run, runEventsPath, runSemanticRecordsPath, runsHome, semanticRecordsFromEvent, setMeta, stageAll, toCondition };
2905
- //# sourceMappingURL=chunk-WM5QVHM2.js.map
2906
- //# sourceMappingURL=chunk-WM5QVHM2.js.map
3094
+ export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, ProgressTracker, Stats, addWorktree, agentCheck, agentContract, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, feedbackBlock, fnJob, forgeChecks, formatEvent, fromFile, gateJob, graphPositionBlock, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, isRequiredFeedbackSeverity, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, normalizeFeedbackSeverity, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveNoProgress, resolveSystem, retrieveLedger, reviewContext, reviewPanel, revisionFromOutcome, revisionRequest, run, runEventsPath, runSemanticRecordsPath, runsHome, semanticRecordsFromEvent, setMeta, stageAll, toCondition, workspaceFingerprint };
3095
+ //# sourceMappingURL=chunk-3PMVII43.js.map
3096
+ //# sourceMappingURL=chunk-3PMVII43.js.map