@lmctl-ai/lmctl 0.1.342 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/README.md +11 -51
- package/data/price-aliases.json +1 -0
- package/data/price-data.json +1 -1
- package/dist/cli/batch.d.ts +2 -0
- package/dist/cli/busy-evidence.d.ts +27 -0
- package/dist/cli/chat-cycle.d.ts +59 -0
- package/dist/cli/chat-exec.d.ts +83 -0
- package/dist/cli/chat-policy.d.ts +22 -0
- package/dist/cli/chat.d.ts +23 -0
- package/dist/cli/code-resolver.d.ts +8 -0
- package/dist/cli/common.d.ts +54 -0
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/cost.d.ts +2 -0
- package/dist/cli/export.d.ts +2 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +1062 -1400
- package/dist/cli/infra.d.ts +78 -0
- package/dist/cli/ls.d.ts +2 -0
- package/dist/cli/mailbox-cli.d.ts +6 -0
- package/dist/cli/managed.d.ts +62 -0
- package/dist/cli/mcp-install.d.ts +23 -0
- package/dist/cli/mcp.d.ts +2 -0
- package/dist/cli/paths.d.ts +50 -0
- package/dist/cli/plan-wizard.d.ts +42 -0
- package/dist/cli/plan.d.ts +20 -0
- package/dist/cli/queue-delivery.d.ts +107 -0
- package/dist/cli/ratelimit.d.ts +3 -0
- package/dist/cli/remote-fetch.d.ts +7 -0
- package/dist/cli/run-queries.d.ts +82 -0
- package/dist/cli/running.d.ts +5 -0
- package/dist/cli/schema.sql +20 -1
- package/dist/cli/script.d.ts +6 -0
- package/dist/cli/script.js +2097 -0
- package/dist/cli/self-identity.d.ts +25 -0
- package/dist/cli/session-idle.d.ts +118 -0
- package/dist/cli/status.d.ts +22 -0
- package/dist/cli/tail.d.ts +18 -0
- package/dist/cli/target-resolver.d.ts +139 -0
- package/dist/cli/team-status.d.ts +91 -0
- package/dist/cli/teamfile-compat.d.ts +71 -0
- package/dist/cli/teamfile-store.d.ts +18 -0
- package/dist/cli/teamfile-template.d.ts +40 -0
- package/dist/cli/template-env.d.ts +25 -0
- package/dist/cli/terminal-lock.d.ts +25 -0
- package/dist/cli/terminal.d.ts +23 -0
- package/dist/cli/time-filter.d.ts +10 -0
- package/dist/cli/update-notifier.d.ts +11 -0
- package/dist/cli/version-util.d.ts +2 -0
- package/dist/config/seed_templates.d.ts +1 -0
- package/dist/db/backfill-event-log.d.ts +26 -0
- package/dist/db/delegation.d.ts +37 -0
- package/dist/db/escalation_vocab.d.ts +45 -0
- package/dist/db/event-log.d.ts +40 -0
- package/dist/db/index.d.ts +1213 -0
- package/dist/db/inflight-acquire.d.ts +70 -0
- package/dist/db/invocation-event-projections.d.ts +65 -0
- package/dist/db/ledger-identity.test.d.ts +1 -0
- package/dist/db/live-event-projections.d.ts +92 -0
- package/dist/db/migrate.d.ts +51 -0
- package/dist/db/open-readonly.d.ts +2 -0
- package/dist/db/process-liveness.d.ts +37 -0
- package/dist/db/sqlite.d.ts +47 -0
- package/dist/db/sqlite.test.d.ts +1 -0
- package/dist/db/terminal-lock.d.ts +14 -0
- package/dist/db/tracked-invocation-reconcile.d.ts +52 -0
- package/dist/db/tracked-invocation.d.ts +116 -0
- package/dist/mcp/dispatch.d.ts +45 -0
- package/dist/mcp/mediation.d.ts +128 -0
- package/dist/mcp/path-warning.d.ts +7 -0
- package/dist/mcp/server.d.ts +63 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/providers/adapter.d.ts +265 -0
- package/dist/providers/adapters/agy-adapter.d.ts +17 -0
- package/dist/providers/adapters/claude-adapter.d.ts +20 -0
- package/dist/providers/adapters/codex-adapter.d.ts +22 -0
- package/dist/providers/adapters/copilot-adapter.d.ts +16 -0
- package/dist/providers/adapters/dsh-adapter.d.ts +89 -0
- package/dist/providers/adapters/gemini-adapter.d.ts +44 -0
- package/dist/providers/adapters/hermes-adapter.d.ts +71 -0
- package/dist/providers/adapters/index.d.ts +35 -0
- package/dist/providers/adapters/kimi-adapter.d.ts +16 -0
- package/dist/providers/adapters/lmplayer-adapter.d.ts +14 -0
- package/dist/providers/adapters/opencode-adapter.d.ts +11 -0
- package/dist/providers/adapters/pi-adapter.d.ts +17 -0
- package/dist/providers/adapters/print-turn.d.ts +29 -0
- package/dist/providers/adapters/qwen-adapter.d.ts +11 -0
- package/dist/providers/agy.d.ts +32 -0
- package/dist/providers/base-adapter.d.ts +107 -0
- package/dist/providers/base.d.ts +12 -0
- package/dist/providers/binary-check.d.ts +102 -0
- package/dist/providers/bundle.d.ts +118 -0
- package/dist/providers/claude-mock.d.ts +90 -0
- package/dist/providers/claude.d.ts +21 -0
- package/dist/providers/codex-config.d.ts +15 -0
- package/dist/providers/codex.d.ts +25 -0
- package/dist/providers/config/model-selection-lint.d.ts +45 -0
- package/dist/providers/config/team-config.d.ts +183 -0
- package/dist/providers/config/types.d.ts +146 -0
- package/dist/providers/config/user-config.d.ts +88 -0
- package/dist/providers/copilot.d.ts +21 -0
- package/dist/providers/debug.d.ts +128 -0
- package/dist/providers/dsh-sidecar.d.ts +65 -0
- package/dist/providers/dsh-zstd.d.ts +27 -0
- package/dist/providers/dsh.d.ts +37 -0
- package/dist/providers/frontmatter.d.ts +46 -0
- package/dist/providers/gemini-session-reader.d.ts +151 -0
- package/dist/providers/gemini.d.ts +39 -0
- package/dist/providers/hermes.d.ts +90 -0
- package/dist/providers/index.d.ts +421 -0
- package/dist/providers/kimi.d.ts +37 -0
- package/dist/providers/model-catalog.d.ts +141 -0
- package/dist/providers/one-shot-timeout.d.ts +1 -0
- package/dist/providers/opencode-config.d.ts +62 -0
- package/dist/providers/opencode.d.ts +40 -0
- package/dist/providers/parsers/acp.d.ts +47 -0
- package/dist/providers/parsers/codex-app-server.d.ts +60 -0
- package/dist/providers/parsers/stream-json.d.ts +57 -0
- package/dist/providers/passive-readers/agy.d.ts +155 -0
- package/dist/providers/passive-readers/copilot.d.ts +173 -0
- package/dist/providers/passive-readers/dsh.d.ts +34 -0
- package/dist/providers/passive-readers/kimi.d.ts +44 -0
- package/dist/providers/passive-readers/opencode.d.ts +89 -0
- package/dist/providers/passive-readers/qwen.d.ts +151 -0
- package/dist/providers/passive-readers/shared.d.ts +138 -0
- package/dist/providers/passive-session-reader.d.ts +188 -0
- package/dist/providers/path-utils.d.ts +1 -0
- package/dist/providers/pi.d.ts +32 -0
- package/dist/providers/pricing.d.ts +107 -0
- package/dist/providers/qwen.d.ts +22 -0
- package/dist/providers/ratelimit-readings.d.ts +32 -0
- package/dist/providers/registry.d.ts +31 -0
- package/dist/providers/resume.d.ts +43 -0
- package/dist/providers/seed-instruction.d.ts +3 -0
- package/dist/providers/self-session-env.d.ts +21 -0
- package/dist/providers/session-limit.d.ts +9 -0
- package/dist/providers/strip-ansi.d.ts +5 -0
- package/dist/providers/transport-errors.d.ts +94 -0
- package/dist/providers/transport-lifecycle.d.ts +63 -0
- package/dist/providers/transport-wrappers.d.ts +110 -0
- package/dist/providers/transports/acp-profiles/config-option-step.d.ts +24 -0
- package/dist/providers/transports/acp-profiles/copilot.d.ts +12 -0
- package/dist/providers/transports/acp-profiles/gemini.d.ts +22 -0
- package/dist/providers/transports/acp-profiles/hermes.d.ts +27 -0
- package/dist/providers/transports/acp-profiles/index.d.ts +19 -0
- package/dist/providers/transports/acp-profiles/kimi.d.ts +24 -0
- package/dist/providers/transports/acp-profiles/opencode.d.ts +26 -0
- package/dist/providers/transports/acp-profiles/types.d.ts +258 -0
- package/dist/providers/transports/acp-rpc.d.ts +18 -0
- package/dist/providers/transports/acp.d.ts +215 -0
- package/dist/providers/transports/codex-app-server.d.ts +190 -0
- package/dist/providers/transports/hold-open.d.ts +120 -0
- package/dist/providers/transports/manager.d.ts +21 -0
- package/dist/providers/transports/stream-json.d.ts +162 -0
- package/dist/providers/transports/types.d.ts +56 -0
- package/dist/providers/types.d.ts +154 -0
- package/dist/providers/yolo-contract.d.ts +131 -0
- package/dist/script/adapter.d.ts +101 -0
- package/dist/script/batch-script.d.ts +7 -0
- package/dist/script/index.d.ts +3 -0
- package/dist/script/lmscript.d.ts +550 -0
- package/dist/shared/policy.d.ts +74 -0
- package/dist/shared-lib/db.d.ts +25 -0
- package/dist/shared-lib/passive-session-reader.d.ts +2 -0
- package/dist/shared-lib/provider-parse.d.ts +6 -0
- package/dist/templates/basedir.d.ts +52 -0
- package/dist/uri/lmctl_uri.d.ts +38 -0
- package/durable-memory/model_selection.md +108 -25
- package/examples/README.md +103 -0
- package/examples/anonymous-panel.lms +3 -0
- package/examples/batch-prompts.txt +9 -0
- package/examples/batchscript.lms +67 -0
- package/examples/n8n/agent-lib.lms +56 -0
- package/examples/n8n/git-lib.lms +93 -0
- package/examples/n8n/mock-support.db +0 -0
- package/examples/n8n/multi-agent-review.lms +107 -0
- package/examples/n8n/news-digest.lms +159 -0
- package/examples/n8n/rag-search.lms +163 -0
- package/examples/n8n/support-agent.lms +88 -0
- package/examples/n8n/ticket-triage.lms +156 -0
- package/package.json +13 -17
- 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>`
|
|
8
|
-
|
|
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.
|
|
38
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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,54 @@ 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). **Not yet
|
|
224
|
+
mapped:** the 3 non-Gemini agy models (`Claude Sonnet 4.6 (Thinking)`, `Claude Opus 4.6 (Thinking)`,
|
|
225
|
+
`GPT-OSS 120B (Medium)`) — smaller population, deferred, not the highest-value row. `claude opus 4.6
|
|
226
|
+
(thinking)` already has an established display-label alias in lmprice's table (like the 4 Gemini
|
|
227
|
+
ones above); `claude sonnet 4.6 (thinking)` and `gpt-oss 120b (medium)` don't — lmprice's alias table
|
|
228
|
+
has a DASHED-slug entry for claude-sonnet-4-6 but under a spelling (`agy/claude-sonnet-4-6`) that
|
|
229
|
+
doesn't match what agy actually declares (`Claude Sonnet 4.6 (Thinking)`), so it's a live gap too.
|
|
230
|
+
|
|
147
231
|
## OpenCode
|
|
148
232
|
|
|
149
233
|
opencode model ids use `<provider>/<model>` shape. **lmctl does NOT manage opencode
|
|
@@ -182,7 +266,7 @@ does not edit your config.
|
|
|
182
266
|
`effort=`/variants: an opencode member's `effort=` maps to opencode's `--variant`
|
|
183
267
|
(reasoning effort). Declare the variants on the model in your `opencode.json` (see
|
|
184
268
|
the sample's Claude/GPT entries with `variants: { high, xhigh, max }`); on the ACP
|
|
185
|
-
|
|
269
|
+
prompt path lmctl sends `session/set_config_option` with `configId: "model"` and
|
|
186
270
|
`configId: "effort"` before `session/prompt`.
|
|
187
271
|
|
|
188
272
|
For each opencode member with `model=`, `lmctl lint` runs the same ACP
|
|
@@ -214,8 +298,7 @@ config-file-YOLO provider). lmctl forces it into full-auto via an ACP protocol
|
|
|
214
298
|
call (`session/set_config_option`, `configId: "mode"`, `value: "auto"`) sent
|
|
215
299
|
after `session/new`/`session/load` and before the first `session/prompt` —
|
|
216
300
|
fail-closed: if that call is rejected, lmctl throws rather than proceeding, on
|
|
217
|
-
|
|
218
|
-
persistent `lmctl serve`/daemon path (`src/serve/persistent-adapters.ts`). See
|
|
301
|
+
the one-shot transport (`src/providers/transports/acp.ts`). See
|
|
219
302
|
`src/providers/yolo-contract.ts`'s `PROTOCOL_LEVEL_YOLO` set for the contract.
|
|
220
303
|
|
|
221
304
|
Kimi 0.28.x exposes `thinking` through ACP only as an `on`/`off` toggle.
|
|
@@ -260,7 +343,7 @@ the value should carry a `provider:` prefix, because without one Hermes
|
|
|
260
343
|
has to infer the provider by model-name detection and fails for models it
|
|
261
344
|
cannot place.
|
|
262
345
|
|
|
263
|
-
V1 supports ACP seeding and structured
|
|
346
|
+
V1 supports ACP seeding and structured prompt/dispatch against one effective
|
|
264
347
|
`HERMES_HOME`. Existing session IDs default to strict resume. Yolo mode enables
|
|
265
348
|
both the shell approval bypass and the per-process ACP edit mode; plan mode is
|
|
266
349
|
rejected. Native deletion, interactive resume, fork, and persistent daemon
|
package/examples/README.md
CHANGED
|
@@ -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
|