@agentionai/fieldwork-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/dist/changelog.js +24 -48
- package/dist/client.js +16 -0
- package/dist/fieldwork-skill.md +33 -5
- package/dist/main.js +65 -13
- package/dist/version.js +50 -0
- package/dist/workspace.js +4 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,19 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
Requires Node.js **22+** and npm. Version **0.7.0** is
|
|
7
|
+
Requires Node.js **22+** and npm. Version **0.7.0** is published on npm:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install --global
|
|
10
|
+
npm install --global @agentionai/fieldwork-cli@0.7.0
|
|
11
11
|
fieldwork --version
|
|
12
12
|
fieldwork --help
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```sh
|
|
18
|
-
npm install --global @agentionai/fieldwork-cli@0.7.0
|
|
19
|
-
```
|
|
15
|
+
A locally built archive installs the same way: `npm install --global ./agentionai-fieldwork-cli-0.7.0.tgz`.
|
|
20
16
|
|
|
21
17
|
Pin the version. The npm registry currently holds 0.3.0 and 0.4.0, which predate credentials and paged responses and cannot use the hosted service, so an unpinned install gets a client that fails against it.
|
|
22
18
|
|
package/dist/changelog.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
export const fullHistory = 'docs/cli-changelog.md in the Fieldwork repository';
|
|
2
2
|
export const entries = [
|
|
3
|
+
{
|
|
4
|
+
version: '0.8.0',
|
|
5
|
+
date: '2026-09-26',
|
|
6
|
+
status: 'prepared',
|
|
7
|
+
changes: [
|
|
8
|
+
{
|
|
9
|
+
summary: "Every request names the CLI's version. When the server says a newer release exists, a command that works prints one UPDATE_AVAILABLE notice on stderr, at most once a day per server (FIELDWORK_NO_UPDATE_NOTICE=1 silences it); a failure's error carries an update object, and says to update first when this version is too old.",
|
|
10
|
+
requiresApi: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
summary: "A refusal's own hint from the server -- over a limit, a plan limit, a deletion needing confirmation -- is shown as the hint. Help says Fieldwork is a ledger of experiments, not a log store.",
|
|
14
|
+
requiresApi: false,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
summary: 'runs backfill REF fills in a parameter or comparison-context field a run left empty, finished runs included, once: never over a recorded value, never observations. schemas extend backfill fills forgotten fields too.',
|
|
18
|
+
requiresApi: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
summary: 'products, campaigns and experiments gain archive and restore; experiments list --include-archived; stubs of archived records still resolve. delete takes --confirm NAME when the unit holds work, which is deleted with it.',
|
|
22
|
+
requiresApi: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
3
26
|
{
|
|
4
27
|
version: '0.7.0',
|
|
5
28
|
date: '2026-09-22',
|
|
6
|
-
status: '
|
|
29
|
+
status: 'published',
|
|
7
30
|
changes: [
|
|
8
31
|
{
|
|
9
32
|
summary: 'The default server is the hosted service, https://app.fieldworkledger.com, instead of http://127.0.0.1:4310. A local server needs --url or FIELDWORK_URL; a workspace set up against one keeps using it.',
|
|
@@ -46,53 +69,6 @@ export const entries = [
|
|
|
46
69
|
},
|
|
47
70
|
],
|
|
48
71
|
},
|
|
49
|
-
{
|
|
50
|
-
version: '0.5.0',
|
|
51
|
-
date: '2026-09-18',
|
|
52
|
-
status: 'prepared',
|
|
53
|
-
changes: [
|
|
54
|
-
{
|
|
55
|
-
summary: 'runs list and experiments list select and project rows: --where FIELD=VALUE (also != >= <= > <, repeatable), --fields PATHS, --format tsv, and runs list --experiment REF. Selection and projection only; no values are computed.',
|
|
56
|
-
requiresApi: false,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
summary: 'runs list and experiments list exclude superseded records (extras.superseded_by) and work under abandoned experiments by default; --include-superseded and --include-abandoned restore them. This changes the default output of an existing command.',
|
|
60
|
-
requiresApi: false,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
summary: 'fieldwork changelog reports recent releases offline, marking which changes need an updated API.',
|
|
64
|
-
requiresApi: false,
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
summary: 'Network failures, timeouts and invalid server responses have distinct error codes and retry guidance; API failures retain the server request ID.',
|
|
68
|
-
requiresApi: false,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
summary: 'runs record REF applies observations, extras, comparison context, artifacts, a status, an error summary or a logs URI in one call; the server reads the current revision, a result may be recorded directly from planned, and an identical revisionless retry reads back the committed result.',
|
|
72
|
-
requiresApi: true,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
summary: 'runs create accepts status, stub, startedAt, finishedAt and errorSummary, so a finished or historical run is a single call. Dates are order-checked and never invented.',
|
|
76
|
-
requiresApi: true,
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
summary: 'Schema fields support type ref with optional refKind recipe or file, validated against the product artifact registry. A recorded ref links and freezes its artifact, and works as a bar X axis and groupBy key.',
|
|
80
|
-
requiresApi: true,
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
summary: 'Charts admit runs pinned to other versions of the same schema when every charted field keeps its value kind, unit and direction; schemaVersions "pinned" opts out, and charts data reports each version with its verdict.',
|
|
84
|
-
requiresApi: true,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
summary: 'charts data leaves out runs marked extras.superseded_by, reporting each under excluded with the replacement named, so a pooled run can no longer be averaged in beside the per-set runs that replaced it and understate the spread.',
|
|
88
|
-
requiresApi: true,
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
summary: 'Experiment and run responses repeat schemaVersionId, varying, observations, comparisonContext, extras and artifactIds at the top level; research remains canonical.',
|
|
92
|
-
requiresApi: true,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
72
|
];
|
|
97
73
|
function order(version) {
|
|
98
74
|
return version.split('.').map(Number);
|
package/dist/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLI_VERSION, noticeUpdate, updateAdvice } from './version.js';
|
|
1
2
|
const retryHint = (method) => method === 'GET'
|
|
2
3
|
? 'This read is safe to retry.'
|
|
3
4
|
: 'The server may have committed this write. Inspect the target record before retrying.';
|
|
@@ -8,6 +9,8 @@ export async function request(baseUrl, path, method = 'GET', body, credentials =
|
|
|
8
9
|
response = await fetch(`${origin}/api/v1${path}`, {
|
|
9
10
|
method,
|
|
10
11
|
headers: {
|
|
12
|
+
// Which client this is, so the server can say when a newer one exists.
|
|
13
|
+
'X-Fieldwork-Client': `fieldwork-cli/${CLI_VERSION}`,
|
|
11
14
|
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
12
15
|
...(credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {}),
|
|
13
16
|
...(credentials.organization
|
|
@@ -32,6 +35,9 @@ export async function request(baseUrl, path, method = 'GET', body, credentials =
|
|
|
32
35
|
cause: error,
|
|
33
36
|
});
|
|
34
37
|
}
|
|
38
|
+
const update = updateAdvice(response.headers);
|
|
39
|
+
if (update && response.ok)
|
|
40
|
+
noticeUpdate(origin, update);
|
|
35
41
|
if (response.status === 204)
|
|
36
42
|
return { deleted: true };
|
|
37
43
|
let data;
|
|
@@ -57,6 +63,16 @@ export async function request(baseUrl, path, method = 'GET', body, credentials =
|
|
|
57
63
|
: 'This credential is valid but not permitted here. Check the selected organization with --org or FIELDWORK_ORGANIZATION, and that its access has not been revoked.',
|
|
58
64
|
}
|
|
59
65
|
: {}),
|
|
66
|
+
// A refusal that says what to do instead -- a limit, say -- carries its own hint. An
|
|
67
|
+
// outdated client comes first when the server says this one is too old: the failure
|
|
68
|
+
// may well be the version, and retrying will not change that. Not for a credential
|
|
69
|
+
// refusal, which is about the credential whatever the version.
|
|
70
|
+
...(update?.required && response.status !== 401 && response.status !== 403
|
|
71
|
+
? { hint: update.hint }
|
|
72
|
+
: data.hint
|
|
73
|
+
? { hint: data.hint }
|
|
74
|
+
: {}),
|
|
75
|
+
...(update ? { update } : {}),
|
|
60
76
|
...(data.details ? { details: data.details } : {}),
|
|
61
77
|
...(data.requestId ? { requestId: data.requestId } : {}),
|
|
62
78
|
});
|
package/dist/fieldwork-skill.md
CHANGED
|
@@ -5,11 +5,11 @@ description: Use the Fieldwork CLI to manage products, campaign goals, experimen
|
|
|
5
5
|
|
|
6
6
|
# Fieldwork research bookkeeping
|
|
7
7
|
|
|
8
|
-
Use this skill when a user asks you to organize or report long-running research in Fieldwork. Fieldwork stores intent, hypotheses, configuration, and execution state. It does NOT launch, schedule, monitor, or stop processes. Run actual work with separately authorized tools; only record observed state in Fieldwork.
|
|
8
|
+
Use this skill when a user asks you to organize or report long-running research in Fieldwork. Fieldwork stores intent, hypotheses, configuration, and execution state. It does NOT launch, schedule, monitor, or stop processes. Run actual work with separately authorized tools; only record observed state in Fieldwork. It is a ledger of experiments, not a log or metrics store: see "What to record" below.
|
|
9
9
|
|
|
10
10
|
## Prerequisites and invocation
|
|
11
11
|
|
|
12
|
-
You need Node.js 22+, the `@agentionai/fieldwork-cli` package installed on PATH, and access to a running compatible Fieldwork API server. The CLI package contains no server or web app and does not start either.
|
|
12
|
+
You need Node.js 22+, the `@agentionai/fieldwork-cli` package installed on PATH, and access to a running compatible Fieldwork API server. The CLI package contains no server or web app and does not start either. Install it with `npm install --global @agentionai/fieldwork-cli@0.7.0`, or a local archive with `npm install --global ./agentionai-fieldwork-cli-0.7.0.tgz`. Installing from npm does not require pnpm or a checkout.
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
15
|
fieldwork --help
|
|
@@ -34,6 +34,34 @@ The intended CLI interface is stub-first: readable references such as `model-a`,
|
|
|
34
34
|
|
|
35
35
|
Current compatibility: the CLI resolves stubs for campaigns, products, experiments, and runs within their parent scope; UUIDs remain accepted everywhere. Discover records with list commands when the stub is unknown. Do not pass display names as stubs. Discover existing records before creating duplicates. There is no automatic idempotency key; do not blindly retry creates after ambiguous network failures.
|
|
36
36
|
|
|
37
|
+
## What to record: a ledger, not a log
|
|
38
|
+
|
|
39
|
+
A run records what was tried, under what conditions, and the few numbers that decide a comparison -- so that months later someone can still tell what was measured and whether two results are comparable. It is not where logs, traces, per-step metrics, raw model outputs or datasets go.
|
|
40
|
+
|
|
41
|
+
- Record: the parameters that varied, the comparison context that must match for results to compare (hardware, build or version, git commit, dataset and its version, harness, time or budget caps), and summary observations (a mean, a p95, a score, a size) with their sample count where it matters.
|
|
42
|
+
- Link, do not paste: put a log or output location in `logsUri`, and files that define the work (recipes, datasets, heads) in artifacts. A path is a reference, not a copy.
|
|
43
|
+
- Aggregate before recording: one run per configuration measured, with its summary numbers -- not one run per request, step or sample.
|
|
44
|
+
- Forgot something? A parameter or context field a run left empty can be filled in later with `fieldwork runs backfill`, visibly and with provenance; do not re-record the run. Measurements taken later are a new run.
|
|
45
|
+
|
|
46
|
+
Work that is finished or no longer relevant is archived, not deleted: `fieldwork campaigns archive REF` (or `products`, `experiments`) takes it out of lists and closes it to new work, and `restore` brings it back; nothing is lost. Deleting (`delete ... --confirm NAME`) removes a unit and everything in it for good -- do it only when the user asks for that, and never to get around a limit or an archived refusal (`ARCHIVED`: restore it, or record elsewhere). A finished run is never deleted: mark it superseded or its experiment abandoned.
|
|
47
|
+
|
|
48
|
+
On the hosted service an organization's plan may limit how many products, active campaigns, active experiments per campaign, agent credentials and members it has. A refusal is `PLAN_LIMIT`, with a hint: tell the user, and suggest archiving finished work (archived work does not count) rather than deleting anything or creating work elsewhere to get around it.
|
|
49
|
+
|
|
50
|
+
Keep the CLI current. It names its version to the server on every request; when a newer release exists, a successful command prints one `{"notice":"UPDATE_AVAILABLE",...}` line on stderr (at most once a day), and a failed command's error carries an `update` object. When it says `"required": true`, update before retrying: the failure may be the version, and retrying will not change that. Install the version it names with `npm install --global @agentionai/fieldwork-cli@<latest>`. Stdout is never affected.
|
|
51
|
+
|
|
52
|
+
The hosted service enforces limits sized for that, and each refusal carries a `hint` saying what to do instead. Read it, and change what you record rather than retrying:
|
|
53
|
+
|
|
54
|
+
| Limit | Value | Refusal |
|
|
55
|
+
| ------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
56
|
+
| One run's or experiment's own data (parameters, observations, context, environment, input refs, extras) | 64 KB of JSON | `RECORD_TOO_LARGE` (413) |
|
|
57
|
+
| Observations on one record | 200 | `TOO_MANY_OBSERVATIONS` (413) |
|
|
58
|
+
| Any request body | 256 KB | `PAYLOAD_TOO_LARGE` (413) |
|
|
59
|
+
| Runs in one experiment | 5,000 | `EXPERIMENT_FULL` (409): if you are looping, stop; otherwise split the study into experiments by what varies |
|
|
60
|
+
| Runs in one shared snapshot | 1,000 | `SHARE_TOO_LARGE` (413) |
|
|
61
|
+
| Writes by one caller (member or agent credential) | 120 a minute | `RATE_LIMITED` (429): wait; reads are never limited |
|
|
62
|
+
|
|
63
|
+
Reaching a limit in ordinary use usually means the record is carrying something that belongs elsewhere. Organizations with a genuine need for more may be offered higher limits (enterprise accounts are planned); until then, do not work around a limit by splitting one record's data across several.
|
|
64
|
+
|
|
37
65
|
## Local campaign workspace (available via `setup campaign`)
|
|
38
66
|
|
|
39
67
|
Attach a directory to a campaign so commands work relative to its product/campaign context without repeating IDs:
|
|
@@ -91,7 +119,7 @@ Commands below use `fieldwork` as the executable. `--help` is available on every
|
|
|
91
119
|
| context | [--campaign REF] |
|
|
92
120
|
| changelog | [--since VERSION] [--release VERSION]; offline, never contacts the API |
|
|
93
121
|
| schemas | list [--inherited]; publish --json JSON; default; set-default --json JSON; these accept product/campaign/experiment scope; get VERSION_ID; template VERSION_ID; validate VERSION_ID [--ready] --json JSON |
|
|
94
|
-
| charts | fields; list; create --json JSON; these accept [--campaign REF] [--experiment REF]; get CHART_ID; data CHART_ID; series CHART_ID --json JSON; frontier CHART_ID --json JSON; delete CHART_ID
|
|
122
|
+
| charts | fields; list; create --json JSON; these accept [--campaign REF] [--experiment REF]; get CHART_ID; data CHART_ID; series CHART_ID --json JSON; frontier CHART_ID --json JSON; delete CHART_ID |
|
|
95
123
|
|
|
96
124
|
`--json -` reads a JSON object from stdin. Use it for multiline text and configuration files rather than constructing shell strings from untrusted text. Successful data commands emit JSON on stdout. Failures emit JSON on stderr and exit nonzero. Help/version are human-readable. Direct invocation avoids package-manager output mixed into machine-readable streams.
|
|
97
125
|
|
|
@@ -216,7 +244,7 @@ API equivalents: `GET /api/v1/{campaigns|experiments}/:id/charts/fields`, `GET/P
|
|
|
216
244
|
|
|
217
245
|
## Fieldwork identity and compatibility
|
|
218
246
|
|
|
219
|
-
Product: Agention Fieldwork. npm package: `@agentionai/fieldwork-cli`. Executable: `fieldwork`.
|
|
247
|
+
Product: Agention Fieldwork. npm package: `@agentionai/fieldwork-cli`. Executable: `fieldwork`. It is published on npm; repository `install.sh --help` also describes local tarball installation. No service is installed or started.
|
|
220
248
|
|
|
221
249
|
New setup uses `.fieldwork/workspace.json` and `fieldwork-skill.md`. Existing `.lab/workspace.json` bindings and `LAB_URL` remain supported; `FIELDWORK_URL` takes precedence over the legacy variable. Existing files are never renamed automatically. The server database location is unchanged.
|
|
222
250
|
|
|
@@ -246,7 +274,7 @@ Extension accepts new optional observations/parameters, enum expansion, relaxed
|
|
|
246
274
|
|
|
247
275
|
This is the exception to ordinary pin immutability: a new immutable successor is created, with `extendedFrom` and `extensionImpact`, and all matching experiment/run/default/chart pins advance atomically. Old schema definitions, recorded values, execution dates, and creation snapshots remain intact. Revisions increment: reload affected records before writing. Existing terminal runs may then receive optional observations via normal revision-checked updates. No run recreation or fabricated lifecycle is necessary. Dry run writes nothing; apply revalidates and is not reserved by the preview. Stale-source errors require inspection, not a blind retry. Independently published versions are not auto-merged, and breaking re-pinning is still blocked.
|
|
248
276
|
|
|
249
|
-
Widening an enum (adding values) is an ordinary compatible extension. A parameter or comparison-context field added to the schema after runs were recorded can be filled in on those runs, finished ones included, in any later extension: `"backfill":{"packager":{"RUN_ID":"unsloth"}}` (or `"parameters.packager"` / `"comparisonContext.driver"` when a bare name is ambiguous). Backfill accepts
|
|
277
|
+
Widening an enum (adding values) is an ordinary compatible extension. A parameter or comparison-context field added to the schema after runs were recorded can be filled in on those runs, finished ones included, in any later extension: `"backfill":{"packager":{"RUN_ID":"unsloth"}}` (or `"parameters.packager"` / `"comparisonContext.driver"` when a bare name is ambiguous). Backfill accepts runs re-pinned by this extension that left the field empty -- whether it is new or was forgotten -- and never replaces a recorded value; values are validated against the new definition, applied in the same atomic write, and listed under `impact.backfilled` (dry runs included). Each run notes them in `research.backfilled` with the supplying schema version, time and author. A backfilled value describes a run rather than records how it ran, so it stays correctable with a revision-checked `runs update`; executed configuration and context stay frozen. Do not re-record finished runs just to add a field. When the field already exists in the run's schema and was simply not logged -- a git commit, a build -- fill it in directly: `fieldwork runs backfill RUN --json '{"comparisonContext":{"git_commit":"a1b2c3d"}}'`.
|
|
250
278
|
|
|
251
279
|
Recipe/artifact commands are available in CLI 0.4.0 with the updated API. Use the explicit artifacts reference array; a config stub alone is not an enforced recipe link.
|
|
252
280
|
|
package/dist/main.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { request, requestAll } from './client.js';
|
|
5
|
+
import { CLI_VERSION } from './version.js';
|
|
5
6
|
import { DEFAULT_SERVER, Scope, setupCampaign } from './workspace.js';
|
|
6
7
|
import { filterRows, parseCondition, projectRows, resolve, toTsv } from './select.js';
|
|
7
8
|
import { selectChangelog } from './changelog.js';
|
|
8
9
|
import { credentialsPath, forgetCredential, resolveCredential, storeCredential, } from './credentials.js';
|
|
9
|
-
const version =
|
|
10
|
+
const version = CLI_VERSION;
|
|
10
11
|
const program = new Command()
|
|
11
12
|
.name('fieldwork')
|
|
12
13
|
.description('Fieldwork API client; JSON results on stdout, JSON failures on stderr')
|
|
@@ -196,12 +197,17 @@ campaigns
|
|
|
196
197
|
campaigns
|
|
197
198
|
.command('delete')
|
|
198
199
|
.argument('<ref>')
|
|
199
|
-
.requiredOption('--revision <number>', 'Expected revision
|
|
200
|
+
.requiredOption('--revision <number>', 'Expected revision')
|
|
201
|
+
.option('--confirm <name>', 'The campaign name, required when it holds work: everything in it is deleted too')
|
|
202
|
+
.description('Delete a campaign and everything in it, for good. To keep the work out of the way instead, use archive')
|
|
200
203
|
.action(async (reference, options) => {
|
|
201
204
|
const revision = Number(options.revision);
|
|
202
205
|
if (!Number.isSafeInteger(revision) || revision < 1)
|
|
203
206
|
throw new Error('Revision must be a positive integer');
|
|
204
|
-
return send(path((await apiScope().campaign(reference)).id), 'DELETE', {
|
|
207
|
+
return send(path((await apiScope().campaign(reference)).id), 'DELETE', {
|
|
208
|
+
revision,
|
|
209
|
+
...(options.confirm ? { confirm: options.confirm } : {}),
|
|
210
|
+
});
|
|
205
211
|
});
|
|
206
212
|
const artifacts = program
|
|
207
213
|
.command('artifacts')
|
|
@@ -319,7 +325,7 @@ schemas
|
|
|
319
325
|
.requiredOption('--json <json|->', 'Complete replacement definition: {definition:{parameters,observations,comparisonContext}}; optional backfill:{field:{RUN_ID:value}} for fields a run predates')
|
|
320
326
|
.option('--dry-run', 'Validate and preview affected records without saving')
|
|
321
327
|
.description('Publish a compatible successor and atomically re-pin matching experiments, runs, defaults and charts; old versions and snapshots remain intact')
|
|
322
|
-
.addHelpText('after', '\nbackfill fills in parameter or context fields
|
|
328
|
+
.addHelpText('after', '\nbackfill fills in parameter or context fields runs left empty -- added by this extension,\nor there all along and forgotten -- on runs this extension re-pins, finished runs included, e.g.\n {"definition":{...},"backfill":{"packager":{"RUN_ID":"unsloth"}}}\nName a field parameters.<field> or comparisonContext.<field> if the bare name is\nambiguous. Never over a recorded value; values are validated with the extension. Runs note\nthem in research.backfilled with when and by whom, and a backfilled value stays correctable\nwith runs update. For a field the schema already has, runs backfill needs no extension.\nAdding enum values is an ordinary extension.')
|
|
323
329
|
.action(async (ref, options) => {
|
|
324
330
|
const data = input(options.json);
|
|
325
331
|
if (options.dryRun)
|
|
@@ -424,8 +430,9 @@ products
|
|
|
424
430
|
products
|
|
425
431
|
.command('delete')
|
|
426
432
|
.argument('<ref>')
|
|
427
|
-
.
|
|
428
|
-
.
|
|
433
|
+
.option('--confirm <name>', 'The product name, required when it holds work: its campaigns and everything in them go too')
|
|
434
|
+
.description('Delete a product and everything in it, for good. To keep the work out of the way instead, use archive')
|
|
435
|
+
.action(async (reference, options) => send(`/products/${encodeURIComponent((await apiScope().product(reference)).id)}`, 'DELETE', options.confirm ? { confirm: options.confirm } : undefined));
|
|
429
436
|
for (const kind of ['experiments', 'runs']) {
|
|
430
437
|
const group = program
|
|
431
438
|
.command(kind)
|
|
@@ -441,14 +448,16 @@ for (const kind of ['experiments', 'runs']) {
|
|
|
441
448
|
.option('--include-abandoned', kind === 'runs' ? 'Include runs of abandoned experiments' : 'Include abandoned experiments');
|
|
442
449
|
if (kind === 'runs')
|
|
443
450
|
list.option('--experiment <ref>', 'Only runs of this experiment, by stub or ID');
|
|
451
|
+
else
|
|
452
|
+
list.option('--include-archived', 'Include archived experiments');
|
|
444
453
|
list
|
|
445
|
-
.description(`List ${kind}; superseded records and
|
|
454
|
+
.description(`List ${kind}; superseded records, abandoned and archived work are excluded unless asked for`)
|
|
446
455
|
.action(async (options) => {
|
|
447
456
|
if (!['json', 'tsv'].includes(options.format))
|
|
448
457
|
fail('--format must be json or tsv');
|
|
449
458
|
const scope = apiScope();
|
|
450
459
|
const campaign = await scope.campaign(options.campaign);
|
|
451
|
-
let rows = await requestAll(url(), `${path(campaign.id)}/${kind}`, credentials());
|
|
460
|
+
let rows = await requestAll(url(), `${path(campaign.id)}/${kind}${options.includeArchived ? '?archived=include' : ''}`, credentials());
|
|
452
461
|
if (options.experiment) {
|
|
453
462
|
const experimentId = await scope.work('experiments', options.experiment, campaign.id);
|
|
454
463
|
rows = rows.filter((row) => row['experimentId'] === experimentId);
|
|
@@ -482,8 +491,16 @@ for (const kind of ['experiments', 'runs']) {
|
|
|
482
491
|
.argument('<ref>')
|
|
483
492
|
.option('--campaign <ref>', 'Defaults to the local workspace campaign')
|
|
484
493
|
.requiredOption('--json <json|->', 'Object with observations and/or a status; optional errorSummary, extras, extrasMode (replace|merge), comparisonContext, allowIncompleteComparisonContext, artifacts, startedAt, finishedAt, revision')
|
|
485
|
-
.description('Record an outcome in one call: the server reads the current revision, and a result may be recorded directly from planned')
|
|
494
|
+
.description('Record an outcome in one call: the server reads the current revision, and a result may be recorded directly from planned. Summary observations, not logs: link those with logsUri')
|
|
486
495
|
.action(async (reference, options) => send(`/runs/${encodeURIComponent(await resolveRef(reference, options.campaign))}/record`, 'POST', input(options.json)));
|
|
496
|
+
if (kind === 'runs')
|
|
497
|
+
group
|
|
498
|
+
.command('backfill')
|
|
499
|
+
.argument('<ref>')
|
|
500
|
+
.option('--campaign <ref>', 'Defaults to the local workspace campaign')
|
|
501
|
+
.requiredOption('--json <json|->', 'Object with parameters and/or comparisonContext: the fields to fill in, e.g. {"comparisonContext":{"git_commit":"a1b2c3d"}}')
|
|
502
|
+
.description('Fill in a parameter or comparison-context field a run left empty, finished runs included. Never over a recorded value; noted as added later, with when and by whom. A field the schema lacks is added with schemas extend.')
|
|
503
|
+
.action(async (reference, options) => send(`/runs/${encodeURIComponent(await resolveRef(reference, options.campaign))}/backfill`, 'POST', input(options.json)));
|
|
487
504
|
for (const operation of ['get', 'context'])
|
|
488
505
|
group
|
|
489
506
|
.command(operation)
|
|
@@ -499,7 +516,7 @@ for (const kind of ['experiments', 'runs']) {
|
|
|
499
516
|
create.option('--experiment <ref>', 'Experiment stub or ID in campaign scope; cannot combine with JSON experimentId');
|
|
500
517
|
create
|
|
501
518
|
.description(kind === 'runs'
|
|
502
|
-
? 'Create a run record, optionally already terminal with its results; this does not execute a job'
|
|
519
|
+
? 'Create a run record, optionally already terminal with its results; this does not execute a job. Record parameters, comparison context and summary numbers; link logs and raw outputs with logsUri or artifacts (run data is limited by plan: 64 KB on Free)'
|
|
503
520
|
: 'Create an experiment with a hypothesis and optional schema version')
|
|
504
521
|
.requiredOption('--json <json|->', kind === 'runs'
|
|
505
522
|
? 'Object requiring title; optional stub, status, config, observations, comparisonContext, extras, startedAt, finishedAt, errorSummary; - reads stdin'
|
|
@@ -528,15 +545,49 @@ for (const kind of ['experiments', 'runs']) {
|
|
|
528
545
|
.command('delete')
|
|
529
546
|
.argument('<ref>')
|
|
530
547
|
.option('--campaign <ref>', 'Defaults to the local workspace campaign')
|
|
531
|
-
.requiredOption('--revision <number>',
|
|
548
|
+
.requiredOption('--revision <number>', kind === 'runs'
|
|
549
|
+
? 'Expected revision; only a planned run can be deleted, a run that started is kept'
|
|
550
|
+
: 'Expected revision')
|
|
551
|
+
.option('--confirm <name>', kind === 'experiments'
|
|
552
|
+
? 'The experiment name, required when it holds runs: they are deleted too'
|
|
553
|
+
: 'Not used for runs')
|
|
532
554
|
.action(async (reference, options) => {
|
|
533
555
|
const revision = Number(options.revision);
|
|
534
556
|
if (!Number.isSafeInteger(revision) || revision < 1)
|
|
535
557
|
throw new Error('Revision must be a positive integer');
|
|
536
|
-
return send(`/${kind}/${encodeURIComponent(await resolveRef(reference, options.campaign))}`, 'DELETE', {
|
|
558
|
+
return send(`/${kind}/${encodeURIComponent(await resolveRef(reference, options.campaign))}`, 'DELETE', {
|
|
559
|
+
revision,
|
|
560
|
+
...(options.confirm && kind === 'experiments' ? { confirm: options.confirm } : {}),
|
|
561
|
+
});
|
|
537
562
|
});
|
|
563
|
+
if (kind === 'experiments')
|
|
564
|
+
for (const action of ['archive', 'restore'])
|
|
565
|
+
group
|
|
566
|
+
.command(action)
|
|
567
|
+
.argument('<ref>')
|
|
568
|
+
.option('--campaign <ref>', 'Defaults to the local workspace campaign')
|
|
569
|
+
.description(action === 'archive'
|
|
570
|
+
? 'File an experiment away: out of lists and closed to new runs; nothing is lost'
|
|
571
|
+
: 'Bring an archived experiment back')
|
|
572
|
+
.action(async (reference, options) => send(`/experiments/${encodeURIComponent(await resolveRef(reference, options.campaign))}/${action}`, 'POST'));
|
|
573
|
+
}
|
|
574
|
+
for (const action of ['archive', 'restore']) {
|
|
575
|
+
campaigns
|
|
576
|
+
.command(action)
|
|
577
|
+
.argument('<ref>')
|
|
578
|
+
.description(action === 'archive'
|
|
579
|
+
? 'File a campaign away: out of lists and closed to new work; nothing is lost'
|
|
580
|
+
: 'Bring an archived campaign back')
|
|
581
|
+
.action(async (reference) => send(`${path((await apiScope().campaign(reference)).id)}/${action}`, 'POST'));
|
|
582
|
+
products
|
|
583
|
+
.command(action)
|
|
584
|
+
.argument('<ref>')
|
|
585
|
+
.description(action === 'archive'
|
|
586
|
+
? 'File a product away: out of lists and closed to new campaigns; nothing is lost'
|
|
587
|
+
: 'Bring an archived product back')
|
|
588
|
+
.action(async (reference) => send(`/products/${encodeURIComponent((await apiScope().product(reference)).id)}/${action}`, 'POST'));
|
|
538
589
|
}
|
|
539
|
-
program.addHelpText('after', '\nExamples:\n fieldwork setup campaign --product model-a --campaign memory-study\n fieldwork runs create --experiment awq --json \'{"title":"Attempt 1"}\'\n fieldwork schemas validate VERSION_ID --ready --json -\n\nData commands output JSON. Validation reports go to stdout; invalid reports exit 1.\nUse fieldwork <group> <command> --help for payload and scope guidance.');
|
|
590
|
+
program.addHelpText('after', '\nExamples:\n fieldwork setup campaign --product model-a --campaign memory-study\n fieldwork runs create --experiment awq --json \'{"title":"Attempt 1"}\'\n fieldwork schemas validate VERSION_ID --ready --json -\n\nData commands output JSON. Validation reports go to stdout; invalid reports exit 1.\nUse fieldwork <group> <command> --help for payload and scope guidance.\n\nFieldwork is a ledger of experiments, not a log store: record parameters, comparison\ncontext and summary numbers, and link logs and raw outputs with logsUri or artifacts.\nA refusal over a limit carries a hint saying what to record instead.');
|
|
540
591
|
for (const group of program.commands) {
|
|
541
592
|
for (const command of group.commands) {
|
|
542
593
|
if (!command.description())
|
|
@@ -582,6 +633,7 @@ catch (error) {
|
|
|
582
633
|
...(failure.status ? { status: failure.status } : {}),
|
|
583
634
|
...(failure.requestId ? { requestId: failure.requestId } : {}),
|
|
584
635
|
...(failure.details !== undefined ? { details: failure.details } : {}),
|
|
636
|
+
...(failure.update !== undefined ? { update: failure.update } : {}),
|
|
585
637
|
}));
|
|
586
638
|
process.exitCode = 1;
|
|
587
639
|
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { credentialsPath } from './credentials.js';
|
|
4
|
+
/** This CLI's version, from its package: the one `--version` prints and every request names. */
|
|
5
|
+
export const CLI_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
6
|
+
export function updateAdvice(headers) {
|
|
7
|
+
const latest = headers.get('X-Fieldwork-Cli-Latest');
|
|
8
|
+
const hint = headers.get('X-Fieldwork-Cli-Hint');
|
|
9
|
+
if (!latest || !hint)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
current: CLI_VERSION,
|
|
13
|
+
latest,
|
|
14
|
+
required: headers.get('X-Fieldwork-Cli-Update') === 'required',
|
|
15
|
+
hint,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
19
|
+
/** Tells whoever runs a command that worked that a newer CLI exists: one JSON line on
|
|
20
|
+
* stderr, at most once a day per server, so stdout stays exactly the command's result and
|
|
21
|
+
* a loop of commands is not a loop of notices. A failure always carries the advice in its
|
|
22
|
+
* own error instead. FIELDWORK_NO_UPDATE_NOTICE=1 silences it. */
|
|
23
|
+
export function noticeUpdate(origin, advice) {
|
|
24
|
+
if (process.env['FIELDWORK_NO_UPDATE_NOTICE'])
|
|
25
|
+
return;
|
|
26
|
+
const path = join(dirname(credentialsPath()), 'update-notice.json');
|
|
27
|
+
let seen = {};
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(path))
|
|
30
|
+
seen = JSON.parse(readFileSync(path, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// A notice is a courtesy: an unreadable record of it means one notice too many.
|
|
34
|
+
}
|
|
35
|
+
if (Date.now() - (seen[origin] ?? 0) < DAY)
|
|
36
|
+
return;
|
|
37
|
+
console.error(JSON.stringify({
|
|
38
|
+
notice: advice.required ? 'UPDATE_REQUIRED' : 'UPDATE_AVAILABLE',
|
|
39
|
+
message: advice.hint,
|
|
40
|
+
current: advice.current,
|
|
41
|
+
latest: advice.latest,
|
|
42
|
+
}));
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
writeFileSync(path, JSON.stringify({ ...seen, [origin]: Date.now() }));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Unwritable config: the notice repeats, which is harmless.
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/workspace.js
CHANGED
|
@@ -248,14 +248,15 @@ export class Scope {
|
|
|
248
248
|
fail('Server differs from workspace; supply explicit scope instead of reusing local IDs');
|
|
249
249
|
return this.workspace?.config;
|
|
250
250
|
}
|
|
251
|
+
// Resolution sees archived records too: restoring one, or reading it, names it by stub.
|
|
251
252
|
async product(reference) {
|
|
252
|
-
return select(await records(this.url, '/products', this.credentials), reference);
|
|
253
|
+
return select(await records(this.url, '/products?archived=include', this.credentials), reference);
|
|
253
254
|
}
|
|
254
255
|
async campaign(reference) {
|
|
255
256
|
const target = reference ?? this.local()?.campaign.id;
|
|
256
257
|
if (!target)
|
|
257
258
|
fail('Supply --campaign or run setup campaign first');
|
|
258
|
-
const campaign = select(await records(this.url, '/campaigns', this.credentials), target);
|
|
259
|
+
const campaign = select(await records(this.url, '/campaigns?archived=include', this.credentials), target);
|
|
259
260
|
if (!reference && campaign.productId !== this.local()?.product?.id)
|
|
260
261
|
fail('Campaign parent changed; inspect workspace binding');
|
|
261
262
|
return campaign;
|
|
@@ -264,7 +265,7 @@ export class Scope {
|
|
|
264
265
|
if (!campaignRef && !this.workspace && /^[0-9a-f-]{36}$/i.test(reference))
|
|
265
266
|
return reference;
|
|
266
267
|
const campaign = await this.campaign(campaignRef);
|
|
267
|
-
return select(await records(this.url, `/campaigns/${campaign.id}/${kind}`, this.credentials), reference).id;
|
|
268
|
+
return select(await records(this.url, `/campaigns/${campaign.id}/${kind}?archived=include`, this.credentials), reference).id;
|
|
268
269
|
}
|
|
269
270
|
async context(reference) {
|
|
270
271
|
const campaign = await this.campaign(reference);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentionai/fieldwork-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Agention Fieldwork CLI for research campaigns, experiments and run records",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/main.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"dist/workspace.js",
|
|
10
10
|
"dist/select.js",
|
|
11
11
|
"dist/changelog.js",
|
|
12
|
+
"dist/version.js",
|
|
12
13
|
"dist/fieldwork-skill.md"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|