@bongos/core 1.19.556 → 1.19.557
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/.bongos-core.json +14 -14
- package/clients/bongos-client/index.d.ts +1 -2
- package/docs/api/openapi.json +3 -23
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-goals.js +18 -1
- package/modules/lifecycle/routes/done-when.js +10 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/goal_achievement.mjs +64 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.557",
|
|
6
|
+
"core_contract": "1.19.557",
|
|
7
|
+
"source_commit": "07517c180e4fca730285ee6533c21d6b54f08273",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-06T16:
|
|
9
|
+
"built_at": "2026-09-06T16:32:40.671Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 443,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2487,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "a3c96b5cf7b985332e67132f9626118afc2968733a0e5cd2cf0a7fbe7d885540",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -392,7 +392,7 @@
|
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.d.ts",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "24cbe2964b71993d9d6e55daaf0dc26c938f820c7c4f6c01579d9dd2508e89ba"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1782,7 +1782,7 @@
|
|
|
1782
1782
|
{
|
|
1783
1783
|
"path": "docs/api/openapi.json",
|
|
1784
1784
|
"mode": "0000644",
|
|
1785
|
-
"sha256": "
|
|
1785
|
+
"sha256": "1ad70f7f1cb7f53735f17fbd8e05287c9191a1d249493c928efb91692b491941"
|
|
1786
1786
|
},
|
|
1787
1787
|
{
|
|
1788
1788
|
"path": "docs/architecture.md",
|
|
@@ -2657,7 +2657,7 @@
|
|
|
2657
2657
|
{
|
|
2658
2658
|
"path": "docs/module-api-changelog.md",
|
|
2659
2659
|
"mode": "0000644",
|
|
2660
|
-
"sha256": "
|
|
2660
|
+
"sha256": "99b29fcf1f9a58ee17ba9d77e3c0c30b37940ee8af0c71396813f7514d960ba1"
|
|
2661
2661
|
},
|
|
2662
2662
|
{
|
|
2663
2663
|
"path": "docs/modules-contract.md",
|
|
@@ -5432,7 +5432,7 @@
|
|
|
5432
5432
|
{
|
|
5433
5433
|
"path": "modules/lifecycle/db-goals.js",
|
|
5434
5434
|
"mode": "0000644",
|
|
5435
|
-
"sha256": "
|
|
5435
|
+
"sha256": "abba32642e123860fee7b33fe429294783480afd2e25a78f63f9d4af3b1613e8"
|
|
5436
5436
|
},
|
|
5437
5437
|
{
|
|
5438
5438
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5637,7 +5637,7 @@
|
|
|
5637
5637
|
{
|
|
5638
5638
|
"path": "modules/lifecycle/routes/done-when.js",
|
|
5639
5639
|
"mode": "0000644",
|
|
5640
|
-
"sha256": "
|
|
5640
|
+
"sha256": "25b18aa82f5b2951f3a9560fa5285dc2793556303ed68ae0aa9ea781ec2d45c9"
|
|
5641
5641
|
},
|
|
5642
5642
|
{
|
|
5643
5643
|
"path": "modules/lifecycle/routes/gate-approvals.js",
|
|
@@ -7477,12 +7477,12 @@
|
|
|
7477
7477
|
{
|
|
7478
7478
|
"path": "package-lock.json",
|
|
7479
7479
|
"mode": "0000644",
|
|
7480
|
-
"sha256": "
|
|
7480
|
+
"sha256": "248924f051a38d806cdf6818c66768dd3d1cf7f62196a01aee776f7f1849374e"
|
|
7481
7481
|
},
|
|
7482
7482
|
{
|
|
7483
7483
|
"path": "package.json",
|
|
7484
7484
|
"mode": "0000644",
|
|
7485
|
-
"sha256": "
|
|
7485
|
+
"sha256": "3fac6b2004dd142b67afdce4e5939f5166ab777e74900e25b522af68568eb23f"
|
|
7486
7486
|
},
|
|
7487
7487
|
{
|
|
7488
7488
|
"path": "public-docs/index.html",
|
|
@@ -9177,7 +9177,7 @@
|
|
|
9177
9177
|
{
|
|
9178
9178
|
"path": "src/module-api.js",
|
|
9179
9179
|
"mode": "0000644",
|
|
9180
|
-
"sha256": "
|
|
9180
|
+
"sha256": "e0d8e56756e81c431cc74eaf5cea2ed87839bf7e6a222c98e43c279fd66d5beb"
|
|
9181
9181
|
},
|
|
9182
9182
|
{
|
|
9183
9183
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10247,7 +10247,7 @@
|
|
|
10247
10247
|
{
|
|
10248
10248
|
"path": "tests/goal_achievement.mjs",
|
|
10249
10249
|
"mode": "0000644",
|
|
10250
|
-
"sha256": "
|
|
10250
|
+
"sha256": "3e157d5bf44370e4935b70203fe59e40cf9120d12c472f24ed7bd85cb33cbe91"
|
|
10251
10251
|
},
|
|
10252
10252
|
{
|
|
10253
10253
|
"path": "tests/goal_advisory.mjs",
|
|
@@ -243,7 +243,6 @@ export interface PostCostResponse { skipped?: boolean; reason?: unknown; id?: un
|
|
|
243
243
|
export interface PostDependenciesResponse { ok: boolean; dependency: unknown }
|
|
244
244
|
export interface PostDiscordChannelsReconcileResponse { ok: boolean; applied: unknown; failed: unknown; total: unknown; warnings: unknown }
|
|
245
245
|
export interface PostDoneWhenCriterionIdSatisfyRequest { satisfied_by_task_id?: number }
|
|
246
|
-
export interface PostDoneWhenCriterionIdSatisfyResponse { criterion: unknown; goal_achieved: unknown; goal: unknown }
|
|
247
246
|
export interface PostDoneWhenCriterionIdUnsatisfyResponse { criterion: unknown }
|
|
248
247
|
export interface PostGateApprovalsPrApproveResponse { ok: boolean; pr: unknown; already_clear?: boolean; message: unknown; head_sha?: unknown; status_set?: boolean; recheck?: unknown }
|
|
249
248
|
export interface PostGithubRepoVisibilityRequest { repo: string; visibility: "public" | "private" }
|
|
@@ -638,7 +637,7 @@ export interface BongosClient {
|
|
|
638
637
|
/** PATCH /done-when/{criterionId} — rank: metic+archon */
|
|
639
638
|
patchDoneWhenCriterionId(args: RequestArgs & { body: PatchDoneWhenCriterionIdRequest }): Promise<PatchDoneWhenCriterionIdResponse>;
|
|
640
639
|
/** POST /done-when/{criterionId}/satisfy — rank: metic+archon */
|
|
641
|
-
postDoneWhenCriterionIdSatisfy(args?: RequestArgs & { body?: PostDoneWhenCriterionIdSatisfyRequest }): Promise<
|
|
640
|
+
postDoneWhenCriterionIdSatisfy(args?: RequestArgs & { body?: PostDoneWhenCriterionIdSatisfyRequest }): Promise<ApiResponse>;
|
|
642
641
|
/** POST /done-when/{criterionId}/unsatisfy — rank: metic+archon */
|
|
643
642
|
postDoneWhenCriterionIdUnsatisfy(args?: RequestArgs): Promise<PostDoneWhenCriterionIdUnsatisfyResponse>;
|
|
644
643
|
/** GET /done-when/pending-review — rank: metic+archon */
|
package/docs/api/openapi.json
CHANGED
|
@@ -4397,14 +4397,7 @@
|
|
|
4397
4397
|
},
|
|
4398
4398
|
"responses": {
|
|
4399
4399
|
"200": {
|
|
4400
|
-
"description": "Success."
|
|
4401
|
-
"content": {
|
|
4402
|
-
"application/json": {
|
|
4403
|
-
"schema": {
|
|
4404
|
-
"$ref": "#/components/schemas/PostDoneWhenCriterionIdSatisfyResponse"
|
|
4405
|
-
}
|
|
4406
|
-
}
|
|
4407
|
-
}
|
|
4400
|
+
"description": "Success."
|
|
4408
4401
|
},
|
|
4409
4402
|
"400": {
|
|
4410
4403
|
"$ref": "#/components/responses/ValidationFailed"
|
|
@@ -19629,19 +19622,6 @@
|
|
|
19629
19622
|
},
|
|
19630
19623
|
"additionalProperties": false
|
|
19631
19624
|
},
|
|
19632
|
-
"PostDoneWhenCriterionIdSatisfyResponse": {
|
|
19633
|
-
"type": "object",
|
|
19634
|
-
"properties": {
|
|
19635
|
-
"criterion": {},
|
|
19636
|
-
"goal_achieved": {},
|
|
19637
|
-
"goal": {}
|
|
19638
|
-
},
|
|
19639
|
-
"required": [
|
|
19640
|
-
"criterion",
|
|
19641
|
-
"goal_achieved",
|
|
19642
|
-
"goal"
|
|
19643
|
-
]
|
|
19644
|
-
},
|
|
19645
19625
|
"PostDoneWhenCriterionIdUnsatisfyResponse": {
|
|
19646
19626
|
"type": "object",
|
|
19647
19627
|
"properties": {
|
|
@@ -22460,8 +22440,8 @@
|
|
|
22460
22440
|
}
|
|
22461
22441
|
},
|
|
22462
22442
|
"x-endpoint-count": 356,
|
|
22463
|
-
"x-schema-count":
|
|
22443
|
+
"x-schema-count": 390,
|
|
22464
22444
|
"x-undocumented-bodies": 9,
|
|
22465
|
-
"x-response-schemas":
|
|
22445
|
+
"x-response-schemas": 264,
|
|
22466
22446
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
22467
22447
|
}
|
|
@@ -1561,5 +1561,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1561
1561
|
landed since 1.19.554 with no explicit bump. run 34045075186. (task 1002620)
|
|
1562
1562
|
1.19.556 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1563
1563
|
landed since 1.19.555 with no explicit bump. run 34045323629. (task 1002620)
|
|
1564
|
+
1.19.557 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1565
|
+
landed since 1.19.556 with no explicit bump. run 34045753301. (task 1002620)
|
|
1564
1566
|
---------------------------------------------------------------------------
|
|
1565
1567
|
```
|
|
@@ -15,6 +15,7 @@ const { pool } = api;
|
|
|
15
15
|
const doneWhen = require('./done-when.js');
|
|
16
16
|
const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
17
17
|
const { nonTerminalSql } = require('./task-status');
|
|
18
|
+
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
18
19
|
|
|
19
20
|
// -------------------------------------------------------------------------
|
|
20
21
|
// Goals (ADR 0086 §2 / BV1.R56 — task 1510)
|
|
@@ -199,16 +200,32 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
|
|
|
199
200
|
if (!goal || goal.status !== 'open') return { achieved: false, goal: goal ?? null };
|
|
200
201
|
const { rows } = await activePool.query(
|
|
201
202
|
`SELECT count(*)::int AS total,
|
|
202
|
-
count(*) FILTER (WHERE NOT satisfied)::int AS unsatisfied
|
|
203
|
+
count(*) FILTER (WHERE NOT satisfied)::int AS unsatisfied,
|
|
204
|
+
(SELECT count(*)::int FROM tasks t
|
|
205
|
+
WHERE t.goal_id = $1 AND ${nonTerminalSql()}
|
|
206
|
+
AND ${SMOKE_NOT_LIKE.replace(/title/, 't.title')}) AS unfinished_tasks
|
|
203
207
|
FROM done_when_criteria
|
|
204
208
|
WHERE goal_id = $1`,
|
|
205
209
|
[goalId]
|
|
206
210
|
);
|
|
207
211
|
const total = rows[0]?.total ?? 0;
|
|
208
212
|
const unsatisfied = rows[0]?.unsatisfied ?? 0;
|
|
213
|
+
const unfinishedTasks = rows[0]?.unfinished_tasks ?? 0;
|
|
209
214
|
// Need ≥1 criterion and zero unsatisfied — confirming the LAST one is the
|
|
210
215
|
// trigger. An empty goal stays open (nothing could complete it).
|
|
211
216
|
if (total === 0 || unsatisfied > 0) return { achieved: false, goal };
|
|
217
|
+
// BV1.R08 (task 1003595, goal 1000086, ADR 0250 D2): AND no task in the goal is
|
|
218
|
+
// still non-terminal. Criteria alone were never the whole question — a task
|
|
219
|
+
// linked to NO criterion held nothing open, so a goal auto-achieved while
|
|
220
|
+
// carrying any number of backlog or ready tasks. Terminal is `shipped` or
|
|
221
|
+
// deliberately `abandoned` (task-status.js); `completed` and `confirmed` are
|
|
222
|
+
// mid-ship states that can still fail a grade or strand, so they hold the goal
|
|
223
|
+
// open too. Smoke rows are excluded the way version_progress excludes them — a
|
|
224
|
+
// test fixture must not be able to wedge a real goal open forever.
|
|
225
|
+
//
|
|
226
|
+
// This never blocks a SHIP; it declines only the goal's flip to achieved, and
|
|
227
|
+
// the goal simply stays open until its last task lands or is abandoned.
|
|
228
|
+
if (unfinishedTasks > 0) return { achieved: false, goal, unfinishedTasks };
|
|
212
229
|
const updated = await setGoalStatus({ goalId, status: 'achieved' }, deps);
|
|
213
230
|
return { achieved: true, goal: updated };
|
|
214
231
|
}
|
|
@@ -212,7 +212,16 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
212
212
|
if (updated?.goal_id != null) {
|
|
213
213
|
goalResult = await db.achieveGoalIfComplete({ goalId: updated.goal_id });
|
|
214
214
|
}
|
|
215
|
-
|
|
215
|
+
// BV1.R08 (task 1003595): when the goal did NOT flip because it still holds
|
|
216
|
+
// unfinished work, say so. Confirming the last criterion and getting a bare
|
|
217
|
+
// `goal_achieved: false` back reads as a bug to the agent that just did it —
|
|
218
|
+
// it would retry, or file one. The count is the whole explanation.
|
|
219
|
+
res.json({
|
|
220
|
+
criterion: updated,
|
|
221
|
+
goal_achieved: goalResult.achieved,
|
|
222
|
+
goal: goalResult.goal,
|
|
223
|
+
...(goalResult.unfinishedTasks ? { goal_held_open_by_tasks: goalResult.unfinishedTasks } : {}),
|
|
224
|
+
});
|
|
216
225
|
}, { errorCode: 'satisfy_failed' }));
|
|
217
226
|
|
|
218
227
|
// PATCH /done-when/:criterionId — correct a criterion's TEXT (task 1003102).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.557",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.557",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.557",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
49
49
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
50
50
|
// the entry to that file. Look for a version's history there, not here.
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
|
-
const CORE_VERSION = '1.19.
|
|
52
|
+
const CORE_VERSION = '1.19.557'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
53
53
|
|
|
54
54
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
55
55
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { strict as assert } from 'node:assert';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
23
24
|
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
24
25
|
|
|
25
26
|
process.env.NODE_ENV = 'test';
|
|
@@ -47,7 +48,10 @@ const GOAL = (over = {}) => ({
|
|
|
47
48
|
function goalHandler({ goal, counts }) {
|
|
48
49
|
return (s) => {
|
|
49
50
|
if (/^SELECT .*FROM goals WHERE id = \$1/.test(s)) return { rows: goal ? [goal] : [] };
|
|
50
|
-
|
|
51
|
+
// BV1.R08: the criteria count and the unfinished-task count come back in ONE
|
|
52
|
+
// row. `unfinished_tasks` defaults to 0 so every pre-existing case below still
|
|
53
|
+
// means what it meant — criteria alone decide it when no task is open.
|
|
54
|
+
if (/FROM done_when_criteria WHERE goal_id = \$1/.test(s)) return { rows: [{ unfinished_tasks: 0, ...counts }] };
|
|
51
55
|
if (/^UPDATE goals/.test(s)) return { rows: [{ ...goal, status: 'achieved', updated_at: 'T1' }] };
|
|
52
56
|
return null;
|
|
53
57
|
};
|
|
@@ -145,4 +149,63 @@ await test('listPendingReviewCriteria: SQL encodes the auto-flag (all shipped) +
|
|
|
145
149
|
});
|
|
146
150
|
|
|
147
151
|
// -------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
// =========================================================================
|
|
154
|
+
// BV1.R08 (task 1003595, goal 1000086, ADR 0250 D2) — criteria are not the whole
|
|
155
|
+
// question: a goal cannot close while it still holds non-terminal work.
|
|
156
|
+
//
|
|
157
|
+
// The hole this shuts: achievement was computed from done_when_criteria alone, so
|
|
158
|
+
// a task linked to NO criterion held nothing open and a goal auto-achieved while
|
|
159
|
+
// carrying any number of backlog or ready tasks. Fourteen goals on the live
|
|
160
|
+
// instance were in exactly that state, holding thirty-five tasks between them.
|
|
161
|
+
// =========================================================================
|
|
162
|
+
|
|
163
|
+
await test('R08: all criteria satisfied but ONE unfinished task remains → no flip', async () => {
|
|
164
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 3, unsatisfied: 0, unfinished_tasks: 1 } }));
|
|
165
|
+
const out = await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
166
|
+
assert.equal(out.achieved, false, 'satisfied criteria are necessary, not sufficient');
|
|
167
|
+
assert.equal(out.goal.status, 'open', 'the goal stays open until the work lands');
|
|
168
|
+
assert.equal(out.unfinishedTasks, 1, 'and it reports WHY, so a caller can say so');
|
|
169
|
+
assert.equal(find(pool.queries, /^UPDATE goals/), undefined, 'must NOT flip the status');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('R08: the count is asked in the SAME round trip as the criteria count', async () => {
|
|
173
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 1, unsatisfied: 0, unfinished_tasks: 0 } }));
|
|
174
|
+
await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
175
|
+
const q = find(pool.queries, /FROM done_when_criteria WHERE goal_id = \$1/);
|
|
176
|
+
const sql = String(q.sql).replace(/\s+/g, ' ');
|
|
177
|
+
assert.match(sql, /FROM tasks t WHERE t\.goal_id = \$1/, 'one query, not a second trip');
|
|
178
|
+
assert.match(sql, /t\.status NOT IN \('shipped', 'abandoned'\)/,
|
|
179
|
+
"the terminal set comes from task-status.js — mid-ship states hold the goal open");
|
|
180
|
+
assert.match(sql, /t\.title NOT LIKE/, 'a smoke fixture must not wedge a real goal open forever');
|
|
181
|
+
assert.deepEqual(q.params, [7], 'the goal id is bound once and reused');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await test('R08: zero unfinished tasks and all criteria satisfied → still flips', async () => {
|
|
185
|
+
// The rule must not become "goals never close". This is the whole happy path.
|
|
186
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 2, unsatisfied: 0, unfinished_tasks: 0 } }));
|
|
187
|
+
const out = await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
188
|
+
assert.equal(out.achieved, true, 'finished work + satisfied criteria = achieved');
|
|
189
|
+
assert.ok(find(pool.queries, /^UPDATE goals/), 'the flip is persisted');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await test('R08: an unsatisfied criterion still short-circuits before the task check', async () => {
|
|
193
|
+
// Ordering matters for the message, not the outcome: a goal with open criteria
|
|
194
|
+
// AND open tasks is refused on the criteria, and reports no task count.
|
|
195
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 3, unsatisfied: 2, unfinished_tasks: 9 } }));
|
|
196
|
+
const out = await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
197
|
+
assert.equal(out.achieved, false);
|
|
198
|
+
assert.equal(out.unfinishedTasks, undefined,
|
|
199
|
+
'the criteria refusal is the honest reason when both are true');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await test('R08: the satisfy route tells the caller a goal was held open by work', async () => {
|
|
203
|
+
// A bare goal_achieved:false after confirming the LAST criterion reads as a bug
|
|
204
|
+
// to the agent that just did it. The route names the reason when there is one.
|
|
205
|
+
const src = readFileSync(new URL('../modules/lifecycle/routes/done-when.js', import.meta.url), 'utf8');
|
|
206
|
+
assert.match(src, /goal_held_open_by_tasks: goalResult\.unfinishedTasks/);
|
|
207
|
+
assert.match(src, /\.\.\.\(goalResult\.unfinishedTasks \?/,
|
|
208
|
+
'and stays silent on the ordinary path, so the response shape is unchanged when nothing is held');
|
|
209
|
+
});
|
|
210
|
+
|
|
148
211
|
summary();
|