@galda/cli 0.10.41 → 0.10.42
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 +49 -1
- package/engine/lib.mjs +42 -0
- package/engine/server.mjs +45 -4
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -1218,6 +1218,14 @@
|
|
|
1218
1218
|
.gearpop .srow.dis{opacity:.38;pointer-events:none}
|
|
1219
1219
|
.gearpop .srow .sl{flex:1;min-width:0;font-size:12.5px;color:var(--ink)}
|
|
1220
1220
|
.gearpop .srow .sd{display:block;font-size:10.5px;color:var(--ink3);margin-top:2px}
|
|
1221
|
+
/* Project rules read-only view (§1.5a): a quiet framed panel under its row —
|
|
1222
|
+
mono filename label + the file body, scrollable. Reuses --canvas/--hair so it
|
|
1223
|
+
matches the MCP box; no new surface invented. */
|
|
1224
|
+
.gearpop .rulesview{margin:2px 0 9px;display:flex;flex-direction:column;gap:8px}
|
|
1225
|
+
.gearpop .rulesfile{background:var(--canvas);border-radius:8px;overflow:hidden}
|
|
1226
|
+
.gearpop .rulesname{font:600 9px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);padding:8px 10px 0}
|
|
1227
|
+
.gearpop .rulestext{margin:6px 0 0;padding:0 10px 10px;max-height:220px;overflow:auto;font:400 11px/1.6 var(--mono);color:var(--ink2);white-space:pre-wrap;word-break:break-word}
|
|
1228
|
+
.gearpop .rulesempty{color:var(--ink3);font-style:italic}
|
|
1221
1229
|
/* MCP tab: live status dot + connect command + quiet explanation */
|
|
1222
1230
|
.gearpop .mcpstat{display:flex;align-items:center;gap:8px;font:600 11px/1 var(--ui);color:var(--ink);padding:2px 0 10px}
|
|
1223
1231
|
.gearpop .mcpstat i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px rgba(var(--green-rgb),.5)}
|
|
@@ -3276,7 +3284,7 @@ const savedTheme = (() => {
|
|
|
3276
3284
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3277
3285
|
return v;
|
|
3278
3286
|
})();
|
|
3279
|
-
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
3287
|
+
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
|
|
3280
3288
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3281
3289
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3282
3290
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -10526,8 +10534,44 @@ function gearSetHTML(){
|
|
|
10526
10534
|
<button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
|
|
10527
10535
|
<button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>
|
|
10528
10536
|
${billingRow}
|
|
10537
|
+
${gearRulesHTML()}
|
|
10529
10538
|
<div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
|
|
10530
10539
|
}
|
|
10540
|
+
// ONE-CONVERSATION §1.5a: the project's working rules — the file(s) the connected
|
|
10541
|
+
// AI actually reads (Claude Code → CLAUDE.md, Codex → AGENTS.md). Read-only: Galda
|
|
10542
|
+
// does NOT own these rules, it only surfaces them (editing is done in an editor or
|
|
10543
|
+
// by asking the AI in chat). §3 段階開示 — when the project has no such file the row
|
|
10544
|
+
// is absent entirely (never an empty container; the "ask once" flow is §1.5b).
|
|
10545
|
+
function gearRulesHTML(){
|
|
10546
|
+
const r = state.projectRules[state.active];
|
|
10547
|
+
if (!r || !r.files || !r.files.length) return '';
|
|
10548
|
+
const names = r.files.map((f) => f.name).join(' · ');
|
|
10549
|
+
const view = state.rulesOpen
|
|
10550
|
+
? `<div class="rulesview">${r.files.map((f) =>
|
|
10551
|
+
`<div class="rulesfile"><div class="rulesname">${esc(f.name)}</div>`
|
|
10552
|
+
+ `<pre class="rulestext">${esc(f.content) || '<span class="rulesempty">(empty file)</span>'}</pre></div>`).join('')}</div>`
|
|
10553
|
+
: '';
|
|
10554
|
+
return `
|
|
10555
|
+
<div class="srow"><div class="sl">Project rules<span class="sd">What this project tells the AI — read-only · ${esc(names)}</span></div>
|
|
10556
|
+
<button class="optchip${state.rulesOpen ? ' on' : ''}" data-grules="1">${state.rulesOpen ? 'Hide' : 'View'}</button></div>
|
|
10557
|
+
${view}`;
|
|
10558
|
+
}
|
|
10559
|
+
// Fetch the active project's rules files once (cached per project), then rebuild
|
|
10560
|
+
// the settings panel so the row appears. The browser can't read the host FS, so
|
|
10561
|
+
// this is the only way in — GET /api/projects/:id/rules (engine, §1.5a).
|
|
10562
|
+
let rulesFetching = null;
|
|
10563
|
+
async function loadProjectRules(){
|
|
10564
|
+
const pid = state.active;
|
|
10565
|
+
if (state.projectRules[pid] || rulesFetching === pid) return;
|
|
10566
|
+
rulesFetching = pid;
|
|
10567
|
+
try {
|
|
10568
|
+
const r = await fetch(withKey(`/api/projects/${pid}/rules`));
|
|
10569
|
+
state.projectRules[pid] = r.ok ? await r.json() : { files: [] };
|
|
10570
|
+
} catch { state.projectRules[pid] = { files: [] }; }
|
|
10571
|
+
finally { rulesFetching = null; }
|
|
10572
|
+
// Only rebuild if the settings tab is still the one on screen.
|
|
10573
|
+
if ($('gearPop') && custTab === 'settings') buildGearPop();
|
|
10574
|
+
}
|
|
10531
10575
|
// MCP (v46): connection status dot + connectCommand + COPY + quiet explanation.
|
|
10532
10576
|
// The dot is the app's own live engine link (SSE up/down) — the only real signal here.
|
|
10533
10577
|
function gearMcpHTML(){
|
|
@@ -10578,6 +10622,9 @@ function buildGearPop(){
|
|
|
10578
10622
|
try { await fetch(withKey('/api/signout'), { method: 'POST' }); recentlySignedOut = true; }
|
|
10579
10623
|
finally { await refresh(); if ($('gearPop')) buildGearPop(); }
|
|
10580
10624
|
};
|
|
10625
|
+
const rulesBtn = pop.querySelector('[data-grules]');
|
|
10626
|
+
if (rulesBtn) rulesBtn.onclick = () => { state.rulesOpen = !state.rulesOpen; buildGearPop(); };
|
|
10627
|
+
loadProjectRules();
|
|
10581
10628
|
return;
|
|
10582
10629
|
}
|
|
10583
10630
|
if (custTab === 'mcp') {
|
|
@@ -10711,6 +10758,7 @@ function custPlace(){ // flagship: top-anchored band beside the rail (v45 §2);
|
|
|
10711
10758
|
}
|
|
10712
10759
|
function custOpen(tab){
|
|
10713
10760
|
custTab = tab || 'theme'; // v46 deep-link (flagship __custOpen(tab)): menu entries land on their tab
|
|
10761
|
+
state.rulesOpen = false; // rules panel starts collapsed each time the gear opens
|
|
10714
10762
|
custSnap = { cust: { ...cust }, theme: state.theme, band: fsUI.band };
|
|
10715
10763
|
buildGearPop(); custPlace();
|
|
10716
10764
|
$('gearPop').classList.add('open');
|
package/engine/lib.mjs
CHANGED
|
@@ -1135,6 +1135,26 @@ export function classifyTestGate({ current = [], baseline = [] } = {}) {
|
|
|
1135
1135
|
return { newFailures, preExisting, blocked: newFailures.length > 0 };
|
|
1136
1136
|
}
|
|
1137
1137
|
|
|
1138
|
+
// A manager test-run that was KILLED before it could finish is inconclusive, not
|
|
1139
|
+
// a red. When the machine is saturated (prod + several worker goals all standing
|
|
1140
|
+
// up real servers + headless Chrome), `npm test` gets killed at its execFile
|
|
1141
|
+
// timeout BEFORE node --test can print its "ℹ pass N / ℹ fail N" summary — so the
|
|
1142
|
+
// parse sees 0 passed and the killed process is counted as a single phantom
|
|
1143
|
+
// failure. That is the machine being too busy to run the suite, not the goal's
|
|
1144
|
+
// code breaking, and blocking a finished goal (with a PR) on it strands good work
|
|
1145
|
+
// (dogfood #602/#604, 2026-07-23: passed:0, "1 failing", every listed failure a
|
|
1146
|
+
// 30s timeout, no summary line).
|
|
1147
|
+
//
|
|
1148
|
+
// The signal is `timedOut` (execFile killed the process — killed/SIGTERM/ETIMEDOUT),
|
|
1149
|
+
// NOT the mere absence of a summary: a small suite that printed `not ok 1` and
|
|
1150
|
+
// exited 1 on its own is a REAL failure and must still block. We also require
|
|
1151
|
+
// passed===0 (a killed run that had already logged passes is unusual, but if any
|
|
1152
|
+
// test passed the suite clearly ran) and fail CLOSED on a real AssertionError that
|
|
1153
|
+
// shares the killed run.
|
|
1154
|
+
export function isInconclusiveTestRun({ ran = false, timedOut = false, passed = 0, realAssertion = false } = {}) {
|
|
1155
|
+
return Boolean(ran) && Boolean(timedOut) && !realAssertion && passed === 0;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1138
1158
|
// The verification-gate decision (Masa 2026-07-19: "検証/proof は Failed を作らない").
|
|
1139
1159
|
// PROOF IS ADVISORY: a UI goal whose screenshot could not be captured
|
|
1140
1160
|
// (proofMissing) or whose pixel heuristic could not auto-confirm the target
|
|
@@ -1979,6 +1999,28 @@ export function hasGitChanges(porcelainStatus) {
|
|
|
1979
1999
|
return Boolean(String(porcelainStatus ?? '').trim());
|
|
1980
2000
|
}
|
|
1981
2001
|
|
|
2002
|
+
// ---- project rules files (ONE-CONVERSATION §1.5a) --------------------------
|
|
2003
|
+
// The files that hold a project's working rules — the instructions the connected
|
|
2004
|
+
// AI actually reads (Claude Code reads CLAUDE.md from cwd; Codex reads AGENTS.md).
|
|
2005
|
+
// Galda does NOT own these rules: it only surfaces whichever files exist,
|
|
2006
|
+
// read-only. Order is fixed so the settings row is stable, and both are shown
|
|
2007
|
+
// when both exist (never show one and let the other read as "missing").
|
|
2008
|
+
export const PROJECT_RULES_FILENAMES = ['CLAUDE.md', 'AGENTS.md'];
|
|
2009
|
+
|
|
2010
|
+
// Given a project dir and a reader (dir,name)->string|null, return the rules
|
|
2011
|
+
// files that exist, in PROJECT_RULES_FILENAMES order. A file the reader can't
|
|
2012
|
+
// return (absent → null, or throws) is omitted, so the UI shows only what is
|
|
2013
|
+
// really there rather than an empty rules pane.
|
|
2014
|
+
export function collectProjectRules(dir, readFile) {
|
|
2015
|
+
const files = [];
|
|
2016
|
+
for (const name of PROJECT_RULES_FILENAMES) {
|
|
2017
|
+
let content = null;
|
|
2018
|
+
try { content = readFile(dir, name); } catch { content = null; }
|
|
2019
|
+
if (typeof content === 'string') files.push({ name, content });
|
|
2020
|
+
}
|
|
2021
|
+
return files;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1982
2024
|
// ---- merge-time conflict detection -----------------------------------------
|
|
1983
2025
|
// Parallel goals never corrupt each other's WORKING files (each runs in its
|
|
1984
2026
|
// own worktree) — but two goals that edited the same repo path will still
|
package/engine/server.mjs
CHANGED
|
@@ -21,8 +21,9 @@ import { homedir } from 'node:os';
|
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
|
+
import { collectProjectRules } from './lib.mjs';
|
|
26
27
|
import { openPR } from './pr.mjs';
|
|
27
28
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
28
29
|
|
|
@@ -2465,8 +2466,11 @@ const testRunQueue = createSerialQueue();
|
|
|
2465
2466
|
// project's own `npm test` in the goal's worktree and parse node --test's
|
|
2466
2467
|
// "ℹ pass N / ℹ fail N" (or TAP "# pass/# fail"). Returns {ran,passed,failed,ok}.
|
|
2467
2468
|
function runTestsOnce(dir) {
|
|
2469
|
+
// The timeout is env-overridable so tests can force the killed-run path fast;
|
|
2470
|
+
// production keeps the 4-minute default.
|
|
2471
|
+
const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
|
|
2468
2472
|
return testRunQueue.run(() => new Promise((res) => {
|
|
2469
|
-
execFile('npm', ['test'], { cwd: dir, timeout:
|
|
2473
|
+
execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
|
|
2470
2474
|
const text = `${out}\n${err}`;
|
|
2471
2475
|
const n = (re) => { const m = text.match(re); return m ? Number(m[1]) : null; };
|
|
2472
2476
|
const passed = n(/ℹ pass (\d+)/) ?? n(/# pass (\d+)/) ?? n(/(\d+) passing/) ?? 0;
|
|
@@ -2480,9 +2484,15 @@ function runTestsOnce(dir) {
|
|
|
2480
2484
|
const timeoutFlakes = (text.match(/did not report ready in time/g) || []).length;
|
|
2481
2485
|
const realAssertion = /AssertionError|Expected values to be|expected .+ (?:to |but )/i.test(text);
|
|
2482
2486
|
const failed = realAssertion ? failedTotal : Math.max(0, failedTotal - timeoutFlakes);
|
|
2487
|
+
// Was the run KILLED at the execFile timeout (machine too saturated to
|
|
2488
|
+
// finish), rather than exiting on its own? execFile sets killed=true and
|
|
2489
|
+
// signal='SIGTERM' on timeout. That is the inconclusive signal — a normal
|
|
2490
|
+
// non-zero exit (a small suite that printed `not ok` and exited 1) is NOT
|
|
2491
|
+
// killed and must still be treated as a real result (isInconclusiveTestRun).
|
|
2492
|
+
const timedOut = Boolean(e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'));
|
|
2483
2493
|
// Capture the failing test NAMES too (not just the count) so the gate can
|
|
2484
2494
|
// diff them against the base's failures and block only on NEW ones.
|
|
2485
|
-
res({ passed, failed, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2495
|
+
res({ passed, failed, timedOut, realAssertion, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2486
2496
|
});
|
|
2487
2497
|
}));
|
|
2488
2498
|
}
|
|
@@ -2501,11 +2511,16 @@ async function runProjectTests(dir) {
|
|
|
2501
2511
|
r = {
|
|
2502
2512
|
passed: Math.max(r.passed, r2.passed),
|
|
2503
2513
|
failed: Math.min(r.failed, r2.failed),
|
|
2514
|
+
// Only an ALL-killed pair is inconclusive: if EITHER attempt exited on its
|
|
2515
|
+
// own (timedOut=false) the result is conclusive. realAssertion is OR-ed so
|
|
2516
|
+
// a real red in either run still fails closed.
|
|
2517
|
+
timedOut: Boolean(r.timedOut && r2.timedOut),
|
|
2518
|
+
realAssertion: r.realAssertion || r2.realAssertion,
|
|
2504
2519
|
detail: kept.detail || r.detail || r2.detail || '',
|
|
2505
2520
|
failingNames: kept.failingNames ?? r.failingNames ?? [],
|
|
2506
2521
|
};
|
|
2507
2522
|
}
|
|
2508
|
-
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
2523
|
+
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
2509
2524
|
}
|
|
2510
2525
|
|
|
2511
2526
|
// Failing tests on the BASE (pre-change) checkout — the set the review gate
|
|
@@ -2946,6 +2961,16 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2946
2961
|
// Manager-shaped repos.
|
|
2947
2962
|
const proofFailing = siblings.some((t) => t.proof && t.proof.pass === false);
|
|
2948
2963
|
let testsFailing = goal.testResult.ran && goal.testResult.failed > 0;
|
|
2964
|
+
// Inconclusive run (2026-07-23): the suite was killed before it could finish
|
|
2965
|
+
// (0 passed, no summary, no real assertion) — the machine was too saturated to
|
|
2966
|
+
// run the tests, not the code failing. Blocking a finished goal (with a PR) on
|
|
2967
|
+
// that stranded #602/#604. Treat it like the infra-flake discount above: don't
|
|
2968
|
+
// block, note it, and let a human re-run when the machine is idle.
|
|
2969
|
+
if (testsFailing && isInconclusiveTestRun(goal.testResult)) {
|
|
2970
|
+
testsFailing = false;
|
|
2971
|
+
goal.testResult.inconclusive = true;
|
|
2972
|
+
goalAct('Project tests could not complete — the suite was killed before finishing (0 passed, no real assertion), which means the machine was too saturated to run them, not a code failure. Treating as inconclusive, not blocking. Re-run the tests when the machine is idle to confirm.');
|
|
2973
|
+
}
|
|
2949
2974
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
2950
2975
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
|
2951
2976
|
// origin/main — is pre-existing noise and must not knock a proof-verified
|
|
@@ -3410,6 +3435,22 @@ const server = createServer(async (req, res) => {
|
|
|
3410
3435
|
return json(res, 200, { repos: detectRepos(HOME), home: HOME });
|
|
3411
3436
|
}
|
|
3412
3437
|
|
|
3438
|
+
// The project's working rules (ONE-CONVERSATION §1.5a): the settings panel
|
|
3439
|
+
// shows a read-only view of whichever of CLAUDE.md / AGENTS.md exist in the
|
|
3440
|
+
// project dir — the instructions the connected AI actually reads. The browser
|
|
3441
|
+
// can't touch the filesystem, so it reads them here. Filenames are fixed
|
|
3442
|
+
// (collectProjectRules), so `project.dir` is the only path input and there is
|
|
3443
|
+
// no traversal surface.
|
|
3444
|
+
const rulesMatch = url.pathname.match(/^\/api\/projects\/([\w-]+)\/rules$/);
|
|
3445
|
+
if (rulesMatch && req.method === 'GET') {
|
|
3446
|
+
const project = projects.find((p) => p.id === rulesMatch[1]);
|
|
3447
|
+
if (!project) return json(res, 404, { error: 'project not found' });
|
|
3448
|
+
const files = collectProjectRules(project.dir, (dir, name) => {
|
|
3449
|
+
try { return readFileSync(join(dir, name), 'utf8'); } catch { return null; }
|
|
3450
|
+
});
|
|
3451
|
+
return json(res, 200, { dir: project.dir, files });
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3413
3454
|
if (url.pathname === '/api/projects' && req.method === 'POST') {
|
|
3414
3455
|
let body = '';
|
|
3415
3456
|
req.on('data', (d) => { body += d; });
|
package/package.json
CHANGED