@juzser/blacksmith 0.3.0 → 0.5.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/.claude/agents/{auditor.md → bs-auditor.md} +8 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +16 -9
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +16 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +9 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +18 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +7 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +45 -31
- package/.claude/skills/bs/dispatch.md +133 -47
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +57 -16
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +190 -52
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +143 -50
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +70 -13
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +245 -20
- package/factory/orchestrator/dist/autonomy.js +6 -2
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +1087 -116
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crossFinding.js +20 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +352 -67
- package/factory/orchestrator/dist/db/projector.js +327 -54
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +108 -0
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +494 -42
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +182 -11
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +164 -14
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +108 -14
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/gh.js +19 -2
- package/factory/orchestrator/dist/git.js +20 -2
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +411 -39
- package/factory/orchestrator/dist/integration.js +20 -8
- package/factory/orchestrator/dist/issueReporter.js +81 -16
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +239 -0
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/logCache.js +291 -0
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +20 -7
- package/factory/orchestrator/dist/plan.js +399 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/projects.js +12 -1
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +471 -30
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/scheduler.js +3 -3
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +246 -12
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +192 -31
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/taxonomy.js +11 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +20 -16
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +113 -48
- package/factory/orchestrator/dist/waivers.js +99 -12
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/waveSchedule.js +84 -0
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +76 -21
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +27 -3
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- package/.claude/agents/uiux.md +0 -133
|
@@ -4,7 +4,7 @@ import { claimCoversPath } from './claims.js';
|
|
|
4
4
|
import { SmithError } from './errors.js';
|
|
5
5
|
import { appendEvent, readLineageEvents } from './events.js';
|
|
6
6
|
import { describeType } from './plan.js';
|
|
7
|
-
import { compileSchemas, validateRecord } from './schemas.js';
|
|
7
|
+
import { compileSchemas, validateEachShape, validateRecord, } from './schemas.js';
|
|
8
8
|
import { readAddedTasks } from './taskEvents.js';
|
|
9
9
|
import { bareTaskId, epicOfTaskId, taskIdsMatch } from './taskId.js';
|
|
10
10
|
import { loadTaxonomy, validateTag } from './taxonomy.js';
|
|
@@ -250,7 +250,9 @@ function escapeRegExp(text) {
|
|
|
250
250
|
* forms) — never a bare `:digits` anywhere in the text. A blanket
|
|
251
251
|
* `:\d+` strip is wrong: free-text like "debug endpoint exposed on :8080"
|
|
252
252
|
* would collide with an unrelated ":9090" finding (reproduced regression,
|
|
253
|
-
* reviewer finding #3).
|
|
253
|
+
* reviewer finding #3). The line number may itself be a range
|
|
254
|
+
* (`Widget.tsx:1549-1576`), which a re-review reports as often as a single
|
|
255
|
+
* line (issue #178).
|
|
254
256
|
*/
|
|
255
257
|
function stripPathLineRef(summary, filePath) {
|
|
256
258
|
const forwardSlashPath = filePath.replace(/\\/g, '/');
|
|
@@ -259,16 +261,37 @@ function stripPathLineRef(summary, filePath) {
|
|
|
259
261
|
for (const candidate of candidates) {
|
|
260
262
|
if (!candidate)
|
|
261
263
|
continue;
|
|
262
|
-
const pattern = new RegExp(`${escapeRegExp(candidate)}:\\d+(?::\\d+)?`, 'g');
|
|
264
|
+
const pattern = new RegExp(`${escapeRegExp(candidate)}:\\d+(?:[-–—]\\d+)?(?::\\d+)?`, 'g');
|
|
263
265
|
result = result.replace(pattern, candidate);
|
|
264
266
|
}
|
|
265
267
|
return result;
|
|
266
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* A `lines N-M` / `lines N–M` (en dash) / `line N to line M` range — the
|
|
271
|
+
* shape a judge describing a large component actually writes, and the one
|
|
272
|
+
* the original single `line N` strip below never covered (issue #178).
|
|
273
|
+
* Anchored on the word "line"/"lines" immediately before the numbers, so
|
|
274
|
+
* free-text numerics elsewhere in the sentence are never touched.
|
|
275
|
+
*/
|
|
276
|
+
const LINE_SPAN_RE = /\blines?\s+\d[\d,]*\s*(?:[-–—]|to(?:\s+lines?)?)\s*\d[\d,]*\b/g;
|
|
277
|
+
/**
|
|
278
|
+
* `~5,050-line` / `6,720-line` — a size given as a hyphenated adjective
|
|
279
|
+
* before "line"/"component"/"file"/etc, approximate or exact. Only the
|
|
280
|
+
* `<number>-line` token is removed; the noun it modifies is untouched, and
|
|
281
|
+
* a hyphenated count before any OTHER noun (`3-day`, `5-retry`) is left
|
|
282
|
+
* alone — it is not a line reference.
|
|
283
|
+
*/
|
|
284
|
+
const LINE_COUNT_RE = /~?\d[\d,]*-line\b/g;
|
|
285
|
+
/** `L1549-L6600` — a GitHub-style permalink line range, bare or after `#`. */
|
|
286
|
+
const GITHUB_LINE_RANGE_RE = /\bl\d+-l\d+\b/g;
|
|
267
287
|
/** lowercase, whitespace collapsed (spec wording; path-anchored line refs stripped by the caller). */
|
|
268
288
|
function normalizeSummary(summary) {
|
|
269
289
|
return summary
|
|
270
290
|
.toLowerCase()
|
|
291
|
+
.replace(LINE_SPAN_RE, '')
|
|
271
292
|
.replace(/\bline\s+\d+\b/g, '')
|
|
293
|
+
.replace(LINE_COUNT_RE, '')
|
|
294
|
+
.replace(GITHUB_LINE_RANGE_RE, '')
|
|
272
295
|
.replace(/\s+/g, ' ')
|
|
273
296
|
.trim();
|
|
274
297
|
}
|
|
@@ -303,6 +326,27 @@ const ORCHESTRATOR_OWNED_FINDING_FIELDS = [
|
|
|
303
326
|
function mintFindingId(taskId, fingerprint) {
|
|
304
327
|
return `f-${taskId}-${fingerprint.slice(0, 8)}`;
|
|
305
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* A shallow clone with the orchestrator-owned fields removed, so the
|
|
331
|
+
* schema-shape check below runs against exactly the judge's half of the
|
|
332
|
+
* contract. `finding-evidence.schema.json` declares `additionalProperties:
|
|
333
|
+
* false` and never lists these six fields — they are the orchestrator's to
|
|
334
|
+
* mint, never the judge's to write (interview N-2) — so validating a raw
|
|
335
|
+
* evidence item against it, un-stripped, would reject every legitimate item
|
|
336
|
+
* that happens to be re-minted from a stored finding and would make the
|
|
337
|
+
* dedicated `findings.evidence-carries-identity` error below unreachable.
|
|
338
|
+
*/
|
|
339
|
+
function stripOwnedFields(item) {
|
|
340
|
+
const clone = { ...item };
|
|
341
|
+
for (const field of ORCHESTRATOR_OWNED_FINDING_FIELDS)
|
|
342
|
+
delete clone[field];
|
|
343
|
+
return clone;
|
|
344
|
+
}
|
|
345
|
+
function describeShapeFailures(failures) {
|
|
346
|
+
return failures
|
|
347
|
+
.map((f) => `index ${f.index} (${f.errors.map((e) => e.message).join('; ')})`)
|
|
348
|
+
.join(', ');
|
|
349
|
+
}
|
|
306
350
|
/**
|
|
307
351
|
* Guess which taxonomy value a judge meant when it wrote something close but
|
|
308
352
|
* not equal to one. Derived from the vocabulary itself rather than a synonym
|
|
@@ -344,7 +388,20 @@ function suggestCanonical(values, written) {
|
|
|
344
388
|
* `test-gap` for `test-coverage` cost a whole gate round that way.
|
|
345
389
|
*/
|
|
346
390
|
export function mintFindings(evidence, ctx, opts = {}) {
|
|
347
|
-
const { taxonomy } = resolveTaxonomyAndSchemas(opts);
|
|
391
|
+
const { taxonomy, schemas } = resolveTaxonomyAndSchemas(opts);
|
|
392
|
+
// Issue #233: a wrong field name (`category`/`title`/`evidence` for
|
|
393
|
+
// `finding_category`/`summary`/none-of-those) or a wrong-shaped field
|
|
394
|
+
// (`failure_scenario` given as a string instead of {inputs, expected,
|
|
395
|
+
// actual}) used to reach `suggestCanonical`/`computeFingerprint` below and
|
|
396
|
+
// crash with a raw TypeError instead of a FindingError. Checked here,
|
|
397
|
+
// against the schema `finding-evidence.schema.json` already documents as
|
|
398
|
+
// the judge's half of the contract, every item's shape is settled before
|
|
399
|
+
// any item's fingerprint is computed or any finding is minted — so a batch
|
|
400
|
+
// with one bad item mints nothing, not "the good ones, then a crash."
|
|
401
|
+
const shapeFailures = validateEachShape(schemas, 'finding-evidence', evidence.map(stripOwnedFields));
|
|
402
|
+
if (shapeFailures.length > 0) {
|
|
403
|
+
throw new FindingError('findings.invalid-evidence', `Finding evidence does not match finding-evidence.schema.json at ${describeShapeFailures(shapeFailures)}. Expected {file_path, finding_category, severity, summary, failure_scenario: {inputs, expected, actual}} (criterion_ref optional).`, { failures: shapeFailures });
|
|
404
|
+
}
|
|
348
405
|
const severities = taxonomy.dimensions.severity ?? [];
|
|
349
406
|
const categories = taxonomy.dimensions.finding_category ?? [];
|
|
350
407
|
return evidence.map((item, index) => {
|
|
@@ -508,7 +565,7 @@ export async function raiseFinding(input, ctx, opts = {}) {
|
|
|
508
565
|
}, opts);
|
|
509
566
|
return { suppressed: true, fingerprint, taskId: finding.task_id };
|
|
510
567
|
}
|
|
511
|
-
await appendEvent({
|
|
568
|
+
const stored = await appendEvent({
|
|
512
569
|
session_id: ctx.sessionId,
|
|
513
570
|
actor: ctx.actor ?? finding.found_by,
|
|
514
571
|
event_type: 'finding-raised',
|
|
@@ -517,7 +574,7 @@ export async function raiseFinding(input, ctx, opts = {}) {
|
|
|
517
574
|
causal_parent: ctx.causalParent,
|
|
518
575
|
payload: finding,
|
|
519
576
|
}, opts);
|
|
520
|
-
return { suppressed: false, finding };
|
|
577
|
+
return { suppressed: false, finding, event_id: stored.event_id };
|
|
521
578
|
}
|
|
522
579
|
/**
|
|
523
580
|
* Current state of every finding in this session's LINEAGE: a fold, never a
|
|
@@ -847,7 +904,7 @@ export async function transition(findingId, newStatus, ctx, opts = {}, extra = {
|
|
|
847
904
|
// answer to a caller who named none, rather than a status complaint.
|
|
848
905
|
if (current.finding_status === 'waived') {
|
|
849
906
|
if (extra.waiverRevokedBy === undefined) {
|
|
850
|
-
throw new FindingError('findings.waiver-revocation-unproven', `Finding "${findingId}" is waived, and a waiver is undone by revoking it, not by typing a status at it. Record the operator's reversal first — "
|
|
907
|
+
throw new FindingError('findings.waiver-revocation-unproven', `Finding "${findingId}" is waived, and a waiver is undone by revoking it, not by typing a status at it. Record the operator's reversal first — "bs waiver apply" with a "denied" decision on this fingerprint reopens the finding as part of writing the denial.`, { findingId, to: newStatus });
|
|
851
908
|
}
|
|
852
909
|
if (newStatus !== preWaiverStatus) {
|
|
853
910
|
throw new FindingError('findings.waiver-revocation-wrong-status', `Finding "${findingId}" was waived out of "${preWaiverStatus ?? 'an unrecorded status'}", so revoking that waiver returns it there, not to "${newStatus}". A revocation restores what the waiver closed over; it does not re-grade the finding.`, { findingId, to: newStatus, preWaiverStatus: preWaiverStatus ?? null });
|
|
@@ -860,14 +917,14 @@ export async function transition(findingId, newStatus, ctx, opts = {}, extra = {
|
|
|
860
917
|
throw new FindingError('findings.not-amendable', `Finding "${findingId}" is scoped "${findingScope(current)}", so it cannot take the amendment path at "${newStatus}": only a spec finding closes by amending the plan. A defect in the diff is closed by fixing the diff.`, { findingId, scope: findingScope(current), to: newStatus });
|
|
861
918
|
}
|
|
862
919
|
if (newStatus === AMEND_PENDING_STATUS && (extra.amendsTaskIds?.length ?? 0) === 0) {
|
|
863
|
-
throw new FindingError('findings.amendment-without-obligation', `Finding "${findingId}" cannot enter "${AMEND_PENDING_STATUS}" naming no task ids. An amendment that obligates nothing discharges the finding the moment it is written, which is D-127 — say which added or superseded task this finding is now waiting on. "
|
|
920
|
+
throw new FindingError('findings.amendment-without-obligation', `Finding "${findingId}" cannot enter "${AMEND_PENDING_STATUS}" naming no task ids. An amendment that obligates nothing discharges the finding the moment it is written, which is D-127 — say which added or superseded task this finding is now waiting on. "bs plan amend" derives that list from the plan diff and is the only verb that does (D-136).`, { findingId });
|
|
864
921
|
}
|
|
865
922
|
if (newStatus === AMENDED_STATUS) {
|
|
866
923
|
const outstanding = outstandingObligations(current, extra.amendsSatisfiedBy ?? []);
|
|
867
924
|
if (outstanding === null || outstanding.length > 0) {
|
|
868
925
|
throw new FindingError('findings.amendment-not-discharged', outstanding === null
|
|
869
926
|
? `Finding "${findingId}" is at "${AMEND_PENDING_STATUS}" but names no task ids at a plan version, so no evidence can discharge it. Expire it at the epic boundary instead.`
|
|
870
|
-
: `Finding "${findingId}" cannot close at "${AMENDED_STATUS}": ${outstanding.join(', ')} has not been shown to have landed at plan version ${current.amends_plan_version} or later. An amendment is discharged by its tasks landing, not by being told they did — this is D-127. "
|
|
927
|
+
: `Finding "${findingId}" cannot close at "${AMENDED_STATUS}": ${outstanding.join(', ')} has not been shown to have landed at plan version ${current.amends_plan_version} or later. An amendment is discharged by its tasks landing, not by being told they did — this is D-127. "bs epic close" computes and supplies that evidence.`, {
|
|
871
928
|
findingId,
|
|
872
929
|
outstanding: outstanding ?? [],
|
|
873
930
|
amendsPlanVersion: current.amends_plan_version,
|
|
@@ -895,7 +952,7 @@ export async function transition(findingId, newStatus, ctx, opts = {}, extra = {
|
|
|
895
952
|
// landed tasks and their versions rather than having to re-derive them.
|
|
896
953
|
if (extra.amendsSatisfiedBy !== undefined)
|
|
897
954
|
payload.amends_satisfied_by = extra.amendsSatisfiedBy.map((row) => ({ ...row }));
|
|
898
|
-
await appendEvent({
|
|
955
|
+
const stored = await appendEvent({
|
|
899
956
|
session_id: ctx.sessionId,
|
|
900
957
|
actor: ctx.actor ?? 'system',
|
|
901
958
|
event_type: 'finding-transitioned',
|
|
@@ -912,6 +969,12 @@ export async function transition(findingId, newStatus, ctx, opts = {}, extra = {
|
|
|
912
969
|
...(extra.amendsPlanVersion !== undefined
|
|
913
970
|
? { amends_plan_version: extra.amendsPlanVersion }
|
|
914
971
|
: {}),
|
|
972
|
+
// #286: the id of the `finding-transitioned` event this call just
|
|
973
|
+
// appended, the same shape #238 gave `judge report` -- so a caller
|
|
974
|
+
// chaining transitions as `--causal-parent` has it without falling back
|
|
975
|
+
// to "log line count - 1", which names a sibling's event under parallel
|
|
976
|
+
// writers.
|
|
977
|
+
event_id: stored.event_id,
|
|
915
978
|
};
|
|
916
979
|
// A reopened finding carrying the id of the waiver that no longer holds it
|
|
917
980
|
// is the same split state D-180 is about, one field smaller.
|
|
@@ -1045,7 +1108,7 @@ export async function repairObligation(input, ctx, opts = {}) {
|
|
|
1045
1108
|
if (malformedEntries.length === 0) {
|
|
1046
1109
|
throw new FindingError('findings.repair-not-corrupt', `Finding "${input.findingId}"'s obligation names ${original.length} task id(s), all ` +
|
|
1047
1110
|
'well-formed — there is nothing to repair. This verb repairs malformed amends_task_ids ' +
|
|
1048
|
-
'data; it never renegotiates a well-formed obligation. Use "
|
|
1111
|
+
'data; it never renegotiates a well-formed obligation. Use "bs plan amend" to change ' +
|
|
1049
1112
|
'what a finding is waiting on.', { findingId: input.findingId });
|
|
1050
1113
|
}
|
|
1051
1114
|
// Guard 4: reason required, non-empty. An unaudited repair is not a repair.
|
|
@@ -1093,7 +1156,7 @@ export async function repairObligation(input, ctx, opts = {}) {
|
|
|
1093
1156
|
if (current.amends_plan_version !== undefined) {
|
|
1094
1157
|
payload.amends_plan_version = current.amends_plan_version;
|
|
1095
1158
|
}
|
|
1096
|
-
await appendEvent({
|
|
1159
|
+
const stored = await appendEvent({
|
|
1097
1160
|
session_id: ctx.sessionId,
|
|
1098
1161
|
actor: ctx.actor ?? 'user',
|
|
1099
1162
|
event_type: FINDING_OBLIGATION_REPAIRED_EVENT_TYPE,
|
|
@@ -1102,7 +1165,12 @@ export async function repairObligation(input, ctx, opts = {}) {
|
|
|
1102
1165
|
causal_parent: ctx.causalParent,
|
|
1103
1166
|
payload,
|
|
1104
1167
|
}, opts);
|
|
1105
|
-
return {
|
|
1168
|
+
return {
|
|
1169
|
+
...current,
|
|
1170
|
+
amends_task_ids: toObligation,
|
|
1171
|
+
obligation_repair_reason: reason,
|
|
1172
|
+
event_id: stored.event_id,
|
|
1173
|
+
};
|
|
1106
1174
|
}
|
|
1107
1175
|
/**
|
|
1108
1176
|
* Which open findings are answering about code that no longer exists.
|
|
@@ -1181,9 +1249,34 @@ export async function staleFindings(sessionId, opts = {}) {
|
|
|
1181
1249
|
const filePath = finding.file_path;
|
|
1182
1250
|
if (filePath === undefined)
|
|
1183
1251
|
continue;
|
|
1252
|
+
// #217: the finding's OWN task merging its file is not a rewrite — it is
|
|
1253
|
+
// exactly the diff the reviewer read when raising the finding, so
|
|
1254
|
+
// treating it as staleness meant a waiver batch refused to grant over
|
|
1255
|
+
// evidence a merge had not touched at all. But a re-cut does not always
|
|
1256
|
+
// get a new task_id: a same-id supersede (spec.ts's "a same-id supersede
|
|
1257
|
+
// needs no pairing") keeps the original id, so the same task_id CAN
|
|
1258
|
+
// appear in `merges` twice — once for the diff already read, once for a
|
|
1259
|
+
// genuine later rewrite. Only that one reviewed-diff merge is excluded;
|
|
1260
|
+
// a second one is real evidence of a rewrite and must still be able to
|
|
1261
|
+
// mark the finding stale.
|
|
1262
|
+
//
|
|
1263
|
+
// The reviewed-diff merge is anchored to `raisedIndex`, not `freshUntil`:
|
|
1264
|
+
// it is the first merge of the finding's own task after the RAISE, and a
|
|
1265
|
+
// later reverify must not shift which merge that is. Anchoring to
|
|
1266
|
+
// `freshUntil` instead let raise → v1 merge → reverify → v2 merge skip
|
|
1267
|
+
// TWO merges — `freshUntil` (moved past v1 by the reverify) filtered v1
|
|
1268
|
+
// out of the loop before the self-merge check ever saw it, and the
|
|
1269
|
+
// loop's "first self-merge encountered" rule then wrongly treated v2,
|
|
1270
|
+
// the genuine post-reverify rewrite, as if it were the reviewed diff.
|
|
1271
|
+
// Computed once by identity (its event index) rather than re-derived
|
|
1272
|
+
// inside the loop, so the freshUntil filter below cannot shift which
|
|
1273
|
+
// merge counts as "already read".
|
|
1274
|
+
const reviewedMergeIndex = merges.find((merge) => merge.index > raisedIndex && taskIdsMatch(merge.taskId, finding.task_id))?.index;
|
|
1184
1275
|
for (const merge of merges) {
|
|
1185
1276
|
if (merge.index <= freshUntil)
|
|
1186
1277
|
continue;
|
|
1278
|
+
if (merge.index === reviewedMergeIndex)
|
|
1279
|
+
continue;
|
|
1187
1280
|
if (merge.filesChanged) {
|
|
1188
1281
|
const hit = merge.filesChanged.find((changed) => normalizeFilePath(changed) === filePath);
|
|
1189
1282
|
if (hit === undefined)
|
|
@@ -1237,7 +1330,7 @@ export async function reverifyFinding(findingId, note, ctx, opts = {}) {
|
|
|
1237
1330
|
findingId,
|
|
1238
1331
|
});
|
|
1239
1332
|
}
|
|
1240
|
-
await appendEvent({
|
|
1333
|
+
const stored = await appendEvent({
|
|
1241
1334
|
session_id: ctx.sessionId,
|
|
1242
1335
|
actor: ctx.actor ?? 'system',
|
|
1243
1336
|
event_type: 'finding-reverified',
|
|
@@ -1251,4 +1344,5 @@ export async function reverifyFinding(findingId, note, ctx, opts = {}) {
|
|
|
1251
1344
|
note,
|
|
1252
1345
|
},
|
|
1253
1346
|
}, opts);
|
|
1347
|
+
return { event_id: stored.event_id };
|
|
1254
1348
|
}
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { TASK_RESULT_EVENT_TYPE } from './agents-registry.js';
|
|
5
5
|
import { checkArtifacts } from './artifacts.js';
|
|
6
6
|
import { recordReattribution, routeFindings } from './attribution.js';
|
|
7
|
+
import { readTokenUsage } from './budgetAlarm.js';
|
|
7
8
|
import { checkTaskBudget } from './budgets.js';
|
|
8
9
|
import { certifyCommit } from './commit.js';
|
|
9
10
|
import { collectCoverageEvidence } from './coverage.js';
|
|
@@ -11,6 +12,7 @@ import { loadCrosscheckPolicy } from './crosscheck.js';
|
|
|
11
12
|
import { DiffstatError, measureDiff } from './diffstat.js';
|
|
12
13
|
import { appendEvent, readLineageEvents } from './events.js';
|
|
13
14
|
import { findingScope, raiseFinding, SPEC_FINDING_SCOPE, transition, } from './findings.js';
|
|
15
|
+
import { runGit } from './git.js';
|
|
14
16
|
import { outstandingJudges, readJudgeTurns } from './judges.js';
|
|
15
17
|
import { enabledExternalProviders, findingJudgeRequest, nativeProviderName, runQuorumCase, } from './quorum.js';
|
|
16
18
|
import { compileSchemas, validateRecord, } from './schemas.js';
|
|
@@ -19,6 +21,7 @@ import { readAddedTasks } from './taskEvents.js';
|
|
|
19
21
|
import { taskIdsMatch } from './taskId.js';
|
|
20
22
|
import { loadTaxonomy } from './taxonomy.js';
|
|
21
23
|
import { run as runTestgate } from './testgate.js';
|
|
24
|
+
import { checkUiux } from './uiuxGate.js';
|
|
22
25
|
let cachedTaxonomy;
|
|
23
26
|
let cachedSchemas;
|
|
24
27
|
function resolveTaxonomyAndSchemas(opts) {
|
|
@@ -31,7 +34,7 @@ function resolveTaxonomyAndSchemas(opts) {
|
|
|
31
34
|
return { taxonomy, schemas };
|
|
32
35
|
}
|
|
33
36
|
async function emit(eventType, payload, taskId, ctx, opts) {
|
|
34
|
-
|
|
37
|
+
return appendEvent({
|
|
35
38
|
session_id: ctx.sessionId,
|
|
36
39
|
actor: ctx.actor ?? 'system',
|
|
37
40
|
event_type: eventType,
|
|
@@ -97,7 +100,7 @@ function resultHash(result) {
|
|
|
97
100
|
async function recordResult(result, taskId, ctx, opts) {
|
|
98
101
|
const hash = resultHash(result);
|
|
99
102
|
const events = await readLineageEvents(ctx.sessionId, opts);
|
|
100
|
-
for (const { record } of events) {
|
|
103
|
+
for (const { event_id, record } of events) {
|
|
101
104
|
if (record.event_type !== TASK_RESULT_EVENT_TYPE)
|
|
102
105
|
continue;
|
|
103
106
|
// Matched, not compared: the same task reaches the log under both
|
|
@@ -111,9 +114,78 @@ async function recordResult(result, taskId, ctx, opts) {
|
|
|
111
114
|
// Result verbatim (db/projector.ts reads it as one), and a hash field
|
|
112
115
|
// wedged in beside it would be a Result that no longer validates.
|
|
113
116
|
if (resultHash(record.payload) === hash)
|
|
114
|
-
return;
|
|
117
|
+
return { eventId: event_id, deduped: true };
|
|
115
118
|
}
|
|
116
|
-
await emit(TASK_RESULT_EVENT_TYPE, result, taskId, ctx, opts);
|
|
119
|
+
const stored = await emit(TASK_RESULT_EVENT_TYPE, result, taskId, ctx, opts);
|
|
120
|
+
return { eventId: stored.event_id, deduped: false };
|
|
121
|
+
}
|
|
122
|
+
/** `name-desktop-light.png` and the three siblings a visual pass has to cover. */
|
|
123
|
+
const SCREENSHOT_RE = /-(desktop|mobile)-(light|dark)\.png$/;
|
|
124
|
+
function screenshotsFromArtifacts(artifacts) {
|
|
125
|
+
const shots = [];
|
|
126
|
+
for (const a of artifacts) {
|
|
127
|
+
const match = SCREENSHOT_RE.exec(a.path);
|
|
128
|
+
const viewport = match?.[1];
|
|
129
|
+
const theme = match?.[2];
|
|
130
|
+
if (viewport !== undefined && theme !== undefined)
|
|
131
|
+
shots.push({ path: a.path, viewport, theme });
|
|
132
|
+
}
|
|
133
|
+
return shots;
|
|
134
|
+
}
|
|
135
|
+
export async function recordTaskResult(input, ctx, opts = {}) {
|
|
136
|
+
const { taxonomy, schemas } = resolveTaxonomyAndSchemas(opts);
|
|
137
|
+
const schemaResult = validateRecord(schemas, taxonomy, 'result', input.result);
|
|
138
|
+
await emit('schema-check-result', { valid: schemaResult.valid, errors: schemaResult.valid ? [] : schemaResult.errors }, input.taskId, ctx, opts);
|
|
139
|
+
if (!schemaResult.valid) {
|
|
140
|
+
return {
|
|
141
|
+
outcome: 'blocked',
|
|
142
|
+
taskId: input.taskId,
|
|
143
|
+
reason: 'schema-invalid',
|
|
144
|
+
schemaErrors: schemaResult.errors,
|
|
145
|
+
artifactIssues: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// The worker's Result is a fact about a run that already happened, so it is
|
|
149
|
+
// recorded before the artifact check can block on it (P9-12) — a tester
|
|
150
|
+
// whose screenshot path was wrong still ran, and the projector should see
|
|
151
|
+
// the Result even when the artifact check below refuses it.
|
|
152
|
+
const recorded = await recordResult(input.result, input.taskId, ctx, opts);
|
|
153
|
+
const artifacts = input.result.artifacts ?? [];
|
|
154
|
+
const artifactCheck = checkArtifacts(artifacts, {
|
|
155
|
+
taskId: input.taskId,
|
|
156
|
+
artifactsDir: input.artifactsDir,
|
|
157
|
+
});
|
|
158
|
+
const freshness = input.worktreeDir !== undefined
|
|
159
|
+
? {
|
|
160
|
+
head: runGit(input.worktreeDir, ['rev-parse', 'HEAD']),
|
|
161
|
+
screenshots: screenshotsFromArtifacts(artifacts),
|
|
162
|
+
}
|
|
163
|
+
: {};
|
|
164
|
+
await emit('artifact-check-result', {
|
|
165
|
+
ok: artifactCheck.ok,
|
|
166
|
+
checked: artifactCheck.checked,
|
|
167
|
+
home: artifactCheck.home,
|
|
168
|
+
issues: artifactCheck.issues,
|
|
169
|
+
...freshness,
|
|
170
|
+
}, input.taskId, ctx, opts);
|
|
171
|
+
if (!artifactCheck.ok) {
|
|
172
|
+
return {
|
|
173
|
+
outcome: 'blocked',
|
|
174
|
+
taskId: input.taskId,
|
|
175
|
+
reason: 'artifacts-missing',
|
|
176
|
+
schemaErrors: [],
|
|
177
|
+
artifactIssues: artifactCheck.issues,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
outcome: 'recorded',
|
|
182
|
+
taskId: input.taskId,
|
|
183
|
+
eventId: recorded.eventId,
|
|
184
|
+
// Additive alongside the existing `eventId` (camelCase, pre-existing):
|
|
185
|
+
// same value, snake_case, matching every other write verb's printed id.
|
|
186
|
+
event_id: recorded.eventId,
|
|
187
|
+
deduped: recorded.deduped,
|
|
188
|
+
};
|
|
117
189
|
}
|
|
118
190
|
/** A judge call is a network round-trip, not a test: generous enough that a reasoning model finishes, bounded so one hung provider can't hold the gate open. */
|
|
119
191
|
const DEFAULT_JUDGE_BUDGET = { timeout_ms: 120_000, max_output_bytes: 262_144 };
|
|
@@ -231,7 +303,7 @@ function isRecord(value) {
|
|
|
231
303
|
* load-bearing only if something loads it.
|
|
232
304
|
*
|
|
233
305
|
* The verdict lives at `.structured_output` of the result envelope the grader
|
|
234
|
-
* writes (.claude/agents/grader.md, "Output contract"); the envelope itself is
|
|
306
|
+
* writes (.claude/agents/bs-grader.md, "Output contract"); the envelope itself is
|
|
235
307
|
* a Result and is not re-checked here. Three ways to not pass: the file is not
|
|
236
308
|
* shaped like a grader result (`grader-invalid`), the run came back `dead`
|
|
237
309
|
* because the task spec had no checkable acceptance criteria (`grader-fail`,
|
|
@@ -707,6 +779,38 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
707
779
|
outstandingJudges: outstanding,
|
|
708
780
|
}, ctx, opts);
|
|
709
781
|
}
|
|
782
|
+
// U2 S3: a ui-affecting task owes a reviewed, fresh screenshot set before
|
|
783
|
+
// the diff goes any further, measured against this run's own commit (D2).
|
|
784
|
+
// `uiAffecting` absent means no `--plan` was given at all, so the flag
|
|
785
|
+
// cannot be resolved either way — the stage records that honestly rather
|
|
786
|
+
// than guessing, and does not block (D6). `uiAffecting: false` (including a
|
|
787
|
+
// task with no flag at all, D3) means the stage has nothing to check.
|
|
788
|
+
let uiuxCheck = 'skipped';
|
|
789
|
+
if (input.uiAffecting === undefined) {
|
|
790
|
+
uiuxCheck = 'unverifiable';
|
|
791
|
+
}
|
|
792
|
+
else if (input.uiAffecting) {
|
|
793
|
+
uiuxCheck = 'checked';
|
|
794
|
+
const uiux = await checkUiux({
|
|
795
|
+
taskId: input.taskId,
|
|
796
|
+
...(commitCheck.head === null ? {} : { head: commitCheck.head }),
|
|
797
|
+
artifactsDir: input.artifactsDir,
|
|
798
|
+
}, ctx, opts);
|
|
799
|
+
if (uiux.outcome === 'blocked') {
|
|
800
|
+
return finalize({
|
|
801
|
+
outcome: 'blocked',
|
|
802
|
+
taskId: input.taskId,
|
|
803
|
+
reason: uiux.reason,
|
|
804
|
+
testResult: null,
|
|
805
|
+
schemaErrors: [],
|
|
806
|
+
blockingFindings: [],
|
|
807
|
+
artifactIssues: [],
|
|
808
|
+
commitCheck,
|
|
809
|
+
uiuxCheck,
|
|
810
|
+
}, ctx, opts);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const uiuxStatus = { uiuxCheck };
|
|
710
814
|
// Before the tests, not after: the grader has already run, and if its rubric
|
|
711
815
|
// says the criteria were not met the diff bounces whatever the suite says —
|
|
712
816
|
// so a full test run here is spent for nothing. It sits behind the commit
|
|
@@ -724,6 +828,7 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
724
828
|
blockingFindings: [],
|
|
725
829
|
artifactIssues: [],
|
|
726
830
|
commitCheck,
|
|
831
|
+
...uiuxStatus,
|
|
727
832
|
}, ctx, opts);
|
|
728
833
|
}
|
|
729
834
|
}
|
|
@@ -753,6 +858,7 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
753
858
|
artifactIssues: [],
|
|
754
859
|
commitCheck,
|
|
755
860
|
...budget,
|
|
861
|
+
...uiuxStatus,
|
|
756
862
|
}, ctx, opts);
|
|
757
863
|
}
|
|
758
864
|
// After the tests pass, before the findings are judged: a green coverage
|
|
@@ -789,6 +895,7 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
789
895
|
commitCheck,
|
|
790
896
|
...budget,
|
|
791
897
|
...withCoverage,
|
|
898
|
+
...uiuxStatus,
|
|
792
899
|
}, ctx, opts);
|
|
793
900
|
}
|
|
794
901
|
}
|
|
@@ -822,6 +929,7 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
822
929
|
...diverted,
|
|
823
930
|
...budget,
|
|
824
931
|
...withCoverage,
|
|
932
|
+
...uiuxStatus,
|
|
825
933
|
}, ctx, opts);
|
|
826
934
|
}
|
|
827
935
|
if (pendingWaivers.length > 0) {
|
|
@@ -836,6 +944,7 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
836
944
|
...diverted,
|
|
837
945
|
...budget,
|
|
838
946
|
...withCoverage,
|
|
947
|
+
...uiuxStatus,
|
|
839
948
|
}, ctx, opts);
|
|
840
949
|
}
|
|
841
950
|
return finalize({
|
|
@@ -848,12 +957,13 @@ export async function runGate(input, ctx, opts = {}) {
|
|
|
848
957
|
...diverted,
|
|
849
958
|
...budget,
|
|
850
959
|
...withCoverage,
|
|
960
|
+
...uiuxStatus,
|
|
851
961
|
}, ctx, opts);
|
|
852
962
|
}
|
|
853
|
-
/** Real token spend off the already-schema-valid result
|
|
963
|
+
/** Real token spend off the already-schema-valid result; undefined when absent or an unmeasured placeholder. */
|
|
854
964
|
function tokensSpent(result) {
|
|
855
|
-
const
|
|
856
|
-
return
|
|
965
|
+
const read = readTokenUsage(result?.token_usage);
|
|
966
|
+
return read.measured ? read.total : undefined;
|
|
857
967
|
}
|
|
858
968
|
/**
|
|
859
969
|
* Declared budget vs. measured spend, for one task. The check never blocks: an
|
|
@@ -906,6 +1016,6 @@ function runBudgetCheck(input) {
|
|
|
906
1016
|
}
|
|
907
1017
|
}
|
|
908
1018
|
async function finalize(outcome, ctx, opts) {
|
|
909
|
-
await emit('gate-outcome', { outcome: outcome.outcome, reason: 'reason' in outcome ? outcome.reason : null }, outcome.taskId, ctx, opts);
|
|
910
|
-
return outcome;
|
|
1019
|
+
const stored = await emit('gate-outcome', { outcome: outcome.outcome, reason: 'reason' in outcome ? outcome.reason : null }, outcome.taskId, ctx, opts);
|
|
1020
|
+
return { ...outcome, event_id: stored.event_id };
|
|
911
1021
|
}
|
|
@@ -141,9 +141,26 @@ function writeBodyFile(body) {
|
|
|
141
141
|
export function buildCreateIssueArgv(repo, title, body) {
|
|
142
142
|
return ['issue', 'create', '--repo', repo, '--title', title, '--body-file', writeBodyFile(body)];
|
|
143
143
|
}
|
|
144
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Pure builder: the argv for `gh issue list` restricted to open issues
|
|
146
|
+
* matching `query`. Without --json, gh prints a human-readable table that
|
|
147
|
+
* parseSearchResult can never parse as JSON, folding a real search into a
|
|
148
|
+
* generic failure indistinguishable from an actual nonzero exit. The fields
|
|
149
|
+
* requested here are exactly the ones parseSearchResult reads off each item.
|
|
150
|
+
*/
|
|
145
151
|
export function buildSearchIssuesArgv(repo, query) {
|
|
146
|
-
return [
|
|
152
|
+
return [
|
|
153
|
+
'issue',
|
|
154
|
+
'list',
|
|
155
|
+
'--repo',
|
|
156
|
+
repo,
|
|
157
|
+
'--state',
|
|
158
|
+
'open',
|
|
159
|
+
'--json',
|
|
160
|
+
'number,body,url',
|
|
161
|
+
'--search',
|
|
162
|
+
query,
|
|
163
|
+
];
|
|
147
164
|
}
|
|
148
165
|
/** Pure builder: the argv for `gh issue comment`. Spawns nothing. */
|
|
149
166
|
export function buildCommentArgv(repo, issueNumber, body) {
|
|
@@ -44,7 +44,11 @@ export function redactCredentials(text) {
|
|
|
44
44
|
return colon === -1 ? `${scheme}***@` : `${scheme}${userinfo.slice(0, colon)}:***@`;
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* `onConflict`, when given, reads what git said from stdout on exit 1 — for
|
|
49
|
+
* the commands (merge-tree) that report a conflict there, not on stderr.
|
|
50
|
+
*/
|
|
51
|
+
function exec(cwd, args, onConflict) {
|
|
48
52
|
try {
|
|
49
53
|
return execFileSync('git', args, {
|
|
50
54
|
cwd,
|
|
@@ -57,7 +61,9 @@ function exec(cwd, args) {
|
|
|
57
61
|
// A child that never spawned has no exit code; `code` carries the errno
|
|
58
62
|
// string instead, and that is the only thing worth reporting.
|
|
59
63
|
const status = typeof e.status === 'number' ? e.status : null;
|
|
60
|
-
const said =
|
|
64
|
+
const said = onConflict !== undefined && status === 1
|
|
65
|
+
? onConflict(e.stdout ?? '')
|
|
66
|
+
: (e.stderr ?? (typeof e.code === 'string' ? `git could not be run (${e.code})` : ''));
|
|
61
67
|
throw new GitCommandError(cwd, args, status, redactCredentials(said));
|
|
62
68
|
}
|
|
63
69
|
}
|
|
@@ -72,6 +78,18 @@ export function runGit(cwd, args) {
|
|
|
72
78
|
export function runGitRaw(cwd, args) {
|
|
73
79
|
return exec(cwd, args);
|
|
74
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* `git merge-tree --write-tree` of two commits: the merged tree's id, with no
|
|
83
|
+
* working tree or index touched. A conflict (exit 1) throws GitCommandError
|
|
84
|
+
* carrying the conflicted paths and git's CONFLICT messages, which merge-tree
|
|
85
|
+
* prints on stdout rather than stderr.
|
|
86
|
+
*/
|
|
87
|
+
export function writeMergeTree(cwd, base, head) {
|
|
88
|
+
const args = ['merge-tree', '--write-tree', '--name-only', base, head];
|
|
89
|
+
// stdout on a conflict is the would-be tree id, then what conflicted.
|
|
90
|
+
const out = exec(cwd, args, (stdout) => stdout.split('\n').slice(1).join('\n'));
|
|
91
|
+
return out.split('\n')[0];
|
|
92
|
+
}
|
|
75
93
|
/**
|
|
76
94
|
* The `origin` remote's URL, exactly as git reports it — credentials and
|
|
77
95
|
* all, since redaction happens only on the error path inside `exec`. Throws
|
|
@@ -197,7 +197,7 @@ export function goalCheckBlockers(epicId, status, planVersion, livePlanTaskIds)
|
|
|
197
197
|
}
|
|
198
198
|
if (check === null) {
|
|
199
199
|
return [
|
|
200
|
-
`Epic "${epicId}" has no spec-vs-goal check on record: nothing has asked whether its plan answers the goal milestone "${goal.milestoneId}" declares. Run \`
|
|
200
|
+
`Epic "${epicId}" has no spec-vs-goal check on record: nothing has asked whether its plan answers the goal milestone "${goal.milestoneId}" declares. Run \`bs epic goal-check --epic ${epicId}\`.`,
|
|
201
201
|
];
|
|
202
202
|
}
|
|
203
203
|
if (check.goalDigest !== goal.digest) {
|
|
@@ -246,7 +246,7 @@ export function goalCheckBlockers(epicId, status, planVersion, livePlanTaskIds)
|
|
|
246
246
|
const uncovered = check.coverage.filter((entry) => entry.verdict === 'uncovered');
|
|
247
247
|
if (uncovered.length > 0) {
|
|
248
248
|
return [
|
|
249
|
-
`The spec-vs-goal check for "${epicId}" left ${uncovered.length} goal clause(s) uncovered by plan v${check.planVersion}: ${uncovered.map((entry) => JSON.stringify(entry.clause)).join('; ')}. Answer them with \`
|
|
249
|
+
`The spec-vs-goal check for "${epicId}" left ${uncovered.length} goal clause(s) uncovered by plan v${check.planVersion}: ${uncovered.map((entry) => JSON.stringify(entry.clause)).join('; ')}. Answer them with \`bs plan amend\` — no task diff can contain the fix.`,
|
|
250
250
|
];
|
|
251
251
|
}
|
|
252
252
|
return [];
|