@gajae-code/coding-agent 0.12.2 → 0.12.4
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/CHANGELOG.md +11 -2
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -2
- package/package.json +8 -8
- package/src/config/provider-ranking.ts +1 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
- package/src/gjc-runtime/ultragoal-evidence.ts +160 -31
- package/src/gjc-runtime/ultragoal-runtime.ts +231 -19
- package/src/internal-urls/docs-index.generated.ts +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { getConfigRootDir } from "@gajae-code/utils";
|
|
4
5
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
@@ -1794,27 +1795,46 @@ export function hasTypedVerifiedReceipt(value: unknown): boolean {
|
|
|
1794
1795
|
return Boolean(type && id && (status === "verified" || status === "passed"));
|
|
1795
1796
|
}
|
|
1796
1797
|
|
|
1797
|
-
|
|
1798
|
+
async function resolveExistingArtifactPathUnderCwd(
|
|
1799
|
+
cwd: string,
|
|
1800
|
+
value: unknown,
|
|
1801
|
+
fieldName: string,
|
|
1802
|
+
): Promise<string | null> {
|
|
1798
1803
|
const artifactPath = nonEmptyString(value);
|
|
1799
|
-
if (!artifactPath) return
|
|
1800
|
-
const
|
|
1804
|
+
if (!artifactPath) return null;
|
|
1805
|
+
const lexicalRoot = path.resolve(cwd);
|
|
1806
|
+
const lexical = path.resolve(lexicalRoot, artifactPath);
|
|
1807
|
+
const lexicalRelative = path.relative(lexicalRoot, lexical);
|
|
1808
|
+
if (lexicalRelative === ".." || lexicalRelative.startsWith(`..${path.sep}`) || path.isAbsolute(lexicalRelative)) {
|
|
1809
|
+
throw new Error(`qualityGate ${fieldName} artifact path must resolve under the repository cwd`);
|
|
1810
|
+
}
|
|
1801
1811
|
try {
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1812
|
+
const [root, resolved] = await Promise.all([fs.realpath(lexicalRoot), fs.realpath(lexical)]);
|
|
1813
|
+
const relative = path.relative(root, resolved);
|
|
1814
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1815
|
+
throw new Error(`qualityGate ${fieldName} artifact path must not escape the repository cwd through symlinks`);
|
|
1816
|
+
}
|
|
1817
|
+
if (!(await fs.stat(resolved)).isFile()) {
|
|
1818
|
+
throw new Error(`qualityGate ${fieldName} artifact path must reference a regular file`);
|
|
1819
|
+
}
|
|
1820
|
+
return resolved;
|
|
1804
1821
|
} catch (error) {
|
|
1805
|
-
if (isEnoent(error)) return
|
|
1822
|
+
if (isEnoent(error)) return null;
|
|
1806
1823
|
throw error;
|
|
1807
1824
|
}
|
|
1808
1825
|
}
|
|
1809
1826
|
|
|
1827
|
+
export async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
|
|
1828
|
+
const resolved = await resolveExistingArtifactPathUnderCwd(cwd, value, "artifact");
|
|
1829
|
+
if (!resolved) return false;
|
|
1830
|
+
return (await fs.stat(resolved)).size > 0;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1810
1833
|
export async function readArtifactBytes(cwd: string, row: JsonObject, fieldName: string): Promise<Buffer | null> {
|
|
1811
|
-
const
|
|
1812
|
-
if (!
|
|
1813
|
-
const resolved = path.resolve(cwd, artifactPath);
|
|
1834
|
+
const resolved = await resolveExistingArtifactPathUnderCwd(cwd, row.path, fieldName);
|
|
1835
|
+
if (!resolved) return null;
|
|
1814
1836
|
try {
|
|
1815
|
-
|
|
1816
|
-
if (!(await file.exists())) return null;
|
|
1817
|
-
return Buffer.from(await file.arrayBuffer());
|
|
1837
|
+
return Buffer.from(await Bun.file(resolved).arrayBuffer());
|
|
1818
1838
|
} catch (error) {
|
|
1819
1839
|
if (isEnoent(error)) return null;
|
|
1820
1840
|
throw new Error(`qualityGate ${fieldName} artifact could not be read: ${String(error)}`);
|
|
@@ -1823,6 +1843,7 @@ export async function readArtifactBytes(cwd: string, row: JsonObject, fieldName:
|
|
|
1823
1843
|
|
|
1824
1844
|
import {
|
|
1825
1845
|
readCliReplayRecord,
|
|
1846
|
+
resolveCliReplayCommand,
|
|
1826
1847
|
validateArtifactProof,
|
|
1827
1848
|
validateCliReplay,
|
|
1828
1849
|
validateLiveSurfaceProofPresence,
|
|
@@ -1834,6 +1855,7 @@ import {
|
|
|
1834
1855
|
|
|
1835
1856
|
export type { ReplayProcessHandle } from "./ultragoal-evidence";
|
|
1836
1857
|
export {
|
|
1858
|
+
resolveCliReplayCommand,
|
|
1837
1859
|
validateArtifactProof,
|
|
1838
1860
|
validateCliReplay,
|
|
1839
1861
|
validateLiveSurfaceProofPresence,
|
|
@@ -2230,6 +2252,169 @@ function validateDeferredLaneDeclaration(deferred: JsonObject, fieldName: string
|
|
|
2230
2252
|
}
|
|
2231
2253
|
}
|
|
2232
2254
|
|
|
2255
|
+
/**
|
|
2256
|
+
* Agent-friendly deferred-gate hydration: the runtime already stores the durable
|
|
2257
|
+
* batch tuple and computes the cumulative change set itself, so a deferred gate
|
|
2258
|
+
* only has to prove what the runtime cannot know — that targeted verification ran.
|
|
2259
|
+
* Every mechanical field (`kind`, the batch tuple, `deferredLanes`, and the whole
|
|
2260
|
+
* `changeSet` block including `paths` and `changeSetHash`) is auto-filled when
|
|
2261
|
+
* omitted. Explicitly supplied values are never overwritten, so a wrong
|
|
2262
|
+
* declaration still fails closed.
|
|
2263
|
+
*/
|
|
2264
|
+
function hydrateDeferredGateDefaults(
|
|
2265
|
+
gate: JsonObject,
|
|
2266
|
+
goal: UltragoalGoal,
|
|
2267
|
+
changeSet: UltragoalChangeSet | undefined,
|
|
2268
|
+
): JsonObject {
|
|
2269
|
+
const deferred = qualityGateObject(gate.deferredToBatch);
|
|
2270
|
+
if (!deferred) return gate;
|
|
2271
|
+
const metadata = goal.validationBatch;
|
|
2272
|
+
// The final member must carry the full strict gate; never help it defer.
|
|
2273
|
+
if (metadata && goal.id === metadata.finalGoalId) return gate;
|
|
2274
|
+
const hydrated: JsonObject = { ...deferred };
|
|
2275
|
+
if (hydrated.kind === undefined) hydrated.kind = "validation-batch-deferred";
|
|
2276
|
+
if (metadata) {
|
|
2277
|
+
if (hydrated.schemaVersion === undefined) hydrated.schemaVersion = metadata.schemaVersion;
|
|
2278
|
+
if (hydrated.batchId === undefined) hydrated.batchId = metadata.batchId;
|
|
2279
|
+
if (hydrated.memberIds === undefined) hydrated.memberIds = [...metadata.memberIds];
|
|
2280
|
+
if (hydrated.finalGoalId === undefined) hydrated.finalGoalId = metadata.finalGoalId;
|
|
2281
|
+
if (hydrated.metadataHash === undefined) hydrated.metadataHash = metadata.metadataHash;
|
|
2282
|
+
}
|
|
2283
|
+
if (hydrated.deferredLanes === undefined) hydrated.deferredLanes = ["architectReview", "executorQa"];
|
|
2284
|
+
const computedRows = (changeSet?.paths ?? []).map(row => ({
|
|
2285
|
+
path: row.path,
|
|
2286
|
+
status: row.status,
|
|
2287
|
+
...(row.oldPath ? { oldPath: row.oldPath } : {}),
|
|
2288
|
+
}));
|
|
2289
|
+
const computedByPath = new Map(computedRows.map(row => [row.path, row]));
|
|
2290
|
+
const declared = qualityGateObject(hydrated.changeSet);
|
|
2291
|
+
const changeSetRecord: JsonObject = declared ? { ...declared } : {};
|
|
2292
|
+
if (changeSetRecord.memberGoalId === undefined) changeSetRecord.memberGoalId = goal.id;
|
|
2293
|
+
if (changeSetRecord.cumulativeFromBase === undefined) changeSetRecord.cumulativeFromBase = true;
|
|
2294
|
+
if (changeSetRecord.paths === undefined) {
|
|
2295
|
+
changeSetRecord.paths = computedRows;
|
|
2296
|
+
} else if (Array.isArray(changeSetRecord.paths)) {
|
|
2297
|
+
// Accept plain-string rows and rows without a status; resolve the status
|
|
2298
|
+
// from the computed change set instead of demanding git trivia.
|
|
2299
|
+
changeSetRecord.paths = changeSetRecord.paths.map(row => {
|
|
2300
|
+
const record = typeof row === "string" ? { path: row } : qualityGateObject(row);
|
|
2301
|
+
if (!record) return row;
|
|
2302
|
+
const pathValue = nonEmptyString(record.path);
|
|
2303
|
+
if (!pathValue || nonEmptyString(record.status)) return record;
|
|
2304
|
+
const computed = computedByPath.get(normalizeRepoPath(pathValue));
|
|
2305
|
+
return {
|
|
2306
|
+
...record,
|
|
2307
|
+
status: computed?.status ?? "unknown",
|
|
2308
|
+
...(computed?.oldPath && record.oldPath === undefined ? { oldPath: computed.oldPath } : {}),
|
|
2309
|
+
};
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
if (changeSetRecord.changeSetHash === undefined) {
|
|
2313
|
+
try {
|
|
2314
|
+
changeSetRecord.changeSetHash = changeSetHashForPaths(
|
|
2315
|
+
canonicalChangeSetRows(changeSetRecord.paths, "deferredToBatch.changeSet.paths"),
|
|
2316
|
+
);
|
|
2317
|
+
} catch {
|
|
2318
|
+
// Malformed rows: leave the hash unset so validation reports the row defect.
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
hydrated.changeSet = changeSetRecord;
|
|
2322
|
+
return { ...gate, deferredToBatch: hydrated };
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
/**
|
|
2326
|
+
* Agent-friendly batch-close hydration, mirroring `hydrateDeferredGateDefaults`:
|
|
2327
|
+
* every `validationBatchClose` field except `coverageEvidence` is derivable from
|
|
2328
|
+
* durable state (batch metadata, member receipts) and the computed cumulative
|
|
2329
|
+
* change set, so it is auto-filled when omitted. Supplied values are never
|
|
2330
|
+
* overwritten and still fail closed when wrong. The minimal close is the full
|
|
2331
|
+
* strict gate plus `{"validationBatchClose":{"coverageEvidence":"..."}}`.
|
|
2332
|
+
*/
|
|
2333
|
+
function hydrateBatchCloseDefaults(input: {
|
|
2334
|
+
gate: JsonObject;
|
|
2335
|
+
plan: UltragoalPlan;
|
|
2336
|
+
goal: UltragoalGoal;
|
|
2337
|
+
ledger: readonly UltragoalLedgerEvent[];
|
|
2338
|
+
changeSet?: UltragoalChangeSet;
|
|
2339
|
+
}): JsonObject {
|
|
2340
|
+
const metadata = input.goal.validationBatch;
|
|
2341
|
+
if (!metadata || input.goal.id !== metadata.finalGoalId) return input.gate;
|
|
2342
|
+
const requested = qualityGateObject(input.gate.validationBatchClose);
|
|
2343
|
+
if (!requested) return input.gate;
|
|
2344
|
+
// The replacement-close kind has its own dedicated hydrator.
|
|
2345
|
+
if (requested.kind === "review-blocker-replacement-close") return input.gate;
|
|
2346
|
+
const close: JsonObject = { ...requested };
|
|
2347
|
+
if (close.schemaVersion === undefined) close.schemaVersion = 1;
|
|
2348
|
+
if (close.kind === undefined) close.kind = "validation-batch-close";
|
|
2349
|
+
if (close.batchId === undefined) close.batchId = metadata.batchId;
|
|
2350
|
+
if (close.finalGoalId === undefined) close.finalGoalId = metadata.finalGoalId;
|
|
2351
|
+
if (close.memberIds === undefined) close.memberIds = [...metadata.memberIds];
|
|
2352
|
+
const derivedMetadataHashes: Record<string, string> = {};
|
|
2353
|
+
const derivedChangeSetHashes: Record<string, string> = {};
|
|
2354
|
+
const derivedReceipts: JsonObject[] = [];
|
|
2355
|
+
for (const memberId of metadata.memberIds) {
|
|
2356
|
+
const member = input.plan.goals.find(goal => goal.id === memberId);
|
|
2357
|
+
if (member?.validationBatch) derivedMetadataHashes[memberId] = member.validationBatch.metadataHash;
|
|
2358
|
+
if (!member || memberId === input.goal.id) continue;
|
|
2359
|
+
try {
|
|
2360
|
+
const receipt = requireDeferredMemberReceiptFresh(
|
|
2361
|
+
input.plan,
|
|
2362
|
+
input.ledger,
|
|
2363
|
+
member,
|
|
2364
|
+
"validationBatchClose hydration",
|
|
2365
|
+
);
|
|
2366
|
+
derivedChangeSetHashes[memberId] = receipt.validationBatch.changeSetHash;
|
|
2367
|
+
derivedReceipts.push({
|
|
2368
|
+
goalId: memberId,
|
|
2369
|
+
receiptId: receipt.receiptId,
|
|
2370
|
+
checkpointLedgerEventId: receipt.checkpointLedgerEventId,
|
|
2371
|
+
qualityGateHash: receipt.qualityGateHash,
|
|
2372
|
+
changeSetHash: receipt.validationBatch.changeSetHash,
|
|
2373
|
+
role: "deferred-member",
|
|
2374
|
+
});
|
|
2375
|
+
} catch {
|
|
2376
|
+
// Member not fresh/complete: omit it so validation reports the real defect.
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
const suppliedMetadataHashes = qualityGateObject(close.memberMetadataHashes);
|
|
2380
|
+
if (close.memberMetadataHashes === undefined) close.memberMetadataHashes = derivedMetadataHashes;
|
|
2381
|
+
else if (suppliedMetadataHashes)
|
|
2382
|
+
close.memberMetadataHashes = { ...derivedMetadataHashes, ...suppliedMetadataHashes };
|
|
2383
|
+
if (close.memberReceipts === undefined) close.memberReceipts = derivedReceipts;
|
|
2384
|
+
const requestedUnion = qualityGateObject(close.unionChangeSet);
|
|
2385
|
+
const union: JsonObject = requestedUnion ? { ...requestedUnion } : {};
|
|
2386
|
+
if (union.source === undefined) union.source = "validation-batch";
|
|
2387
|
+
if (union.paths === undefined) {
|
|
2388
|
+
union.paths = (input.changeSet?.paths ?? []).map(row => ({
|
|
2389
|
+
path: row.path,
|
|
2390
|
+
status: row.status,
|
|
2391
|
+
...(row.oldPath ? { oldPath: row.oldPath } : {}),
|
|
2392
|
+
}));
|
|
2393
|
+
}
|
|
2394
|
+
try {
|
|
2395
|
+
const unionRows = canonicalChangeSetRows(union.paths, "validationBatchClose.unionChangeSet.paths");
|
|
2396
|
+
const suppliedChangeSetHashes = qualityGateObject(union.memberChangeSetHashes);
|
|
2397
|
+
const memberChangeSetHashes: Record<string, unknown> = {
|
|
2398
|
+
...derivedChangeSetHashes,
|
|
2399
|
+
[metadata.finalGoalId]: changeSetHashForPaths(unionRows),
|
|
2400
|
+
...suppliedChangeSetHashes,
|
|
2401
|
+
};
|
|
2402
|
+
if (union.memberChangeSetHashes === undefined || suppliedChangeSetHashes) {
|
|
2403
|
+
union.memberChangeSetHashes = memberChangeSetHashes;
|
|
2404
|
+
}
|
|
2405
|
+
if (union.unionHash === undefined) {
|
|
2406
|
+
union.unionHash = hashStructuredValue({
|
|
2407
|
+
memberChangeSetHashes: qualityGateObject(union.memberChangeSetHashes) ?? memberChangeSetHashes,
|
|
2408
|
+
paths: unionRows.map(row => ({ path: row.path, status: row.status, oldPath: row.oldPath })),
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
} catch {
|
|
2412
|
+
// Malformed rows: leave derived hashes unset so validation reports the row defect.
|
|
2413
|
+
}
|
|
2414
|
+
close.unionChangeSet = union;
|
|
2415
|
+
return { ...input.gate, validationBatchClose: close };
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2233
2418
|
function validateDeferredCompletionQualityGate(
|
|
2234
2419
|
gate: JsonObject,
|
|
2235
2420
|
goal: UltragoalGoal,
|
|
@@ -2251,7 +2436,9 @@ function validateDeferredCompletionQualityGate(
|
|
|
2251
2436
|
}
|
|
2252
2437
|
const deferredLanes = stringArray(deferred.deferredLanes)?.filter(Boolean).sort();
|
|
2253
2438
|
if (deferredLanes?.join(",") !== "architectReview,executorQa")
|
|
2254
|
-
throw new Error(
|
|
2439
|
+
throw new Error(
|
|
2440
|
+
"deferredToBatch.deferredLanes must be architectReview and executorQa (or omitted; the runtime fills it)",
|
|
2441
|
+
);
|
|
2255
2442
|
const targeted = qualityGateObject(deferred.targetedVerification);
|
|
2256
2443
|
if (!targeted || targeted.status !== PASSED_STATUS || !nonEmptyStringArray(targeted.commands))
|
|
2257
2444
|
throw new Error("deferredToBatch.targetedVerification must pass with non-empty commands");
|
|
@@ -2276,13 +2463,17 @@ function validateDeferredCompletionQualityGate(
|
|
|
2276
2463
|
const declaredChangeSet = qualityGateObject(deferred.changeSet);
|
|
2277
2464
|
if (!declaredChangeSet) throw new Error("deferredToBatch.changeSet is required");
|
|
2278
2465
|
if (declaredChangeSet.memberGoalId !== goal.id)
|
|
2279
|
-
throw new Error(
|
|
2466
|
+
throw new Error(
|
|
2467
|
+
`deferredToBatch.changeSet.memberGoalId must label the checkpointed goal ${goal.id} (or be omitted; the runtime fills it)`,
|
|
2468
|
+
);
|
|
2280
2469
|
if (declaredChangeSet.cumulativeFromBase !== true)
|
|
2281
|
-
throw new Error("deferredToBatch.changeSet.cumulativeFromBase must be true");
|
|
2470
|
+
throw new Error("deferredToBatch.changeSet.cumulativeFromBase must be true (or omitted; the runtime fills it)");
|
|
2282
2471
|
const paths = canonicalChangeSetRows(declaredChangeSet.paths, "deferredToBatch.changeSet.paths");
|
|
2283
2472
|
requireChangeSetCoverage(changeSet, paths, "deferredToBatch.changeSet.paths");
|
|
2284
2473
|
if (declaredChangeSet.changeSetHash !== changeSetHashForPaths(paths))
|
|
2285
|
-
throw new Error(
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
"deferredToBatch.changeSet.changeSetHash does not match declared paths; omit changeSetHash and the runtime computes it",
|
|
2476
|
+
);
|
|
2286
2477
|
}
|
|
2287
2478
|
const COHORT_LANE_KEYS = ["cleaner", "architect", "qa"] as const;
|
|
2288
2479
|
|
|
@@ -2983,8 +3174,17 @@ export async function validateUltragoalQualityGateReadOnly(input: {
|
|
|
2983
3174
|
}
|
|
2984
3175
|
const ledger = plan ? await readUltragoalLedger(input.cwd, sessionId) : undefined;
|
|
2985
3176
|
const changeSet = await computeCheckpointChangeSet(input.cwd);
|
|
3177
|
+
let hydratedGate = goal ? hydrateDeferredGateDefaults(gate, goal, changeSet) : gate;
|
|
3178
|
+
if (goal && plan && ledger) {
|
|
3179
|
+
hydratedGate = hydrateBatchCloseDefaults({ gate: hydratedGate, plan, goal, ledger, changeSet });
|
|
3180
|
+
}
|
|
2986
3181
|
try {
|
|
2987
|
-
await validateCompletionQualityGate(input.cwd,
|
|
3182
|
+
await validateCompletionQualityGate(input.cwd, hydratedGate, {
|
|
3183
|
+
changeSet,
|
|
3184
|
+
plan: plan ?? undefined,
|
|
3185
|
+
goal,
|
|
3186
|
+
ledger,
|
|
3187
|
+
});
|
|
2988
3188
|
return { valid: true, errors: [] };
|
|
2989
3189
|
} catch (error) {
|
|
2990
3190
|
if (error instanceof UltragoalQualityGateError) return { valid: false, errors: error.diagnostics };
|
|
@@ -3031,13 +3231,25 @@ async function readRequiredCompletionQualityGate(
|
|
|
3031
3231
|
changeSet: options.changeSet,
|
|
3032
3232
|
})
|
|
3033
3233
|
: gateObject;
|
|
3034
|
-
|
|
3234
|
+
let completionGate = options.goal
|
|
3235
|
+
? hydrateDeferredGateDefaults(hydratedGate, options.goal, options.changeSet)
|
|
3236
|
+
: hydratedGate;
|
|
3237
|
+
if (options.goal && options.plan && options.ledger) {
|
|
3238
|
+
completionGate = hydrateBatchCloseDefaults({
|
|
3239
|
+
gate: completionGate,
|
|
3240
|
+
plan: options.plan,
|
|
3241
|
+
goal: options.goal,
|
|
3242
|
+
ledger: options.ledger,
|
|
3243
|
+
changeSet: options.changeSet,
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
await validateCompletionQualityGate(cwd, completionGate, {
|
|
3035
3247
|
changeSet: options.changeSet,
|
|
3036
3248
|
plan: options.plan,
|
|
3037
3249
|
goal: options.goal,
|
|
3038
3250
|
ledger: options.ledger,
|
|
3039
3251
|
});
|
|
3040
|
-
return
|
|
3252
|
+
return completionGate;
|
|
3041
3253
|
}
|
|
3042
3254
|
|
|
3043
3255
|
function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
|