@galda/cli 0.10.98 → 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 +39 -5
- 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
|
|
@@ -10331,6 +10337,24 @@ function saRender(){
|
|
|
10331
10337
|
const previousBody = S.querySelector('.sa-body');
|
|
10332
10338
|
const previousScrollTop = previousBody?.scrollTop ?? 0;
|
|
10333
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
|
+
}
|
|
10334
10358
|
const focusedReplyGoal = document.activeElement?.matches?.('#seeall .sa-chat')
|
|
10335
10359
|
? document.activeElement.dataset.goal
|
|
10336
10360
|
: null;
|
|
@@ -10355,7 +10379,6 @@ function saRender(){
|
|
|
10355
10379
|
SA.lastMarkup = markup;
|
|
10356
10380
|
S.innerHTML = markup;
|
|
10357
10381
|
const body = S.querySelector('.sa-body');
|
|
10358
|
-
if (body && restoreScroll) body.scrollTop = previousScrollTop;
|
|
10359
10382
|
// Once the person scrolls, their position wins over the short opening-focus
|
|
10360
10383
|
// animation. Programmatic opening scroll fires this too, which is fine: the
|
|
10361
10384
|
// first placement happened and later SSE ticks should preserve it.
|
|
@@ -10367,7 +10390,18 @@ function saRender(){
|
|
|
10367
10390
|
// Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
|
|
10368
10391
|
// track what's open per goal+section in SA.detOpen (persists across re-renders)
|
|
10369
10392
|
// and wire each one's toggle to keep that set current.
|
|
10370
|
-
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
|
+
}
|
|
10371
10405
|
for (const det of S.querySelectorAll('[data-detkey]')) {
|
|
10372
10406
|
det.addEventListener('toggle', () => {
|
|
10373
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