@gajae-code/coding-agent 0.12.1 → 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 +39 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
- package/dist/types/task/index.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-acp-conformance.ts +5 -0
- package/src/async/job-manager.ts +22 -20
- package/src/config/provider-ranking.ts +1 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
- 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/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +80 -10
- package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
- package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/DESIGN.md +53 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/tool-status-header.ts +0 -1
- package/src/modes/controllers/command-controller.ts +38 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +186 -34
- package/src/modes/controllers/selector-controller.ts +115 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +1 -0
- package/src/session/agent-session.ts +22 -1
- package/src/session/internal/managed-session-scope.ts +158 -107
- package/src/session/internal/managed-session-storage.ts +445 -57
- package/src/session/session-storage.ts +29 -44
- package/src/task/index.ts +257 -31
- package/src/task/output-manager.ts +24 -14
- package/src/task/receipt.ts +11 -1
- package/src/tools/index.ts +2 -0
|
@@ -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
|
|
|
@@ -2775,6 +2966,182 @@ function hydrateReviewedBatchReplacementClose(input: {
|
|
|
2775
2966
|
},
|
|
2776
2967
|
};
|
|
2777
2968
|
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Scaffold a schema-shaped quality-gate template for selected surfaces (#3474).
|
|
2971
|
+
* The template is intentionally incomplete for live artifact proofs so `quality-gate
|
|
2972
|
+
* validate` can report remaining evidence gaps in one pass after the author fills paths.
|
|
2973
|
+
*/
|
|
2974
|
+
export function buildQualityGateInitTemplate(surfaces: readonly string[]): JsonObject {
|
|
2975
|
+
const normalized = surfaces.length > 0 ? surfaces.map(surface => surface.trim()).filter(Boolean) : ["web"];
|
|
2976
|
+
const unique: string[] = [];
|
|
2977
|
+
for (const surface of normalized) {
|
|
2978
|
+
if (!unique.includes(surface)) unique.push(surface);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
const artifactRefs: JsonObject[] = [
|
|
2982
|
+
{
|
|
2983
|
+
id: "adversarial-report",
|
|
2984
|
+
kind: "failure-mode-test",
|
|
2985
|
+
path: "artifacts/adversarial-report.txt",
|
|
2986
|
+
description: "Adversarial boundary and failure-mode test output",
|
|
2987
|
+
inlineEvidence:
|
|
2988
|
+
"Adversarial boundary cases exercised invalid input, missing state, and repeated submission without violating the contract.",
|
|
2989
|
+
},
|
|
2990
|
+
];
|
|
2991
|
+
const surfaceEvidence: JsonObject[] = [];
|
|
2992
|
+
const contractCoverage: JsonObject[] = [];
|
|
2993
|
+
const adversarialCases: JsonObject[] = [
|
|
2994
|
+
{
|
|
2995
|
+
id: "case-invalid-input",
|
|
2996
|
+
contractRef: "approved-plan:goal",
|
|
2997
|
+
scenario: "Submit invalid or boundary input through the user-facing surface",
|
|
2998
|
+
expectedBehavior: "The implementation rejects or handles the case according to the approved contract",
|
|
2999
|
+
verdict: "passed",
|
|
3000
|
+
artifactRefs: ["adversarial-report"],
|
|
3001
|
+
},
|
|
3002
|
+
];
|
|
3003
|
+
|
|
3004
|
+
unique.forEach((surface, index) => {
|
|
3005
|
+
const surfaceId = `surface-${index + 1}`;
|
|
3006
|
+
const family = surface.toLowerCase();
|
|
3007
|
+
const linked: string[] = [];
|
|
3008
|
+
if (family.includes("web") || family.includes("gui")) {
|
|
3009
|
+
const browserId = `browser-run-${index + 1}`;
|
|
3010
|
+
const shotId = `gui-screenshot-${index + 1}`;
|
|
3011
|
+
linked.push(browserId, shotId);
|
|
3012
|
+
artifactRefs.push(
|
|
3013
|
+
{
|
|
3014
|
+
id: browserId,
|
|
3015
|
+
kind: "browser-automation",
|
|
3016
|
+
path: `artifacts/${browserId}.json`,
|
|
3017
|
+
description: "Browser automation transcript for the approved user-facing flow",
|
|
3018
|
+
inlineEvidence:
|
|
3019
|
+
"Browser automation executed the approved flow, asserted the expected visible result, and captured the final DOM state.",
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
id: shotId,
|
|
3023
|
+
kind: "screenshot",
|
|
3024
|
+
path: `artifacts/${shotId}.png`,
|
|
3025
|
+
description: "Screenshot evidence for the GUI/web surface verdict",
|
|
3026
|
+
inlineEvidence:
|
|
3027
|
+
"Screenshot review confirmed the approved screen state, including the success message and absence of regression indicators.",
|
|
3028
|
+
},
|
|
3029
|
+
);
|
|
3030
|
+
} else if (family.includes("cli")) {
|
|
3031
|
+
const replayId = `cli-replay-${index + 1}`;
|
|
3032
|
+
linked.push(replayId);
|
|
3033
|
+
artifactRefs.push({
|
|
3034
|
+
id: replayId,
|
|
3035
|
+
kind: "cli-replay",
|
|
3036
|
+
path: `artifacts/${replayId}.json`,
|
|
3037
|
+
description: "CLI argv replay transcript for the approved command surface",
|
|
3038
|
+
inlineEvidence:
|
|
3039
|
+
"CLI replay executed the allowlisted command and verified recorded stdout against the approved contract.",
|
|
3040
|
+
});
|
|
3041
|
+
} else if (family.includes("api") || family.includes("package")) {
|
|
3042
|
+
const reportId = `api-report-${index + 1}`;
|
|
3043
|
+
linked.push(reportId);
|
|
3044
|
+
artifactRefs.push({
|
|
3045
|
+
id: reportId,
|
|
3046
|
+
kind: "test-report",
|
|
3047
|
+
path: `artifacts/${reportId}.xml`,
|
|
3048
|
+
description: "API/package black-box test report",
|
|
3049
|
+
inlineEvidence:
|
|
3050
|
+
"API/package suite covered happy path, auth failure, and contract-breaking payloads with non-empty report output.",
|
|
3051
|
+
});
|
|
3052
|
+
} else {
|
|
3053
|
+
const evidenceId = `surface-evidence-${index + 1}`;
|
|
3054
|
+
linked.push(evidenceId);
|
|
3055
|
+
artifactRefs.push({
|
|
3056
|
+
id: evidenceId,
|
|
3057
|
+
kind: "transcript",
|
|
3058
|
+
path: `artifacts/${evidenceId}.txt`,
|
|
3059
|
+
description: `Evidence transcript for surface ${surface}`,
|
|
3060
|
+
inlineEvidence: `Surface ${surface} was exercised against the approved contract with recorded transcript evidence.`,
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
surfaceEvidence.push({
|
|
3065
|
+
id: surfaceId,
|
|
3066
|
+
surface: family.includes("web") || family.includes("gui") ? "gui/web" : surface,
|
|
3067
|
+
contractRef: "approved-plan:goal",
|
|
3068
|
+
invocation: `Exercise surface ${surface} against the approved contract`,
|
|
3069
|
+
verdict: "passed",
|
|
3070
|
+
artifactRefs: linked,
|
|
3071
|
+
});
|
|
3072
|
+
contractCoverage.push({
|
|
3073
|
+
id: `contract-${index + 1}`,
|
|
3074
|
+
contractRef: "approved-plan:goal",
|
|
3075
|
+
obligation: `The completed story satisfies the approved contract on surface ${surface}`,
|
|
3076
|
+
status: "covered",
|
|
3077
|
+
surfaceEvidenceRefs: [surfaceId],
|
|
3078
|
+
adversarialCaseRefs: ["case-invalid-input"],
|
|
3079
|
+
});
|
|
3080
|
+
});
|
|
3081
|
+
|
|
3082
|
+
return {
|
|
3083
|
+
architectReview: {
|
|
3084
|
+
architectureStatus: "CLEAR",
|
|
3085
|
+
productStatus: "CLEAR",
|
|
3086
|
+
codeStatus: "CLEAR",
|
|
3087
|
+
recommendation: "APPROVE",
|
|
3088
|
+
evidence: "architect reviewed architecture, product behavior, and code changes",
|
|
3089
|
+
commands: ["architect-review"],
|
|
3090
|
+
blockers: [],
|
|
3091
|
+
},
|
|
3092
|
+
executorQa: {
|
|
3093
|
+
status: "passed",
|
|
3094
|
+
e2eStatus: "passed",
|
|
3095
|
+
redTeamStatus: "passed",
|
|
3096
|
+
evidence: "executor built and ran e2e plus red-team QA suite",
|
|
3097
|
+
e2eCommands: ["bun test:e2e"],
|
|
3098
|
+
redTeamCommands: ["bun test:red-team"],
|
|
3099
|
+
artifactRefs,
|
|
3100
|
+
contractCoverage,
|
|
3101
|
+
surfaceEvidence,
|
|
3102
|
+
adversarialCases,
|
|
3103
|
+
blockers: [],
|
|
3104
|
+
},
|
|
3105
|
+
criticReview: {
|
|
3106
|
+
verdict: "OKAY",
|
|
3107
|
+
evidence: "critic approved final aggregate",
|
|
3108
|
+
blockers: [],
|
|
3109
|
+
},
|
|
3110
|
+
iteration: {
|
|
3111
|
+
status: "passed",
|
|
3112
|
+
evidence: "no verification findings remain after steering iterations",
|
|
3113
|
+
fullRerun: true,
|
|
3114
|
+
reviewCohort: {
|
|
3115
|
+
reviewGeneration: 1,
|
|
3116
|
+
sourceHash: "sha256:replace-with-frozen-source-hash",
|
|
3117
|
+
joined: true,
|
|
3118
|
+
lanes: {
|
|
3119
|
+
cleaner: {
|
|
3120
|
+
status: "passed",
|
|
3121
|
+
sourceHash: "sha256:replace-with-frozen-source-hash",
|
|
3122
|
+
evidence: "cleaner clean",
|
|
3123
|
+
blockers: [],
|
|
3124
|
+
},
|
|
3125
|
+
architect: {
|
|
3126
|
+
status: "CLEAR",
|
|
3127
|
+
sourceHash: "sha256:replace-with-frozen-source-hash",
|
|
3128
|
+
evidence: "architect clear",
|
|
3129
|
+
blockers: [],
|
|
3130
|
+
},
|
|
3131
|
+
qa: {
|
|
3132
|
+
status: "passed",
|
|
3133
|
+
sourceHash: "sha256:replace-with-frozen-source-hash",
|
|
3134
|
+
evidence: "qa passed",
|
|
3135
|
+
blockers: [],
|
|
3136
|
+
},
|
|
3137
|
+
},
|
|
3138
|
+
},
|
|
3139
|
+
rerunCommands: ["bun test:e2e", "bun test:red-team"],
|
|
3140
|
+
blockers: [],
|
|
3141
|
+
},
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
|
|
2778
3145
|
/**
|
|
2779
3146
|
* Read-only quality-gate validation (#3474). Applies exactly the same rules as
|
|
2780
3147
|
* `checkpoint --status complete` — including deferred-vs-boundary gate selection and
|
|
@@ -2807,8 +3174,17 @@ export async function validateUltragoalQualityGateReadOnly(input: {
|
|
|
2807
3174
|
}
|
|
2808
3175
|
const ledger = plan ? await readUltragoalLedger(input.cwd, sessionId) : undefined;
|
|
2809
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
|
+
}
|
|
2810
3181
|
try {
|
|
2811
|
-
await validateCompletionQualityGate(input.cwd,
|
|
3182
|
+
await validateCompletionQualityGate(input.cwd, hydratedGate, {
|
|
3183
|
+
changeSet,
|
|
3184
|
+
plan: plan ?? undefined,
|
|
3185
|
+
goal,
|
|
3186
|
+
ledger,
|
|
3187
|
+
});
|
|
2812
3188
|
return { valid: true, errors: [] };
|
|
2813
3189
|
} catch (error) {
|
|
2814
3190
|
if (error instanceof UltragoalQualityGateError) return { valid: false, errors: error.diagnostics };
|
|
@@ -2855,13 +3231,25 @@ async function readRequiredCompletionQualityGate(
|
|
|
2855
3231
|
changeSet: options.changeSet,
|
|
2856
3232
|
})
|
|
2857
3233
|
: gateObject;
|
|
2858
|
-
|
|
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, {
|
|
2859
3247
|
changeSet: options.changeSet,
|
|
2860
3248
|
plan: options.plan,
|
|
2861
3249
|
goal: options.goal,
|
|
2862
3250
|
ledger: options.ledger,
|
|
2863
3251
|
});
|
|
2864
|
-
return
|
|
3252
|
+
return completionGate;
|
|
2865
3253
|
}
|
|
2866
3254
|
|
|
2867
3255
|
function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
|
|
@@ -3957,6 +4345,18 @@ function flagValue(args: readonly string[], flag: string): string | undefined {
|
|
|
3957
4345
|
return args[index + 1];
|
|
3958
4346
|
}
|
|
3959
4347
|
|
|
4348
|
+
function flagValues(args: readonly string[], flag: string): string[] {
|
|
4349
|
+
const values: string[] = [];
|
|
4350
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4351
|
+
if (args[index] !== flag) continue;
|
|
4352
|
+
const value = args[index + 1];
|
|
4353
|
+
if (value === undefined || value.startsWith("-")) continue;
|
|
4354
|
+
values.push(value);
|
|
4355
|
+
index += 1;
|
|
4356
|
+
}
|
|
4357
|
+
return values;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
3960
4360
|
function hasFlag(args: readonly string[], flag: string): boolean {
|
|
3961
4361
|
return args.includes(flag);
|
|
3962
4362
|
}
|
|
@@ -3985,6 +4385,8 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
3985
4385
|
"--order-json",
|
|
3986
4386
|
"--classification",
|
|
3987
4387
|
"--validation-batch-json",
|
|
4388
|
+
"--out",
|
|
4389
|
+
"--surface",
|
|
3988
4390
|
]);
|
|
3989
4391
|
|
|
3990
4392
|
function isHelpArg(arg: string): boolean {
|
|
@@ -4114,6 +4516,28 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
4114
4516
|
].join("\n");
|
|
4115
4517
|
}
|
|
4116
4518
|
|
|
4519
|
+
if (subject === "quality-gate") {
|
|
4520
|
+
return [
|
|
4521
|
+
"Run native GJC Ultragoal workflow commands",
|
|
4522
|
+
"",
|
|
4523
|
+
"USAGE",
|
|
4524
|
+
" $ gjc ultragoal quality-gate init [--surface <name> ...] --out <path>",
|
|
4525
|
+
" $ gjc ultragoal quality-gate validate --quality-gate-json <json-or-path> [--goal-id <id>] [--json]",
|
|
4526
|
+
"",
|
|
4527
|
+
"FLAGS",
|
|
4528
|
+
" --surface=<value> Surface to scaffold (repeatable; default web)",
|
|
4529
|
+
" --out=<value> Output path for quality-gate init",
|
|
4530
|
+
" --quality-gate-json=<value> JSON string or path for quality-gate validate",
|
|
4531
|
+
" --goal-id=<value> Optional durable goal id for rule-identical validation",
|
|
4532
|
+
" --json Machine-readable output",
|
|
4533
|
+
"",
|
|
4534
|
+
"EXAMPLES",
|
|
4535
|
+
" $ gjc ultragoal quality-gate init --surface web --surface api --out ./quality-gate.json",
|
|
4536
|
+
" $ gjc ultragoal quality-gate validate --quality-gate-json ./quality-gate.json --json",
|
|
4537
|
+
"",
|
|
4538
|
+
].join("\n");
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4117
4541
|
return [
|
|
4118
4542
|
"Run native GJC Ultragoal workflow commands",
|
|
4119
4543
|
"",
|
|
@@ -4131,10 +4555,11 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
4131
4555
|
" classify-blocker",
|
|
4132
4556
|
" record-critic-verdict",
|
|
4133
4557
|
" record-critic-gate-override",
|
|
4558
|
+
" quality-gate init",
|
|
4134
4559
|
" quality-gate validate",
|
|
4135
4560
|
|
|
4136
4561
|
"",
|
|
4137
|
-
"Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, `gjc ultragoal classify-blocker --help`, `gjc ultragoal record-critic-verdict --help`, or `gjc ultragoal record-critic-gate-override --help` for command-specific requirements.",
|
|
4562
|
+
"Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, `gjc ultragoal classify-blocker --help`, `gjc ultragoal record-critic-verdict --help`, or `gjc ultragoal record-critic-gate-override --help`, or `gjc ultragoal quality-gate --help` for command-specific requirements.",
|
|
4138
4563
|
"",
|
|
4139
4564
|
].join("\n");
|
|
4140
4565
|
}
|
|
@@ -4510,12 +4935,32 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
4510
4935
|
};
|
|
4511
4936
|
}
|
|
4512
4937
|
case "quality-gate": {
|
|
4513
|
-
const positional = args.filter(arg => !arg.startsWith("
|
|
4938
|
+
const positional = args.filter(arg => !arg.startsWith("-"));
|
|
4514
4939
|
const subcommand = positional[1];
|
|
4940
|
+
if (subcommand === "init") {
|
|
4941
|
+
const out = flagValue(args, "--out");
|
|
4942
|
+
if (!out?.trim()) {
|
|
4943
|
+
return { status: 1, stderr: "quality-gate init requires --out <path>\n" };
|
|
4944
|
+
}
|
|
4945
|
+
const surfaces = flagValues(args, "--surface");
|
|
4946
|
+
const template = buildQualityGateInitTemplate(surfaces);
|
|
4947
|
+
const resolved = path.resolve(cwd, out);
|
|
4948
|
+
await Bun.write(resolved, `${JSON.stringify(template, null, 2)}\n`);
|
|
4949
|
+
if (json) {
|
|
4950
|
+
return {
|
|
4951
|
+
status: 0,
|
|
4952
|
+
stdout: `${JSON.stringify({ ok: true, out: resolved, surfaces: surfaces.length > 0 ? surfaces : ["web"] }, null, 2)}\n`,
|
|
4953
|
+
};
|
|
4954
|
+
}
|
|
4955
|
+
return {
|
|
4956
|
+
status: 0,
|
|
4957
|
+
stdout: `Wrote quality-gate template to ${resolved}\n`,
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4515
4960
|
if (subcommand !== "validate") {
|
|
4516
4961
|
return {
|
|
4517
4962
|
status: 1,
|
|
4518
|
-
stderr: `Unknown gjc ultragoal quality-gate subcommand: ${subcommand ?? "(missing)"};
|
|
4963
|
+
stderr: `Unknown gjc ultragoal quality-gate subcommand: ${subcommand ?? "(missing)"}; supported: init, validate\n`,
|
|
4519
4964
|
};
|
|
4520
4965
|
}
|
|
4521
4966
|
const qualityGateJson = flagValue(args, "--quality-gate-json");
|
|
@@ -466,6 +466,9 @@
|
|
|
466
466
|
{
|
|
467
467
|
"id": "critic"
|
|
468
468
|
},
|
|
469
|
+
{
|
|
470
|
+
"id": "disposition"
|
|
471
|
+
},
|
|
469
472
|
{
|
|
470
473
|
"id": "revision"
|
|
471
474
|
},
|
|
@@ -507,6 +510,21 @@
|
|
|
507
510
|
"to": "critic",
|
|
508
511
|
"verb": "write-artifact"
|
|
509
512
|
},
|
|
513
|
+
{
|
|
514
|
+
"from": "critic",
|
|
515
|
+
"to": "disposition",
|
|
516
|
+
"verb": "write-artifact"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"from": "architect",
|
|
520
|
+
"to": "disposition",
|
|
521
|
+
"verb": "write-artifact"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"from": "disposition",
|
|
525
|
+
"to": "revision",
|
|
526
|
+
"verb": "write-artifact"
|
|
527
|
+
},
|
|
510
528
|
{
|
|
511
529
|
"from": "critic",
|
|
512
530
|
"to": "revision",
|
|
@@ -522,6 +540,11 @@
|
|
|
522
540
|
"to": "post-interview",
|
|
523
541
|
"verb": "write-artifact"
|
|
524
542
|
},
|
|
543
|
+
{
|
|
544
|
+
"from": "disposition",
|
|
545
|
+
"to": "post-interview",
|
|
546
|
+
"verb": "write-artifact"
|
|
547
|
+
},
|
|
525
548
|
{
|
|
526
549
|
"from": "post-interview",
|
|
527
550
|
"to": "revision",
|
|
@@ -557,6 +580,11 @@
|
|
|
557
580
|
"to": "handoff",
|
|
558
581
|
"verb": "handoff"
|
|
559
582
|
},
|
|
583
|
+
{
|
|
584
|
+
"from": "disposition",
|
|
585
|
+
"to": "handoff",
|
|
586
|
+
"verb": "handoff"
|
|
587
|
+
},
|
|
560
588
|
{
|
|
561
589
|
"from": "revision",
|
|
562
590
|
"to": "handoff",
|
|
@@ -727,6 +755,7 @@
|
|
|
727
755
|
"planner",
|
|
728
756
|
"architect",
|
|
729
757
|
"critic",
|
|
758
|
+
"disposition",
|
|
730
759
|
"revision",
|
|
731
760
|
"post-interview",
|
|
732
761
|
"adr",
|