@haystackeditor/cli 0.27.0 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/dist/capture/app-config.js +2 -1
- package/dist/commands/init-telemetry.js +62 -12
- package/dist/commands/init.js +107 -44
- package/dist/commands/lockfile-pin.js +4 -8
- package/dist/commands/verify-onboarding.js +29 -9
- package/dist/commands/verify-precompute.js +1 -1
- package/dist/commands/verify.js +13 -8
- package/dist/index.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,10 +41,13 @@ or `HAYSTACK_TELEMETRY_DISABLED=1` to turn this telemetry off.
|
|
|
41
41
|
|
|
42
42
|
Most users of this CLI are coding agents. What to know:
|
|
43
43
|
|
|
44
|
-
- **After a change,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
- **After a change, two steps** inside the checkout (nothing needs to be
|
|
45
|
+
committed or pushed):
|
|
46
|
+
1. `haystack pre-verify` shows what the change touches, what production and
|
|
47
|
+
real users do there, and Haystack's ideas of what could break.
|
|
48
|
+
2. `haystack verify --intent "<the task, in the user's words>" --idea "<as you
|
|
49
|
+
would tell a tester>"` explores the app with your steering: keep the ideas
|
|
50
|
+
worth trying, add your own (repeat `--idea`); yours are explored first.
|
|
48
51
|
- **First time in a repository**, `haystack init --yes` sets it up and starts
|
|
49
52
|
onboarding the app. `haystack verify onboarding` shows where onboarding is.
|
|
50
53
|
When onboarding asks a question, answer it with
|
|
@@ -74,10 +77,11 @@ It then finds your crawl of that exact capture: the same code and the same
|
|
|
74
77
|
change title (HEAD's subject, which tells the crawl's judge what the change is;
|
|
75
78
|
committing or rewording keeps the code but changes the title). When there is
|
|
76
79
|
none, or the one it finds was cancelled, stopped before finishing or is being
|
|
77
|
-
cancelled,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
cancelled, or the stop hook only built it, it asks the service for the crawl
|
|
81
|
+
(the service starts it, runs the stopped one again once it has shut down, or
|
|
82
|
+
turns the hook's prepared run into a crawl that starts from the app the hook
|
|
83
|
+
already built), says so in one line, and follows that crawl; it never shows a
|
|
84
|
+
crawl of another revision or another title.
|
|
81
85
|
|
|
82
86
|
A crawl builds the app with and without your change, starts from the changed
|
|
83
87
|
code, reaches it in the running app, explores outward with both builds side by
|
|
@@ -108,18 +112,20 @@ up, what became of each idea, and what never ran (`haystack schema verify`).
|
|
|
108
112
|
it is large (`haystack schema verify-raw`). Exit codes: 0 when the crawl
|
|
109
113
|
answered or finished (the bugs
|
|
110
114
|
it found are in the output) or is still running under `--no-wait`; 2 when it ended
|
|
111
|
-
without finishing (stopped early, or cancelled
|
|
112
|
-
|
|
115
|
+
without finishing (stopped early, or cancelled) or the machines it used could
|
|
116
|
+
not be proven shut down;
|
|
113
117
|
1 when the command failed or no crawl could be started.
|
|
114
118
|
|
|
115
119
|
**The stop hook.** `haystack init` installs a Claude Code Stop hook that runs
|
|
116
120
|
`haystack verify precompute --hook` whenever an agent turn stops. It captures
|
|
117
|
-
the checkout within five seconds and hands the request to a detached sender
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
the checkout within five seconds and hands the request to a detached sender.
|
|
122
|
+
The service then builds the app with and without the change, works out what
|
|
123
|
+
the change touches (the brief `haystack pre-verify` prints) and sets up starting
|
|
124
|
+
points next to it, and stops there: it never crawls. `haystack verify` then
|
|
125
|
+
crawls from that prepared app instead of building it again. No run cancels
|
|
126
|
+
another: each runs to its end unless it is cancelled. `haystack verify precompute`
|
|
127
|
+
(without `--hook`) submits the same capture in the foreground and prints one
|
|
128
|
+
line for the run it started.
|
|
123
129
|
|
|
124
130
|
Retained fleet cases remain available through their exact case IDs:
|
|
125
131
|
|
|
@@ -62,7 +62,8 @@ export function recordedApps(gitRoot, candidates) {
|
|
|
62
62
|
return readCaptureConfig(join(gitRoot, dir))?.app === dir;
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
65
|
-
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(dir === '.' ? message : message.replace(CAPTURE_CONFIG_PATH, posix.join(dir, CAPTURE_CONFIG_PATH)));
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
69
|
}
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* stores or reads one), production's environment, the start command, the deploy and the off switches (13c).
|
|
22
22
|
*/
|
|
23
23
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
24
|
-
import { existsSync, lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { accessSync, constants, existsSync, lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { createRequire } from 'node:module';
|
|
26
|
-
import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
26
|
+
import { delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import chalk from 'chalk';
|
|
29
29
|
import { classifyHttpError, repoApiPath } from '../utils/haystack-api.js';
|
|
@@ -103,7 +103,8 @@ function section(json, name) {
|
|
|
103
103
|
}
|
|
104
104
|
const LOCKFILES = [['pnpm-lock.yaml', 'pnpm'], ['package-lock.json', 'npm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun']];
|
|
105
105
|
/** The package manager that owns the app's lockfile: the nearest `packageManager` field, else the nearest lockfile, from
|
|
106
|
-
* the app up to the repository root. null: no lockfile, so the dependency is written into package.json
|
|
106
|
+
* the app up to the repository root (`from` says which). null: no lockfile, so the dependency is written into package.json
|
|
107
|
+
* itself. */
|
|
107
108
|
function packageManager(gitRoot, appDir) {
|
|
108
109
|
for (let directory = appDir;; directory = dirname(directory)) {
|
|
109
110
|
const manifest = join(directory, 'package.json');
|
|
@@ -111,16 +112,32 @@ function packageManager(gitRoot, appDir) {
|
|
|
111
112
|
const declared = readPackage(manifest).json.packageManager;
|
|
112
113
|
const name = typeof declared === 'string' ? /^(pnpm|npm|yarn|bun)@/.exec(declared)?.[1] : undefined;
|
|
113
114
|
if (name)
|
|
114
|
-
return { name: name, root: directory };
|
|
115
|
+
return { name: name, root: directory, from: `${inside(gitRoot, manifest)}'s packageManager field` };
|
|
115
116
|
}
|
|
116
117
|
for (const [lockfile, name] of LOCKFILES) {
|
|
117
118
|
if (isFile(join(directory, lockfile)))
|
|
118
|
-
return { name, root: directory };
|
|
119
|
+
return { name, root: directory, from: inside(gitRoot, join(directory, lockfile)) };
|
|
119
120
|
}
|
|
120
121
|
if (directory === gitRoot || dirname(directory) === directory)
|
|
121
122
|
return null;
|
|
122
123
|
}
|
|
123
124
|
}
|
|
125
|
+
/** Whether `command` is an executable on PATH (a package manager init would run, checked before it plans to). */
|
|
126
|
+
function onPath(command) {
|
|
127
|
+
const extensions = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
|
|
128
|
+
for (const directory of (process.env.PATH ?? '').split(delimiter)) {
|
|
129
|
+
if (directory === '')
|
|
130
|
+
continue;
|
|
131
|
+
for (const extension of extensions) {
|
|
132
|
+
try {
|
|
133
|
+
accessSync(join(directory, `${command}${extension}`), constants.X_OK);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch { /* not in this directory */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
124
141
|
/** The first release whose sites record repository-relative paths (what a registered tenant's src_prefix '' relies on)
|
|
125
142
|
* and that ships the `./next` entry: earlier releases are not compatible with either integration. */
|
|
126
143
|
const CLI_COMPATIBLE_SINCE = [0, 24, 0];
|
|
@@ -216,14 +233,29 @@ function planDependency(gitRoot, appDir, pkg, after, need) {
|
|
|
216
233
|
const spec = `${CLI_PACKAGE}@${version}`;
|
|
217
234
|
const dev = field === 'devDependencies';
|
|
218
235
|
const workspaceRoot = isFile(join(appDir, 'pnpm-workspace.yaml')) ? ['--workspace-root'] : [];
|
|
219
|
-
// Each package manager's add, as it changes no lockfile entry but the CLI's own (lockfile-pin.ts
|
|
220
|
-
// nested under the CLI (hoisting would replace packages the app already locks), pnpm through the merge.
|
|
236
|
+
// Each package manager's add, as it changes no lockfile entry but the CLI's own (lockfile-pin.ts; pnpm through the merge).
|
|
221
237
|
const argv = manager === null ? null : {
|
|
222
238
|
pnpm: ['pnpm', 'add', '--save-exact', dev ? '--save-dev' : '--save-prod', ...workspaceRoot, spec],
|
|
223
|
-
npm: ['npm', 'install', '--save-exact', dev ? '--save-dev' : '--save-prod',
|
|
239
|
+
npm: ['npm', 'install', '--save-exact', dev ? '--save-dev' : '--save-prod', spec],
|
|
224
240
|
yarn: ['yarn', 'add', '--exact', ...(dev ? ['--dev'] : []), spec],
|
|
225
241
|
bun: ['bun', 'add', '--exact', ...(dev ? ['--dev'] : []), spec],
|
|
226
242
|
}[manager.name];
|
|
243
|
+
const appPath = inside(gitRoot, appDir) || '.';
|
|
244
|
+
// npm re-resolves entries the CLI does not own whatever the flags (39 in fosrl/pangolin, with --install-strategy=nested
|
|
245
|
+
// too), so init never rewrites an npm lockfile: the person adds the CLI with npm and sees the lockfile's diff.
|
|
246
|
+
if (manager?.name === 'npm' && argv) {
|
|
247
|
+
return { kind: 'manual', why: `${appPath} uses npm (${manager.from}), whose install also re-resolves lockfile entries the CLI does not own, so init`
|
|
248
|
+
+ ` does not run it. Run \`${argv.join(' ')}\` in ${appPath}, check package-lock.json's diff, then run \`haystack init\` again` };
|
|
249
|
+
}
|
|
250
|
+
// A package manager that is not installed is never run (rule 13: telemetry never fails init).
|
|
251
|
+
if (manager && argv && !onPath(manager.name)) {
|
|
252
|
+
const provide = (manager.name === 'pnpm' || manager.name === 'yarn') && onPath('corepack')
|
|
253
|
+
? `run \`corepack enable ${manager.name}\` (corepack is installed here)`
|
|
254
|
+
: `install it (${manager.name === 'bun' ? 'https://bun.sh' : manager.name === 'yarn' ? 'https://yarnpkg.com/getting-started/install' : 'https://pnpm.io/installation'})`;
|
|
255
|
+
return { kind: 'manual', why: `${appPath} uses ${manager.name} (${manager.from}), which is not installed here, so init did not run it. Either`
|
|
256
|
+
+ ` ${provide} and run \`haystack init\` again, or run \`${argv.join(' ')}\` in ${appPath} wherever ${manager.name} is installed and then`
|
|
257
|
+
+ ' run `haystack init` again' };
|
|
258
|
+
}
|
|
227
259
|
const action = promoting ? `Moved from devDependencies to dependencies at ${version}` : `Pinned to ${version}`;
|
|
228
260
|
const lockfileNote = manager?.name === 'pnpm'
|
|
229
261
|
? `, keeping every other entry of pnpm's lockfile as it was (pnpm's own add re-resolves packages the CLI shares with the app; init adds only the CLI's entries, then \`pnpm install --frozen-lockfile\`)`
|
|
@@ -247,12 +279,14 @@ function planDependency(gitRoot, appDir, pkg, after, need) {
|
|
|
247
279
|
}
|
|
248
280
|
/** The lockfile a package manager writes, in a format lockfile-pin.ts reads (bun's binary lockb aside). */
|
|
249
281
|
function textLockfile(manager) {
|
|
250
|
-
|
|
282
|
+
if (manager.name === 'npm')
|
|
283
|
+
throw new Error('init never runs npm to pin the CLI');
|
|
284
|
+
const name = { pnpm: 'pnpm-lock.yaml', yarn: 'yarn.lock', bun: 'bun.lock' }[manager.name];
|
|
251
285
|
const path = join(manager.root, name);
|
|
252
286
|
return isFile(path) ? { path, format: manager.name } : null;
|
|
253
287
|
}
|
|
254
288
|
/** Runs the add, so that the lockfile gains the CLI's entries and nothing else moves (rule 13); throws (init then restores
|
|
255
|
-
*
|
|
289
|
+
* telemetry's files and leaves telemetry manual) when the package manager moved anything else. */
|
|
256
290
|
function pin(manager, appDir, argv) {
|
|
257
291
|
const run = (command) => { execFileSync(command[0], command.slice(1), { cwd: appDir, stdio: ['ignore', 2, 2] }); };
|
|
258
292
|
const lockfile = textLockfile(manager);
|
|
@@ -272,8 +306,8 @@ function pin(manager, appDir, argv) {
|
|
|
272
306
|
const moved = lockfileDrift(lockfile.format, before, readFileSync(lockfile.path, 'utf8'), importer, CLI_PACKAGE);
|
|
273
307
|
if (moved.length > 0) {
|
|
274
308
|
throw new Error(`\`${argv.join(' ')}\` also changed ${moved.length} lockfile ${moved.length === 1 ? 'entry' : 'entries'} the CLI does not own`
|
|
275
|
-
+ ` (${moved.slice(0, 5).join(', ')}${moved.length > 5 ? ', …' : ''})
|
|
276
|
-
+ `
|
|
309
|
+
+ ` (${moved.slice(0, 5).join(', ')}${moved.length > 5 ? ', …' : ''}). Add ${CLI_PACKAGE} at its exact version to`
|
|
310
|
+
+ ` ${inside(manager.root, appDir) || '.'} yourself, keeping the lockfile's other entries, then run \`haystack init\` again`);
|
|
277
311
|
}
|
|
278
312
|
}
|
|
279
313
|
function isNode(value) {
|
|
@@ -1006,6 +1040,22 @@ export function previewTelemetry(plan, repository, notes) {
|
|
|
1006
1040
|
const profile = dbProfile(repository, null, notes);
|
|
1007
1041
|
return { telemetry: { capture: plan.capture, server: serverPartOf(plan), dbProfileCommand: profile?.command ?? null }, steps };
|
|
1008
1042
|
}
|
|
1043
|
+
/** A telemetry plan with nothing planned, for a run whose telemetry could not be planned at all (failedTelemetry says why). */
|
|
1044
|
+
export function emptyTelemetryPlan() {
|
|
1045
|
+
return { app: null, changes: [], notices: [], server: { status: 'manual', kind: null, steps: [] }, settings: null,
|
|
1046
|
+
capture: { status: 'manual', steps: [], received: null }, captureRegistration: null };
|
|
1047
|
+
}
|
|
1048
|
+
/** Telemetry that could not be set up (a package manager failed, a file init could not read, Haystack not answering): each
|
|
1049
|
+
* part it would have set up is manual with what happened, every file telemetry changed is back, and the rest of init stands
|
|
1050
|
+
* (telemetry never fails init). */
|
|
1051
|
+
export function failedTelemetry(plan, failure) {
|
|
1052
|
+
const step = `init could not set up telemetry: ${failure.replace(/\.?\s*$/, '')}. It put back every file telemetry changed; the rest of init is`
|
|
1053
|
+
+ ' done. Fix that, then run `haystack init` again.';
|
|
1054
|
+
// A part that was already waiting for answers keeps its questions after what happened.
|
|
1055
|
+
const part = (value) => (value.status === 'unsupported' ? value
|
|
1056
|
+
: { status: 'manual', steps: [step, ...(value.status === 'manual' ? value.steps : [])], received: null });
|
|
1057
|
+
return { telemetry: { capture: part(plan.capture), server: part(serverPartOf(plan)), dbProfileCommand: null }, steps: [step] };
|
|
1058
|
+
}
|
|
1009
1059
|
/** Rule 8(e): the report, and what the person does for telemetry to arrive (the token, production's environment, the start,
|
|
1010
1060
|
* the deploy) with the off switches (rule 13c). `onboarding` and `notes` feed the data profile. */
|
|
1011
1061
|
export async function telemetryReport(plan, context) {
|
package/dist/commands/init.js
CHANGED
|
@@ -45,16 +45,21 @@ export const AGENT_NOTE = [
|
|
|
45
45
|
NOTE_BEGIN,
|
|
46
46
|
'## Haystack',
|
|
47
47
|
'',
|
|
48
|
-
'After you change this app\'s code, or when the user asks you to check a change,
|
|
49
|
-
'
|
|
50
|
-
'
|
|
48
|
+
'After you change this app\'s code, or when the user asks you to check a change, check it with Haystack before you call',
|
|
49
|
+
'the work done. It runs the app with and without the change, explores it, and reports what broke. It changes nothing,',
|
|
50
|
+
'so run it without asking.',
|
|
51
51
|
'',
|
|
52
|
-
'
|
|
52
|
+
'1. `haystack pre-verify`: what your change touches, what production and real users do there, and Haystack\'s ideas',
|
|
53
|
+
' of what could break.',
|
|
54
|
+
'2. `haystack verify --intent "<what you were asked to do, in the user\'s words>" --idea "<something to try>"`: keep the',
|
|
55
|
+
' ideas worth trying, add your own (repeat --idea); yours are explored first.',
|
|
56
|
+
'',
|
|
57
|
+
'- `verify` often takes 10 to 15 minutes: give it your longest timeout or run it in the background, and do not pipe it',
|
|
53
58
|
' through `tail` or `head`, which hide its progress. If your command times out, run it again: it picks up the same crawl.',
|
|
54
59
|
'- Fix each bug it reports that your change caused, then run `haystack verify` again.',
|
|
55
60
|
'- If it says onboarding needs an answer, ask the user that question and file their choice with',
|
|
56
61
|
' `haystack verify answer <question-id> <choice>`.',
|
|
57
|
-
'- Read
|
|
62
|
+
'- Read verify\'s short report. `--json` prints the same report as data; `--raw` prints the crawl\'s whole record (large), for scripts;',
|
|
58
63
|
' `haystack verify --help` explains the output and exit codes.',
|
|
59
64
|
'- If `haystack` is not installed, run each command as `npx -y -p @haystackeditor/cli@latest haystack <command>`.',
|
|
60
65
|
'- If Haystack fails, confuses you or gets in your way, say so in a sentence with',
|
|
@@ -299,6 +304,33 @@ export function planInit(gitRoot) {
|
|
|
299
304
|
const notices = [];
|
|
300
305
|
return { changes: [...planStopHook(gitRoot, notices), ...planAgentNotes(gitRoot, notices)], notices };
|
|
301
306
|
}
|
|
307
|
+
/** Makes `changes` all or nothing: one that fails (an install that does not end as planned) restores every file they
|
|
308
|
+
* touched (bytes, not text: a lockfile can be binary), so no edit is left referencing what was never installed. */
|
|
309
|
+
function applyAll(gitRoot, changes, say) {
|
|
310
|
+
const before = new Map();
|
|
311
|
+
for (const planned of changes) {
|
|
312
|
+
for (const path of [planned.target, ...(planned.run?.touches ?? [])]) {
|
|
313
|
+
if (!before.has(path))
|
|
314
|
+
before.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
for (const planned of changes) {
|
|
319
|
+
applyChange(gitRoot, planned);
|
|
320
|
+
say(chalk.green(`✓ ${planned.action === 'create' ? 'Created' : planned.action === 'delete' ? 'Deleted' : 'Updated'} ${planned.path}`));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
for (const [path, content] of before) {
|
|
325
|
+
if (content !== null)
|
|
326
|
+
writeFileSync(path, content);
|
|
327
|
+
else if (existsSync(path))
|
|
328
|
+
unlinkSync(path);
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
302
334
|
function applyChange(gitRoot, planned) {
|
|
303
335
|
const inside = relative(gitRoot, planned.target);
|
|
304
336
|
if (!inside.startsWith('..') && !isAbsolute(inside))
|
|
@@ -362,19 +394,35 @@ export async function initCommand(options) {
|
|
|
362
394
|
// Read before anything else, so notes that cannot be used stop init with nothing done.
|
|
363
395
|
const notes = options.notes === undefined ? null : readNotes(options.notes);
|
|
364
396
|
const plan = planInit(gitRoot);
|
|
365
|
-
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it.
|
|
397
|
+
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it. It never fails
|
|
398
|
+
// init: whatever goes wrong with it (a file it cannot read, a package manager that fails, Haystack not answering) puts its
|
|
399
|
+
// own files back and leaves it manual with what happened, and the rest of init goes on.
|
|
366
400
|
const telemetryModule = await import('./init-telemetry.js');
|
|
367
401
|
const telemetryOptions = { app: options.app, origins: options.origin ?? [], consent: options.consent, urlRewrites: options.urlRewrites };
|
|
368
|
-
let
|
|
402
|
+
let telemetryFailure = null;
|
|
403
|
+
let telemetryPlan = telemetryModule.emptyTelemetryPlan();
|
|
404
|
+
try {
|
|
405
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, telemetryOptions);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
telemetryFailure = errorText(error);
|
|
409
|
+
}
|
|
369
410
|
const telemetryFrom = plan.changes.length;
|
|
370
411
|
plan.changes.push(...telemetryPlan.changes);
|
|
371
412
|
plan.notices.push(...telemetryPlan.notices);
|
|
372
413
|
let notesWritten = null;
|
|
373
414
|
let telemetry = null;
|
|
415
|
+
/** Telemetry did not happen: its changes leave the plan (none of them was made), and the report says why. */
|
|
416
|
+
const telemetryFailed = (error) => {
|
|
417
|
+
telemetryFailure = errorText(error);
|
|
418
|
+
plan.changes.splice(telemetryFrom);
|
|
419
|
+
say(chalk.yellow(`Telemetry was not set up: ${telemetryFailure} The rest of init goes on; the telemetry report says what to do.`));
|
|
420
|
+
};
|
|
374
421
|
// Before the apply (a preview, or no login), the report is the preview: what telemetry will do, `received` unchecked (null).
|
|
375
|
-
const report = () =>
|
|
422
|
+
const report = () => (telemetryFailure !== null ? telemetryModule.failedTelemetry(telemetryPlan, telemetryFailure)
|
|
423
|
+
: telemetry ?? telemetryModule.previewTelemetry(telemetryPlan, repository, notes));
|
|
376
424
|
const showPreview = () => {
|
|
377
|
-
const preview =
|
|
425
|
+
const preview = report();
|
|
378
426
|
for (const line of telemetryModule.formatTelemetry(preview.telemetry, preview.steps))
|
|
379
427
|
say(line);
|
|
380
428
|
say('');
|
|
@@ -424,47 +472,52 @@ export async function initCommand(options) {
|
|
|
424
472
|
return;
|
|
425
473
|
}
|
|
426
474
|
}
|
|
427
|
-
//
|
|
428
|
-
|
|
475
|
+
// Init's own changes (the agent instructions, the Stop hook) first, all or nothing: a failure there is init's.
|
|
476
|
+
try {
|
|
477
|
+
applyAll(gitRoot, plan.changes.slice(0, telemetryFrom), say);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
throw new Error(`${errorText(error)} Every file init changed was restored; nothing was changed.`);
|
|
481
|
+
}
|
|
482
|
+
// Then telemetry, all or nothing on its own. Browser capture's registration (rule 8b), which the preview announced, is
|
|
483
|
+
// checked with Haystack first, and when the files need a new key the telemetry changes are planned again with it (or
|
|
484
|
+
// without capture's, when it is refused).
|
|
429
485
|
const approved = options.yes === true || plan.changes.length > 0;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
for (const planned of plan.changes) {
|
|
441
|
-
for (const path of [planned.target, ...(planned.run?.touches ?? [])]) {
|
|
442
|
-
if (!before.has(path))
|
|
443
|
-
before.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
486
|
+
let capture = null;
|
|
487
|
+
if (telemetryFailure === null) {
|
|
488
|
+
try {
|
|
489
|
+
capture = telemetryPlan.captureRegistration
|
|
490
|
+
? await telemetryModule.prepareCapture(telemetryPlan.captureRegistration, token, say, approved) : null;
|
|
491
|
+
if (capture?.replan) {
|
|
492
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, { ...telemetryOptions, registration: capture.replan });
|
|
493
|
+
plan.changes.splice(telemetryFrom, plan.changes.length - telemetryFrom, ...telemetryPlan.changes);
|
|
494
|
+
}
|
|
495
|
+
applyAll(gitRoot, plan.changes.slice(telemetryFrom), say);
|
|
444
496
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
497
|
+
catch (error) {
|
|
498
|
+
try {
|
|
499
|
+
await capture?.rollback(say);
|
|
500
|
+
}
|
|
501
|
+
catch (rollbackError) {
|
|
502
|
+
say(chalk.yellow(`Capture's registration could not be put back: ${errorText(rollbackError)}`));
|
|
503
|
+
}
|
|
504
|
+
telemetryFailed(error);
|
|
450
505
|
}
|
|
451
506
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
507
|
+
if (telemetryFailure === null && capture) {
|
|
508
|
+
try {
|
|
509
|
+
const captureFollowUp = await capture.finish(say);
|
|
510
|
+
if (captureFollowUp)
|
|
511
|
+
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, captureFollowUp);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, `Run \`haystack init\` again: Haystack did not take the app's origins (${errorText(error)}).`);
|
|
458
515
|
}
|
|
459
|
-
await capture?.rollback(say);
|
|
460
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)} Every file init changed was restored; nothing was changed.`);
|
|
461
516
|
}
|
|
462
|
-
const captureFollowUp = await capture?.finish(say);
|
|
463
|
-
if (captureFollowUp)
|
|
464
|
-
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, captureFollowUp);
|
|
465
517
|
let onboarding;
|
|
466
518
|
try {
|
|
467
|
-
// The notes first: onboarding plans from the facts it starts with,
|
|
519
|
+
// The notes first: onboarding plans from the facts it starts with (rule 2, amendment 34: new notes onboard again only while
|
|
520
|
+
// the repository has no setup, after a run that ended without one).
|
|
468
521
|
if (notes) {
|
|
469
522
|
notesWritten = await writeOnboardingNotes(repository, notes, token);
|
|
470
523
|
say(notesWritten === 'written' ? chalk.green('✓ Wrote the onboarding notes') : chalk.dim('The onboarding notes are already these.'));
|
|
@@ -485,8 +538,18 @@ export async function initCommand(options) {
|
|
|
485
538
|
reportOnboardingState(onboarding);
|
|
486
539
|
say(formatOnboarding(onboarding));
|
|
487
540
|
say('');
|
|
488
|
-
|
|
489
|
-
|
|
541
|
+
if (telemetryFailure === null) {
|
|
542
|
+
try {
|
|
543
|
+
telemetry = await telemetryModule.telemetryReport(telemetryPlan, { repository, token, onboarding, notes });
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
// What Haystack holds could not be read: the report as previewed (`received` unchecked), and why.
|
|
547
|
+
const preview = telemetryModule.previewTelemetry(telemetryPlan, repository, notes);
|
|
548
|
+
telemetry = { telemetry: preview.telemetry, steps: [...preview.steps, `Run \`haystack init --json\` again to read what telemetry Haystack has received (it could not: ${errorText(error)}).`] };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const shown = report();
|
|
552
|
+
for (const line of telemetryModule.formatTelemetry(shown.telemetry, shown.steps))
|
|
490
553
|
say(line);
|
|
491
554
|
say('');
|
|
492
555
|
if (onboarding.state === 'ready')
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* locks older versions of packages the CLI also uses):
|
|
5
5
|
* - yarn 1, yarn 4 and bun add only the new descriptors and entries; nothing else moves.
|
|
6
6
|
* - npm hoists the new tree and replaces hoisted packages the app already locks (ws, yaml, zod moved to the CLI's
|
|
7
|
-
* versions)
|
|
7
|
+
* versions), and re-resolves other entries even with `--install-strategy=nested` (39 in fosrl/pangolin): init never
|
|
8
|
+
* runs it (init-telemetry.ts leaves the npm install to the person, who sees the lockfile's diff).
|
|
8
9
|
* - pnpm re-resolves the dependencies of every package the new tree shares with the old one to the newest version in the
|
|
9
10
|
* graph (47 entries in Rallly, from @smithy/* to express), and no flag stops it. So init lets pnpm resolve the new
|
|
10
11
|
* dependency (`--lockfile-only`), keeps every entry the lockfile had, adds the entries only the new dependency
|
|
11
12
|
* reaches (mergePnpmLockfile), and installs exactly that (`pnpm install --frozen-lockfile`, which also proves it).
|
|
12
|
-
* lockfileDrift checks the result for every format init
|
|
13
|
-
* change (and init restores
|
|
13
|
+
* lockfileDrift checks the result for every format init runs, so a package manager that moves anything else fails the
|
|
14
|
+
* change (and init restores telemetry's files, leaving telemetry manual).
|
|
14
15
|
*/
|
|
15
16
|
function unquote(text) {
|
|
16
17
|
if (text.startsWith('\'') && text.endsWith('\''))
|
|
@@ -271,11 +272,6 @@ function entriesOf(format, text, importer) {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
|
-
else if (format === 'npm') {
|
|
275
|
-
const packages = JSON.parse(text).packages ?? {};
|
|
276
|
-
for (const [key, value] of Object.entries(packages))
|
|
277
|
-
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : key, JSON.stringify(value));
|
|
278
|
-
}
|
|
279
275
|
else if (format === 'yarn') {
|
|
280
276
|
for (const block of text.split(/\n\s*\n/)) {
|
|
281
277
|
const lines = block.split('\n').filter(line => line.trim() !== '' && !line.startsWith('#'));
|
|
@@ -106,6 +106,13 @@ export function parseOnboardingStatus(value, expected) {
|
|
|
106
106
|
invalid('its review');
|
|
107
107
|
if (value.state !== 'ready' && review !== null)
|
|
108
108
|
invalid('its review');
|
|
109
|
+
// Rule 2 (amendment 34): a ready status names the base its record was made at and when it was published.
|
|
110
|
+
const ready = value.state === 'ready';
|
|
111
|
+
if (ready ? !(typeof value.recordBaseCommit === 'string' && COMMIT.test(value.recordBaseCommit)) : value.recordBaseCommit !== null)
|
|
112
|
+
invalid('its record base');
|
|
113
|
+
if (ready ? !(typeof value.recordPublishedAt === 'string' && !Number.isNaN(Date.parse(value.recordPublishedAt)))
|
|
114
|
+
: value.recordPublishedAt !== null)
|
|
115
|
+
invalid('its record time');
|
|
109
116
|
if (value.block !== null)
|
|
110
117
|
parseOnboardingBlock(value.block);
|
|
111
118
|
const checkpoint = value.checkpoint;
|
|
@@ -147,8 +154,8 @@ async function body(response, what) {
|
|
|
147
154
|
throw new OnboardingResponseError(`The onboarding service returned ${what} that is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
|
-
/** The onboarding of `repository
|
|
151
|
-
*
|
|
157
|
+
/** The onboarding of `repository`, asked at `baseCommit` (rule 2, amendment 34: one setup per repository, the same at every
|
|
158
|
+
* base); with `onboardRunId`, that run's, or the repository's record once it has one: what a waiting command follows. */
|
|
152
159
|
export async function readOnboardingStatus(repository, baseCommit, token, onboardRunId = null) {
|
|
153
160
|
if (!COMMIT.test(baseCommit))
|
|
154
161
|
throw new Error(`Invalid base commit ${baseCommit}.`);
|
|
@@ -159,7 +166,7 @@ export async function readOnboardingStatus(repository, baseCommit, token, onboar
|
|
|
159
166
|
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
160
167
|
return parseOnboardingStatus(await body(response, 'a status'), { repository, baseCommit });
|
|
161
168
|
}
|
|
162
|
-
/** Rule 12's start POST: onboard
|
|
169
|
+
/** Rule 12's start POST: onboard the repository now, at this base, before any change exists. Idempotent: a
|
|
163
170
|
* ready, blocked or running onboarding is answered as it is. */
|
|
164
171
|
export async function startOnboarding(repository, baseCommit, token) {
|
|
165
172
|
if (!COMMIT.test(baseCommit))
|
|
@@ -198,8 +205,7 @@ export async function waitForOnboarding(first, token, progress, resume) {
|
|
|
198
205
|
await delay(ONBOARDING_READ_EVERY_MS);
|
|
199
206
|
let next;
|
|
200
207
|
try {
|
|
201
|
-
// The run this wait follows,
|
|
202
|
-
// not-started while the run goes on to admit this command's crawl (galaxy's n8n replays, 2026-10-03).
|
|
208
|
+
// The run this wait follows, until the repository has a record.
|
|
203
209
|
next = await readOnboardingStatus(status.repository, status.baseCommit, token, status.onboardRunId);
|
|
204
210
|
}
|
|
205
211
|
catch (error) {
|
|
@@ -282,16 +288,16 @@ export function blockLines(block) {
|
|
|
282
288
|
return lines;
|
|
283
289
|
}
|
|
284
290
|
export function formatOnboarding(status) {
|
|
285
|
-
const lines = [chalk.dim(` Onboarding ${status.onboardRunId ?? '(none)'} of ${safe(status.repository)} at base ${status.baseCommit.slice(0, 12)}`)];
|
|
291
|
+
const lines = [chalk.dim(` Onboarding ${status.onboardRunId ?? '(none)'} of ${safe(status.repository)}, asked at base ${status.baseCommit.slice(0, 12)}`)];
|
|
286
292
|
switch (status.state) {
|
|
287
293
|
case 'ready':
|
|
288
|
-
lines.push(
|
|
294
|
+
lines.push(readyLine(status));
|
|
289
295
|
break;
|
|
290
296
|
case 'onboarding':
|
|
291
297
|
lines.push(onboardingStep(status));
|
|
292
298
|
break;
|
|
293
299
|
case 'not-started':
|
|
294
|
-
lines.push('Onboarding has not started for this
|
|
300
|
+
lines.push('Onboarding has not started for this repository. `haystack init` starts it.');
|
|
295
301
|
break;
|
|
296
302
|
case 'incomplete':
|
|
297
303
|
lines.push(`Onboarding stopped before finishing${status.checkpoint?.error ? `: ${safe(status.checkpoint.error.message)}` : ''}.`
|
|
@@ -319,6 +325,20 @@ export function formatOnboarding(status) {
|
|
|
319
325
|
lines.push('', ...unreviewed.map(note => chalk.yellow(`Note: ${safe(note)}`)));
|
|
320
326
|
return lines.join('\n');
|
|
321
327
|
}
|
|
328
|
+
/** Rule 2 (amendment 34): the repository's one setup, which base it was made at and how long ago, in one sentence. */
|
|
329
|
+
export function readyLine(status, now = Date.now()) {
|
|
330
|
+
if (status.recordBaseCommit === null || status.recordPublishedAt === null)
|
|
331
|
+
return 'The app is onboarded.';
|
|
332
|
+
return `The app is onboarded: its setup was made at base ${status.recordBaseCommit.slice(0, 12)} `
|
|
333
|
+
+ `${age(now - Date.parse(status.recordPublishedAt))} and serves every base.`;
|
|
334
|
+
}
|
|
335
|
+
/** "just now", "5 minutes ago", "3 hours ago", "2 days ago". */
|
|
336
|
+
function age(ms) {
|
|
337
|
+
const minutes = Math.floor(Math.max(0, ms) / 60_000);
|
|
338
|
+
const [count, unit] = minutes < 1 ? [0, ''] : minutes < 60 ? [minutes, 'minute'] : minutes < 2_880 ? [Math.floor(minutes / 60), 'hour']
|
|
339
|
+
: [Math.floor(minutes / 1_440), 'day'];
|
|
340
|
+
return count === 0 ? 'just now' : `${count} ${unit}${count === 1 ? '' : 's'} ago`;
|
|
341
|
+
}
|
|
322
342
|
/** Rule 17 (amendment 32): an onboarding the independent review skipped, as the sentence every result shows; none otherwise. */
|
|
323
343
|
export function reviewNotes(review) {
|
|
324
344
|
if (review?.verdict !== 'skipped')
|
|
@@ -397,7 +417,7 @@ export async function verifyAnswerCommand(questionId, choice, options) {
|
|
|
397
417
|
}
|
|
398
418
|
}
|
|
399
419
|
/** Makes the coding agent's notes the repository's onboarding notes (rule 7, amendment 18), unless they are exactly these
|
|
400
|
-
* already: 'unchanged' then, so running init again with the same notes
|
|
420
|
+
* already: 'unchanged' then, so running init again with the same notes changes no facts. */
|
|
401
421
|
export async function writeOnboardingNotes(repository, notes, token) {
|
|
402
422
|
for (let attempt = 1;; attempt += 1) {
|
|
403
423
|
const facts = await readFacts(repository, token);
|
|
@@ -486,7 +486,7 @@ function printResult(result) {
|
|
|
486
486
|
else if (crawl?.status === 'unavailable')
|
|
487
487
|
console.log(`Crawl unavailable: ${crawlUnavailableText(crawl.reason)}.`);
|
|
488
488
|
else if (crawl?.status === 'onboarding') {
|
|
489
|
-
console.log(`The app is being onboarded
|
|
489
|
+
console.log(`The app is being onboarded (${crawl.onboardRunId}); this change is built and frozen when it is ready.`
|
|
490
490
|
+ ' Run `haystack verify` to check it.');
|
|
491
491
|
}
|
|
492
492
|
else if (crawl?.status === 'onboarding-blocked')
|
package/dist/commands/verify.js
CHANGED
|
@@ -32,7 +32,7 @@ import { GATEWAY_TIMEOUT_MS, gatewayFetch } from './case-batch.js';
|
|
|
32
32
|
import { CRAWL_BRIEF_MAX_FUNCTIONS, CRAWL_BRIEF_MAX_GROUPS, CRAWL_BRIEF_MAX_PRODUCTION_ROWS, CRAWL_BUDGET_MAX_MS, CRAWL_BUDGET_MIN_MS, CRAWL_MAX_FINDINGS, CRAWL_MAX_FINDING_STEPS, CRAWL_MAX_IDEAS, CRAWL_MAX_STAND_IN_LINES, CRAWL_MAX_STAND_IN_ROWS, CRAWL_MAX_TEXT_CHARS, CRAWL_WAIT_MAX_MS, CRAWL_POOLS, } from './crawl-contract.js';
|
|
33
33
|
import { captureCheckout, crawlUnavailableText, EXPLICIT_WALL_MS, normalModeFailure, postPrecomputeCapture, } from './verify-precompute.js';
|
|
34
34
|
import { SPOT_WORDS, TERMINAL, VERDICT_WORDS, VERDICTS, currentStep, errorWords, headline, plural, reportedStatus, results, verifyReport, } from './crawl-report.js';
|
|
35
|
-
import { formatOnboarding, onboardingExitCode, readOnboardingStatus, reportOnboardingState, reviewNotes, standInGapNotes, waitForOnboarding, } from './verify-onboarding.js';
|
|
35
|
+
import { formatOnboarding, onboardingExitCode, readOnboardingStatus, readyLine, reportOnboardingState, reviewNotes, standInGapNotes, waitForOnboarding, } from './verify-onboarding.js';
|
|
36
36
|
import { formatCapture, readPreVerifyCapture } from './capture-brief.js';
|
|
37
37
|
/** A held read (waitAfter): the service's hold, then the time any read gets. */
|
|
38
38
|
const HELD_READ_TIMEOUT_MS = CRAWL_WAIT_MAX_MS + GATEWAY_TIMEOUT_MS;
|
|
@@ -660,11 +660,11 @@ async function submitCapture(capture, token, found) {
|
|
|
660
660
|
line: `${before}, and ${found === null ? 'one' : 'another'} could not be started: ${crawlUnavailableText(crawl.reason)}.` };
|
|
661
661
|
}
|
|
662
662
|
if (crawl.status === 'onboarding') {
|
|
663
|
-
return { runId: null, onboarding: 'onboarding', line: `${before}. The app is not onboarded
|
|
663
|
+
return { runId: null, onboarding: 'onboarding', line: `${before}. The app is not onboarded yet: `
|
|
664
664
|
+ `the service is preparing it (${crawl.onboardRunId}) and runs the crawl when it is ready.` };
|
|
665
665
|
}
|
|
666
666
|
if (crawl.status === 'onboarding-blocked') {
|
|
667
|
-
return { runId: null, onboarding: 'onboarding-blocked', line: `${before}, and the app's onboarding
|
|
667
|
+
return { runId: null, onboarding: 'onboarding-blocked', line: `${before}, and the app's onboarding is blocked.` };
|
|
668
668
|
}
|
|
669
669
|
if (found === null) {
|
|
670
670
|
return { runId: crawl.runId, onboarding: null, line: crawl.status === 'queued'
|
|
@@ -834,9 +834,12 @@ export function formatBrief(brief) {
|
|
|
834
834
|
lines.push(` ${safe(change.changeKey)}: ${safe(change.reason)}`);
|
|
835
835
|
}
|
|
836
836
|
}
|
|
837
|
-
lines.push('', 'Next: pick the ideas worth trying, add your own, and say what you were asked to do (the task in the user\'s words, not what your code does):', ' haystack verify --intent "<what you were asked to do>" --idea "<something to try>" [--idea ...]', 'Your ideas are explored first; the ideas above are explored in every crawl too.');
|
|
838
837
|
return lines.join('\n');
|
|
839
838
|
}
|
|
839
|
+
/** The handshake's question, asked after everything pre-verify shows. */
|
|
840
|
+
const STEER_LINES = ['', 'Next: pick the ideas worth trying, add your own, and say what you were asked to do (the task in the user\'s words, not what your code does):',
|
|
841
|
+
' haystack verify --intent "<what you were asked to do>" --idea "<something to try>" [--idea ...]',
|
|
842
|
+
'Your ideas are explored first; the ideas above are explored in every crawl too.'];
|
|
840
843
|
/** `haystack pre-verify` (CRAWL-V1 amendment 18): the brief of the current change, before any crawl. It captures the checkout as
|
|
841
844
|
* the turn-end hook does, prepares it when no run of that capture is under way or done (it never widens a run to a crawl), and
|
|
842
845
|
* prints the run's brief. Exit codes: 0 with a brief, 2 when the run ended without one, 3 onboarding blocked, 1 otherwise. */
|
|
@@ -886,7 +889,7 @@ export async function preVerifyCommand(options) {
|
|
|
886
889
|
process.exitCode = onboardingExitCode(onboarding);
|
|
887
890
|
return;
|
|
888
891
|
}
|
|
889
|
-
note(
|
|
892
|
+
note(readyLine(onboarding));
|
|
890
893
|
submitted = await submitCapture(capture, auth.token, found);
|
|
891
894
|
note(submitted.line);
|
|
892
895
|
}
|
|
@@ -920,6 +923,8 @@ export async function preVerifyCommand(options) {
|
|
|
920
923
|
if (!options.json && captured !== null)
|
|
921
924
|
for (const section of captured)
|
|
922
925
|
console.log(`\n${formatCapture(section)}`);
|
|
926
|
+
if (!options.json && brief !== null)
|
|
927
|
+
console.log(STEER_LINES.join('\n'));
|
|
923
928
|
if (brief === null && TERMINAL.has(reportedStatus(view)))
|
|
924
929
|
process.exitCode = 2;
|
|
925
930
|
}
|
|
@@ -981,9 +986,9 @@ export async function verifyCommand(options) {
|
|
|
981
986
|
finishWithOnboarding(onboarding, options, change);
|
|
982
987
|
return;
|
|
983
988
|
}
|
|
984
|
-
// Ready: the service admitted the waiting crawl when the record published
|
|
985
|
-
// submission of the same capture finds it
|
|
986
|
-
note(
|
|
989
|
+
// Ready: the service admitted the waiting crawl when the record published (amendment 12:
|
|
990
|
+
// every waiting request is admitted); this submission of the same capture finds it.
|
|
991
|
+
note(readyLine(onboarding));
|
|
987
992
|
submitted = await submitCapture(capture, auth.token, found);
|
|
988
993
|
note(submitted.line);
|
|
989
994
|
}
|
package/dist/index.js
CHANGED
|
@@ -119,7 +119,9 @@ an hour). The file is JSON with any of three notes, each plain text:
|
|
|
119
119
|
signIn how a person gets an account and signs in, and the kinds of users
|
|
120
120
|
workflow what a signed-in user mainly does, step by step
|
|
121
121
|
Leave out what you do not know. Onboarding still proves everything it takes
|
|
122
|
-
from the notes.
|
|
122
|
+
from the notes. The app is onboarded once: notes written before that feed its
|
|
123
|
+
onboarding (an onboarding that stopped on a question starts again with them);
|
|
124
|
+
after it, new notes start no onboarding.
|
|
123
125
|
|
|
124
126
|
Every run also plans telemetry for the app: --app names it when the repository
|
|
125
127
|
has several (init never picks one), --origin its production origins and
|
|
@@ -180,7 +182,8 @@ your crawl of that exact capture: the same code and the same change title (the
|
|
|
180
182
|
title tells the crawl's judge what the change is). When there is none, or the
|
|
181
183
|
one it finds was cancelled, stopped before finishing or is being cancelled, it
|
|
182
184
|
submits the capture (the service starts the crawl, or runs the stopped one
|
|
183
|
-
again), says so, and follows that crawl.
|
|
185
|
+
again), says so, and follows that crawl. When the stop hook already built and
|
|
186
|
+
froze this change, the crawl starts from that instead of building it again.
|
|
184
187
|
|
|
185
188
|
The coding agent that made the change can say what it was asked to do (--intent:
|
|
186
189
|
the task in the user's words, not a description of its code) and what to try (--idea, as many as it likes, or --ideas-file). Each idea is
|