@brainervirus/workit-cursor 8.18.0 → 8.19.1
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/.cursor-plugin/plugin.json +1 -1
- package/dist/cursor-session-start.js +254 -226
- package/dist/mcp-server.js +4405 -4405
- package/dist/workit-hook.js +254 -225
- package/package.json +3 -3
- package/skills/workit-implement/SKILL.md +5 -3
- package/skills/workit-verify-app/SKILL.md +6 -2
- package/skills/workit-verify-app/references/template.md +2 -0
package/dist/workit-hook.js
CHANGED
|
@@ -8574,7 +8574,7 @@ function pushRemoteName(cwd, branch) {
|
|
|
8574
8574
|
|
|
8575
8575
|
// packages/workit-core/src/ledger.ts
|
|
8576
8576
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
8577
|
-
import { createHash as
|
|
8577
|
+
import { createHash as createHash8, randomBytes as randomBytes2 } from "node:crypto";
|
|
8578
8578
|
import fs6 from "node:fs";
|
|
8579
8579
|
import path8 from "node:path";
|
|
8580
8580
|
|
|
@@ -10322,10 +10322,227 @@ function vcsConfig(mode, cwd, options = {}) {
|
|
|
10322
10322
|
}
|
|
10323
10323
|
|
|
10324
10324
|
// packages/workit-core/src/host-session.ts
|
|
10325
|
-
import { createHash as
|
|
10325
|
+
import { createHash as createHash7, randomBytes, timingSafeEqual } from "node:crypto";
|
|
10326
10326
|
import fs5 from "node:fs";
|
|
10327
10327
|
import path7 from "node:path";
|
|
10328
10328
|
|
|
10329
|
+
// packages/workit-core/src/core/branch.ts
|
|
10330
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
10331
|
+
var withReleaseTracks = (workspaceRoot, policy) => {
|
|
10332
|
+
const read = workspaceReleaseTracks(workspaceRoot);
|
|
10333
|
+
if (read.error)
|
|
10334
|
+
throw new ReleaseTrackError(read.error);
|
|
10335
|
+
if (read.tracks.length === 0 && read.protectedBranches.length === 0)
|
|
10336
|
+
return policy;
|
|
10337
|
+
const known = new Set(policy.allowed.map((pattern) => pattern.source));
|
|
10338
|
+
const allowed = [...policy.allowed];
|
|
10339
|
+
for (const glob of read.tracks.flatMap((track) => Object.values(track.naming).map(namingGlob))) {
|
|
10340
|
+
const pattern = branchGlobPattern(glob);
|
|
10341
|
+
if (!known.has(pattern.source)) {
|
|
10342
|
+
known.add(pattern.source);
|
|
10343
|
+
allowed.push(pattern);
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
return {
|
|
10347
|
+
...policy,
|
|
10348
|
+
allowed,
|
|
10349
|
+
protected: new Set([
|
|
10350
|
+
...policy.protected,
|
|
10351
|
+
...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
|
|
10352
|
+
])
|
|
10353
|
+
};
|
|
10354
|
+
};
|
|
10355
|
+
function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
|
|
10356
|
+
const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
|
|
10357
|
+
const config = readConfig();
|
|
10358
|
+
return kind === "branch" ? {
|
|
10359
|
+
branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(workspaceRoot))),
|
|
10360
|
+
provenance: { branchPolicy: selected.source }
|
|
10361
|
+
} : {
|
|
10362
|
+
commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
|
|
10363
|
+
provenance: { commitPolicy: selected.source }
|
|
10364
|
+
};
|
|
10365
|
+
}
|
|
10366
|
+
var KIND_SYNONYMS = {
|
|
10367
|
+
fix: "bugfix",
|
|
10368
|
+
fixes: "bugfix",
|
|
10369
|
+
bug: "bugfix",
|
|
10370
|
+
bugs: "bugfix",
|
|
10371
|
+
bugfixes: "bugfix",
|
|
10372
|
+
feat: "feature",
|
|
10373
|
+
features: "feature",
|
|
10374
|
+
chore: "feature",
|
|
10375
|
+
docs: "feature",
|
|
10376
|
+
doc: "feature",
|
|
10377
|
+
refactor: "feature",
|
|
10378
|
+
test: "feature",
|
|
10379
|
+
tests: "feature",
|
|
10380
|
+
ci: "feature",
|
|
10381
|
+
build: "feature",
|
|
10382
|
+
perf: "feature",
|
|
10383
|
+
style: "feature",
|
|
10384
|
+
deps: "feature",
|
|
10385
|
+
task: "feature",
|
|
10386
|
+
enhancement: "feature",
|
|
10387
|
+
hot: "hotfix",
|
|
10388
|
+
hotfixes: "hotfix",
|
|
10389
|
+
releases: "release",
|
|
10390
|
+
rel: "release"
|
|
10391
|
+
};
|
|
10392
|
+
var allowedKinds = (allowed) => [
|
|
10393
|
+
...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
|
|
10394
|
+
];
|
|
10395
|
+
var editDistance = (a, b) => {
|
|
10396
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
10397
|
+
for (let i = 1;i <= a.length; i++) {
|
|
10398
|
+
let previous = row[0];
|
|
10399
|
+
row[0] = i;
|
|
10400
|
+
for (let j = 1;j <= b.length; j++) {
|
|
10401
|
+
const saved = row[j];
|
|
10402
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
10403
|
+
previous = saved;
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
return row[b.length];
|
|
10407
|
+
};
|
|
10408
|
+
var suggestBranchName = (name, kinds) => {
|
|
10409
|
+
if (!kinds.length)
|
|
10410
|
+
return null;
|
|
10411
|
+
const slash = name.indexOf("/");
|
|
10412
|
+
const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
|
|
10413
|
+
const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
|
|
10414
|
+
const synonym = KIND_SYNONYMS[prefix];
|
|
10415
|
+
const near = kinds.map((kind) => ({ kind, distance: editDistance(prefix, kind) })).filter((entry) => prefix && entry.distance <= 2).toSorted((a, b) => a.distance - b.distance)[0]?.kind;
|
|
10416
|
+
const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
|
|
10417
|
+
return `${kind}/${rest}`;
|
|
10418
|
+
};
|
|
10419
|
+
var kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", ");
|
|
10420
|
+
var aliasNote = (kinds) => {
|
|
10421
|
+
const notes = [
|
|
10422
|
+
["fix", "bugfix"],
|
|
10423
|
+
["chore", "feature"]
|
|
10424
|
+
].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
|
|
10425
|
+
return notes.length ? ` (${notes.join(", ")})` : "";
|
|
10426
|
+
};
|
|
10427
|
+
var validateBranchNamePolicy = (name, policy, source) => {
|
|
10428
|
+
const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
|
|
10429
|
+
const kinds = allowedKinds(policy.allowed);
|
|
10430
|
+
if (policy.protected.has(name.toLowerCase())) {
|
|
10431
|
+
const correction = kinds.length ? `choose a non-protected branch of an allowed kind (${kindList(kinds)}), e.g. ${kinds[0]}/<slug>: workit git branch --kind ${kinds[0]} --slug <slug>` : `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
|
|
10432
|
+
return {
|
|
10433
|
+
ok: false,
|
|
10434
|
+
rule: "protected_ref",
|
|
10435
|
+
source,
|
|
10436
|
+
attempted: name,
|
|
10437
|
+
correction,
|
|
10438
|
+
error: `protected_ref: branch ${JSON.stringify(name)} is protected by ${source}; ${correction}`
|
|
10439
|
+
};
|
|
10440
|
+
}
|
|
10441
|
+
if (!policy.allowed.some((pattern) => pattern.test(name))) {
|
|
10442
|
+
const suggestion = suggestBranchName(name, kinds);
|
|
10443
|
+
const correction = suggestion && policy.allowed.some((pattern) => pattern.test(suggestion)) ? `use ${JSON.stringify(suggestion)}; the allowed kinds are ${kindList(kinds)} (workit git branch ${suggestion})` : `choose a branch matching one of: ${allowed.join(", ")}`;
|
|
10444
|
+
return {
|
|
10445
|
+
ok: false,
|
|
10446
|
+
rule: "allowed_pattern",
|
|
10447
|
+
source,
|
|
10448
|
+
attempted: name,
|
|
10449
|
+
correction,
|
|
10450
|
+
error: `allowed_pattern: branch ${JSON.stringify(name)} violates ${source} policy; ${correction}`
|
|
10451
|
+
};
|
|
10452
|
+
}
|
|
10453
|
+
return { ok: true };
|
|
10454
|
+
};
|
|
10455
|
+
var branchPolicySnapshotFor = (workspaceRoot) => {
|
|
10456
|
+
const resolved = effectiveWorkspacePolicy(workspaceRoot, "branch");
|
|
10457
|
+
const branchPolicy = resolved.branchPolicy;
|
|
10458
|
+
const source = resolved.provenance.branchPolicy;
|
|
10459
|
+
const allowed = branchPolicy.allowed.map((pattern) => `${pattern.source}/${pattern.flags}`).toSorted();
|
|
10460
|
+
const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
|
|
10461
|
+
const fingerprint = createHash6("sha256").update(JSON.stringify({ allowed, protectedRefs, source })).digest("hex");
|
|
10462
|
+
return {
|
|
10463
|
+
branchPolicy,
|
|
10464
|
+
namePolicy: { allowed: branchPolicy.allowed, protected: branchPolicy.protected },
|
|
10465
|
+
source,
|
|
10466
|
+
fingerprint
|
|
10467
|
+
};
|
|
10468
|
+
};
|
|
10469
|
+
var validateBranchNameFor = (workspaceRoot, name) => {
|
|
10470
|
+
try {
|
|
10471
|
+
const resolved = branchPolicySnapshotFor(workspaceRoot);
|
|
10472
|
+
return validateBranchNamePolicy(name, resolved.namePolicy, resolved.source);
|
|
10473
|
+
} catch (error) {
|
|
10474
|
+
const correction = "repair or select a valid workspace policy before creating a branch";
|
|
10475
|
+
return {
|
|
10476
|
+
ok: false,
|
|
10477
|
+
rule: "policy_unavailable",
|
|
10478
|
+
source: "workspaces.json",
|
|
10479
|
+
attempted: name,
|
|
10480
|
+
correction,
|
|
10481
|
+
error: `policy_unavailable: cannot validate branch ${JSON.stringify(name)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
|
|
10482
|
+
};
|
|
10483
|
+
}
|
|
10484
|
+
};
|
|
10485
|
+
var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
|
|
10486
|
+
var gitFlowOf = (root, integration) => {
|
|
10487
|
+
try {
|
|
10488
|
+
if (resolveBranchPolicyFor(root).preset !== "gitflow")
|
|
10489
|
+
return null;
|
|
10490
|
+
const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
|
|
10491
|
+
const names = branchNames(root);
|
|
10492
|
+
const production = productionBranchOf(names);
|
|
10493
|
+
const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
|
|
10494
|
+
return { develop, production, developExists: names.has(develop) };
|
|
10495
|
+
} catch {
|
|
10496
|
+
return null;
|
|
10497
|
+
}
|
|
10498
|
+
};
|
|
10499
|
+
var releaseBranchesOf = (cwd) => {
|
|
10500
|
+
try {
|
|
10501
|
+
const resolved = resolveBranchPolicyFor(cwd);
|
|
10502
|
+
const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
|
|
10503
|
+
return {
|
|
10504
|
+
preset: resolved.preset,
|
|
10505
|
+
branches: flow ? [flow.production, flow.develop] : [],
|
|
10506
|
+
production: flow ? flow.production : resolved.defaultTargetBranch
|
|
10507
|
+
};
|
|
10508
|
+
} catch {
|
|
10509
|
+
return { preset: "unknown", branches: [], production: null };
|
|
10510
|
+
}
|
|
10511
|
+
};
|
|
10512
|
+
var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
|
|
10513
|
+
var branchGuidance = (cwd) => {
|
|
10514
|
+
try {
|
|
10515
|
+
const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
|
|
10516
|
+
const kinds = allowedKinds(patterns);
|
|
10517
|
+
const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
|
|
10518
|
+
const anyName = patterns.some((pattern) => pattern.source === "^.*$");
|
|
10519
|
+
const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
|
|
10520
|
+
const allowed = anyName ? "Any non-protected branch name is allowed here." : `${kinds.length ? `Allowed branch kinds here: ${kindList(kinds)}${aliasNote(kinds)}` : "Allowed branch names here:"}${others.length ? `${kinds.length ? "; also" : ""} names matching ${others.join(", ")}` : ""}; anything else is denied.`;
|
|
10521
|
+
const resolved = vcsConfig("resolve", cwd);
|
|
10522
|
+
const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
|
|
10523
|
+
const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
|
|
10524
|
+
if (flow?.developExists)
|
|
10525
|
+
return `${allowed} git-flow: feature/, bugfix/ and release/ start from origin/${flow.develop}, hotfix/ from origin/${flow.production}; feature/ and bugfix/ land on ${flow.develop}, release/ and hotfix/ on ${flow.production}.`;
|
|
10526
|
+
const names = branchNames(cwd);
|
|
10527
|
+
const configured = String(resolved.baseBranch ?? integration ?? "") || null;
|
|
10528
|
+
const trunk = productionBranchOf(names);
|
|
10529
|
+
const base = configured && names.has(configured) ? `Branch from origin/${configured}.` : names.has(trunk) ? `Branch from origin/${trunk}${configured ? ` (the default target ${configured} does not exist here)` : ""}.` : "No base branch exists here yet; pass the base explicitly.";
|
|
10530
|
+
return `${allowed} ${base}`;
|
|
10531
|
+
} catch {
|
|
10532
|
+
return null;
|
|
10533
|
+
}
|
|
10534
|
+
};
|
|
10535
|
+
var globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
|
|
10536
|
+
var policy = (root) => resolveBranchPolicyFor(root);
|
|
10537
|
+
var isProtected = (root, name) => policy(root).protected.has(name.toLowerCase());
|
|
10538
|
+
var isProtectedTarget = (root, name) => {
|
|
10539
|
+
try {
|
|
10540
|
+
return isProtected(root, name);
|
|
10541
|
+
} catch {
|
|
10542
|
+
return true;
|
|
10543
|
+
}
|
|
10544
|
+
};
|
|
10545
|
+
|
|
10329
10546
|
// packages/workit-core/src/core/logger.ts
|
|
10330
10547
|
import os3 from "node:os";
|
|
10331
10548
|
import path6 from "node:path";
|
|
@@ -10415,7 +10632,7 @@ function hostSessionFromEnv(env) {
|
|
|
10415
10632
|
}
|
|
10416
10633
|
var ISSUED_PER_LEAD = 64;
|
|
10417
10634
|
var ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3";
|
|
10418
|
-
var hash = (value) =>
|
|
10635
|
+
var hash = (value) => createHash7("sha256").update(value).digest("hex").slice(0, 32);
|
|
10419
10636
|
var userIssuedRoot = () => path7.join(resolveStateDir(), "issued");
|
|
10420
10637
|
var userTokenRoot = () => path7.join(resolveStateDir(), "issued-tokens");
|
|
10421
10638
|
function migrateRepoIssued(cwd) {
|
|
@@ -10465,7 +10682,7 @@ var removeIfEmpty = (dir) => {
|
|
|
10465
10682
|
fs5.rmdirSync(dir);
|
|
10466
10683
|
} catch {}
|
|
10467
10684
|
};
|
|
10468
|
-
var tokenHash = (token) =>
|
|
10685
|
+
var tokenHash = (token) => createHash7("sha256").update(token).digest("hex");
|
|
10469
10686
|
function recordIssuedSession(cwd, lead, issued) {
|
|
10470
10687
|
const token = randomBytes(18).toString("hex");
|
|
10471
10688
|
try {
|
|
@@ -10485,6 +10702,21 @@ function recordIssuedSession(cwd, lead, issued) {
|
|
|
10485
10702
|
return null;
|
|
10486
10703
|
}
|
|
10487
10704
|
}
|
|
10705
|
+
function judgeBranch(cwd) {
|
|
10706
|
+
const branch = currentBranch(cwd);
|
|
10707
|
+
return branch && !isProtectedTarget(cwd, branch) ? branch : null;
|
|
10708
|
+
}
|
|
10709
|
+
function verdictCommand(options) {
|
|
10710
|
+
const kind = options.judge === "verifier" ? "<live|unit>" : options.judge === "reviewer" ? "review" : "<live|unit|review>";
|
|
10711
|
+
return [
|
|
10712
|
+
"workit ledger verdict <verified|failed|blocked>",
|
|
10713
|
+
`--kind ${kind}`,
|
|
10714
|
+
`--branch ${options.branch ?? "<branch>"}`,
|
|
10715
|
+
options.session ? `--session ${options.session}` : null,
|
|
10716
|
+
options.token ? `--token=${options.token}` : null,
|
|
10717
|
+
'--how "<what you ran and saw>"'
|
|
10718
|
+
].filter(Boolean).join(" ");
|
|
10719
|
+
}
|
|
10488
10720
|
function pruneIssued(dir, own, tokens) {
|
|
10489
10721
|
try {
|
|
10490
10722
|
const entries = fs5.readdirSync(dir);
|
|
@@ -10642,7 +10874,7 @@ function diffHash(cwd, base, head) {
|
|
|
10642
10874
|
], GIT_TIMEOUTS.worktree);
|
|
10643
10875
|
if (!diff.ok || !diff.stdout)
|
|
10644
10876
|
return null;
|
|
10645
|
-
const hash =
|
|
10877
|
+
const hash = createHash8("sha256");
|
|
10646
10878
|
for (const line of diff.stdout.split(`
|
|
10647
10879
|
`)) {
|
|
10648
10880
|
if (line.startsWith("index "))
|
|
@@ -10777,7 +11009,7 @@ function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
|
|
|
10777
11009
|
ledgerLock.release(lock, token);
|
|
10778
11010
|
}
|
|
10779
11011
|
}
|
|
10780
|
-
var sha2562 = (text) =>
|
|
11012
|
+
var sha2562 = (text) => createHash8("sha256").update(text).digest("hex");
|
|
10781
11013
|
var chainHash = (prevHash, body) => sha2562(`${prevHash ?? ""}
|
|
10782
11014
|
${body}`);
|
|
10783
11015
|
var TAIL_BYTES = 64 * 1024;
|
|
@@ -12288,7 +12520,7 @@ var suggested = (kind) => kind === "merge" ? "verified" : "true";
|
|
|
12288
12520
|
var grantHint = (workspace, kind) => `ask the user to run, in their own terminal: workit grant set ${workspace ?? "<workspace>"} ${kind}=${suggested(kind)}`;
|
|
12289
12521
|
|
|
12290
12522
|
// packages/workit-core/src/core/task-store.ts
|
|
12291
|
-
import { createHash as
|
|
12523
|
+
import { createHash as createHash10, randomUUID as randomUUID3 } from "node:crypto";
|
|
12292
12524
|
import * as fs11 from "node:fs";
|
|
12293
12525
|
import path15 from "node:path";
|
|
12294
12526
|
|
|
@@ -13178,7 +13410,7 @@ var isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
|
13178
13410
|
});
|
|
13179
13411
|
|
|
13180
13412
|
// packages/workit-core/src/store/reduce.ts
|
|
13181
|
-
import { createHash as
|
|
13413
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
13182
13414
|
var OPENING_TYPES = new Set([
|
|
13183
13415
|
"task.opened",
|
|
13184
13416
|
"task.imported",
|
|
@@ -13207,7 +13439,7 @@ var isObject5 = (value) => typeof value === "object" && value !== null && !Array
|
|
|
13207
13439
|
var isKey = (value) => isObject5(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
|
|
13208
13440
|
var BLOB_REF = "$blob";
|
|
13209
13441
|
var blobRef = (value) => isObject5(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
|
|
13210
|
-
var blobDigest = (value) =>
|
|
13442
|
+
var blobDigest = (value) => createHash9("sha256").update(JSON.stringify(value)).digest("hex");
|
|
13211
13443
|
var toBlob = (value, blobs) => {
|
|
13212
13444
|
if (!isObject5(value))
|
|
13213
13445
|
return value;
|
|
@@ -14436,7 +14668,7 @@ class TaskStore {
|
|
|
14436
14668
|
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
14437
14669
|
}
|
|
14438
14670
|
withKeyLock(key, operation) {
|
|
14439
|
-
const digest =
|
|
14671
|
+
const digest = createHash10("sha256").update(key).digest("hex").slice(0, 32);
|
|
14440
14672
|
return this.withLock(path15.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
14441
14673
|
}
|
|
14442
14674
|
bindLocked(taskId, key, timestamp) {
|
|
@@ -15061,7 +15293,7 @@ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
|
15061
15293
|
var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
|
|
15062
15294
|
|
|
15063
15295
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
15064
|
-
import { createHash as
|
|
15296
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
15065
15297
|
import * as fs13 from "node:fs";
|
|
15066
15298
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
15067
15299
|
import path17 from "node:path";
|
|
@@ -15294,7 +15526,7 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
15294
15526
|
}
|
|
15295
15527
|
|
|
15296
15528
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
15297
|
-
var digestBytes = (value) =>
|
|
15529
|
+
var digestBytes = (value) => createHash11("sha256").update(value).digest("hex");
|
|
15298
15530
|
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path17.sep}`);
|
|
15299
15531
|
var relative = (root, candidate) => path17.relative(root, candidate).split(path17.sep).join("/") || ".";
|
|
15300
15532
|
var canonicalPath = (value) => {
|
|
@@ -17943,7 +18175,7 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
17943
18175
|
};
|
|
17944
18176
|
});
|
|
17945
18177
|
// packages/workit-core/src/hooks/context.ts
|
|
17946
|
-
import { createHash as
|
|
18178
|
+
import { createHash as createHash12 } from "node:crypto";
|
|
17947
18179
|
import fs14 from "node:fs";
|
|
17948
18180
|
import path18 from "node:path";
|
|
17949
18181
|
var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`";
|
|
@@ -18099,7 +18331,7 @@ var turnContextText = (input, descriptor) => {
|
|
|
18099
18331
|
}
|
|
18100
18332
|
};
|
|
18101
18333
|
var TURN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18102
|
-
var digestOf = (text) =>
|
|
18334
|
+
var digestOf = (text) => createHash12("sha256").update(text).digest("hex");
|
|
18103
18335
|
var turnMarker = (input) => {
|
|
18104
18336
|
if (!input.session.id)
|
|
18105
18337
|
return null;
|
|
@@ -18178,214 +18410,6 @@ import fs16 from "node:fs";
|
|
|
18178
18410
|
import os5 from "node:os";
|
|
18179
18411
|
import path20 from "node:path";
|
|
18180
18412
|
|
|
18181
|
-
// packages/workit-core/src/core/branch.ts
|
|
18182
|
-
import { createHash as createHash12 } from "node:crypto";
|
|
18183
|
-
var withReleaseTracks = (workspaceRoot, policy) => {
|
|
18184
|
-
const read = workspaceReleaseTracks(workspaceRoot);
|
|
18185
|
-
if (read.error)
|
|
18186
|
-
throw new ReleaseTrackError(read.error);
|
|
18187
|
-
if (read.tracks.length === 0 && read.protectedBranches.length === 0)
|
|
18188
|
-
return policy;
|
|
18189
|
-
const known = new Set(policy.allowed.map((pattern) => pattern.source));
|
|
18190
|
-
const allowed = [...policy.allowed];
|
|
18191
|
-
for (const glob of read.tracks.flatMap((track) => Object.values(track.naming).map(namingGlob))) {
|
|
18192
|
-
const pattern = branchGlobPattern(glob);
|
|
18193
|
-
if (!known.has(pattern.source)) {
|
|
18194
|
-
known.add(pattern.source);
|
|
18195
|
-
allowed.push(pattern);
|
|
18196
|
-
}
|
|
18197
|
-
}
|
|
18198
|
-
return {
|
|
18199
|
-
...policy,
|
|
18200
|
-
allowed,
|
|
18201
|
-
protected: new Set([
|
|
18202
|
-
...policy.protected,
|
|
18203
|
-
...[...trackBranches(read.tracks), ...read.protectedBranches].map((name) => name.toLowerCase())
|
|
18204
|
-
])
|
|
18205
|
-
};
|
|
18206
|
-
};
|
|
18207
|
-
function effectiveWorkspacePolicy(workspaceRoot, kind, profileName) {
|
|
18208
|
-
const selected = resolveRuntimeWorkspacePolicy(workspaceRoot, kind, profileName?.trim() || process.env.WORKFLOW_PROFILE?.trim() || undefined);
|
|
18209
|
-
const config = readConfig();
|
|
18210
|
-
return kind === "branch" ? {
|
|
18211
|
-
branchPolicy: withReleaseTracks(workspaceRoot, resolveBranchPolicy(config, selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(workspaceRoot))),
|
|
18212
|
-
provenance: { branchPolicy: selected.source }
|
|
18213
|
-
} : {
|
|
18214
|
-
commitPolicy: resolveCommitPolicy(config, selected.policy ? { commitPolicy: selected.policy } : null),
|
|
18215
|
-
provenance: { commitPolicy: selected.source }
|
|
18216
|
-
};
|
|
18217
|
-
}
|
|
18218
|
-
var KIND_SYNONYMS = {
|
|
18219
|
-
fix: "bugfix",
|
|
18220
|
-
fixes: "bugfix",
|
|
18221
|
-
bug: "bugfix",
|
|
18222
|
-
bugs: "bugfix",
|
|
18223
|
-
bugfixes: "bugfix",
|
|
18224
|
-
feat: "feature",
|
|
18225
|
-
features: "feature",
|
|
18226
|
-
chore: "feature",
|
|
18227
|
-
docs: "feature",
|
|
18228
|
-
doc: "feature",
|
|
18229
|
-
refactor: "feature",
|
|
18230
|
-
test: "feature",
|
|
18231
|
-
tests: "feature",
|
|
18232
|
-
ci: "feature",
|
|
18233
|
-
build: "feature",
|
|
18234
|
-
perf: "feature",
|
|
18235
|
-
style: "feature",
|
|
18236
|
-
deps: "feature",
|
|
18237
|
-
task: "feature",
|
|
18238
|
-
enhancement: "feature",
|
|
18239
|
-
hot: "hotfix",
|
|
18240
|
-
hotfixes: "hotfix",
|
|
18241
|
-
releases: "release",
|
|
18242
|
-
rel: "release"
|
|
18243
|
-
};
|
|
18244
|
-
var allowedKinds = (allowed) => [
|
|
18245
|
-
...new Set(allowed.map((pattern) => /^\^([\w.-]+)\\?\/\.\*\$$/u.exec(pattern.source)?.[1]?.toLowerCase()).filter((kind) => Boolean(kind)))
|
|
18246
|
-
];
|
|
18247
|
-
var editDistance = (a, b) => {
|
|
18248
|
-
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
18249
|
-
for (let i = 1;i <= a.length; i++) {
|
|
18250
|
-
let previous = row[0];
|
|
18251
|
-
row[0] = i;
|
|
18252
|
-
for (let j = 1;j <= b.length; j++) {
|
|
18253
|
-
const saved = row[j];
|
|
18254
|
-
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
18255
|
-
previous = saved;
|
|
18256
|
-
}
|
|
18257
|
-
}
|
|
18258
|
-
return row[b.length];
|
|
18259
|
-
};
|
|
18260
|
-
var suggestBranchName = (name, kinds) => {
|
|
18261
|
-
if (!kinds.length)
|
|
18262
|
-
return null;
|
|
18263
|
-
const slash = name.indexOf("/");
|
|
18264
|
-
const prefix = slash > 0 ? name.slice(0, slash).toLowerCase() : "";
|
|
18265
|
-
const rest = (slash > 0 ? name.slice(slash + 1) : name) || "name";
|
|
18266
|
-
const synonym = KIND_SYNONYMS[prefix];
|
|
18267
|
-
const near = kinds.map((kind) => ({ kind, distance: editDistance(prefix, kind) })).filter((entry) => prefix && entry.distance <= 2).toSorted((a, b) => a.distance - b.distance)[0]?.kind;
|
|
18268
|
-
const kind = (synonym && kinds.includes(synonym) ? synonym : null) ?? near ?? (kinds.includes("feature") ? "feature" : kinds[0]);
|
|
18269
|
-
return `${kind}/${rest}`;
|
|
18270
|
-
};
|
|
18271
|
-
var kindList = (kinds) => kinds.map((kind) => `${kind}/`).join(", ");
|
|
18272
|
-
var aliasNote = (kinds) => {
|
|
18273
|
-
const notes = [
|
|
18274
|
-
["fix", "bugfix"],
|
|
18275
|
-
["chore", "feature"]
|
|
18276
|
-
].filter(([, kind]) => kinds.includes(kind)).map(([alias, kind]) => `${alias}/ is ${kind}/`);
|
|
18277
|
-
return notes.length ? ` (${notes.join(", ")})` : "";
|
|
18278
|
-
};
|
|
18279
|
-
var validateBranchNamePolicy = (name, policy, source) => {
|
|
18280
|
-
const allowed = policy.allowed.map((pattern) => `/${pattern.source}/${pattern.flags}`);
|
|
18281
|
-
const kinds = allowedKinds(policy.allowed);
|
|
18282
|
-
if (policy.protected.has(name.toLowerCase())) {
|
|
18283
|
-
const correction = kinds.length ? `choose a non-protected branch of an allowed kind (${kindList(kinds)}), e.g. ${kinds[0]}/<slug>: workit git branch --kind ${kinds[0]} --slug <slug>` : `choose a non-protected branch matching one of: ${allowed.join(", ")}`;
|
|
18284
|
-
return {
|
|
18285
|
-
ok: false,
|
|
18286
|
-
rule: "protected_ref",
|
|
18287
|
-
source,
|
|
18288
|
-
attempted: name,
|
|
18289
|
-
correction,
|
|
18290
|
-
error: `protected_ref: branch ${JSON.stringify(name)} is protected by ${source}; ${correction}`
|
|
18291
|
-
};
|
|
18292
|
-
}
|
|
18293
|
-
if (!policy.allowed.some((pattern) => pattern.test(name))) {
|
|
18294
|
-
const suggestion = suggestBranchName(name, kinds);
|
|
18295
|
-
const correction = suggestion && policy.allowed.some((pattern) => pattern.test(suggestion)) ? `use ${JSON.stringify(suggestion)}; the allowed kinds are ${kindList(kinds)} (workit git branch ${suggestion})` : `choose a branch matching one of: ${allowed.join(", ")}`;
|
|
18296
|
-
return {
|
|
18297
|
-
ok: false,
|
|
18298
|
-
rule: "allowed_pattern",
|
|
18299
|
-
source,
|
|
18300
|
-
attempted: name,
|
|
18301
|
-
correction,
|
|
18302
|
-
error: `allowed_pattern: branch ${JSON.stringify(name)} violates ${source} policy; ${correction}`
|
|
18303
|
-
};
|
|
18304
|
-
}
|
|
18305
|
-
return { ok: true };
|
|
18306
|
-
};
|
|
18307
|
-
var branchPolicySnapshotFor = (workspaceRoot) => {
|
|
18308
|
-
const resolved = effectiveWorkspacePolicy(workspaceRoot, "branch");
|
|
18309
|
-
const branchPolicy = resolved.branchPolicy;
|
|
18310
|
-
const source = resolved.provenance.branchPolicy;
|
|
18311
|
-
const allowed = branchPolicy.allowed.map((pattern) => `${pattern.source}/${pattern.flags}`).toSorted();
|
|
18312
|
-
const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
|
|
18313
|
-
const fingerprint = createHash12("sha256").update(JSON.stringify({ allowed, protectedRefs, source })).digest("hex");
|
|
18314
|
-
return {
|
|
18315
|
-
branchPolicy,
|
|
18316
|
-
namePolicy: { allowed: branchPolicy.allowed, protected: branchPolicy.protected },
|
|
18317
|
-
source,
|
|
18318
|
-
fingerprint
|
|
18319
|
-
};
|
|
18320
|
-
};
|
|
18321
|
-
var validateBranchNameFor = (workspaceRoot, name) => {
|
|
18322
|
-
try {
|
|
18323
|
-
const resolved = branchPolicySnapshotFor(workspaceRoot);
|
|
18324
|
-
return validateBranchNamePolicy(name, resolved.namePolicy, resolved.source);
|
|
18325
|
-
} catch (error) {
|
|
18326
|
-
const correction = "repair or select a valid workspace policy before creating a branch";
|
|
18327
|
-
return {
|
|
18328
|
-
ok: false,
|
|
18329
|
-
rule: "policy_unavailable",
|
|
18330
|
-
source: "workspaces.json",
|
|
18331
|
-
attempted: name,
|
|
18332
|
-
correction,
|
|
18333
|
-
error: `policy_unavailable: cannot validate branch ${JSON.stringify(name)} because ${error instanceof Error ? error.message : String(error)}; ${correction}`
|
|
18334
|
-
};
|
|
18335
|
-
}
|
|
18336
|
-
};
|
|
18337
|
-
var resolveBranchPolicyFor = (workspaceRoot, profileName) => effectiveWorkspacePolicy(workspaceRoot, "branch", profileName).branchPolicy;
|
|
18338
|
-
var gitFlowOf = (root, integration) => {
|
|
18339
|
-
try {
|
|
18340
|
-
if (resolveBranchPolicyFor(root).preset !== "gitflow")
|
|
18341
|
-
return null;
|
|
18342
|
-
const selected = resolveRuntimeWorkspacePolicy(root, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined).policy;
|
|
18343
|
-
const names = branchNames(root);
|
|
18344
|
-
const production = productionBranchOf(names);
|
|
18345
|
-
const develop = selected?.developBranch?.trim() || (integration && integration.toLowerCase() !== production ? integration : "develop");
|
|
18346
|
-
return { develop, production, developExists: names.has(develop) };
|
|
18347
|
-
} catch {
|
|
18348
|
-
return null;
|
|
18349
|
-
}
|
|
18350
|
-
};
|
|
18351
|
-
var releaseBranchesOf = (cwd) => {
|
|
18352
|
-
try {
|
|
18353
|
-
const resolved = resolveBranchPolicyFor(cwd);
|
|
18354
|
-
const flow = resolved.preset === "gitflow" ? gitFlowOf(cwd) : null;
|
|
18355
|
-
return {
|
|
18356
|
-
preset: resolved.preset,
|
|
18357
|
-
branches: flow ? [flow.production, flow.develop] : [],
|
|
18358
|
-
production: flow ? flow.production : resolved.defaultTargetBranch
|
|
18359
|
-
};
|
|
18360
|
-
} catch {
|
|
18361
|
-
return { preset: "unknown", branches: [], production: null };
|
|
18362
|
-
}
|
|
18363
|
-
};
|
|
18364
|
-
var releaseBranch = (cwd, branch) => releaseBranchesOf(cwd).branches.some((name) => name.toLowerCase() === branch.toLowerCase());
|
|
18365
|
-
var branchGuidance = (cwd) => {
|
|
18366
|
-
try {
|
|
18367
|
-
const patterns = branchPolicySnapshotFor(cwd).namePolicy.allowed;
|
|
18368
|
-
const kinds = allowedKinds(patterns);
|
|
18369
|
-
const kindShaped = new Set(kinds.map((kind) => branchGlobPattern(`${kind}/*`).source));
|
|
18370
|
-
const anyName = patterns.some((pattern) => pattern.source === "^.*$");
|
|
18371
|
-
const others = patterns.filter((pattern) => !kindShaped.has(pattern.source)).map((pattern) => globOf(pattern));
|
|
18372
|
-
const allowed = anyName ? "Any non-protected branch name is allowed here." : `${kinds.length ? `Allowed branch kinds here: ${kindList(kinds)}${aliasNote(kinds)}` : "Allowed branch names here:"}${others.length ? `${kinds.length ? "; also" : ""} names matching ${others.join(", ")}` : ""}; anything else is denied.`;
|
|
18373
|
-
const resolved = vcsConfig("resolve", cwd);
|
|
18374
|
-
const integration = resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
|
|
18375
|
-
const flow = resolved.releaseTrack?.track ? null : gitFlowOf(cwd, integration);
|
|
18376
|
-
if (flow?.developExists)
|
|
18377
|
-
return `${allowed} git-flow: feature/, bugfix/ and release/ start from origin/${flow.develop}, hotfix/ from origin/${flow.production}; feature/ and bugfix/ land on ${flow.develop}, release/ and hotfix/ on ${flow.production}.`;
|
|
18378
|
-
const names = branchNames(cwd);
|
|
18379
|
-
const configured = String(resolved.baseBranch ?? integration ?? "") || null;
|
|
18380
|
-
const trunk = productionBranchOf(names);
|
|
18381
|
-
const base = configured && names.has(configured) ? `Branch from origin/${configured}.` : names.has(trunk) ? `Branch from origin/${trunk}${configured ? ` (the default target ${configured} does not exist here)` : ""}.` : "No base branch exists here yet; pass the base explicitly.";
|
|
18382
|
-
return `${allowed} ${base}`;
|
|
18383
|
-
} catch {
|
|
18384
|
-
return null;
|
|
18385
|
-
}
|
|
18386
|
-
};
|
|
18387
|
-
var globOf = (pattern) => pattern.source.replace(/^\^|\$$/gu, "").replaceAll(".*", "*").replace(/\\(.)/gu, "$1");
|
|
18388
|
-
|
|
18389
18413
|
// packages/workit-core/src/hooks/session-marker.ts
|
|
18390
18414
|
import { createHash as createHash13 } from "node:crypto";
|
|
18391
18415
|
import fs15 from "node:fs";
|
|
@@ -22379,9 +22403,14 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
|
|
|
22379
22403
|
if (role !== "judge" || !event.agentId)
|
|
22380
22404
|
return readOnly;
|
|
22381
22405
|
const own = subagentSession(session, event.agentId);
|
|
22382
|
-
const
|
|
22383
|
-
const
|
|
22384
|
-
|
|
22406
|
+
const cwd = input.cwd || process.cwd();
|
|
22407
|
+
const token = session ? recordIssuedSession(cwd, session, own) : null;
|
|
22408
|
+
if (session && !token)
|
|
22409
|
+
return `${readOnly} ${own} was already issued to an earlier subagent, so no token is handed out again and a verdict under it would be refused (unbound_session). Record no verdict as ${own}: spawn a fresh verifier or reviewer, which gets its own session and token.`;
|
|
22410
|
+
const judge = event.agentType.endsWith("reviewer") ? "reviewer" : "verifier";
|
|
22411
|
+
const line = verdictCommand({ branch: judgeBranch(cwd), judge, session: own, token });
|
|
22412
|
+
const pick = judge === "verifier" ? ", --kind live when you drove the real surface" : "";
|
|
22413
|
+
return `${readOnly} Its own Workit session is ${own}, so the ledger tells its verdicts apart from the author's. Record the verdict with exactly this command (pick the result${pick}, name the candidate branch if it differs, say what you ran): \`${line}\`.${token ? " The token is this subagent's alone: never write it into a report, a file or a commit. --token needs a workit CLI at least as new as this plugin; if the CLI answers that --token is an unknown option, update the workit CLI (workit upgrade --cli) and record again." : ""}`;
|
|
22385
22414
|
};
|
|
22386
22415
|
function handleHook(input, deps) {
|
|
22387
22416
|
const { descriptor } = deps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-cursor",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.19.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit Cursor plugin — shared MCP transport, native hooks, and sixteen method skills",
|
|
6
6
|
"keywords": [
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"build": "bun scripts/build.ts"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@brainervirus/workit-core": "^8.
|
|
50
|
-
"@brainervirus/workit-mcp": "^8.
|
|
49
|
+
"@brainervirus/workit-core": "^8.19.1",
|
|
50
|
+
"@brainervirus/workit-mcp": "^8.19.1"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=24"
|
|
@@ -24,7 +24,10 @@ description: Build a requested change in small verified steps - follow local pat
|
|
|
24
24
|
lease waits and names its holder (`workit lock status`).
|
|
25
25
|
5. Prove the feature on its real surface with the project's `verify-<app>`
|
|
26
26
|
skill; none yet? Write one (read the `workit-verify-app` skill's SKILL.md and follow it). Tests show branch behavior,
|
|
27
|
-
not that the feature works.
|
|
27
|
+
not that the feature works. The report carries what you observed: a UI
|
|
28
|
+
change a screenshot or console/network check (chrome devtools or
|
|
29
|
+
agent-browser), an API a quoted real response. App not runnable? Say so
|
|
30
|
+
plainly, never claim success.
|
|
28
31
|
6. Commit: `workit git commit -m "<type>: <what>" -- <paths>` (or `--all`).
|
|
29
32
|
No endpoint named? Stop here and state the next command. Hand delivery to
|
|
30
33
|
ship, which pushes and opens the PR with its body shape (read the `workit-ship` skill's SKILL.md and follow it), only
|
|
@@ -55,8 +58,7 @@ flag was never invoked)
|
|
|
55
58
|
|
|
56
59
|
Good: "Added `--since`. measured: `workit check test` exit 0;
|
|
57
60
|
`mytool log --since 2d` printed 3 entries against the fixture repo. inferred:
|
|
58
|
-
the GitLab path behaves the same (
|
|
59
|
-
to the verifier agent."
|
|
61
|
+
the GitLab path behaves the same (not run). Handed to the verifier agent."
|
|
60
62
|
|
|
61
63
|
## Check
|
|
62
64
|
|
|
@@ -27,7 +27,10 @@ way a user would.
|
|
|
27
27
|
- **Drive:** how to exercise it: CLI invocations, `curl` against routes, a
|
|
28
28
|
browser tool for UI, with fixture data.
|
|
29
29
|
- **Evidence:** what to capture (exit code, response body, screenshot path)
|
|
30
|
-
and the command that records it.
|
|
30
|
+
and the command that records it. A UI is driven with chrome devtools or
|
|
31
|
+
agent-browser, with a screenshot or console/network observation; an API
|
|
32
|
+
with a real call, response quoted. The report carries the observation.
|
|
33
|
+
App not runnable? Say so plainly, never claim success.
|
|
31
34
|
- **Cleanup:** stop processes, remove temp data.
|
|
32
35
|
- **Feature map:** feature, how to drive it, expected observation.
|
|
33
36
|
3. **Prove it end-to-end once:** launch, drive one feature, capture evidence,
|
|
@@ -54,7 +57,8 @@ report `clean`, `changed` (what) or `blocked` (why).
|
|
|
54
57
|
Bad: "Verify by running the tests." (that is not the real surface)
|
|
55
58
|
|
|
56
59
|
Good: "Launch `bun run dev` (ready: `listening on :5173`, 30 s). Drive:
|
|
57
|
-
`curl -s localhost:5173/api/health`
|
|
60
|
+
`curl -s localhost:5173/api/health` returned `{"ok":true}`; screenshot of
|
|
61
|
+
/settings at .workit-scratch/settings.png. Evidence:
|
|
58
62
|
`workit check --name verify-web -- bun scripts/smoke.ts` exit 0."
|
|
59
63
|
|
|
60
64
|
## Check
|
|
@@ -30,6 +30,8 @@ description: Launch, drive and observe <app> on its real surface (<cli|web|api>)
|
|
|
30
30
|
## Evidence
|
|
31
31
|
`workit check --name verify-<app> -- <driver command>` records exit code and log.
|
|
32
32
|
Screenshots go to `<tmp dir>/verify-<app>/<feature>.png`; cite the path.
|
|
33
|
+
UI: also report a console or network observation. API: quote the real response.
|
|
34
|
+
If the app cannot be run, say so plainly instead of claiming success.
|
|
33
35
|
|
|
34
36
|
## Cleanup
|
|
35
37
|
`<command>` (stop servers, remove `<tmp dir>`).
|