@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 CHANGED
@@ -4,19 +4,15 @@
4
4
 
5
5
  ## Install
6
6
 
7
- Requires Node.js **22+** and npm. Version **0.7.0** is prepared for publication; until published, install the supplied archive:
7
+ Requires Node.js **22+** and npm. Version **0.7.0** is published on npm:
8
8
 
9
9
  ```sh
10
- npm install --global ./agentionai-fieldwork-cli-0.7.0.tgz
10
+ npm install --global @agentionai/fieldwork-cli@0.7.0
11
11
  fieldwork --version
12
12
  fieldwork --help
13
13
  ```
14
14
 
15
- After publication:
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: 'prepared',
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
  });
@@ -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. Publication is pending; install a prepared local archive with `npm install --global ./agentionai-fieldwork-cli-0.7.0.tgz`, or the repository installer. After publication, use `npm install --global @agentionai/fieldwork-cli@0.7.0`. Installing from npm does not require pnpm or a checkout.
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`. The package is not published yet; repository `install.sh --help` describes local tarball installation and future version-pinned npm installation. No service is installed or started.
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 only runs created before the field first appeared in the schema's lineage and re-pinned by this extension, and never replaces a recorded value; a run recorded after the field existed keeps its empty value as recorded; 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 and time. 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.
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 = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).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; deletion is blocked by experiments or runs')
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', { revision });
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 a run predates -- added to the schema\nafter the run was recorded -- 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, never a field the run was recorded with; values are\nvalidated with the extension. Runs note them in research.backfilled, and a backfilled\nvalue stays correctable with runs update. Adding enum values is an ordinary extension.')
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
- .description('Delete an empty product by stub or ID; existing product API does not require a revision')
428
- .action(async (reference) => send(`/products/${encodeURIComponent((await apiScope().product(reference)).id)}`, 'DELETE'));
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 abandoned work are excluded unless asked for`)
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>', 'Expected revision; executed runs are retained')
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', { revision });
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
  }
@@ -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.7.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": {