staysfixed 0.7.2 → 0.9.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/CHANGELOG.md +429 -0
- package/README.md +193 -57
- package/docs/design-v2.md +24 -4
- package/docs/getting-started.md +19 -6
- package/docs/guards.md +2 -2
- package/docs/how-v2-works.md +12 -11
- package/docs/mcp.md +17 -8
- package/docs/settings.md +564 -0
- package/docs/watching.md +10 -4
- package/examples/staysfixed.config.electron.js +17 -6
- package/examples/staysfixed.config.web.js +22 -5
- package/package.json +2 -1
- package/src/cli/index.js +55 -46
- package/src/cli/status.js +45 -1
- package/src/cli/watch-flags.js +54 -0
- package/src/core/config.js +54 -3
- package/src/core/paths.js +15 -0
- package/src/guard/run.js +70 -3
- package/src/report/console.js +50 -6
- package/src/run.js +11 -0
- package/src/types.js +3 -0
- package/src/v2/adapters/android-driver.js +6 -1
- package/src/v2/adapters/android.js +97 -2
- package/src/v2/adapters/child.js +101 -0
- package/src/v2/adapters/contract.js +42 -5
- package/src/v2/adapters/electron.js +72 -6
- package/src/v2/adapters/http.js +18 -11
- package/src/v2/adapters/ios-driver.js +64 -14
- package/src/v2/adapters/ios.js +247 -25
- package/src/v2/adapters/process.js +783 -71
- package/src/v2/adapters/python.js +495 -0
- package/src/v2/adapters/source.js +373 -18
- package/src/v2/adapters/web-driver.js +134 -24
- package/src/v2/adapters/web.js +149 -18
- package/src/v2/adapters/windows.js +18 -1
- package/src/v2/browsers.js +66 -3
- package/src/v2/cause.js +61 -17
- package/src/v2/check.js +653 -69
- package/src/v2/ci.js +130 -35
- package/src/v2/cli.js +65 -42
- package/src/v2/cluster.js +220 -14
- package/src/v2/coverage.js +43 -176
- package/src/v2/detect.js +308 -60
- package/src/v2/doctor.js +353 -54
- package/src/v2/escalate.js +5 -1
- package/src/v2/init.js +183 -66
- package/src/v2/intent.js +9 -23
- package/src/v2/journeys/from-suite.js +336 -30
- package/src/v2/journeys/index.js +99 -6
- package/src/v2/mcp/tools.js +90 -16
- package/src/v2/normalise.js +169 -23
- package/src/v2/observation.js +19 -33
- package/src/v2/rank.js +216 -23
- package/src/v2/reference.js +160 -24
- package/src/v2/remote.js +113 -18
- package/src/v2/run.js +103 -14
- package/src/v2/sealed.js +0 -20
- package/src/v2/selfcheck.js +190 -13
- package/src/v2/ship.js +55 -5
- package/src/v2/store.js +67 -1
- package/src/v2/types.js +12 -2
- package/src/v2/waiver.js +64 -54
- package/src/v2/watch/events.js +60 -215
- package/src/v2/watch/focus.js +14 -4
- package/src/v2/watch/panel.js +167 -17
package/src/v2/init.js
CHANGED
|
@@ -78,6 +78,14 @@ import { PRODUCT_KINDS, detectProject } from './detect.js';
|
|
|
78
78
|
* one need said twice — doctor asks "what is missing on this
|
|
79
79
|
* machine", this file asks "what is missing from these
|
|
80
80
|
* settings", and on a server with no snapshot both answer.
|
|
81
|
+
* @property {boolean} [stopgap] A need that exists only so a surface can never read as ready
|
|
82
|
+
* because the machine check came back with nothing to say. The
|
|
83
|
+
* machine check has actually looked at this machine, so when it
|
|
84
|
+
* does answer on the same topic its answer is better and this
|
|
85
|
+
* one steps aside. It is here for the case where it answers
|
|
86
|
+
* nothing at all, which is exactly when it goes quiet: doctor
|
|
87
|
+
* returns no Windows needs when there is no Windows host, which
|
|
88
|
+
* is the very situation somebody has to be told about.
|
|
81
89
|
*/
|
|
82
90
|
|
|
83
91
|
/**
|
|
@@ -267,7 +275,7 @@ const SURFACE_FOR_PRODUCT = /** @type {Record<string, string>} */ ({
|
|
|
267
275
|
* @param {Capabilities|null} machine
|
|
268
276
|
* @returns {Readiness[]}
|
|
269
277
|
*/
|
|
270
|
-
function readinessFor(project, machine) {
|
|
278
|
+
export function readinessFor(project, machine) {
|
|
271
279
|
/** @type {Readiness[]} */
|
|
272
280
|
const out = [];
|
|
273
281
|
|
|
@@ -296,8 +304,18 @@ function readinessFor(project, machine) {
|
|
|
296
304
|
}
|
|
297
305
|
|
|
298
306
|
const mine = productNeeds(product, project);
|
|
307
|
+
// A stopgap is deliberately kept out of what the machine check is told is already
|
|
308
|
+
// covered, so the machine check still gets to answer on that topic. If it does, its
|
|
309
|
+
// answer wins — it has looked at this actual machine and can name the host it found —
|
|
310
|
+
// and the stopgap drops out. If it says nothing, the stopgap is what stops the product
|
|
311
|
+
// being called ready over a surface that cannot run at all.
|
|
312
|
+
const fromMachine = machineNeeds(surface, product, mine.filter((need) => !need.stopgap));
|
|
313
|
+
const answered = new Set(fromMachine.map((need) => need.topic).filter(Boolean));
|
|
299
314
|
/** @type {Need[]} */
|
|
300
|
-
const needs = [
|
|
315
|
+
const needs = [
|
|
316
|
+
...mine.filter((need) => !(need.stopgap && need.topic && answered.has(need.topic))),
|
|
317
|
+
...fromMachine,
|
|
318
|
+
];
|
|
301
319
|
|
|
302
320
|
if (needs.length === 0) {
|
|
303
321
|
out.push({
|
|
@@ -311,15 +329,25 @@ function readinessFor(project, machine) {
|
|
|
311
329
|
continue;
|
|
312
330
|
}
|
|
313
331
|
|
|
314
|
-
|
|
332
|
+
// A need nobody can clear is not a job, and telling somebody they have work to do when
|
|
333
|
+
// they have none is how a set-up list gets a line that never comes off it. It still keeps
|
|
334
|
+
// the product out of "covered in full", which is the entire reason it exists.
|
|
335
|
+
const actionable = needs.filter((need) => need.who !== 'nobody');
|
|
336
|
+
const permanent = needs.filter((need) => need.who === 'nobody');
|
|
337
|
+
const everythingIsACommand = actionable.length > 0 && actionable.every((need) => need.who === 'the agent');
|
|
338
|
+
const alsoPermanent = permanent.length > 0 && actionable.length > 0
|
|
339
|
+
? ` One other thing about it can never be checked here, and it is listed below.`
|
|
340
|
+
: '';
|
|
315
341
|
out.push({
|
|
316
342
|
product: product.name,
|
|
317
343
|
kind: product.kind,
|
|
318
344
|
surface: product.surface,
|
|
319
345
|
state: everythingIsACommand ? 'the agent can fix this' : 'only a person can do this',
|
|
320
|
-
summary:
|
|
321
|
-
? `${sentenceCase(product.name)}
|
|
322
|
-
:
|
|
346
|
+
summary: actionable.length === 0
|
|
347
|
+
? `${sentenceCase(product.name)} is checked as far as this tool can reach it, and one thing about it can never be checked here: ${permanent[0].what}.`
|
|
348
|
+
: everythingIsACommand
|
|
349
|
+
? `${sentenceCase(product.name)} needs ${actionable.length === 1 ? 'one thing' : `${actionable.length} things`} setting up, and all of it can be done without asking anybody.${alsoPermanent}`
|
|
350
|
+
: `${sentenceCase(product.name)} needs ${actionable.filter((n) => n.who === 'a person').length === 1 ? 'one thing' : 'a few things'} only you can supply.${alsoPermanent}`,
|
|
323
351
|
needs,
|
|
324
352
|
});
|
|
325
353
|
}
|
|
@@ -372,6 +400,26 @@ function productNeeds(product, project) {
|
|
|
372
400
|
const needs = [];
|
|
373
401
|
const suggest = product.suggest ?? {};
|
|
374
402
|
|
|
403
|
+
// A product this tool can boot and can run but cannot READ. Nothing anybody does clears
|
|
404
|
+
// this one, and that is exactly why it is here: without it a Go server that gets booted
|
|
405
|
+
// and answered was reported as covered in full, and "in full" was a lie about the half
|
|
406
|
+
// nobody was looking at. A permanent hole said out loud is worth more than a clean result
|
|
407
|
+
// that means less than it says.
|
|
408
|
+
if (product.sourceBlind) {
|
|
409
|
+
const { language, reads } = product.sourceBlind;
|
|
410
|
+
needs.push({
|
|
411
|
+
what: reads ? `the rest of the ${language} source, which nothing here reads` : `the ${language} source, which nothing here reads`,
|
|
412
|
+
why: reads
|
|
413
|
+
? `The source channel reads ${reads} out of this ${language} project. Everything else it holds — what it exports, what it reads out of the environment — is not read at all, so a change to any of that is invisible to this tool.`
|
|
414
|
+
: `Nothing here reads ${language} source. ${product.kind === 'server' ? 'The server is booted and watched, but its addresses were never read, so a route that quietly disappears is invisible.' : 'The command is run and every word it prints is compared, but a change inside it that never reaches the output is invisible.'}`,
|
|
415
|
+
unlocks: 'nothing anybody can do today — it is written down so a clean run here is never mistaken for a full one',
|
|
416
|
+
fix: `Nobody can fix this. It is a limit of this tool, not of your project. What IS checked here is real: ${product.kind === 'server' ? 'the server is booted on a spare port and asked for every address that could be read' : 'the command is run and everything it prints, returns and touches is compared'}.`,
|
|
417
|
+
who: 'nobody',
|
|
418
|
+
product: product.name,
|
|
419
|
+
topic: 'source',
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
375
423
|
if (product.kind === 'electron') {
|
|
376
424
|
if (!product.built.found) {
|
|
377
425
|
needs.push({
|
|
@@ -429,6 +477,83 @@ function productNeeds(product, project) {
|
|
|
429
477
|
});
|
|
430
478
|
}
|
|
431
479
|
|
|
480
|
+
// An Android app with no package built. Exactly the same hole the iPhone one above was
|
|
481
|
+
// written to close, and it was still open here: a bare Gradle project was told "the Android
|
|
482
|
+
// app can be checked here now" and "nothing is being left out", and then the very next
|
|
483
|
+
// command said there was nothing to walk in this project at all. Setup promising more than
|
|
484
|
+
// the run can deliver is the one failure this tool cannot afford, because it is the failure
|
|
485
|
+
// that makes a clean result mean nothing.
|
|
486
|
+
if (product.kind === 'android' && !product.built.found) {
|
|
487
|
+
const build = typeof suggest.buildWith === 'string' ? String(suggest.buildWith) : null;
|
|
488
|
+
needs.push({
|
|
489
|
+
what: 'the app built as a package',
|
|
490
|
+
why: 'An Android app is checked by installing a built package on an emulator. There is no built package here yet, and a repository usually does not commit one.',
|
|
491
|
+
unlocks: 'opening the app on an emulator and reading what the screen says every control is and does',
|
|
492
|
+
fix: build
|
|
493
|
+
? `cd ${product.where} && ${build} (then set android.apk in the settings to the .apk it wrote)`
|
|
494
|
+
: 'Build the app the way this project normally does, then set android.apk in the settings to the .apk it wrote.',
|
|
495
|
+
who: build ? 'the agent' : 'a person',
|
|
496
|
+
product: product.name,
|
|
497
|
+
topic: 'app',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// A native desktop app, which needs two things this machine may not have, and which had no
|
|
502
|
+
// branch here at all. Its adapter refuses to run without a machine of the right operating
|
|
503
|
+
// system AND a built program — and doctor returns NO Windows needs at all when there is no
|
|
504
|
+
// Windows host, which is precisely the case somebody has to be told about. Empty needs plus
|
|
505
|
+
// an empty machine list read as "ready", so a Tauri app on a machine with no Windows box
|
|
506
|
+
// was reported as covered in full, and then the run said there was nowhere to open it.
|
|
507
|
+
//
|
|
508
|
+
// Both are stopgaps: on a machine where doctor DID find a host it says so by name, which is
|
|
509
|
+
// more useful than anything that can be written here, and these step aside for it.
|
|
510
|
+
if (product.kind === 'desktopNative') {
|
|
511
|
+
needs.push({
|
|
512
|
+
what: 'a machine running the operating system this app is built for',
|
|
513
|
+
why: 'A native window can only be read from the operating system it runs on, so this needs a machine running that one. Any ssh host that gets you a shell counts, including a shell on a Windows box, and nothing has to be installed on it.',
|
|
514
|
+
unlocks: 'opening the window and reading what it says every control is and does',
|
|
515
|
+
fix: 'Put {"host": "your-windows-box"} under "windows" in the settings.',
|
|
516
|
+
who: 'a person',
|
|
517
|
+
product: product.name,
|
|
518
|
+
topic: 'host',
|
|
519
|
+
stopgap: true,
|
|
520
|
+
});
|
|
521
|
+
if (!product.built.found) {
|
|
522
|
+
needs.push({
|
|
523
|
+
what: 'the built program',
|
|
524
|
+
why: 'A native app is checked by opening the built program, not the source, and there is no built program here yet.',
|
|
525
|
+
unlocks: 'opening the real window instead of reading about it',
|
|
526
|
+
fix: `Build it the way this project normally does${project.scripts.build ? ` — \`${project.scripts.build}\`` : ''}, then put {"exe": "path/to/YourApp.exe"} under "windows" in the settings — or {"remoteExe": "C:\\\\path\\\\to\\\\YourApp.exe"} if the build already lives on that machine, which is much faster.`,
|
|
527
|
+
who: project.scripts.build ? 'the agent' : 'a person',
|
|
528
|
+
product: product.name,
|
|
529
|
+
topic: 'app',
|
|
530
|
+
stopgap: true,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// A command-line program or a library with nothing named to run or import. The process
|
|
536
|
+
// adapter refuses outright in that state, and nothing here said so: doctor's answer for
|
|
537
|
+
// this surface is hardcoded to an empty list, so an empty needs list met an empty machine
|
|
538
|
+
// list and the product read as ready over an adapter that would not start.
|
|
539
|
+
const somethingToRun = (Array.isArray(suggest.commands) ? suggest.commands.length : 0)
|
|
540
|
+
+ (Array.isArray(suggest.imports) ? suggest.imports.length : 0);
|
|
541
|
+
if ((product.kind === 'cli' || product.kind === 'library') && somethingToRun === 0) {
|
|
542
|
+
needs.push({
|
|
543
|
+
what: product.kind === 'library' ? 'something to import and compare' : 'a list of commands worth running',
|
|
544
|
+
why: 'Nothing was worked out here that this could actually run or import, and a run with nothing to do proves nothing about anything.',
|
|
545
|
+
unlocks: product.kind === 'library'
|
|
546
|
+
? 'comparing what it exports and what those exports do'
|
|
547
|
+
: 'every word of what a command prints, what it exits with and every file it touches',
|
|
548
|
+
fix: product.kind === 'library'
|
|
549
|
+
? 'Put {"imports": [{"name": "the package entry", "module": "./src/index.js"}]} under "process" in the settings.'
|
|
550
|
+
: 'Put {"commands": [{"name": "help", "run": "your-command --help"}]} under "process" in the settings. Only ever --help: a command in a manifest could deploy or publish, and running one because it was there would be this tool causing the damage it exists to catch.',
|
|
551
|
+
who: 'a person',
|
|
552
|
+
product: product.name,
|
|
553
|
+
topic: 'commands',
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
432
557
|
// A command-line program that has to be built before it can be run. This is what a product
|
|
433
558
|
// nothing in package.json names looks like on a fresh clone: the source is there, the
|
|
434
559
|
// program is real, and the file that would be run does not exist yet.
|
|
@@ -651,7 +776,16 @@ function topicOf(text) {
|
|
|
651
776
|
const words = text.toLowerCase();
|
|
652
777
|
if (/snapshot|restore|database|the same data|data folder/.test(words)) return 'data';
|
|
653
778
|
if (/starts it|command that starts|start\b/.test(words)) return 'start';
|
|
654
|
-
|
|
779
|
+
// "a built app", "a built iPhone app", "a built package" and "the app built as a package"
|
|
780
|
+
// are one missing thing, not four. The pattern used to be the two literal words "built app"
|
|
781
|
+
// and so it matched none of the ones with the platform's name in the middle — which is how
|
|
782
|
+
// an iPhone app with nothing built ended up asked for twice on one screen, once with a
|
|
783
|
+
// command to run and once with a paragraph saying the tool would never run it. Two lines
|
|
784
|
+
// about one missing file, apparently disagreeing, in front of somebody who does not write
|
|
785
|
+
// code.
|
|
786
|
+
if (/built (?:[a-z]+ )?(?:app|bundle|package|program)|built as a (?:package|bundle)|app\.binary|electron\.binary|android\.apk|\bapk\b|windows\.exe|remoteexe/.test(words)) return 'app';
|
|
787
|
+
if (/machine with a windows desktop|windows machine|ssh host|"host":/.test(words)) return 'host';
|
|
788
|
+
if (/commands worth running|to import and compare|process\.commands/.test(words)) return 'commands';
|
|
655
789
|
if (/device id|identity|identityenv/.test(words)) return 'identity';
|
|
656
790
|
if (/sample|real value/.test(words)) return 'samples';
|
|
657
791
|
if (/browser|playwright|chromium/.test(words)) return 'browser';
|
|
@@ -690,7 +824,10 @@ function sortNeeds(readiness, project, machine) {
|
|
|
690
824
|
what: 'one build on record as working',
|
|
691
825
|
why: 'Until one build has been recorded there is nothing to compare a new one against, and a clean result would mean nothing at all.',
|
|
692
826
|
unlocks: 'every check from then on',
|
|
693
|
-
|
|
827
|
+
// `check --paired` was named here first, and it does not do this: run it twice on a
|
|
828
|
+
// fresh project and both runs answer "there is no build on record as working". Only
|
|
829
|
+
// shipping cuts a reference, on purpose — no agent may bless its own work.
|
|
830
|
+
fix: 'staysfixed ship (only shipping records what "working" means — run it once at the end of your release)',
|
|
694
831
|
who: 'the agent',
|
|
695
832
|
});
|
|
696
833
|
}
|
|
@@ -1014,7 +1151,10 @@ export function configText(project) {
|
|
|
1014
1151
|
w(' // "working" means, so the name is how two of them are told apart.');
|
|
1015
1152
|
if (project.products.length > 1) {
|
|
1016
1153
|
w(' // This repository makes more than one thing. A check covers whichever of them the');
|
|
1017
|
-
|
|
1154
|
+
// `--product` is not an option on `check` and never has been. Naming it here sent people
|
|
1155
|
+
// to a flag the CLI rejects, in the settings file the tool itself wrote for them.
|
|
1156
|
+
w(' // settings below describe. To check one of the others, run `staysfixed check` from');
|
|
1157
|
+
w(' // inside that package, or point at its settings with `--config <file>`.');
|
|
1018
1158
|
}
|
|
1019
1159
|
w(` product: ${JSON.stringify(project.name)},`);
|
|
1020
1160
|
w('');
|
|
@@ -1208,7 +1348,12 @@ export function configText(project) {
|
|
|
1208
1348
|
} else if (project.pages.length > 0) {
|
|
1209
1349
|
w(`${webOn}// ${project.pages.length} page address${project.pages.length === 1 ? '' : 'es'} are read out of your folder names automatically — nothing to list here.`);
|
|
1210
1350
|
w(`${webOn}// Add a screen only for something a walk has to DO rather than just open:`);
|
|
1211
|
-
|
|
1351
|
+
// `fill:` and `with:` are not words this tool knows — the verb is `type:` and the value
|
|
1352
|
+
// is `text:`. An unknown key used to be skipped in silence, so this example, handed to
|
|
1353
|
+
// every stranger with a sign-in, filled nothing, clicked Sign in on an empty form, and
|
|
1354
|
+
// then photographed the login page for every screen behind the wall while reporting a
|
|
1355
|
+
// clean run. The example that teaches the vocabulary has to be IN the vocabulary.
|
|
1356
|
+
w(`${webOn}// screens: [{ name: 'signing in', url: '/login', steps: [{ type: '#email', text: 'a@b.c' }, { type: '#password', text: 'secret' }, { click: 'Sign in' }] }],`);
|
|
1212
1357
|
} else {
|
|
1213
1358
|
w(`${webOn}// screens: [{ name: 'the front page', url: '/' }],`);
|
|
1214
1359
|
}
|
|
@@ -1421,15 +1566,29 @@ function whatItCovers(readiness) {
|
|
|
1421
1566
|
const covered = readiness.filter((r) => r.state === 'ready').map((r) => r.product);
|
|
1422
1567
|
const partly = readiness.filter((r) => r.state === 'the agent can fix this' || r.state === 'only a person can do this').map((r) => r.product);
|
|
1423
1568
|
const notCovered = readiness.filter((r) => r.state === 'not possible here').map((r) => r.product);
|
|
1569
|
+
// A product whose only outstanding thing is a permanent limit is still not covered in
|
|
1570
|
+
// full — but "not covered YET" would promise somebody a job that will never exist, so it
|
|
1571
|
+
// gets the sentence further down instead of this one.
|
|
1572
|
+
const waiting = readiness
|
|
1573
|
+
.filter((r) => partly.includes(r.product) && r.needs.some((n) => n.who !== 'nobody'))
|
|
1574
|
+
.map((r) => r.product);
|
|
1424
1575
|
|
|
1425
1576
|
/** @type {string[]} */
|
|
1426
1577
|
const parts = [];
|
|
1427
1578
|
if (covered.length > 0) parts.push(`Right now a check here covers ${plainList(covered)} in full.`);
|
|
1428
1579
|
else parts.push('Right now a check here covers nothing in full.');
|
|
1429
|
-
if (
|
|
1580
|
+
if (waiting.length > 0) parts.push(`${plainList(waiting, true)} ${waiting.length === 1 ? 'is' : 'are'} not covered yet, and the list below says exactly what is in the way and who has to do it.`);
|
|
1430
1581
|
if (notCovered.length > 0) parts.push(`${plainList(notCovered, true)} ${notCovered.length === 1 ? 'is' : 'are'} not checked at all, so a clean result says nothing whatever about ${notCovered.length === 1 ? 'it' : 'them'}.`);
|
|
1431
1582
|
if (partly.length === 0 && notCovered.length === 0 && covered.length > 0) parts.push('Nothing is being left out.');
|
|
1432
1583
|
|
|
1584
|
+
// The hole that never closes, named with the language that causes it. "Not covered yet"
|
|
1585
|
+
// reads as a job somebody will get to; this one is nobody's job and saying so is the
|
|
1586
|
+
// difference between an honest limit and a promise that is never kept.
|
|
1587
|
+
const blind = readiness.filter((r) => r.needs.some((n) => n.who === 'nobody' && n.topic === 'source'));
|
|
1588
|
+
if (blind.length > 0) {
|
|
1589
|
+
parts.push(`${plainList(blind.map((r) => r.product), true)} ${blind.length === 1 ? 'is' : 'are'} written in a language this tool cannot read the way it reads JavaScript, so what is watched is what ${blind.length === 1 ? 'it does' : 'they do'} when run rather than what the code says — the list below names the language. No amount of setting up changes that.`);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1433
1592
|
return { covered, partly, notCovered, short: parts.join(' ') };
|
|
1434
1593
|
}
|
|
1435
1594
|
|
|
@@ -1452,9 +1611,14 @@ function nextCommands(readiness, project) {
|
|
|
1452
1611
|
if (reachable.length > 0) {
|
|
1453
1612
|
next.push({
|
|
1454
1613
|
command: 'staysfixed check --paired',
|
|
1614
|
+
// It does NOT record what "working" means, and saying so here sent everybody round a
|
|
1615
|
+
// loop: run it, be told there is no build on record, run it again, be told the same
|
|
1616
|
+
// thing. Only `ship` cuts a reference — that is the rule the whole product rests on,
|
|
1617
|
+
// because an agent that can bless its own work is not a safety net. So this says what
|
|
1618
|
+
// the run actually does, and `ship` below says what only it can do.
|
|
1455
1619
|
what: reachable.some((r) => r.state === 'ready')
|
|
1456
|
-
? 'The first real run. It
|
|
1457
|
-
: 'The first real run. Nothing here is fully set up yet, so it
|
|
1620
|
+
? 'The first real run. It walks everything and shows you what it sees. It cannot record what "working" means — only shipping does that.'
|
|
1621
|
+
: 'The first real run. Nothing here is fully set up yet, so it walks what it can reach and says plainly what it left out — which is more useful than waiting.',
|
|
1458
1622
|
});
|
|
1459
1623
|
}
|
|
1460
1624
|
if (project.tests.files > 0) {
|
|
@@ -1469,58 +1633,6 @@ function nextCommands(readiness, project) {
|
|
|
1469
1633
|
// Words
|
|
1470
1634
|
// ---------------------------------------------------------------------------
|
|
1471
1635
|
|
|
1472
|
-
/**
|
|
1473
|
-
* The plan, said out loud, short.
|
|
1474
|
-
*
|
|
1475
|
-
* Short is a requirement rather than a preference. Everything here also exists as data in the
|
|
1476
|
-
* plan object, and an agent reads that; a person reads this, and a person reading twenty lines
|
|
1477
|
-
* reads none of them.
|
|
1478
|
-
*
|
|
1479
|
-
* @param {InitPlan} plan
|
|
1480
|
-
* @returns {string[]}
|
|
1481
|
-
*/
|
|
1482
|
-
export function describePlan(plan) {
|
|
1483
|
-
/** @type {string[]} */
|
|
1484
|
-
const lines = [];
|
|
1485
|
-
lines.push(plan.project.summary);
|
|
1486
|
-
lines.push('');
|
|
1487
|
-
lines.push(plan.covers.short);
|
|
1488
|
-
|
|
1489
|
-
if (plan.needs.person.length > 0) {
|
|
1490
|
-
lines.push('');
|
|
1491
|
-
lines.push(plan.needs.person.length === 1 ? 'One thing needs you:' : `${plan.needs.person.length} things need you:`);
|
|
1492
|
-
for (const need of plan.needs.person) {
|
|
1493
|
-
lines.push(` ${need.what} — ${need.why} It unlocks ${need.unlocks}.`);
|
|
1494
|
-
lines.push(` ${need.fix}`);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
for (const need of plan.needs.impossible) {
|
|
1498
|
-
lines.push('');
|
|
1499
|
-
lines.push(`Not possible here: ${need.what}. ${need.fix}`);
|
|
1500
|
-
}
|
|
1501
|
-
// Code nothing could account for is the quietest way a run over-claims, so it is said in
|
|
1502
|
-
// the summary a person reads rather than left in the data an agent might not open.
|
|
1503
|
-
if (plan.project.unsure.length > 0) {
|
|
1504
|
-
lines.push('');
|
|
1505
|
-
lines.push('Worth knowing:');
|
|
1506
|
-
for (const doubt of plan.project.unsure) lines.push(` ${doubt}`);
|
|
1507
|
-
}
|
|
1508
|
-
return lines;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
/**
|
|
1512
|
-
* @param {InitResult} result
|
|
1513
|
-
* @returns {string[]}
|
|
1514
|
-
*/
|
|
1515
|
-
export function describeResult(result) {
|
|
1516
|
-
const lines = describePlan(result.plan);
|
|
1517
|
-
lines.push('');
|
|
1518
|
-
if (result.written.length > 0) lines.push(`Written: ${result.written.map((f) => shortPath(f)).join(', ')}.`);
|
|
1519
|
-
if (result.kept.length > 0) lines.push(`Left exactly as it was: ${result.kept.map((f) => shortPath(f)).join(', ')}.`);
|
|
1520
|
-
for (const problem of result.problems) lines.push(problem);
|
|
1521
|
-
return lines;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
1636
|
// ---------------------------------------------------------------------------
|
|
1525
1637
|
// Small helpers
|
|
1526
1638
|
// ---------------------------------------------------------------------------
|
|
@@ -1665,7 +1777,12 @@ export async function run(ctx) {
|
|
|
1665
1777
|
continue;
|
|
1666
1778
|
}
|
|
1667
1779
|
for (const need of item.needs) {
|
|
1668
|
-
|
|
1780
|
+
// A need nobody can clear must not read like a job. "Somebody has to" in front of a
|
|
1781
|
+
// permanent limit sends a person looking for the thing they are supposed to do.
|
|
1782
|
+
const label = need.who === 'the agent' ? 'the tool can do this itself: '
|
|
1783
|
+
: need.who === 'nobody' ? 'nobody can do anything about this: '
|
|
1784
|
+
: 'somebody has to: ';
|
|
1785
|
+
say(paint.grey(` ${mark.info} ${need.what} — ${label}${need.fix}`));
|
|
1669
1786
|
}
|
|
1670
1787
|
}
|
|
1671
1788
|
|
package/src/v2/intent.js
CHANGED
|
@@ -44,6 +44,7 @@ import { promisify } from 'node:util';
|
|
|
44
44
|
import { safeName } from '../core/paths.js';
|
|
45
45
|
import { StaysFixedError } from '../core/errors.js';
|
|
46
46
|
import { referencePointer } from './store.js';
|
|
47
|
+
import { NOT_THE_TOOLS_OWN_FOLDER } from './rank.js';
|
|
47
48
|
|
|
48
49
|
const run = promisify(execFile);
|
|
49
50
|
|
|
@@ -215,17 +216,6 @@ export async function readIntents(store, product) {
|
|
|
215
216
|
return raw.filter((i) => i && typeof i === 'object' && typeof i.id === 'string' && typeof i.summary === 'string');
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
/**
|
|
219
|
-
* Forget a product's intents. Housekeeping, and the way a test starts clean.
|
|
220
|
-
*
|
|
221
|
-
* @param {Store} store
|
|
222
|
-
* @param {string} product
|
|
223
|
-
* @returns {Promise<void>}
|
|
224
|
-
*/
|
|
225
|
-
export async function forgetIntents(store, product) {
|
|
226
|
-
await fsp.rm(intentsFile(store, product), { force: true });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
219
|
// ---------------------------------------------------------------------------
|
|
230
220
|
// Does this finding fall inside what was declared?
|
|
231
221
|
// ---------------------------------------------------------------------------
|
|
@@ -397,11 +387,17 @@ export async function fingerprintTree(root) {
|
|
|
397
387
|
}
|
|
398
388
|
|
|
399
389
|
const branch = await git(['rev-parse', '--abbrev-ref', 'HEAD'], root);
|
|
400
|
-
|
|
390
|
+
// Both questions are asked with this tool's own folder left out, the same way ranking asks
|
|
391
|
+
// them. Without that, sealing an intent moves the tree it has just fingerprinted — the seal
|
|
392
|
+
// writes into .staysfixed, git reports the new file, and the next comparison says the code
|
|
393
|
+
// changed when nobody touched a line of it. In a project that has run `init` the folder is
|
|
394
|
+
// in .gitignore and this makes no difference; in one that has not, it is the difference
|
|
395
|
+
// between "you edited this after you sealed" and the truth.
|
|
396
|
+
const status = (await git(['status', '--porcelain', '--', NOT_THE_TOOLS_OWN_FOLDER], root)) ?? '';
|
|
401
397
|
const changedFiles = filesFromStatus(status);
|
|
402
398
|
// The diff itself is hashed rather than kept. Two moments only ever need to be compared, and
|
|
403
399
|
// keeping the text would put a copy of the working tree in a file people commit by accident.
|
|
404
|
-
const diff = (await git(['diff', 'HEAD'], root)) ?? '';
|
|
400
|
+
const diff = (await git(['diff', 'HEAD', '--', NOT_THE_TOOLS_OWN_FOLDER], root)) ?? '';
|
|
405
401
|
const digest = shortDigest([head, status, diff]);
|
|
406
402
|
|
|
407
403
|
return {
|
|
@@ -523,16 +519,6 @@ async function git(args, cwd) {
|
|
|
523
519
|
// The bookkeeping this lane shares
|
|
524
520
|
// ---------------------------------------------------------------------------
|
|
525
521
|
|
|
526
|
-
/**
|
|
527
|
-
* Where intents and waivers live: beside the engine's observations, not inside them.
|
|
528
|
-
*
|
|
529
|
-
* @param {Store} store
|
|
530
|
-
* @returns {string}
|
|
531
|
-
*/
|
|
532
|
-
export function stateDir(store) {
|
|
533
|
-
return store.dir;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
522
|
/**
|
|
537
523
|
* @param {Store} store
|
|
538
524
|
* @param {string} product
|