start-command 0.42.0 → 0.44.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/.changeset/README.md +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +2 -0
- package/package.json +6 -6
- package/src/lib/args-parser-queries.js +64 -3
- package/src/lib/docker-inspect-batch.js +3 -0
- package/src/lib/execution-attach.js +10 -1
- package/src/lib/execution-cleanup.js +8 -0
- package/src/lib/execution-control.js +14 -3
- package/src/lib/execution-pause.js +701 -0
- package/src/lib/execution-recovery.js +6 -0
- package/src/lib/execution-resume-all.js +14 -0
- package/src/lib/execution-resume.js +16 -1
- package/src/lib/execution-store.js +5 -1
- package/src/lib/formal-ai-client.js +8 -5
- package/src/lib/formal-ai-files.js +474 -61
- package/src/lib/formal-ai.js +170 -7
- package/src/lib/log-sanitizer.js +5 -0
- package/src/lib/query-commands.js +33 -1
- package/src/lib/session-probe.js +27 -1
- package/src/lib/status-formatter.js +13 -3
- package/src/lib/status-probe.js +19 -5
- package/src/lib/usage.js +8 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +3 -3
- package/test/args-parser.js +1 -1
- package/test/fixtures/formal-ai/receipts-258.json +27 -9
- package/test/fixtures/formal-ai/receipts-261.json +589 -0
- package/test/fixtures/formal-ai/replay.cjs +4 -1
- package/test/fixtures/pause-golden.cjs +142 -0
- package/test/regression-243.js +4 -1
- package/test/regression-258-files.js +27 -5
- package/test/regression-258.js +22 -9
- package/test/regression-261.js +856 -0
- package/test/regression-263.js +867 -0
- package/test/shared-golden.js +28 -3
package/.changeset/README.md
CHANGED
|
@@ -5,4 +5,4 @@ with multi-package repos, or single-package repos to help you version and publis
|
|
|
5
5
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
6
6
|
|
|
7
7
|
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
-
[our documentation](https://github.com/changesets/changesets/blob/main/docs/
|
|
8
|
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/faq.md)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 76efaf3: `--pause <id>` / `--unpause <id>` freeze and thaw a detached Docker execution in place (#263).
|
|
8
|
+
|
|
9
|
+
- **Same execution:** `docker pause` (cgroup freezer) keeps the container, its process (same PID), the execution UUID, record and log; `docker unpause` lets the same process continue. No new execution, attempt or recovery is created. Docker is called with argument arrays and a 30 second timeout.
|
|
10
|
+
- **Resources:** CPU is released while memory and disk stay allocated; `--stop` frees the RAM. Reports include the resource state and container PID.
|
|
11
|
+
- **Record:** `paused`, `pausedAt`, `pauseCount`, `pausedBy` and `pauseReason` (`--pause-reason`, `--paused-by`); unpause clears the pause and records `pauseEndedBy`. Pausing an already paused container or unpausing a running one is a no-op; stopped or removed containers fail with a pointer to `--status` and `--resume`. screen and tmux sessions are refused.
|
|
12
|
+
- **Status:** `--status` reports `dockerState`, `paused` and `pausedAt`; a paused execution stays `executing` and `--list --running` includes it. The recorded pause is reconciled with `docker inspect`, so a daemon restart, reboot or external `docker unpause`/`docker rm` never leaves a stale `paused: true`, and a removed container shows `removed`.
|
|
13
|
+
- **Guards:** a paused execution is never attached to, resumed (no snapshot), resumed by `--resume-all` (skipped with a reason), cleaned up or completed by the watcher. `--stop`/`--terminate` clear the pause and count as an intentional stop, so `--on-kill-resume` does not restart it.
|
|
14
|
+
- **Parity:** the JavaScript and Rust pause/unpause paths are held to shared golden contracts (`pause-control` in `parity/fixtures/contracts.json`) covering reports in every output format, errors, docker calls and the saved record.
|
|
15
|
+
|
|
16
|
+
## 0.43.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 8567804: `--use-formal-ai`: bounded typed source edits (#261).
|
|
21
|
+
|
|
22
|
+
- **Bound preimage:** an `edit` or `write` of an existing file is bound to the complete bytes the `read` tool returned for that exact path in the same task. `cat`, a directory listing or a stale read is not enough, and a change of the target while start writes is refused. `read` returns the text as the numbered frame Formal AI binds as a whole-file Read and decodes back exactly.
|
|
23
|
+
- **Owned change only:** `old_string` must occur exactly once (or `replace_all` is set), and only those byte regions change. Unicode, CRLF, a byte order mark and a missing final newline are preserved, and symlinks are followed only inside the working directory.
|
|
24
|
+
- **Receipt and verification:** each change keeps a mutation receipt (`tool`, `target`, `preimage`, `postimage`, byte `matches`, `readback`) in the evidence and returns it to Formal AI. The readback is verified byte for byte against the preimage and the matches, independently of fingerprints; any other byte change ends the run with 70.
|
|
25
|
+
- **Unresolved failures:** a failed `read`, `edit`, `write` or `bash` change stays open until a verified change of the same target (or the same command) resolves it. A later read, a `cp`/`mv` over the file or Formal AI's answer does not resolve it, and the run ends `incomplete` with the original failure and its exit status.
|
|
26
|
+
- **Gap records:** `SourcePreimageUnavailable` is a known `workspace-change-verification-gap/v1` reason. Other JSON gap records and Formal AI's Links Notation `obligation_gap` answers end `incomplete` with exit 65 instead of success, unless they only repeat tool output (including a read frame's text). There is still no shell or vendor fallback.
|
|
27
|
+
- **Dependency:** log sanitization now depends on `@link-foundation/sensitive-data-sanitizer` ^0.14.0, its current release.
|
|
28
|
+
|
|
3
29
|
## 0.42.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-command",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
|
|
5
5
|
"main": "src/bin/cli.js",
|
|
6
6
|
"exports": {
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@changesets/cli": "^3.0.3",
|
|
51
51
|
"@eslint/js": "^10.0.1",
|
|
52
|
-
"eslint": "^10.
|
|
52
|
+
"eslint": "^10.15.0",
|
|
53
53
|
"eslint-config-prettier": "^10.1.8",
|
|
54
54
|
"eslint-plugin-prettier": "^5.5.7",
|
|
55
55
|
"husky": "^9.1.7",
|
|
56
56
|
"lint-staged": "^17.6.0",
|
|
57
|
-
"prettier": "^3.
|
|
57
|
+
"prettier": "^3.10.1"
|
|
58
58
|
},
|
|
59
59
|
"lint-staged": {
|
|
60
60
|
"*.{js,mjs,cjs}": [
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
]
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@link-foundation/sensitive-data-sanitizer": "^0.
|
|
71
|
+
"@link-foundation/sensitive-data-sanitizer": "^0.14.0",
|
|
72
72
|
"command-stream": "^2.6.0",
|
|
73
|
-
"lino-objects-codec": "^0.9.
|
|
73
|
+
"lino-objects-codec": "^0.9.6"
|
|
74
74
|
},
|
|
75
75
|
"overrides": {
|
|
76
|
-
"brace-expansion": "^5.0.
|
|
76
|
+
"brace-expansion": "^5.0.13",
|
|
77
77
|
"flatted": "^3.4.4"
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Query modes: --status, --list [--running]
|
|
9
9
|
* Control modes: --upload-log, --stop, --terminate, --attach [--read-only],
|
|
10
|
+
* --pause [--pause-reason <text>] [--paused-by <name>],
|
|
11
|
+
* --unpause (issue #263),
|
|
10
12
|
* --resume [-- <command>], --resume-all,
|
|
11
13
|
* --cleanup [<uuid-or-session-name>]
|
|
12
14
|
* Naming modes: --alias <id> <name>, --rename <id> <name> (issue #231)
|
|
@@ -33,6 +35,10 @@ function createQueryOptionDefaults() {
|
|
|
33
35
|
outputFormat: null, // Output format (links-notation, json, text)
|
|
34
36
|
stop: null, // UUID/session name to stop gracefully
|
|
35
37
|
terminate: null, // UUID/session name to terminate immediately
|
|
38
|
+
pause: null, // UUID/session name of a docker execution to freeze
|
|
39
|
+
unpause: null, // UUID/session name of a paused docker execution to thaw
|
|
40
|
+
pauseReason: null, // Why --pause froze the execution (recorded)
|
|
41
|
+
pausedBy: null, // Who --pause froze the execution for (recorded)
|
|
36
42
|
attach: null, // UUID/session name whose terminal should be attached
|
|
37
43
|
readOnly: false, // Follow --attach output without forwarding stdin
|
|
38
44
|
resume: null, // UUID/session name to resume (optionally with a new command)
|
|
@@ -55,6 +61,8 @@ const IDENTIFIER_OPTIONS = {
|
|
|
55
61
|
'--upload-log': 'uploadLog',
|
|
56
62
|
'--stop': 'stop',
|
|
57
63
|
'--terminate': 'terminate',
|
|
64
|
+
'--pause': 'pause',
|
|
65
|
+
'--unpause': 'unpause',
|
|
58
66
|
'--attach': 'attach',
|
|
59
67
|
'--resume': 'resume',
|
|
60
68
|
};
|
|
@@ -153,6 +161,32 @@ function parseNamingOption(args, index, options) {
|
|
|
153
161
|
return 3;
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
/**
|
|
165
|
+
* `--pause-reason <text>` and `--paused-by <name>` (issue #263) annotate a
|
|
166
|
+
* `--pause`; their values may contain spaces but not start with `-`.
|
|
167
|
+
*/
|
|
168
|
+
const PAUSE_DETAIL_OPTIONS = {
|
|
169
|
+
'--pause-reason': 'pauseReason',
|
|
170
|
+
'--paused-by': 'pausedBy',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function parsePauseDetailOption(args, index, options) {
|
|
174
|
+
const arg = args[index];
|
|
175
|
+
const equals = arg.indexOf('=');
|
|
176
|
+
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
177
|
+
const key = PAUSE_DETAIL_OPTIONS[name];
|
|
178
|
+
if (!key) {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const inline = equals !== -1;
|
|
182
|
+
const value = inline ? arg.slice(equals + 1) : args[index + 1];
|
|
183
|
+
if (!value || (!inline && value.startsWith('-'))) {
|
|
184
|
+
throw new Error(`Option ${name} requires a value`);
|
|
185
|
+
}
|
|
186
|
+
options[key] = value;
|
|
187
|
+
return inline ? 1 : 2;
|
|
188
|
+
}
|
|
189
|
+
|
|
156
190
|
/**
|
|
157
191
|
* Parse a single query/control option.
|
|
158
192
|
* @param {string[]} args - Arguments array
|
|
@@ -202,6 +236,11 @@ function parseQueryOption(args, index, options) {
|
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
238
|
|
|
239
|
+
const pauseDetailConsumed = parsePauseDetailOption(args, index, options);
|
|
240
|
+
if (pauseDetailConsumed > 0) {
|
|
241
|
+
return pauseDetailConsumed;
|
|
242
|
+
}
|
|
243
|
+
|
|
205
244
|
// --output-format <format>
|
|
206
245
|
if (arg === '--output-format') {
|
|
207
246
|
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
@@ -242,6 +281,12 @@ function activeQueryModes(options) {
|
|
|
242
281
|
if (hasValue(options.terminate)) {
|
|
243
282
|
modes.push('--terminate');
|
|
244
283
|
}
|
|
284
|
+
if (hasValue(options.pause)) {
|
|
285
|
+
modes.push('--pause');
|
|
286
|
+
}
|
|
287
|
+
if (hasValue(options.unpause)) {
|
|
288
|
+
modes.push('--unpause');
|
|
289
|
+
}
|
|
245
290
|
if (hasValue(options.attach)) {
|
|
246
291
|
modes.push('--attach');
|
|
247
292
|
}
|
|
@@ -266,7 +311,14 @@ function activeQueryModes(options) {
|
|
|
266
311
|
/**
|
|
267
312
|
* Formats are only meaningful for modes that print a machine-readable report.
|
|
268
313
|
*/
|
|
269
|
-
const OUTPUT_FORMAT_MODES = [
|
|
314
|
+
const OUTPUT_FORMAT_MODES = [
|
|
315
|
+
'--status',
|
|
316
|
+
'--list',
|
|
317
|
+
'--resume',
|
|
318
|
+
'--resume-all',
|
|
319
|
+
'--pause',
|
|
320
|
+
'--unpause',
|
|
321
|
+
];
|
|
270
322
|
|
|
271
323
|
/**
|
|
272
324
|
* Validate query/control options.
|
|
@@ -292,7 +344,7 @@ function validateQueryOptions(options) {
|
|
|
292
344
|
|
|
293
345
|
if (modes.length > 1) {
|
|
294
346
|
throw new Error(
|
|
295
|
-
'Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename in the same invocation'
|
|
347
|
+
'Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename in the same invocation'
|
|
296
348
|
);
|
|
297
349
|
}
|
|
298
350
|
|
|
@@ -301,7 +353,7 @@ function validateQueryOptions(options) {
|
|
|
301
353
|
!modes.some((mode) => OUTPUT_FORMAT_MODES.includes(mode))
|
|
302
354
|
) {
|
|
303
355
|
throw new Error(
|
|
304
|
-
'--output-format option is only valid with --status, --list, --resume,
|
|
356
|
+
'--output-format option is only valid with --status, --list, --resume, --resume-all, --pause, or --unpause'
|
|
305
357
|
);
|
|
306
358
|
}
|
|
307
359
|
|
|
@@ -312,6 +364,15 @@ function validateQueryOptions(options) {
|
|
|
312
364
|
if (options.readOnly && !hasValue(options.attach)) {
|
|
313
365
|
throw new Error('--read-only option is only valid with --attach');
|
|
314
366
|
}
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
(hasValue(options.pauseReason) || hasValue(options.pausedBy)) &&
|
|
370
|
+
!hasValue(options.pause)
|
|
371
|
+
) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
'--pause-reason and --paused-by options are only valid with --pause'
|
|
374
|
+
);
|
|
375
|
+
}
|
|
315
376
|
}
|
|
316
377
|
|
|
317
378
|
module.exports = {
|
|
@@ -27,6 +27,9 @@ const BATCH_FIELDS = [
|
|
|
27
27
|
'.State.OOMKilled',
|
|
28
28
|
'.State.StartedAt',
|
|
29
29
|
'.State.FinishedAt',
|
|
30
|
+
// Paused state for --status/--list (issue #263).
|
|
31
|
+
'.State.Status',
|
|
32
|
+
'.State.Paused',
|
|
30
33
|
];
|
|
31
34
|
const BATCH_FORMAT = BATCH_FIELDS.map((field) => `{{${field}}}`).join(' ');
|
|
32
35
|
const NOT_FOUND_LINE = /no such (?:object|container):\s*(\S+)/gi;
|
|
@@ -12,7 +12,11 @@ const { spawnSync } = require('child_process');
|
|
|
12
12
|
const { escapeForLinksNotation } = require('./output-blocks');
|
|
13
13
|
const { getDockerCommand, getDockerSpawnOptions } = require('./docker-cleanup');
|
|
14
14
|
const { runCommand } = require('./execution-control');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
SessionState,
|
|
17
|
+
pausedSessionError,
|
|
18
|
+
probeSession,
|
|
19
|
+
} = require('./session-probe');
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Run a command with the parent terminal attached (stdin included unless the
|
|
@@ -104,6 +108,11 @@ function buildAttachPlan(record, options = {}) {
|
|
|
104
108
|
return logFollowPlan(record, backend, sessionName);
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
if (probe.state === SessionState.PAUSED) {
|
|
112
|
+
// `docker attach` to a frozen container would hang (issue #263).
|
|
113
|
+
return pausedSessionError('attach to', record.uuid, sessionName);
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
if (!probe.alive) {
|
|
108
117
|
return notRunningError(record, { ...probe, sessionName });
|
|
109
118
|
}
|
|
@@ -153,6 +153,14 @@ function cleanupExecution(store, identifier, deps = {}) {
|
|
|
153
153
|
}
|
|
154
154
|
const runner = deps.runner || runCommand;
|
|
155
155
|
const dryRun = deps.dryRun === true;
|
|
156
|
+
if (require('./execution-pause').isConfirmedPaused(record, runner)) {
|
|
157
|
+
// A paused container keeps its process; never remove it (issue #263).
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
output: `Retained container ${record.options.sessionName} (paused)`,
|
|
161
|
+
error: `${identifier} is paused; nothing was removed. Use \`$ --unpause ${record.uuid}\` or \`$ --stop ${record.uuid}\` first.`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
156
164
|
const plan = planExecutionCleanup(record, runner);
|
|
157
165
|
const outcomes = plan.containers.map((name) =>
|
|
158
166
|
removeObject('container', name, runner, dryRun)
|
|
@@ -271,16 +271,25 @@ function formatControlResultAsLinksNotation(result) {
|
|
|
271
271
|
return lines.join('\n');
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Whether a deliberate stop must be recorded: with launch-time kill recovery
|
|
276
|
+
* (issue #176), and for a paused container, whose stop is never a crash
|
|
277
|
+
* (issue #263).
|
|
278
|
+
*/
|
|
279
|
+
function tracksStopIntent(options) {
|
|
280
|
+
return Boolean(options.onKillResume) || options.paused === true;
|
|
281
|
+
}
|
|
282
|
+
|
|
274
283
|
/**
|
|
275
284
|
* Record that a stop was requested, for sessions with launch-time kill
|
|
276
|
-
* recovery (issue #176).
|
|
285
|
+
* recovery (issue #176) or a recorded pause (issue #263).
|
|
277
286
|
* @param {object} store - Execution store
|
|
278
287
|
* @param {object} record - Execution record
|
|
279
288
|
* @returns {string|undefined} The previous marker, to restore on failure
|
|
280
289
|
*/
|
|
281
290
|
function markStopRequested(store, record) {
|
|
282
291
|
const options = record.options || {};
|
|
283
|
-
if (!options
|
|
292
|
+
if (!tracksStopIntent(options)) {
|
|
284
293
|
return undefined;
|
|
285
294
|
}
|
|
286
295
|
const previous = options.stopRequestedAt;
|
|
@@ -294,7 +303,7 @@ function markStopRequested(store, record) {
|
|
|
294
303
|
}
|
|
295
304
|
|
|
296
305
|
function restoreStopRequested(store, record, previous) {
|
|
297
|
-
if (!record.options ||
|
|
306
|
+
if (!tracksStopIntent(record.options || {})) {
|
|
298
307
|
return;
|
|
299
308
|
}
|
|
300
309
|
record.options = { ...record.options, stopRequestedAt: previous };
|
|
@@ -374,6 +383,8 @@ function controlExecution(store, identifier, action, runner = runCommand) {
|
|
|
374
383
|
};
|
|
375
384
|
}
|
|
376
385
|
|
|
386
|
+
// Stopping a paused container ends its pause (issue #263).
|
|
387
|
+
require('./execution-pause').clearPauseAfterControl(store, record, action);
|
|
377
388
|
const output = formatControlResultAsLinksNotation({
|
|
378
389
|
action,
|
|
379
390
|
identifier,
|