@galda/cli 0.10.92 → 0.10.93
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/engine/completion-contract.mjs +42 -4
- package/engine/lib.mjs +12 -3
- package/engine/server.mjs +37 -9
- package/package.json +1 -1
|
@@ -1,5 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Which user requests are "change requests" whose ONLY acceptable outcome is the
|
|
2
|
+
// change itself. Deliberately wide on Japanese request forms: the first cut only
|
|
3
|
+
// matched the 「〜して」 imperative stem, so ordinary asks like 「〜してほしい」
|
|
4
|
+
// 「〜できる?」「〜消せますか?」「〜にして」 never qualified and their refusals
|
|
5
|
+
// sailed straight into Review as completed work (2026-07-29 audit).
|
|
6
|
+
const CHANGE_REQUEST_RE = new RegExp([
|
|
7
|
+
'修正', '直して', '直せ', 'なおして', '解決', '改善', '一致させ', '統一',
|
|
8
|
+
'実装', '追加', '削除', '変更', '作って', '対応して', '消して', '消せ',
|
|
9
|
+
'出して', '表示して', '止めて', '揃え', '直したい', '外して', '戻して',
|
|
10
|
+
'できるように', 'にして', 'して(?:ほしい|下さい|ください)', 'られるように',
|
|
11
|
+
'\\bfix\\b', '\\bsolve\\b', '\\bresolve\\b', '\\bimplement\\b', '\\bchange\\b',
|
|
12
|
+
'\\bmake\\b', '\\bremove\\b', '\\badd\\b', '\\bhide\\b', '\\bshow\\b',
|
|
13
|
+
].join('|'), 'i');
|
|
14
|
+
|
|
15
|
+
// What counts as "the agent closed the request by declaring it impossible".
|
|
16
|
+
// The earlier list missed the exact phrasings Masa reported —「仕様なので
|
|
17
|
+
// 変えられません」「対応できません」— plus the English "by design / working as
|
|
18
|
+
// intended" family, which is the same move in a different register.
|
|
19
|
+
const REFUSAL_RESULT_RE = new RegExp([
|
|
20
|
+
'直せません', '直せない', '修正できません', '修正できない',
|
|
21
|
+
'解決できません', '解決できない', '実現できません', '実現できない',
|
|
22
|
+
'変えられません', '変えられない', '変更できません', '変更できない',
|
|
23
|
+
'対応できません', '対応できない', '削除できません', '消せません',
|
|
24
|
+
'実装できません', '実装できない', '不可能です', '仕様(?:のため|なので|上)[^。\\n]{0,20}(?:できません|変えられません|変更できません)',
|
|
25
|
+
'cannot\\s+(?:be\\s+)?(?:fix(?:ed)?|solv(?:e|ed)|implement(?:ed)?|chang(?:e|ed)|remov(?:e|ed)|done)',
|
|
26
|
+
"can't\\s+(?:fix|solve|implement|change|remove|do)",
|
|
27
|
+
'unable\\s+to\\s+(?:fix|solve|implement|change|remove)',
|
|
28
|
+
'not\\s+possible\\s+to\\s+(?:fix|change|implement|remove)',
|
|
29
|
+
'by\\s+design[^.\\n]{0,30}(?:cannot|can\'t|won\'t)',
|
|
30
|
+
'working\\s+as\\s+intended',
|
|
31
|
+
].join('|'), 'i');
|
|
32
|
+
|
|
33
|
+
// An explicit, resolved answer beats the refusal phrase: a worker that says
|
|
34
|
+
// "直せませんでしたが、代わりに X を実装しました" DID deliver. Only the shapes
|
|
35
|
+
// where the refusal is the whole outcome should be blocked.
|
|
36
|
+
const RESOLVED_ANYWAY_RE = /(?:代わりに|その代わり|instead[, ]|workaround|回避策)/i;
|
|
3
37
|
|
|
4
38
|
export const COMPLETION_CONTRACT_PROMPT = [
|
|
5
39
|
'MANAGER COMPLETION CONTRACT:',
|
|
@@ -8,10 +42,14 @@ export const COMPLETION_CONTRACT_PROMPT = [
|
|
|
8
42
|
'- Do not report a change request as complete with “cannot fix”, “直せません”, or equivalent. If a real external blocker remains, state the concrete blocker and leave the work explicitly unfinished instead of presenting it for Review as completed.',
|
|
9
43
|
].join('\n');
|
|
10
44
|
|
|
11
|
-
export function completionContractIssue({ request = '', followUp = '', result = '', status = '' } = {}) {
|
|
45
|
+
export function completionContractIssue({ request = '', followUp = '', result = '', status = '', changedFiles = [] } = {}) {
|
|
12
46
|
if (status !== 'done') return null;
|
|
13
47
|
const requested = `${request}\n${followUp}`;
|
|
14
|
-
|
|
48
|
+
const text = String(result);
|
|
49
|
+
if (!CHANGE_REQUEST_RE.test(requested) || !REFUSAL_RESULT_RE.test(text)) return null;
|
|
50
|
+
// Evidence beats phrasing, in both directions: if the run actually changed
|
|
51
|
+
// files, or explicitly delivered an alternative, it is not a closed door.
|
|
52
|
+
if ((changedFiles ?? []).length > 0 || RESOLVED_ANYWAY_RE.test(text)) return null;
|
|
15
53
|
return {
|
|
16
54
|
kind: 'unresolved-refusal',
|
|
17
55
|
reason: '変更・解決依頼に対して「対応不能」で終了しており、要求した結果が実現されていません。',
|
package/engine/lib.mjs
CHANGED
|
@@ -1346,9 +1346,18 @@ export function looksIncomplete(text) {
|
|
|
1346
1346
|
const t = String(text ?? '').trim();
|
|
1347
1347
|
if (!t) return true;
|
|
1348
1348
|
if (t.length >= 200) return false;
|
|
1349
|
-
// A short blurb ending mid-
|
|
1350
|
-
//
|
|
1351
|
-
|
|
1349
|
+
// A short blurb ending mid-clause reads as truncated. This used to be "no
|
|
1350
|
+
// sentence-final punctuation → truncated", which was far too wide: a terse
|
|
1351
|
+
// but FINISHED one-liner ("worker done", "対応済み") has no period either, and
|
|
1352
|
+
// that false positive marked the task `interrupted` → the whole goal `partial`,
|
|
1353
|
+
// so a legitimate 0-file answer/investigation goal never reached Review
|
|
1354
|
+
// (2026-07-29 audit; it is also what deterministically broke
|
|
1355
|
+
// auto-test-rework.test.mjs). Require an actual DANGLING tail instead: a
|
|
1356
|
+
// trailing connector/particle, a comma/colon/opening bracket, or a hyphen
|
|
1357
|
+
// left mid-word. The fragment this rule was written for (goal-783/task-448)
|
|
1358
|
+
// is still caught — by the future-intent opener below, which is the real
|
|
1359
|
+
// signal there.
|
|
1360
|
+
if (!/[.!?。!?」))]$/.test(t) && /(?:[,、::\-—((「【]|\b(?:and|but|or|so|then|to|the|a|an|of|for|with|because|that|which|while|after|before|from|into)|[でをにがはとしてやりつつ])$/i.test(t)) return true;
|
|
1352
1361
|
// goal-783/task-448's actual fragment ("I'll wait for this background
|
|
1353
1362
|
// full-suite run to complete rather than poll.") IS a grammatically
|
|
1354
1363
|
// complete sentence, so the check above misses it — but it's a one-shot
|
package/engine/server.mjs
CHANGED
|
@@ -2813,6 +2813,7 @@ async function runTask(task) {
|
|
|
2813
2813
|
followUp: task.detail,
|
|
2814
2814
|
result: task.result,
|
|
2815
2815
|
status: task.status,
|
|
2816
|
+
changedFiles: task.changedFiles,
|
|
2816
2817
|
});
|
|
2817
2818
|
if (completionIssue) {
|
|
2818
2819
|
task.status = 'interrupted';
|
|
@@ -3047,6 +3048,7 @@ async function runTask(task) {
|
|
|
3047
3048
|
followUp: task.reply ? task.detail : '',
|
|
3048
3049
|
result: task.result,
|
|
3049
3050
|
status: task.status,
|
|
3051
|
+
changedFiles: task.changedFiles,
|
|
3050
3052
|
});
|
|
3051
3053
|
if (completionIssue) {
|
|
3052
3054
|
task.status = 'interrupted';
|
|
@@ -3105,13 +3107,21 @@ function testEnvironmentPreflight(dir, dependencySourceDir = null) {
|
|
|
3105
3107
|
const sources = nodePath ? nodePath.split(process.platform === 'win32' ? ';' : ':') : [];
|
|
3106
3108
|
const hasPuppeteer = sources.some((p) => existsSync(join(p, 'puppeteer-core', 'package.json')));
|
|
3107
3109
|
const npm = spawnSync('npm', ['--version'], { encoding: 'utf8' });
|
|
3110
|
+
// BLOCKING vs ADVISORY (2026-07-29, docs/INCIDENT-2026-07-25-29-INTERFACE-AUDIT.md).
|
|
3111
|
+
// Only `node`/`npm` make `npm test` impossible to START. `dependencies` and
|
|
3112
|
+
// `puppeteer` are properties of THIS repo's own browser suite — most user
|
|
3113
|
+
// projects legitimately have neither, and treating them as blocking meant
|
|
3114
|
+
// runTestsOnce returned "0 failed" for a suite it never ran, which
|
|
3115
|
+
// runProjectTests then published as ok:true. A missing puppeteer-core is
|
|
3116
|
+
// already discounted downstream by countInfraFlakes, so the honest thing is
|
|
3117
|
+
// to run the suite and report the advisory alongside the real result.
|
|
3108
3118
|
const checks = [
|
|
3109
|
-
{ id: 'node', ok: Boolean(process.execPath), detail: process.version },
|
|
3110
|
-
{ id: 'npm', ok: npm.status === 0, detail: npm.status === 0 ? npm.stdout.trim() : 'npm command unavailable' },
|
|
3111
|
-
{ id: 'dependencies', ok: sources.length > 0, detail: sources.length ? `${sources.length} node_modules source(s)` : 'node_modules not found' },
|
|
3112
|
-
{ id: 'puppeteer', ok: hasPuppeteer, detail: hasPuppeteer ? 'puppeteer-core available' : 'puppeteer-core missing; run npm install in the project directory' },
|
|
3119
|
+
{ id: 'node', ok: Boolean(process.execPath), detail: process.version, blocking: true },
|
|
3120
|
+
{ id: 'npm', ok: npm.status === 0, detail: npm.status === 0 ? npm.stdout.trim() : 'npm command unavailable', blocking: true },
|
|
3121
|
+
{ id: 'dependencies', ok: sources.length > 0, detail: sources.length ? `${sources.length} node_modules source(s)` : 'node_modules not found', blocking: false },
|
|
3122
|
+
{ id: 'puppeteer', ok: hasPuppeteer, detail: hasPuppeteer ? 'puppeteer-core available' : 'puppeteer-core missing; run npm install in the project directory', blocking: false },
|
|
3113
3123
|
];
|
|
3114
|
-
return { ok: checks.every((c) => c.ok), checks };
|
|
3124
|
+
return { ok: checks.every((c) => c.ok), canRun: checks.every((c) => c.ok || !c.blocking), checks };
|
|
3115
3125
|
}
|
|
3116
3126
|
function runTestsOnce(dir, dependencySourceDir = null) {
|
|
3117
3127
|
// The timeout is env-overridable so tests can force the killed-run path fast;
|
|
@@ -3119,7 +3129,12 @@ function runTestsOnce(dir, dependencySourceDir = null) {
|
|
|
3119
3129
|
const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
|
|
3120
3130
|
return testRunQueue.run(() => new Promise((res) => {
|
|
3121
3131
|
const preflight = testEnvironmentPreflight(dir, dependencySourceDir);
|
|
3122
|
-
|
|
3132
|
+
// Never report an unrun suite as a run with zero failures — runProjectTests
|
|
3133
|
+
// turns `failed === 0` into ok:true, and that is the shape a caller reads as
|
|
3134
|
+
// "verification passed". A suite that could not START is `notRun`.
|
|
3135
|
+
if (!preflight.canRun) {
|
|
3136
|
+
return res({ notRun: true, passed: 0, failed: 0, preflight, timedOut: false, realAssertion: false, infraOnly: true, infraFlakes: 0, detail: preflight.checks.filter((c) => !c.ok && c.blocking).map((c) => c.detail).join('\n'), failingNames: [] });
|
|
3137
|
+
}
|
|
3123
3138
|
const nodePath = testNodePath(dir, dependencySourceDir);
|
|
3124
3139
|
execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env, ...(nodePath ? { NODE_PATH: nodePath } : {}) }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
|
|
3125
3140
|
const text = `${out}\n${err}`;
|
|
@@ -3158,6 +3173,12 @@ async function runProjectTests(dir, dependencySourceDir = null) {
|
|
|
3158
3173
|
try { hasTest = !!JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).scripts?.test; } catch { /* no package.json */ }
|
|
3159
3174
|
if (!hasTest) return { ran: false };
|
|
3160
3175
|
let r = await runTestsOnce(dir, dependencySourceDir);
|
|
3176
|
+
// The suite could not be started at all (node/npm missing). Report it as
|
|
3177
|
+
// NOT RUN — the same honest shape as the documentation-only / gate-off skips
|
|
3178
|
+
// — instead of ran:true + ok:true, which reads as "verification passed".
|
|
3179
|
+
if (r.notRun) {
|
|
3180
|
+
return { ran: false, skipped: true, skippedReason: `verification preflight blocked: ${r.detail || 'test environment unavailable'}`, preflight: r.preflight ?? null, detail: r.detail ?? '' };
|
|
3181
|
+
}
|
|
3161
3182
|
// Flake resistance: the integration suite spawns real servers; under load a
|
|
3162
3183
|
// startup can time out. Re-run once and take the fewer failures — a real
|
|
3163
3184
|
// failure fails both runs, a flake clears on the retry — so a good goal is
|
|
@@ -3701,10 +3722,17 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3701
3722
|
if (goal.testResult.ran && goal.testResult.infraOnly && !goal.testResult.inconclusive) {
|
|
3702
3723
|
goalAct(`Project tests: all ${goal.testResult.infraFlakes || goal.testResult.failed || ''} failure(s) were infra flakes (a real server could not boot / a wait timed out under machine load), not code regressions — not blocking. Re-run when the machine is idle to confirm.`.replace(/\s{2,}/g, ' '));
|
|
3703
3724
|
}
|
|
3704
|
-
|
|
3725
|
+
// Preflight is an ADVISORY note, not a verdict (2026-07-29). It used to force
|
|
3726
|
+
// inconclusive=true, which silently disabled the gate for every project that
|
|
3727
|
+
// lacks this repo's puppeteer suite — i.e. essentially every user project —
|
|
3728
|
+
// while still publishing testResult.ok === true. Now: say what is missing, and
|
|
3729
|
+
// let the REAL run decide. A suite that could not start at all never reaches
|
|
3730
|
+
// here (runProjectTests returns ran:false with skippedReason instead).
|
|
3731
|
+
if (goal.testResult.preflight && !goal.testResult.preflight.ok) {
|
|
3705
3732
|
const missing = goal.testResult.preflight.checks.filter((c) => !c.ok).map((c) => c.id).join(', ');
|
|
3706
|
-
goal.testResult.
|
|
3707
|
-
|
|
3733
|
+
goalAct(goal.testResult.ran
|
|
3734
|
+
? `Verification environment note: ${missing} missing. The suite still ran; browser-dependent tests in it may fail for environment reasons (those are discounted as infra flakes).`
|
|
3735
|
+
: `Project tests were not started: verification preflight failed (${missing}). This is an environment issue, not an implementation failure. Fix the listed dependency or tool, then re-run verification.`);
|
|
3708
3736
|
}
|
|
3709
3737
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
3710
3738
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
package/package.json
CHANGED