@galda/cli 0.10.97 → 0.10.99
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/app/index.html +55 -6
- package/engine/requirement-model.mjs +13 -3
- package/engine/requirement-verification.mjs +53 -0
- package/engine/server.mjs +38 -0
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -1389,6 +1389,9 @@
|
|
|
1389
1389
|
Was a pulsing dot (fsrundot). Centre WITHOUT transform (spin's rotate would override a centring
|
|
1390
1390
|
translate → drift bottom-right) via top/left:50% + negative-half margin; concentric with the 4px dot. */
|
|
1391
1391
|
#fsRoot .fsproj.run i{background:none;box-shadow:none;position:relative}
|
|
1392
|
+
/* .fsproj.on i and .fsproj.run i have equal specificity. Keep the running
|
|
1393
|
+
ring visible when the currently selected project is also running. */
|
|
1394
|
+
#fsRoot .fsproj.on.run i{background:none;box-shadow:none}
|
|
1392
1395
|
/* Review-blue comparison baseline: the Doing cue is a 7px / 1.3px broken
|
|
1393
1396
|
ring, while idle and Review remain 4px dots. */
|
|
1394
1397
|
#fsRoot .fsproj.run i::after{content:"";position:absolute;top:50%;left:50%;width:7px;height:7px;margin:-3.5px 0 0 -3.5px;box-sizing:border-box;border-radius:50%;border:1.3px solid var(--green);border-top-color:transparent;animation:spin 1.8s linear infinite}
|
|
@@ -1920,7 +1923,7 @@
|
|
|
1920
1923
|
box-shadow:inset 0 0 0 1px rgba(var(--green-rgb),.30);font:600 12px/1 var(--ui);cursor:pointer;display:inline-flex;align-items:center;gap:7px}
|
|
1921
1924
|
.sa-app:hover{box-shadow:inset 0 0 0 1px var(--green);background:rgba(var(--green-rgb),.12)}
|
|
1922
1925
|
.sa-app svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
|
|
1923
|
-
.sa-approve-wrap{display:flex;flex-direction:column;align-items:
|
|
1926
|
+
.sa-approve-wrap{display:flex;flex-direction:column;align-items:center;gap:5px;flex:0 0 auto}
|
|
1924
1927
|
.sa-approve-wrap + .sa-dis{align-self:flex-start}
|
|
1925
1928
|
.sa-delivery{font:500 9.5px/1.15 var(--mono);color:var(--ink3);text-decoration:none;white-space:nowrap}
|
|
1926
1929
|
a.sa-delivery{color:var(--green)}
|
|
@@ -6532,8 +6535,11 @@ function renderFsRail(){
|
|
|
6532
6535
|
// project the To Do/Kanban view showed as Doing kept a static dot in the rail
|
|
6533
6536
|
// (Masa report 2026-07-27: "全部doingなのに緑のアイコンが回ってない"). Match the
|
|
6534
6537
|
// same bucket the board/list already use, minus Paused (intentionally static).
|
|
6535
|
-
|
|
6536
|
-
|
|
6538
|
+
// A live task is the strongest running signal. Keep the goal-bucket fallback
|
|
6539
|
+
// for Doing goals that have not created their first task yet.
|
|
6540
|
+
const running = ptasks.some((t) => t.status === 'running')
|
|
6541
|
+
|| pgoals.some((g) => fsBoardBucket(g, ptasks) === 'doing'
|
|
6542
|
+
&& !(g.status === 'blocked' && g.blocked?.kind === 'paused'));
|
|
6537
6543
|
const td = ptasks.filter((t) => ['running', 'queued'].includes(t.status)).length;
|
|
6538
6544
|
// The rail's eye icon is the review/attention count. Needs-you goals
|
|
6539
6545
|
// (needsInput/needsApproval/blocked, minus Paused) block on the person the
|
|
@@ -9985,8 +9991,23 @@ function saItemHtml(it, i){
|
|
|
9985
9991
|
const runDeclErrorBody = (!((it.run?.cmd || it.run?.url)) && it.runDeclError)
|
|
9986
9992
|
? `<div class="ropen rdeclerr"><p class="rbody v">${failureLang() === 'ja' ? 'AIが開けるものを用意しようとしましたが、記録が読み取れませんでした。返信して直させてください。' : 'The AI tried to declare something to open here, but the record was unreadable. Reply to have it rewritten.'}</p></div>`
|
|
9987
9993
|
: '';
|
|
9994
|
+
// The worker declared a static file AND changed something else (engine sets
|
|
9995
|
+
// run.alsoChanged; goal #871). Both arrive as a /upload link, so the address
|
|
9996
|
+
// above cannot tell you whether you are about to open the product or a page
|
|
9997
|
+
// the agent wrote about the product — #871 was the second, and the request
|
|
9998
|
+
// ("something I can check locally") had to be made again.
|
|
9999
|
+
//
|
|
10000
|
+
// One sentence of fact in the same voice as the note beside it: no badge, no
|
|
10001
|
+
// colour, no warning tone. The card is not saying the worker did the wrong
|
|
10002
|
+
// thing — a write-up beside a code change is often exactly right. It is
|
|
10003
|
+
// saying which of the two this link is. Absent → nothing renders (§1.5-3).
|
|
10004
|
+
const alsoChanged = it.run?.alsoChanged?.length
|
|
10005
|
+
? `<p class="rbody v">${failureLang() === 'ja'
|
|
10006
|
+
? `AIが書いたファイルを開きます。このタスクは ${esc(it.run.alsoChanged.join(' · '))} も変更しています。`
|
|
10007
|
+
: `Opens a file the agent wrote. This task also changed ${esc(it.run.alsoChanged.join(' · '))}.`}</p>`
|
|
10008
|
+
: '';
|
|
9988
10009
|
const openBody = (it.run?.cmd || it.run?.url)
|
|
9989
|
-
? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}${it.interruptedTask ? ' Available now; it does not wait for this run to finish.' : ''}</p
|
|
10010
|
+
? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}${it.interruptedTask ? ' Available now; it does not wait for this run to finish.' : ''}</p>${alsoChanged}</div>`
|
|
9990
10011
|
: it.pr
|
|
9991
10012
|
? `<div class="ropen"><a class="olink" href="${esc(it.pr)}" target="_blank" rel="noopener">${esc(it.pr)}<span class="ar">↗</span></a><p class="rbody v">Review and merge this pull request to finish the item.</p></div>`
|
|
9992
10013
|
: runDeclErrorBody;
|
|
@@ -10316,6 +10337,24 @@ function saRender(){
|
|
|
10316
10337
|
const previousBody = S.querySelector('.sa-body');
|
|
10317
10338
|
const previousScrollTop = previousBody?.scrollTop ?? 0;
|
|
10318
10339
|
const restoreScroll = Boolean(previousBody) && SA.focusGid == null;
|
|
10340
|
+
// A reply changes the height of the card being replied to (new thread line, status
|
|
10341
|
+
// change) — restoring a raw scrollTop pixel then leaves the viewport at the same
|
|
10342
|
+
// number but showing DIFFERENT content, a visible jolt on whatever card the person
|
|
10343
|
+
// was actually reading (2026-07-29). Anchor on that card's own goalId instead: find
|
|
10344
|
+
// the topmost visible card before the rebuild, and after the rebuild scroll so that
|
|
10345
|
+
// SAME card sits at the SAME on-screen position, regardless of how much other cards
|
|
10346
|
+
// above it grew or shrank.
|
|
10347
|
+
let anchorGid = null, anchorOffset = 0;
|
|
10348
|
+
if (restoreScroll) {
|
|
10349
|
+
const containerTop = previousBody.getBoundingClientRect().top;
|
|
10350
|
+
// The card actually being read sits near the middle of the viewport, not whatever
|
|
10351
|
+
// sliver of a previous card's bottom edge still peeks in at the very top.
|
|
10352
|
+
const midY = containerTop + previousBody.clientHeight / 2;
|
|
10353
|
+
for (const el of previousBody.querySelectorAll('.sa-item')) {
|
|
10354
|
+
const r = el.getBoundingClientRect();
|
|
10355
|
+
if (r.top <= midY && r.bottom >= midY) { anchorGid = el.dataset.gid; anchorOffset = r.top - containerTop; break; }
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10319
10358
|
const focusedReplyGoal = document.activeElement?.matches?.('#seeall .sa-chat')
|
|
10320
10359
|
? document.activeElement.dataset.goal
|
|
10321
10360
|
: null;
|
|
@@ -10340,7 +10379,6 @@ function saRender(){
|
|
|
10340
10379
|
SA.lastMarkup = markup;
|
|
10341
10380
|
S.innerHTML = markup;
|
|
10342
10381
|
const body = S.querySelector('.sa-body');
|
|
10343
|
-
if (body && restoreScroll) body.scrollTop = previousScrollTop;
|
|
10344
10382
|
// Once the person scrolls, their position wins over the short opening-focus
|
|
10345
10383
|
// animation. Programmatic opening scroll fires this too, which is fine: the
|
|
10346
10384
|
// first placement happened and later SSE ticks should preserve it.
|
|
@@ -10352,7 +10390,18 @@ function saRender(){
|
|
|
10352
10390
|
// Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
|
|
10353
10391
|
// track what's open per goal+section in SA.detOpen (persists across re-renders)
|
|
10354
10392
|
// and wire each one's toggle to keep that set current.
|
|
10355
|
-
paintReplyNote(); // the answer/question under a card's reply box survives its rebuild
|
|
10393
|
+
paintReplyNote(); // the answer/question under a card's reply box survives its rebuild — this is
|
|
10394
|
+
// also what actually grows a replied-to card's height, so the scroll anchor
|
|
10395
|
+
// below must run AFTER it, not before (2026-07-29 jolt fix).
|
|
10396
|
+
if (body && restoreScroll) {
|
|
10397
|
+
const anchorEl = anchorGid != null ? body.querySelector(`.sa-item[data-gid="${anchorGid}"]`) : null;
|
|
10398
|
+
if (anchorEl) {
|
|
10399
|
+
const newOffset = anchorEl.getBoundingClientRect().top - body.getBoundingClientRect().top;
|
|
10400
|
+
body.scrollTop += (newOffset - anchorOffset);
|
|
10401
|
+
} else {
|
|
10402
|
+
body.scrollTop = previousScrollTop;
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10356
10405
|
for (const det of S.querySelectorAll('[data-detkey]')) {
|
|
10357
10406
|
det.addEventListener('toggle', () => {
|
|
10358
10407
|
const k = det.dataset.detkey;
|
|
@@ -63,16 +63,26 @@ function normalizeEvidence(value) {
|
|
|
63
63
|
const requirementId = cleanText(value?.requirementId);
|
|
64
64
|
const attemptId = cleanText(value?.attemptId);
|
|
65
65
|
if (!requirementId || !attemptId) return null;
|
|
66
|
-
const claim = CLAIM_STATUSES.has(value.claim) ? value.claim :
|
|
66
|
+
const claim = CLAIM_STATUSES.has(value.claim) ? value.claim : null;
|
|
67
67
|
const coverage = COVERAGE_STATUSES.has(value.coverage) ? value.coverage : 'unverified';
|
|
68
|
+
const workerEvidence = cleanText(value.workerEvidence ?? value.evidence);
|
|
69
|
+
const workerFiles = [...new Set([
|
|
70
|
+
...(Array.isArray(value.workerFiles) ? value.workerFiles : []),
|
|
71
|
+
...(Array.isArray(value.files) ? value.files : []),
|
|
72
|
+
].filter((item) => typeof item === 'string' && item))];
|
|
73
|
+
const observedFiles = [...new Set((Array.isArray(value.observedFiles) ? value.observedFiles : []).filter((item) => typeof item === 'string' && item))];
|
|
68
74
|
return {
|
|
69
75
|
requirementId,
|
|
70
76
|
attemptId,
|
|
71
77
|
taskId: value.taskId ?? null,
|
|
72
78
|
claim,
|
|
73
79
|
coverage,
|
|
74
|
-
evidence:
|
|
75
|
-
files: [...new Set(
|
|
80
|
+
evidence: workerEvidence,
|
|
81
|
+
files: [...new Set([...workerFiles, ...observedFiles])],
|
|
82
|
+
workerEvidence,
|
|
83
|
+
workerFiles,
|
|
84
|
+
observedFiles,
|
|
85
|
+
checks: Array.isArray(value.checks) ? value.checks : [],
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
88
|
|
|
@@ -66,3 +66,56 @@ export function mergeRequirementVerificationEvidence(existing = [], incoming = [
|
|
|
66
66
|
}
|
|
67
67
|
return [...rows.values()];
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Translate Manager-owned observations into explicitly requirement-scoped
|
|
72
|
+
* checks. Merely seeing files or successfully starting a preview is useful
|
|
73
|
+
* evidence, but it is not proof that the requested behavior is correct.
|
|
74
|
+
*/
|
|
75
|
+
export function buildManagerVerificationChecks({
|
|
76
|
+
requirementIds = [], testResult = null, testsFailing = false, proof = null,
|
|
77
|
+
run = null, previewVerification = null, changedFiles = [],
|
|
78
|
+
} = {}) {
|
|
79
|
+
const ids = cleanList(requirementIds);
|
|
80
|
+
const files = cleanList(changedFiles);
|
|
81
|
+
const checks = [];
|
|
82
|
+
|
|
83
|
+
if (files.length) {
|
|
84
|
+
checks.push({
|
|
85
|
+
kind: 'changed-files', status: 'observed', requirementIds: ids, files,
|
|
86
|
+
detail: `${files.length} changed file${files.length === 1 ? '' : 's'} observed`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (testResult) {
|
|
91
|
+
let status = 'observed';
|
|
92
|
+
if (!testResult.ran || testResult.skipped) status = 'not-applicable';
|
|
93
|
+
else if (testsFailing) status = 'failed';
|
|
94
|
+
else if (testResult.ok === true && !testResult.inconclusive && !testResult.infraOnly && !testResult.preExistingOnly) status = 'verified';
|
|
95
|
+
checks.push({
|
|
96
|
+
kind: 'test', status, requirementIds: ids,
|
|
97
|
+
detail: testResult.detail || testResult.skippedReason || `${testResult.passed ?? 0} passed, ${testResult.failed ?? 0} failed`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (proof) {
|
|
102
|
+
checks.push({
|
|
103
|
+
kind: 'proof',
|
|
104
|
+
status: proof.verified === true ? (proof.pass === true ? 'verified' : 'failed') : 'observed',
|
|
105
|
+
requirementIds: ids,
|
|
106
|
+
detail: proof.detail || 'Proof artifact observed',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (run || previewVerification) {
|
|
111
|
+
checks.push({
|
|
112
|
+
kind: 'open-it', status: 'observed', requirementIds: ids,
|
|
113
|
+
detail: previewVerification?.detail || (previewVerification?.status === 'started'
|
|
114
|
+
? 'Local preview started; visual correctness still requires review'
|
|
115
|
+
: 'Worker declared a local preview'),
|
|
116
|
+
...(previewVerification?.url || run?.url ? { url: previewVerification?.url || run.url } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return checks;
|
|
121
|
+
}
|
package/engine/server.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
23
23
|
import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath, parseRequestedWorkspaceDir } from './lib.mjs';
|
|
24
24
|
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
25
25
|
import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
|
|
26
|
+
import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
|
|
26
27
|
import { createSerialQueue } from './lib.mjs';
|
|
27
28
|
import { parseRequirementEvidence, unmappedChangedFiles, previewOpensSomethingElse } from './lib.mjs';
|
|
28
29
|
import { isPrClosed } from './lib.mjs';
|
|
@@ -3854,6 +3855,26 @@ function prSafetyIssue(goal, project, files) {
|
|
|
3854
3855
|
return classifyPrSafety({ baseline, files, currentFiles: gitStatusPaths(lines), currentBranch });
|
|
3855
3856
|
}
|
|
3856
3857
|
|
|
3858
|
+
function syncGoalRequirementVerification(goal, siblings, { testsFailing = false } = {}) {
|
|
3859
|
+
const incoming = siblings.flatMap((task) => buildRequirementVerificationEvidence({
|
|
3860
|
+
requirementIds: task.activeRequirementIds ?? [],
|
|
3861
|
+
attemptId: task.attemptId || `task-${task.id}`,
|
|
3862
|
+
taskId: task.id,
|
|
3863
|
+
workerEvidence: task.requirementEvidence,
|
|
3864
|
+
changedFiles: task.changedFiles ?? [],
|
|
3865
|
+
checks: buildManagerVerificationChecks({
|
|
3866
|
+
requirementIds: task.activeRequirementIds ?? [],
|
|
3867
|
+
testResult: goal.testResult,
|
|
3868
|
+
testsFailing,
|
|
3869
|
+
proof: task.proof,
|
|
3870
|
+
run: task.run,
|
|
3871
|
+
previewVerification: task.previewVerification,
|
|
3872
|
+
changedFiles: task.changedFiles ?? [],
|
|
3873
|
+
}),
|
|
3874
|
+
}));
|
|
3875
|
+
goal.requirementEvidence = mergeRequirementVerificationEvidence(goal.requirementEvidence ?? [], incoming);
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3857
3878
|
// The Manager-verified review gate (shared by goal completion and /reverify):
|
|
3858
3879
|
// run OUR OWN tests, generate a plain-language headline, and require proof for a
|
|
3859
3880
|
// UI change — capturing it (3 tries) if missing. Any failure → 'blocked' (shown
|
|
@@ -3975,6 +3996,10 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3975
3996
|
// Count that drives the block message: the NEW failures when we could diff
|
|
3976
3997
|
// against the base, else the raw total (fail-closed when the base is unknown).
|
|
3977
3998
|
const blockingFailCount = goal.testResult.newFailures?.length ?? goal.testResult.failed;
|
|
3999
|
+
// Persist the Manager's own requirement-scoped observations before any
|
|
4000
|
+
// automatic rework return. A worker claim alone never becomes verified.
|
|
4001
|
+
syncGoalRequirementVerification(goal, siblings, { testsFailing });
|
|
4002
|
+
saveGoal(goal);
|
|
3978
4003
|
// §4補足: requireVerifyPass (opt-in, off by default) makes the gate strict —
|
|
3979
4004
|
// EVERY task must carry a passing proof, not just UI tasks. Only ever tightens;
|
|
3980
4005
|
// never conflicts with §6 (still can't reach Done without a human).
|
|
@@ -5036,14 +5061,27 @@ const server = createServer(async (req, res) => {
|
|
|
5036
5061
|
const goal = goals.find((g) => g.id === task.goalId);
|
|
5037
5062
|
startPreview(task)
|
|
5038
5063
|
.then((p) => {
|
|
5064
|
+
task.previewVerification = {
|
|
5065
|
+
status: 'started', url: p.url, at: new Date().toISOString(),
|
|
5066
|
+
detail: 'Local preview started; visual correctness still requires review',
|
|
5067
|
+
};
|
|
5068
|
+
saveTask(task);
|
|
5039
5069
|
if (goal?.verificationInfraError?.source === 'preview') {
|
|
5040
5070
|
goal.verificationInfraError = null;
|
|
5071
|
+
}
|
|
5072
|
+
if (goal) {
|
|
5073
|
+
syncGoalRequirementVerification(goal, tasks.filter((item) => item.goalId === goal.id), {
|
|
5074
|
+
testsFailing: Boolean(goal.testResult?.ran && goal.testResult.failed > 0
|
|
5075
|
+
&& !goal.testResult.inconclusive && !goal.testResult.infraOnly && !goal.testResult.preExistingOnly),
|
|
5076
|
+
});
|
|
5041
5077
|
saveGoal(goal);
|
|
5042
5078
|
}
|
|
5043
5079
|
json(res, 200, p);
|
|
5044
5080
|
})
|
|
5045
5081
|
.catch((e) => {
|
|
5046
5082
|
const detail = String(e.message ?? e).slice(0, 200);
|
|
5083
|
+
task.previewVerification = { status: 'infra-error', detail, at: new Date().toISOString() };
|
|
5084
|
+
saveTask(task);
|
|
5047
5085
|
if (goal && goal.status === 'review') {
|
|
5048
5086
|
goal.verificationInfraError = buildVerificationInfraError({
|
|
5049
5087
|
source: 'preview',
|
package/package.json
CHANGED