@lmctl-ai/lmctl 0.1.342 → 0.2.3

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 (182) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +11 -51
  3. package/data/price-aliases.json +1 -0
  4. package/data/price-data.json +1 -1
  5. package/dist/cli/batch.d.ts +2 -0
  6. package/dist/cli/busy-evidence.d.ts +27 -0
  7. package/dist/cli/chat-cycle.d.ts +59 -0
  8. package/dist/cli/chat-exec.d.ts +83 -0
  9. package/dist/cli/chat-policy.d.ts +22 -0
  10. package/dist/cli/chat.d.ts +23 -0
  11. package/dist/cli/code-resolver.d.ts +8 -0
  12. package/dist/cli/common.d.ts +54 -0
  13. package/dist/cli/context.d.ts +12 -0
  14. package/dist/cli/cost.d.ts +2 -0
  15. package/dist/cli/export.d.ts +2 -0
  16. package/dist/cli/index.d.ts +3 -0
  17. package/dist/cli/index.js +1062 -1400
  18. package/dist/cli/infra.d.ts +78 -0
  19. package/dist/cli/ls.d.ts +2 -0
  20. package/dist/cli/mailbox-cli.d.ts +6 -0
  21. package/dist/cli/managed.d.ts +62 -0
  22. package/dist/cli/mcp-install.d.ts +23 -0
  23. package/dist/cli/mcp.d.ts +2 -0
  24. package/dist/cli/paths.d.ts +50 -0
  25. package/dist/cli/plan-wizard.d.ts +42 -0
  26. package/dist/cli/plan.d.ts +20 -0
  27. package/dist/cli/queue-delivery.d.ts +107 -0
  28. package/dist/cli/ratelimit.d.ts +3 -0
  29. package/dist/cli/remote-fetch.d.ts +7 -0
  30. package/dist/cli/run-queries.d.ts +82 -0
  31. package/dist/cli/running.d.ts +5 -0
  32. package/dist/cli/schema.sql +20 -1
  33. package/dist/cli/script.d.ts +6 -0
  34. package/dist/cli/script.js +2097 -0
  35. package/dist/cli/self-identity.d.ts +25 -0
  36. package/dist/cli/session-idle.d.ts +118 -0
  37. package/dist/cli/status.d.ts +22 -0
  38. package/dist/cli/tail.d.ts +18 -0
  39. package/dist/cli/target-resolver.d.ts +139 -0
  40. package/dist/cli/team-status.d.ts +91 -0
  41. package/dist/cli/teamfile-compat.d.ts +71 -0
  42. package/dist/cli/teamfile-store.d.ts +18 -0
  43. package/dist/cli/teamfile-template.d.ts +40 -0
  44. package/dist/cli/template-env.d.ts +25 -0
  45. package/dist/cli/terminal-lock.d.ts +25 -0
  46. package/dist/cli/terminal.d.ts +23 -0
  47. package/dist/cli/time-filter.d.ts +10 -0
  48. package/dist/cli/update-notifier.d.ts +11 -0
  49. package/dist/cli/version-util.d.ts +2 -0
  50. package/dist/config/seed_templates.d.ts +1 -0
  51. package/dist/db/backfill-event-log.d.ts +26 -0
  52. package/dist/db/delegation.d.ts +37 -0
  53. package/dist/db/escalation_vocab.d.ts +45 -0
  54. package/dist/db/event-log.d.ts +40 -0
  55. package/dist/db/index.d.ts +1213 -0
  56. package/dist/db/inflight-acquire.d.ts +70 -0
  57. package/dist/db/invocation-event-projections.d.ts +65 -0
  58. package/dist/db/ledger-identity.test.d.ts +1 -0
  59. package/dist/db/live-event-projections.d.ts +92 -0
  60. package/dist/db/migrate.d.ts +51 -0
  61. package/dist/db/open-readonly.d.ts +2 -0
  62. package/dist/db/process-liveness.d.ts +37 -0
  63. package/dist/db/sqlite.d.ts +47 -0
  64. package/dist/db/sqlite.test.d.ts +1 -0
  65. package/dist/db/terminal-lock.d.ts +14 -0
  66. package/dist/db/tracked-invocation-reconcile.d.ts +52 -0
  67. package/dist/db/tracked-invocation.d.ts +116 -0
  68. package/dist/mcp/dispatch.d.ts +45 -0
  69. package/dist/mcp/mediation.d.ts +128 -0
  70. package/dist/mcp/path-warning.d.ts +7 -0
  71. package/dist/mcp/server.d.ts +63 -0
  72. package/dist/mcp/tools.d.ts +24 -0
  73. package/dist/providers/adapter.d.ts +265 -0
  74. package/dist/providers/adapters/agy-adapter.d.ts +17 -0
  75. package/dist/providers/adapters/claude-adapter.d.ts +20 -0
  76. package/dist/providers/adapters/codex-adapter.d.ts +22 -0
  77. package/dist/providers/adapters/copilot-adapter.d.ts +16 -0
  78. package/dist/providers/adapters/dsh-adapter.d.ts +89 -0
  79. package/dist/providers/adapters/gemini-adapter.d.ts +44 -0
  80. package/dist/providers/adapters/hermes-adapter.d.ts +71 -0
  81. package/dist/providers/adapters/index.d.ts +35 -0
  82. package/dist/providers/adapters/kimi-adapter.d.ts +16 -0
  83. package/dist/providers/adapters/lmplayer-adapter.d.ts +14 -0
  84. package/dist/providers/adapters/opencode-adapter.d.ts +11 -0
  85. package/dist/providers/adapters/pi-adapter.d.ts +17 -0
  86. package/dist/providers/adapters/print-turn.d.ts +29 -0
  87. package/dist/providers/adapters/qwen-adapter.d.ts +11 -0
  88. package/dist/providers/agy.d.ts +32 -0
  89. package/dist/providers/base-adapter.d.ts +107 -0
  90. package/dist/providers/base.d.ts +12 -0
  91. package/dist/providers/binary-check.d.ts +102 -0
  92. package/dist/providers/bundle.d.ts +118 -0
  93. package/dist/providers/claude-mock.d.ts +90 -0
  94. package/dist/providers/claude.d.ts +21 -0
  95. package/dist/providers/codex-config.d.ts +15 -0
  96. package/dist/providers/codex.d.ts +25 -0
  97. package/dist/providers/config/model-selection-lint.d.ts +45 -0
  98. package/dist/providers/config/team-config.d.ts +183 -0
  99. package/dist/providers/config/types.d.ts +146 -0
  100. package/dist/providers/config/user-config.d.ts +88 -0
  101. package/dist/providers/copilot.d.ts +21 -0
  102. package/dist/providers/debug.d.ts +128 -0
  103. package/dist/providers/dsh-sidecar.d.ts +65 -0
  104. package/dist/providers/dsh-zstd.d.ts +27 -0
  105. package/dist/providers/dsh.d.ts +37 -0
  106. package/dist/providers/frontmatter.d.ts +46 -0
  107. package/dist/providers/gemini-session-reader.d.ts +151 -0
  108. package/dist/providers/gemini.d.ts +39 -0
  109. package/dist/providers/hermes.d.ts +90 -0
  110. package/dist/providers/index.d.ts +421 -0
  111. package/dist/providers/kimi.d.ts +37 -0
  112. package/dist/providers/model-catalog.d.ts +141 -0
  113. package/dist/providers/one-shot-timeout.d.ts +1 -0
  114. package/dist/providers/opencode-config.d.ts +62 -0
  115. package/dist/providers/opencode.d.ts +40 -0
  116. package/dist/providers/parsers/acp.d.ts +47 -0
  117. package/dist/providers/parsers/codex-app-server.d.ts +60 -0
  118. package/dist/providers/parsers/stream-json.d.ts +57 -0
  119. package/dist/providers/passive-readers/agy.d.ts +155 -0
  120. package/dist/providers/passive-readers/copilot.d.ts +173 -0
  121. package/dist/providers/passive-readers/dsh.d.ts +34 -0
  122. package/dist/providers/passive-readers/kimi.d.ts +44 -0
  123. package/dist/providers/passive-readers/opencode.d.ts +89 -0
  124. package/dist/providers/passive-readers/qwen.d.ts +151 -0
  125. package/dist/providers/passive-readers/shared.d.ts +138 -0
  126. package/dist/providers/passive-session-reader.d.ts +188 -0
  127. package/dist/providers/path-utils.d.ts +1 -0
  128. package/dist/providers/pi.d.ts +32 -0
  129. package/dist/providers/pricing.d.ts +107 -0
  130. package/dist/providers/qwen.d.ts +22 -0
  131. package/dist/providers/ratelimit-readings.d.ts +32 -0
  132. package/dist/providers/registry.d.ts +31 -0
  133. package/dist/providers/resume.d.ts +43 -0
  134. package/dist/providers/seed-instruction.d.ts +3 -0
  135. package/dist/providers/self-session-env.d.ts +21 -0
  136. package/dist/providers/session-limit.d.ts +9 -0
  137. package/dist/providers/strip-ansi.d.ts +5 -0
  138. package/dist/providers/transport-errors.d.ts +94 -0
  139. package/dist/providers/transport-lifecycle.d.ts +63 -0
  140. package/dist/providers/transport-wrappers.d.ts +110 -0
  141. package/dist/providers/transports/acp-profiles/config-option-step.d.ts +24 -0
  142. package/dist/providers/transports/acp-profiles/copilot.d.ts +12 -0
  143. package/dist/providers/transports/acp-profiles/gemini.d.ts +22 -0
  144. package/dist/providers/transports/acp-profiles/hermes.d.ts +27 -0
  145. package/dist/providers/transports/acp-profiles/index.d.ts +19 -0
  146. package/dist/providers/transports/acp-profiles/kimi.d.ts +24 -0
  147. package/dist/providers/transports/acp-profiles/opencode.d.ts +26 -0
  148. package/dist/providers/transports/acp-profiles/types.d.ts +258 -0
  149. package/dist/providers/transports/acp-rpc.d.ts +18 -0
  150. package/dist/providers/transports/acp.d.ts +215 -0
  151. package/dist/providers/transports/codex-app-server.d.ts +190 -0
  152. package/dist/providers/transports/hold-open.d.ts +120 -0
  153. package/dist/providers/transports/manager.d.ts +21 -0
  154. package/dist/providers/transports/stream-json.d.ts +162 -0
  155. package/dist/providers/transports/types.d.ts +56 -0
  156. package/dist/providers/types.d.ts +154 -0
  157. package/dist/providers/yolo-contract.d.ts +131 -0
  158. package/dist/script/adapter.d.ts +101 -0
  159. package/dist/script/batch-script.d.ts +7 -0
  160. package/dist/script/index.d.ts +3 -0
  161. package/dist/script/lmscript.d.ts +550 -0
  162. package/dist/shared/policy.d.ts +74 -0
  163. package/dist/shared-lib/db.d.ts +25 -0
  164. package/dist/shared-lib/passive-session-reader.d.ts +2 -0
  165. package/dist/shared-lib/provider-parse.d.ts +6 -0
  166. package/dist/templates/basedir.d.ts +52 -0
  167. package/dist/uri/lmctl_uri.d.ts +38 -0
  168. package/durable-memory/model_selection.md +122 -25
  169. package/examples/README.md +103 -0
  170. package/examples/anonymous-panel.lms +3 -0
  171. package/examples/batch-prompts.txt +9 -0
  172. package/examples/batchscript.lms +67 -0
  173. package/examples/n8n/agent-lib.lms +56 -0
  174. package/examples/n8n/git-lib.lms +93 -0
  175. package/examples/n8n/mock-support.db +0 -0
  176. package/examples/n8n/multi-agent-review.lms +107 -0
  177. package/examples/n8n/news-digest.lms +159 -0
  178. package/examples/n8n/rag-search.lms +163 -0
  179. package/examples/n8n/support-agent.lms +88 -0
  180. package/examples/n8n/ticket-triage.lms +156 -0
  181. package/package.json +13 -17
  182. package/dist/cli/side_effect_classifier.json +0 -19
@@ -0,0 +1,52 @@
1
+ /** The ONE place the default basedir name lives — re-defaulting is 1 line. */
2
+ export declare const DEFAULT_BASEDIR_DIRNAME = ".lmctl";
3
+ /**
4
+ * The active basedir (tiers 1-3). Never touches the filesystem beyond the
5
+ * project-local probe; pure given env + cwd.
6
+ */
7
+ export declare function resolveBasedir(cwd?: string): string;
8
+ /** `<basedir>/templates/workflows`. */
9
+ export declare function templatesWorkflowsDir(cwd?: string): string;
10
+ /**
11
+ * The read-only shipped-sibling workflows dir. dev (tsx):
12
+ * src/templates/basedir.ts → ../../workflows = <repo>/workflows. built
13
+ * (unbundled) dist/templates/basedir.js → <repo>/workflows. packed bundle:
14
+ * every module collapses onto dist/cli/index.js → ../../workflows =
15
+ * <pkgroot>/workflows (exactly where pack-release.mjs ships the
16
+ * engine-owned built-in). One expression, correct in all three.
17
+ */
18
+ export declare function shippedWorkflowsDir(): string;
19
+ export interface MaterializeResult {
20
+ basedir: string;
21
+ copied: string[];
22
+ refreshed: string[];
23
+ preserved: string[];
24
+ shadowed: string[];
25
+ }
26
+ /**
27
+ * Copy shipped workflow templates into `<basedir>/templates/workflows/`,
28
+ * honouring the sha256 manifest for edit-preservation. Idempotent.
29
+ * Best-effort: a read-only/unwritable basedir does NOT throw fatally for
30
+ * callers that pass `{ swallow: true }` — the per-template resolver still
31
+ * falls back to the shipped sibling so the engine never hard-fails.
32
+ */
33
+ export declare function materializeTemplates(opts?: {
34
+ cwd?: string;
35
+ swallow?: boolean;
36
+ }): MaterializeResult;
37
+ /**
38
+ * The ordered basedir search list (tiers 1-3, same order as
39
+ * `resolveBasedir` but ALL applicable tiers, not just the winner):
40
+ * `$LMCTL_HOME` → nearest `./.lmctl` → `~/.lmctl`. Single source of the
41
+ * tier order — reused by `resolveWorkflowTemplate` and the `lmctl://`
42
+ * resolver so they can never drift. (The read-only shipped-sibling
43
+ * fallback is workflow-only and is appended by `resolveWorkflowTemplate`.)
44
+ */
45
+ export declare function basedirSearchList(cwd?: string): string[];
46
+ /**
47
+ * Resolve a workflow template by name (no extension) to an on-disk path,
48
+ * basedir-first then the read-only shipped sibling. Pure lookup — no
49
+ * mutation. Returns null if it exists nowhere (caller decides: the worker
50
+ * built-in keeps its graceful `catch → null`).
51
+ */
52
+ export declare function resolveWorkflowTemplate(name: string, cwd?: string): string | null;
@@ -0,0 +1,38 @@
1
+ /** The URI scheme prefix — single named constant. */
2
+ export declare const LMCTL_URI_SCHEME = "lmctl://";
3
+ /** F4 (DECIDED 2026-05-19, WIRED) — the live remote catalog base. Single
4
+ * named constant; mirror the local path shape `<base><kind>/<name>`. */
5
+ export declare const LMCTL_REMOTE_CATALOG_BASE = "https://lmctl.com/templates/";
6
+ /** F5 — fetched bytes are cached under `<basedir>/<LMCTL_CACHE_DIRNAME>/`
7
+ * ONLY (never the user-editable `templates/` tree). */
8
+ export declare const LMCTL_CACHE_DIRNAME = ".cache";
9
+ /** Bounded remote fetch — a hung/slow catalog can never wedge resolution. */
10
+ export declare const LMCTL_REMOTE_FETCH_TIMEOUT_MS = 8000;
11
+ export type LmctlUriKind = 'workflow' | 'mcp' | 'skill';
12
+ export interface ParsedLmctlUri {
13
+ kind: LmctlUriKind;
14
+ name: string;
15
+ /** Optional `@<sha256>` integrity pin (F6 hash-pinning). */
16
+ ref?: string;
17
+ }
18
+ /** Distinct failure classes (the directive's classification map):
19
+ * - `malformed` — the URI itself is invalid (parse).
20
+ * - `missing` — not local AND remote unreachable/404/non-2xx.
21
+ * - `validation-failure` — remote 2xx but pin-mismatch or shape-invalid
22
+ * (artifact discarded, NOT cached; loud). */
23
+ export type LmctlUriFailure = 'malformed' | 'missing' | 'validation-failure';
24
+ export declare class LmctlUriError extends Error {
25
+ readonly reason: LmctlUriFailure;
26
+ constructor(message: string, reason?: LmctlUriFailure);
27
+ }
28
+ /** True iff `s` is a string using the `lmctl://` scheme. */
29
+ export declare function isLmctlUri(s: unknown): s is string;
30
+ /** Parse + validate an `lmctl://` URI. Throws `LmctlUriError`
31
+ * (reason `malformed`) on any malformed input — never a raw crash. */
32
+ export declare function parseLmctlUri(uri: string): ParsedLmctlUri;
33
+ /**
34
+ * Resolve an `lmctl://` URI to a concrete EXISTING local path. Local-first;
35
+ * on miss, the wired remote tier (bounded, typed-classified). Async because
36
+ * the remote tier uses `fetch`.
37
+ */
38
+ export declare function resolveLmctlUri(uri: string, cwd?: string): Promise<string>;
@@ -4,8 +4,21 @@
4
4
 
5
5
  For `.lmctl` teamfile compatibility mode:
6
6
 
7
- - `lmctl lint <teamfile>` warns when a requested model is not in the
8
- tested-model catalog for `claude`, `gemini`, or `qwen`.
7
+ - `lmctl lint <teamfile>` resolves `model=` for `claude`, `gemini`, `qwen`,
8
+ and `hermes` against the lmprice model catalog (see
9
+ `## lmprice Model Catalog` below) and warns when the model is not found.
10
+ This REPLACED the static tested-model lists that used to live in this
11
+ document (2026-09-24) — hand-copied lists contradicted the catalog.
12
+
13
+ **Best-effort contract (operator, 2026-09-25): lint is advisory and is NOT
14
+ claimed to be 100% up to date.** Checks can lag provider reality in BOTH
15
+ directions — a model lint warns about may have just been added upstream, and
16
+ a model lint accepts may have just been retired. Every model check is a
17
+ warning, never an error (`lint --strict` is the operator's explicit
18
+ promotion, not the default). The runtime stance for anything unrecognized
19
+ or out of date is always: "not recognized — could still be valid; no cost
20
+ information," never a failure. Cost is equally best-effort (n/a with a
21
+ reason, never a guessed price).
9
22
  - `codex` and `agy` model validation queries each provider's own live
10
23
  catalog command (`codex debug models`, `agy models`) instead of a
11
24
  hand-copied list — see their `## Codex` / `## Antigravity` sections
@@ -34,18 +47,42 @@ For `.lmctl` teamfile compatibility mode:
34
47
  prefix. Hermes only treats a leading `<x>:` as a provider delimiter when
35
48
  `x` is a known provider name (`hermes_cli/models.py:parse_model_input`);
36
49
  a bare id stays the model name and Hermes then has to infer the provider
37
- by detection, which fails for models it cannot place. See the `## Hermes`
38
- section below.
50
+ by detection, which fails for models it cannot place. After the shape
51
+ check, lint ALSO resolves the id against the lmprice catalog (the hermes
52
+ adapter splits the `provider:` prefix) and warns on a catalog miss —
53
+ warning-only, since Hermes may still resolve it from user config. See the
54
+ `## Hermes` section below.
39
55
  - `copilot` accepts `model=` syntactically, but model selection is not
40
56
  supported and lint warns that the value is ignored.
41
57
  - `lmctl seed <teamfile>` passes `model=` through to the provider seed
42
58
  call. It does not contact provider model catalogs.
43
59
 
44
- The runtime also carries a built-in fallback catalog so installed
45
- `lmctl` can lint without requiring this source-tree document to be
46
- present in `dist`. Keep this document and
47
- `src/providers/config/model-selection-lint.ts` synchronized when adding
48
- or retiring tested model ids.
60
+ The `claude`/`gemini`/`qwen` tables in the sections below are HISTORICAL
61
+ evidence records of the retired tested-model lists (pre-2026-09-24); lint no
62
+ longer consults them — it resolves against the lmprice catalog.
63
+
64
+ ## lmprice Model Catalog
65
+
66
+ Since 2026-09-24 (0.2.1), model validation and costing share one catalog
67
+ layer fed by the public lmprice dataset
68
+ (`https://lmctl.com/dataset/model_list.json` + `aliases.json`):
69
+
70
+ - **Loading**: ETag-conditional refresh into `~/.lmctl/cache/lmprice/` with a
71
+ 6h TTL (`LMCTL_PRICE_TTL=0` forces; `LMCTL_PRICE_DATA=<dir>` pins a
72
+ snapshot and disables network); bundled snapshot from build time is the
73
+ offline fallback; the loader never throws.
74
+ - **Resolution order** (provider-aware): provider-qualified alias hit
75
+ (evidence `established`/`asserted` → standards chain → pinned row;
76
+ `unresolved` is terminal, never guessed) → provider adapter family match →
77
+ miss. States: priced / estimated / known-unpriced / unresolved / unknown /
78
+ no-catalog.
79
+ - **Invariant (operator, 2026-09-25): if we can lint, then we can cost.**
80
+ lmprice build validation rejects half-bindings (asserted alias ending in a
81
+ null record pin); lmctl surfaces any legacy known-unpriced as the lint
82
+ warning "recognized but no price information."
83
+ - **Freshness**: the catalog is only as current as the last lmprice publish
84
+ plus the client TTL — see the best-effort contract at the top of this
85
+ document. `cost --debug` shows the dataset's `generated_at`.
49
86
 
50
87
  ## Claude
51
88
 
@@ -69,7 +106,7 @@ week: it kept `gpt-5.4-mini` listed as valid after OpenAI withdrew it from
69
106
  the subscription (194 codex members with no `model=` pinned were silently
70
107
  falling through to it as the ambient default), and separately omitted
71
108
  `gpt-5.6-terra`/`gpt-5.6-luna` after an operator quota downshift moved 260
72
- members onto them — both real, working models (`lmctl chat` confirmed
109
+ members onto them — both real, working models (`lmctl prompt` confirmed
73
110
  live), producing 45 fleet-wide false-positive warnings. `codex debug
74
111
  models` ("Render the raw model catalog as JSON") shipped since the first
75
112
  incident and gives codex the same live-catalog treatment `agy` already
@@ -102,20 +139,19 @@ withdrawn outright and is not expected back.
102
139
 
103
140
  ## Gemini
104
141
 
105
- | Configured `model=` | Evidence |
106
- | --- | --- |
107
- | `gemini-3.1-pro-preview` | Ported from lmctl0 tested-model catalog. |
108
- | `gemini-3-flash-preview` | Ported from lmctl0 tested-model catalog. |
109
- | `gemini-3.1-flash-lite-preview` | Ported from lmctl0 tested-model catalog. |
110
- | `gemini-2.5-flash` | Ported from lmctl0 tested-model catalog. |
111
- | `gemini-2.5-flash-lite` | Ported from lmctl0 tested-model catalog. |
142
+ **2026-09-24: lint no longer reads a tested-model table here** — gemini
143
+ models resolve against the lmprice catalog (chat rows only; embedding /
144
+ image / tts / realtime rows are excluded by the adapter). Note the
145
+ `gemini` provider requires an API/enterprise Google account; personal
146
+ subscriptions should use `agy`.
112
147
 
113
148
  ## Qwen
114
149
 
115
- | Configured `model=` | Evidence |
116
- | --- | --- |
117
- | `qwen3.5-plus` | Ported from lmctl0 tested-model catalog. |
118
- | `qwen3.6-plus` | Ported from lmctl0 tested-model catalog. |
150
+ **2026-09-24: lint no longer reads a tested-model table here** — qwen
151
+ models resolve against the lmprice catalog (bare, `dashscope/*`, and
152
+ `qwen/` rows). `qwen/qwen3.5-plus` is an established identity whose
153
+ current catalog price is null (known-unpriced — not a warning);
154
+ `qwen/qwen3.6-plus` is deliberately unresolved in the alias table.
119
155
 
120
156
  ## Antigravity
121
157
 
@@ -144,6 +180,68 @@ not maintained going forward.
144
180
  | `Claude Opus 4.6 (Thinking)` | Observed from `agy models` on 2026-07-20. |
145
181
  | `GPT-OSS 120B (Medium)` | Observed from `agy models` on 2026-07-20. |
146
182
 
183
+ **Pricing (2026-09-29, operator decision, math.lmctl findings 002/003):** separate from the lint
184
+ question above ("is this a real agy model," answered live) is "does this resolve in the lmprice
185
+ catalog for cost purposes" — the fleet's largest unpriced population (86 members, 4 base models,
186
+ 11 effort-tier declared strings). Root cause: agy exposes Google's Gemini models under its own
187
+ display-label strings with a reasoning-effort tier baked into the name (`"Gemini 3.8 Flash (High)"`),
188
+ and no upstream catalog source (LiteLLM or OpenRouter) will ever carry that exact string —
189
+ normalizing punctuation can't fix a naming SYSTEM difference, only an explicit, evidenced row can
190
+ (see `backlog/`'s finding-002/003 threads for the full reasoning).
191
+
192
+ **Two layers, discovered while fixing this, not designed up front:** lmprice's own `aliases.json`
193
+ already independently covers 4 of the 11 strings (established evidence, sourced from
194
+ antigravity.google/docs/models, added 2026-09-24 — predates this fix): `gemini 3.8 flash (high)`,
195
+ `gemini 3.8 flash (medium)`, `gemini 3.1 pro (high)`. Those resolve via lmctl's existing alias-table
196
+ step and never touch new code. `src/providers/model-catalog.ts`'s `agyAdapter`/
197
+ `AGY_DISPLAY_TO_IDENTITY` (added here) covers the remaining 7 lmprice hasn't published yet — all of
198
+ 3.7-flash and 3.6-flash had zero coverage from either mechanism before this. Confirmed live via
199
+ `resolveOfferForCost`'s `provenance` field: `"alias-pin"` for the 4 lmprice already has,
200
+ `"route-preference"` (this new table) for the other 7 — no conflict, no double-resolution. Raw
201
+ `agy models` output this table was built from (verified live, 2026-09-29, agy binary on this
202
+ machine):
203
+
204
+ ```
205
+ gemini-3.8-flash-high Gemini 3.8 Flash (High)
206
+ gemini-3.8-flash-medium Gemini 3.8 Flash (Medium)
207
+ gemini-3.8-flash-low Gemini 3.8 Flash (Low)
208
+ gemini-3.7-flash-high Gemini 3.7 Flash (High)
209
+ gemini-3.7-flash-medium Gemini 3.7 Flash (Medium)
210
+ gemini-3.7-flash-low Gemini 3.7 Flash (Low)
211
+ gemini-3.6-flash-high Gemini 3.6 Flash (High)
212
+ gemini-3.6-flash-medium Gemini 3.6 Flash (Medium)
213
+ gemini-3.6-flash-low Gemini 3.6 Flash (Low)
214
+ gemini-3.1-pro-high Gemini 3.1 Pro (High)
215
+ gemini-3.1-pro-low Gemini 3.1 Pro (Low)
216
+ claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking)
217
+ claude-opus-4-6-thinking Claude Opus 4.6 (Thinking)
218
+ gpt-oss-120b-medium GPT-OSS 120B (Medium)
219
+ ```
220
+
221
+ Every effort-tier variant of one base model maps to that base model's own catalog identity —
222
+ reasoning effort/thinking budget changes how many tokens a turn generates, not the per-token rate,
223
+ true of every major vendor's reasoning-effort parameter (OpenAI, Claude, Gemini alike).
224
+
225
+ **agy fronts Claude too, not just Gemini (math.lmctl, 2026-09-29):** 3 members declare `Claude Opus
226
+ 4.6 (Thinking)` on `provider=agy`. `coversRecord`/`AGY_FIRST_PARTY_PROVIDERS` were Gemini-only when
227
+ this was first written -- a real gap for Claude-on-agy, caught before it silently priced at zero.
228
+ Verified per-string, live:
229
+
230
+ - `claude opus 4.6 (thinking)` already had an established display-label alias in lmprice's table
231
+ (like the 4 Gemini ones above, added 2026-09-24) -- resolves via `alias-pin`, never touches
232
+ `AGY_MODEL_TO_IDENTITY` at all. No code change needed for this one specifically.
233
+ - `claude sonnet 4.6 (thinking)` had NO such alias and was a genuine `unknown` -- fixed by adding
234
+ `"claude sonnet 4.6": "claude-sonnet-4-6"` to `AGY_MODEL_TO_IDENTITY`, broadening `coversRecord` to
235
+ `/gemini|claude/`, and adding `"anthropic"` to `AGY_FIRST_PARTY_PROVIDERS` so the bare
236
+ `litellm:claude-sonnet-4-6` (provider `anthropic`) row wins the tie-break over routed variants
237
+ (`azure_ai/`, `deepinfra/`, `vertex_ai/`, `snowflake/`, `perplexity/`, `openrouter/`).
238
+ - `gpt-oss 120b (medium)` remains **deliberately unmapped**: unlike Claude/Gemini, OpenAI does not
239
+ host this open-weight model itself, so there is no single first-party row to prefer among a dozen
240
+ equally-plausible third-party inference providers (groq, fireworks_ai, cerebras, deepinfra, ...).
241
+ Picking one without evidence of which route agy actually uses would be exactly the kind of guess
242
+ this whole explicit-mapping design exists to avoid -- stays `unknown` until there's real evidence
243
+ (e.g. an antigravity.google/docs/models entry, same provenance as the existing aliases).
244
+
147
245
  ## OpenCode
148
246
 
149
247
  opencode model ids use `<provider>/<model>` shape. **lmctl does NOT manage opencode
@@ -182,7 +280,7 @@ does not edit your config.
182
280
  `effort=`/variants: an opencode member's `effort=` maps to opencode's `--variant`
183
281
  (reasoning effort). Declare the variants on the model in your `opencode.json` (see
184
282
  the sample's Claude/GPT entries with `variants: { high, xhigh, max }`); on the ACP
185
- chat path lmctl sends `session/set_config_option` with `configId: "model"` and
283
+ prompt path lmctl sends `session/set_config_option` with `configId: "model"` and
186
284
  `configId: "effort"` before `session/prompt`.
187
285
 
188
286
  For each opencode member with `model=`, `lmctl lint` runs the same ACP
@@ -214,8 +312,7 @@ config-file-YOLO provider). lmctl forces it into full-auto via an ACP protocol
214
312
  call (`session/set_config_option`, `configId: "mode"`, `value: "auto"`) sent
215
313
  after `session/new`/`session/load` and before the first `session/prompt` —
216
314
  fail-closed: if that call is rejected, lmctl throws rather than proceeding, on
217
- both the one-shot transport (`src/providers/transports/acp.ts`) and the
218
- persistent `lmctl serve`/daemon path (`src/serve/persistent-adapters.ts`). See
315
+ the one-shot transport (`src/providers/transports/acp.ts`). See
219
316
  `src/providers/yolo-contract.ts`'s `PROTOCOL_LEVEL_YOLO` set for the contract.
220
317
 
221
318
  Kimi 0.28.x exposes `thinking` through ACP only as an `on`/`off` toggle.
@@ -260,7 +357,7 @@ the value should carry a `provider:` prefix, because without one Hermes
260
357
  has to infer the provider by model-name detection and fails for models it
261
358
  cannot place.
262
359
 
263
- V1 supports ACP seeding and structured chat/dispatch against one effective
360
+ V1 supports ACP seeding and structured prompt/dispatch against one effective
264
361
  `HERMES_HOME`. Existing session IDs default to strict resume. Yolo mode enables
265
362
  both the shell approval bypass and the per-process ACP edit mode; plan mode is
266
363
  rejected. Native deletion, interactive resume, fork, and persistent daemon
@@ -4,6 +4,78 @@ This directory contains public examples that are safe to copy into a project and
4
4
  customize. Teamfiles are intentionally unseeded: run `lmctl lint` first, edit
5
5
  providers or models if needed, then run `lmctl seed` in the target project.
6
6
 
7
+ ## Anonymous cross-provider panel
8
+
9
+ [`anonymous-panel.lms`](anonymous-panel.lms) sends the same question concurrently
10
+ through `parallel([...])` to Codex (`gpt-6-astra`), Agy
11
+ (`Gemini 3.8 Flash (High)`), and Claude (`claude-sonnet-5`), then prints the
12
+ answers as three Markdown table columns. It exits with an error if any call
13
+ fails. Each agent is an inline map; no teamfile or manual seed step is needed.
14
+ The three CLIs must be installed and authenticated with access to these models.
15
+ Provider-native session records are still written by the provider CLIs.
16
+
17
+ Run this checkout from the repository root:
18
+
19
+ ```sh
20
+ node bin/lmctl script examples/anonymous-panel.lms
21
+ ```
22
+
23
+ If source changes have not been compiled, run `npm run build` first. With this
24
+ build installed as `lmctl`, the equivalent command is
25
+ `lmctl script examples/anonymous-panel.lms`.
26
+
27
+ The opt-in live test sends a unique challenge to a real Codex agent in an empty
28
+ temporary workspace and asserts the exact reply, a native session ID, and that
29
+ **no workspace file was created**, including any teamfile. It does not claim
30
+ that provider-native session storage outside the workspace is untouched.
31
+
32
+ ```sh
33
+ RUN_AGENT_LIVE_E2E=1 npx vitest run tests/e2e/script-agent-live.test.ts
34
+ ```
35
+
36
+ Optional `LMCTL_AGENT_LIVE_PROVIDER` and `LMCTL_AGENT_LIVE_MODEL` select a different
37
+ provider/model pair. The test uses a 120-second provider idle timeout and no
38
+ wall-clock dispatch cutoff. It is skipped in ordinary hermetic test runs.
39
+
40
+ ## Return an operator decision to a script
41
+
42
+ After an agent turn, pass its map to `prompt_human`:
43
+
44
+ ```lms
45
+ agent = { provider: "codex" };
46
+ result = lm_agent(agent, "Review this repository and suggest the next step");
47
+ decision = prompt_human(agent);
48
+ if (decision.command == "retry") {
49
+ result = lm_agent(agent, join(decision.args, " "));
50
+ }
51
+ ```
52
+
53
+ The operator sees the script's conversation and can send more messages to that
54
+ same native session. Enter `/exit retry check the tests` to return
55
+ `{command: "retry", args: ["check", "the", "tests"]}` to the script. Arguments
56
+ are separated by whitespace. `/exit` returns command `exit`; closed input
57
+ returns `eof`. Provider errors return `error` and stalls return `stalled`, with
58
+ details in `args`. The simple `prompt_human("Question? ")` form still returns a
59
+ string, or null on closed input.
60
+
61
+ ## Batch turns from a member value
62
+
63
+ The member-map form needs no teamfile or prompt-file name:
64
+
65
+ ```lms
66
+ member = { provider: "codex" };
67
+ handle = lm_batch_open(member, { permissionMode: "plan", idleTimeoutMs: 120000 });
68
+ first = lm_batch_turn(handle, "Describe the problem", 1, 2);
69
+ second = lm_batch_turn(handle, "Suggest a solution", 2, 2);
70
+ lm_batch_close(handle);
71
+ ```
72
+
73
+ It retains the native session between turns and stops after a failed turn.
74
+ This form uses sequential agent calls. The existing
75
+ `lm_batch_open(teamfile, alias, batchfile, options)` form retains managed-team
76
+ logging, locking, and bundle optimization; its batchfile argument is a tracking
77
+ label, while `lm_batch_turn` supplies each actual prompt.
78
+
7
79
  ## Teamfile Gallery
8
80
 
9
81
  - [`research-team.lmctl`](research-team.lmctl) - fan out a question to multiple
@@ -21,5 +93,36 @@ lmctl lint examples/book-team.lmctl
21
93
  lmctl lint examples/review-team.lmctl
22
94
  ```
23
95
 
96
+ ## CLI Connectors & Process-Boundary Integration
97
+
98
+ Connectors in LMScript are CLIs executed across process boundaries rather than in-process plugins. Because LLM reasoning dominates execution latency, invoking CLIs via `system()` provides language-agnostic, stateless, crash-isolated integration with negligible overhead.
99
+
100
+ ### Best Practices for CLI Connectors
101
+ 1. **Use Argv Arrays**: Prefer `system(["tool", "arg1", "arg2"])` over shell command strings. The argv form executes directly with `shell: false`, eliminating shell quoting issues and accidental pipe (`|`) interpretation.
102
+ 2. **Inspect Process Records & Output**: `system()` returns a defined record `{ stdout, stderr, code, signal, timed_out, duration_ms, pid }`. Check `result.code` and inspect `result.stderr` or application payloads. `system()` is strictly a completion primitive (always awaited); `code: null` strictly indicates abnormal termination (via signal or timeout), never a running or detached process. Because utilities often exit with code 0 even when reporting errors (e.g. HTTP 404 or batch errors), caller predicates in user space (e.g. `fn curl_ok(r)`) should verify stderr and application status rather than assuming exit code 0 implies success.
103
+ 3. **Structured JSON Envelopes**: When integrating CLIs that output data, prefer returning typed JSON envelopes (with explicit `kind`, `schema`, and `status` fields) parsed with `json(out)`. Distinguishing summary records from match records prevents silent attribute mismatches.
104
+
24
105
  `opencode.json` is a sample OpenCode model catalog. Copy and adjust it only if
25
106
  your teamfile uses `provider=opencode`.
107
+
108
+
109
+ ## Test-only batch comparison
110
+
111
+ The production `lmctl batch` command retains its procedural implementation.
112
+ LMScript adds capability; it does not replace existing commands. The experimental
113
+ comparison path is:
114
+
115
+ ```sh
116
+ lmctl batch team.lmctl Lead prompts.txt --legacy
117
+ lmctl script examples/batchscript.lms team.lmctl Lead prompts.txt --legacy
118
+ ```
119
+
120
+ Both execute prompts, so use separate fixture sessions for a live comparison.
121
+ The hermetic `tests/cli/batch.test.ts` and `batch-bundle.test.ts` suites run the
122
+ same assertions against both paths. `batch_independence.test.ts` pins production
123
+ independence and equality of this file with the embedded `BATCH_LMS` asset.
124
+
125
+ ## Security & Execution Model
126
+
127
+ LMScript executes with full operator authority without sandboxing, equivalent to running an administrative shell or Python script. File operations (`read_file`, `write_file`) and shell invocations (`system()`) run with the credentials and local environment of the invoking operator. Do not execute scripts from untrusted origins.
128
+
@@ -0,0 +1,3 @@
1
+ question = "What is one useful way to check an AI answer? Answer in one sentence under 70 characters. Do not use tools or create files.";
2
+ answers = parallel([lm_agent({ provider: "codex", model: "gpt-6-astra" }, question, { idleTimeoutMs: 120000 }), lm_agent({ provider: "agy", model: "Gemini 3.8 Flash (High)" }, question, { idleTimeoutMs: 120000 }), lm_agent({ provider: "claude", model: "claude-sonnet-5" }, question, { idleTimeoutMs: 120000 })]);
3
+ for (i = 0; i < len(answers); i = i + 1) { answer = answers[i]; if (answer.status != "ok") { lm_error(answer.status, answer.error); lm_exit(1); } } print(format("| Codex / gpt-6-astra | Agy / Gemini 3.8 Flash (High) | Claude / claude-sonnet-5 |\n| --- | --- | --- |\n| {} | {} | {} |", replace(replace(trim(answers[0].reply), "\n", " "), "|", "/"), replace(replace(trim(answers[1].reply), "\n", " "), "|", "/"), replace(replace(trim(answers[2].reply), "\n", " "), "|", "/")));
@@ -0,0 +1,9 @@
1
+ This is a conversation-only batch test. Do not use tools or modify any files during this conversation. Remember the token cedar-731 for later turns. Set a running total to 3. Reply exactly: cedar-731 3
2
+
3
+ Add 100 to the running total. Reply with the remembered token, one space, and the new total only.
4
+
5
+ Multiply the running total by 2. Reply with the remembered token, one space, and the new total only.
6
+
7
+ Subtract 6 from the running total. Reply with the remembered token, one space, and the new total only.
8
+
9
+ What was the token I asked you to remember in the first prompt? Reply with only that token.
@@ -0,0 +1,67 @@
1
+ # batchscript.lms - Test-only comparison with procedural lmctl batch
2
+ # Arguments: [teamfile, alias, batchfile, flags...]
3
+
4
+ if (includes(args, "--help") || includes(args, "-h")) {
5
+ println("usage: lmctl batch <teamfile> <alias> <batchfile> [--legacy] [--single]");
6
+ lm_exit(0);
7
+ }
8
+
9
+ if (len(args) < 3) {
10
+ lm_error("usage: lmctl batch <teamfile> <alias> <batchfile> [--legacy] [--single]");
11
+ lm_exit(1);
12
+ }
13
+
14
+ teamfile = args[0];
15
+ alias = args[1];
16
+ batchfile = args[2];
17
+ legacy = false;
18
+ single = false;
19
+ for (flag_index = 3; flag_index < len(args); flag_index = flag_index + 1) {
20
+ if (args[flag_index] == "--legacy") {
21
+ legacy = true;
22
+ } else if (args[flag_index] == "--single") {
23
+ single = true;
24
+ } else {
25
+ lm_error("batch: unexpected argument: " + args[flag_index]);
26
+ lm_exit(1);
27
+ }
28
+ }
29
+
30
+ # 1. Read prompts
31
+ prompts = lm_read_prompts(batchfile, single);
32
+
33
+ if (len(prompts) == 0) {
34
+ lm_error("batch: " + batchfile + ": no prompts found");
35
+ lm_exit(1);
36
+ }
37
+
38
+ # 2. Open batch session
39
+ handle = lm_batch_open(teamfile, alias, batchfile, {
40
+ promptCount: len(prompts),
41
+ legacy: legacy,
42
+ single: single
43
+ });
44
+
45
+ if (handle == null) {
46
+ lm_exit(1);
47
+ }
48
+
49
+ # 3. Sequential prompt execution
50
+ for (i = 0; i < len(prompts); i = i + 1) {
51
+ promptText = prompts[i];
52
+ res = lm_batch_turn(handle, promptText, i + 1, len(prompts));
53
+
54
+ # Output reply or partial reply on stall
55
+ if (res.reply != null && len(res.reply) > 0) {
56
+ print(res.reply);
57
+ print(""); # Blank line separator
58
+ }
59
+
60
+ if (res.status != "ok") {
61
+ lm_batch_close(handle, "error", res.error);
62
+ lm_exit(1);
63
+ }
64
+ }
65
+
66
+ lm_batch_close(handle, "done");
67
+ lm_exit(0);
@@ -0,0 +1,56 @@
1
+ # agent-lib.lms -- shared user-space helpers for driving agents from LMScript.
2
+ # User-defined, not engine: primitives stay small and stable while user functions carry the size.
3
+
4
+ # The state line is the fleet's contract: every agent turn ends with one of four words.
5
+ # A reply whose last non-blank line is prose is NOT a pass -- it is NO_STATE_LINE, and that is a
6
+ # finding in itself. Never let a missing state line read as success.
7
+ fn state_of(text) {
8
+ if (text == null) { return "NO_REPLY"; }
9
+ raw = text;
10
+ if (type(text) == "object" && has_key(text, "reply")) { raw = text.reply; }
11
+ if (raw == null) { return "NO_REPLY"; }
12
+ lines = split(trim(raw), "\n");
13
+ i = len(lines) - 1;
14
+ while (i >= 0) {
15
+ l = trim(lines[i]);
16
+ i = i - 1;
17
+ if (len(l) == 0) { continue; }
18
+ if (l == "DONE" || l == "NOTHING_TO_DO" || l == "BLOCKED" || l == "OTHER") { return l; }
19
+ return "NO_STATE_LINE";
20
+ }
21
+ return "NO_STATE_LINE";
22
+ }
23
+
24
+ fn needs_human(st) {
25
+ return st == "OTHER" || st == "BLOCKED" || st == "NO_STATE_LINE" || st == "NO_REPLY";
26
+ }
27
+
28
+ # ATTENDED automation's handoff. GAP: a script cannot raise a TUI or hand over control, so printing
29
+ # and stopping is the honest fallback -- never continue past a handoff.
30
+ fn handoff(context, st, reply) {
31
+ print("");
32
+ print("=== HUMAN NEEDED ================================================");
33
+ print(" " + context);
34
+ print(" state " + st);
35
+ if (reply != null) {
36
+ raw = reply;
37
+ if (type(reply) == "object" && has_key(reply, "reply")) { raw = reply.reply; }
38
+ lines = split(trim(raw), "\n");
39
+ start = len(lines) - 20;
40
+ if (start < 0) { start = 0; }
41
+ j = start;
42
+ print(" tail of the reply:");
43
+ while (j < len(lines)) { print(" " + lines[j]); j = j + 1; }
44
+ }
45
+ print("=================================================================");
46
+ }
47
+
48
+ # Run one pipeline stage. Returns the state word; the caller decides what it means.
49
+ fn run_stage(teamfile, alias, stage_name, prompt_text) {
50
+ print(" [" + stage_name + "] ...");
51
+ reply = lm_prompt(teamfile, alias, prompt_text, {});
52
+ st = state_of(reply);
53
+ print(" [" + stage_name + "] " + st);
54
+ if (needs_human(st)) { handoff(stage_name + " on " + teamfile + ":" + alias, st, reply); }
55
+ return [st, reply];
56
+ }
@@ -0,0 +1,93 @@
1
+ # git-lib.lms -- user-defined wrappers around git, in LMScript.
2
+ #
3
+ # No engine module for git: these are ordinary user functions over system(), and any script can
4
+ # import them. The engine owns lmctl's own internals as host functions; everything external is
5
+ # user space -- the same pattern applies to gh, aws, or any other CLI a script wants to wrap.
6
+
7
+ # NOTE: system() goes through a SHELL and there is no quoting helper, so any subcmd containing
8
+ # | & ; < > $ ` must be quoted by the caller. The failure mode is an EMPTY result, not an error --
9
+ # `--pretty=format:%h|%s` silently pipes into `%s` and returns "".
10
+ # NOTE: system() returns [stdout, stderr] and NO exit status, so these wrappers cannot distinguish
11
+ # "command failed" from "command succeeded with no output".
12
+ fn git_run(repo, subcmd) {
13
+ out = system(format("git -C {0} {1} 2>/dev/null", repo, subcmd));
14
+ return trim(out.stdout);
15
+ }
16
+
17
+ fn is_git_repo(path) {
18
+ if (!file_exists(path)) { return false; }
19
+ return git_run(path, "rev-parse --is-inside-work-tree") == "true";
20
+ }
21
+
22
+ fn git_head(repo) {
23
+ return git_run(repo, "log -1 --format=%H");
24
+ }
25
+
26
+ fn git_head_short(repo) {
27
+ return git_run(repo, "log -1 --pretty=format:%h|%s"); # no space: git_run does not quote
28
+ }
29
+
30
+ fn git_head_epoch(repo) {
31
+ s = git_run(repo, "log -1 --format=%ct");
32
+ if (len(s) == 0) { return 0; }
33
+ return num(s);
34
+ }
35
+
36
+ # Changed = tracked-but-modified. Untracked = new and unignored. Kept separate on purpose:
37
+ # conflating them is how "the tree is dirty" stops telling you WHICH kind of dirty.
38
+ fn git_changed(repo) {
39
+ lines = split(git_run(repo, "status --porcelain"), "\n");
40
+ changed = [];
41
+ i = 0;
42
+ while (i < len(lines)) {
43
+ l = lines[i]; i = i + 1;
44
+ if (len(trim(l)) == 0) { continue; }
45
+ if (!starts_with(l, "??")) { push(changed, trim(substring(l, 2))); }
46
+ }
47
+ return changed;
48
+ }
49
+
50
+ fn git_untracked(repo) {
51
+ lines = split(git_run(repo, "status --porcelain"), "\n");
52
+ untracked = [];
53
+ i = 0;
54
+ while (i < len(lines)) {
55
+ l = lines[i]; i = i + 1;
56
+ if (starts_with(l, "??")) { push(untracked, trim(substring(l, 2))); }
57
+ }
58
+ return untracked;
59
+ }
60
+
61
+ fn git_unpushed(repo) {
62
+ # No upstream is NOT an error -- a worktree branch deliberately has none.
63
+ # NOTE: "@{u}" cannot go through format() -- "{u}" is read as a format placeholder.
64
+ up = system("git -C " + repo + " rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null");
65
+ if (len(trim(up.stdout)) == 0) { return -1; } # -1 = no upstream, distinct from 0 = nothing to push
66
+ n = trim(system("git -C " + repo + " rev-list --count @{u}..HEAD 2>/dev/null").stdout);
67
+ if (len(n) == 0) { return -1; }
68
+ return num(n);
69
+ }
70
+
71
+ # "Are these changes AFTER some LLM process?" -- compare the working tree's newest mtime against a
72
+ # marker time (e.g. when an agent turn started).
73
+ fn touched_since(repo, epoch) {
74
+ out = system(format("git -C {0} status --porcelain", repo));
75
+ lines = split(trim(out.stdout), "\n");
76
+ recent = [];
77
+ i = 0;
78
+ while (i < len(lines)) {
79
+ l = trim(lines[i]); i = i + 1;
80
+ if (len(l) == 0) { continue; }
81
+ parts = split(l, " ");
82
+ f = parts[len(parts) - 1];
83
+ st = system(format("stat -c %Y {0}/{1} 2>/dev/null", repo, f));
84
+ m = trim(st.stdout);
85
+ if (len(m) > 0 && num(m) >= epoch) { push(recent, f); }
86
+ }
87
+ return recent;
88
+ }
89
+
90
+ # Did an agent actually do anything? The question behind every "it says DONE" report.
91
+ fn agent_made_progress(repo, since_epoch) {
92
+ return len(touched_since(repo, since_epoch)) > 0 || git_head_epoch(repo) >= since_epoch;
93
+ }
Binary file