@gajae-code/coding-agent 0.2.5 → 0.3.0
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 +10 -0
- package/dist/types/async/job-manager.d.ts +84 -2
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/deep-interview/render-middleware.d.ts +5 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +2 -29
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/types.d.ts +55 -3
- package/dist/types/tools/subagent.d.ts +11 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +298 -6
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/harness.ts +592 -0
- package/src/commands/team.ts +36 -39
- package/src/config/settings-schema.ts +7 -0
- package/src/config/settings.ts +5 -0
- package/src/deep-interview/render-middleware.ts +366 -0
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +25 -10
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +132 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +733 -21
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +718 -0
- package/src/gjc-runtime/team-runtime.ts +1083 -89
- package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
- package/src/gjc-runtime/workflow-manifest.ts +425 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +137 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +553 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +97 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +24 -41
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/hook-selector.ts +72 -2
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +9 -1
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +33 -3
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +231 -33
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +26 -0
- package/src/task/executor.ts +50 -8
- package/src/task/index.ts +120 -8
- package/src/task/render.ts +6 -3
- package/src/task/types.ts +56 -3
- package/src/tools/ask.ts +28 -7
- package/src/tools/subagent.ts +255 -64
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
4
|
import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
|
|
6
5
|
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
6
|
+
import { renderUltragoalStatusMarkdown } from "./state-renderer";
|
|
7
|
+
import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
|
|
7
8
|
|
|
8
9
|
export type UltragoalGjcGoalMode = "aggregate" | "per-story";
|
|
9
10
|
export type UltragoalGoalStatus =
|
|
@@ -103,6 +104,12 @@ const TERMINAL_OR_SKIPPED_STATUSES = new Set<UltragoalGoalStatus>(["complete", "
|
|
|
103
104
|
const CLEAN_ARCHITECT_STATUS = "CLEAR";
|
|
104
105
|
const APPROVE_RECOMMENDATION = "APPROVE";
|
|
105
106
|
const PASSED_STATUS = "passed";
|
|
107
|
+
const NOT_APPLICABLE_STATUS = "not_applicable";
|
|
108
|
+
const COVERED_STATUS = "covered";
|
|
109
|
+
const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
|
|
110
|
+
const MIN_SUBSTANTIVE_EVIDENCE_WORDS = 5;
|
|
111
|
+
const MIN_SUBSTANTIVE_EVIDENCE_CHARS = 32;
|
|
112
|
+
|
|
106
113
|
const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
|
|
107
114
|
const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
|
|
108
115
|
|
|
@@ -143,19 +150,17 @@ function isEnoent(error: unknown): boolean {
|
|
|
143
150
|
);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
async function ensureUltragoalDir(paths: UltragoalPaths): Promise<void> {
|
|
147
|
-
await fs.mkdir(paths.dir, { recursive: true });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
153
|
async function appendLedger(cwd: string, event: JsonObject): Promise<UltragoalLedgerEvent> {
|
|
151
154
|
const paths = getUltragoalPaths(cwd);
|
|
152
|
-
await ensureUltragoalDir(paths);
|
|
153
155
|
const entry: UltragoalLedgerEvent = {
|
|
154
156
|
eventId: typeof event.eventId === "string" ? event.eventId : crypto.randomUUID(),
|
|
155
157
|
...event,
|
|
156
158
|
timestamp: new Date().toISOString(),
|
|
157
159
|
};
|
|
158
|
-
await
|
|
160
|
+
await appendJsonl(paths.ledgerPath, entry, {
|
|
161
|
+
cwd,
|
|
162
|
+
audit: { category: "ledger", verb: "append", owner: "gjc-runtime" },
|
|
163
|
+
});
|
|
159
164
|
return entry;
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -175,9 +180,14 @@ export async function readUltragoalLedger(cwd: string): Promise<UltragoalLedgerE
|
|
|
175
180
|
|
|
176
181
|
async function writePlan(cwd: string, plan: UltragoalPlan): Promise<void> {
|
|
177
182
|
const paths = getUltragoalPaths(cwd);
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
await writeArtifact(paths.briefPath, `${plan.brief.trim()}\n`, {
|
|
184
|
+
cwd,
|
|
185
|
+
audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
|
|
186
|
+
});
|
|
187
|
+
await writeJsonAtomic(paths.goalsPath, plan, {
|
|
188
|
+
cwd,
|
|
189
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime" },
|
|
190
|
+
});
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
function requiredUltragoalGoals(plan: UltragoalPlan): UltragoalGoal[] {
|
|
@@ -565,8 +575,330 @@ function requireEmptyBlockers(value: unknown, fieldName: string): void {
|
|
|
565
575
|
throw new Error(`qualityGate ${fieldName} must be an empty blockers array`);
|
|
566
576
|
}
|
|
567
577
|
}
|
|
578
|
+
function requireQualityGateObject(value: unknown, fieldName: string): JsonObject {
|
|
579
|
+
const object = qualityGateObject(value);
|
|
580
|
+
if (!object) throw new Error(`qualityGate ${fieldName} must be an object`);
|
|
581
|
+
return object;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function requireObjectArray(value: unknown, fieldName: string): JsonObject[] {
|
|
585
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
586
|
+
throw new Error(`qualityGate ${fieldName} must be a non-empty object array`);
|
|
587
|
+
}
|
|
588
|
+
return value.map((item, index) => requireQualityGateObject(item, `${fieldName}[${index}]`));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function requiredStringField(row: JsonObject, key: string, fieldName: string): string {
|
|
592
|
+
const value = row[key];
|
|
593
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
594
|
+
throw new Error(`qualityGate ${fieldName}.${key} must be a non-empty string`);
|
|
595
|
+
}
|
|
596
|
+
return value.trim();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function optionalStatusField(row: JsonObject, fieldName: string): string | null {
|
|
600
|
+
if (row.status === undefined) return null;
|
|
601
|
+
const status = requiredStringField(row, "status", fieldName).toLowerCase();
|
|
602
|
+
if (status === "todo") throw new Error(`qualityGate ${fieldName}.status must not be todo`);
|
|
603
|
+
return status;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function requireProofStatus(status: string, fieldName: string): void {
|
|
607
|
+
if (!ACCEPTED_PROOF_STATUSES.has(status) && status !== NOT_APPLICABLE_STATUS) {
|
|
608
|
+
throw new Error(`qualityGate ${fieldName}.status must be covered, passed, verified, or not_applicable`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function requireSuccessStatus(status: string, fieldName: string): void {
|
|
612
|
+
requireProofStatus(status, fieldName);
|
|
613
|
+
if (status === NOT_APPLICABLE_STATUS) {
|
|
614
|
+
throw new Error(`qualityGate ${fieldName}.status must be covered, passed, or verified`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function rowOutcomeStatuses(row: JsonObject, fieldName: string): string[] {
|
|
619
|
+
const statuses: string[] = [];
|
|
620
|
+
const status = optionalStatusField(row, fieldName);
|
|
621
|
+
if (status) statuses.push(status);
|
|
622
|
+
const verdict = row.verdict;
|
|
623
|
+
if (typeof verdict === "string" && verdict.trim().length > 0) statuses.push(verdict.trim().toLowerCase());
|
|
624
|
+
const result = row.result;
|
|
625
|
+
if (typeof result === "string" && result.trim().length > 0) statuses.push(result.trim().toLowerCase());
|
|
626
|
+
if (statuses.length === 0) throw new Error(`qualityGate ${fieldName}.verdict must be a non-empty string`);
|
|
627
|
+
return statuses;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function requireSuccessfulRowOutcome(row: JsonObject, fieldName: string): void {
|
|
631
|
+
for (const status of rowOutcomeStatuses(row, fieldName)) {
|
|
632
|
+
requireSuccessStatus(status, fieldName);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function requireStringLinks(value: unknown, fieldName: string): string[] {
|
|
637
|
+
const strings = nonEmptyStringArray(value);
|
|
638
|
+
if (!strings) throw new Error(`qualityGate ${fieldName} must be a non-empty string array`);
|
|
639
|
+
return strings.map(item => item.trim());
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function optionalStringLinks(row: JsonObject, key: string, fieldName: string): string[] | null {
|
|
643
|
+
if (row[key] === undefined) return null;
|
|
644
|
+
return requireStringLinks(row[key], `${fieldName}.${key}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function buildRowIdMap(rows: JsonObject[], fieldName: string): Map<string, JsonObject> {
|
|
648
|
+
const ids = new Map<string, JsonObject>();
|
|
649
|
+
for (const [index, row] of rows.entries()) {
|
|
650
|
+
const id = requiredStringField(row, "id", `${fieldName}[${index}]`);
|
|
651
|
+
if (ids.has(id)) throw new Error(`qualityGate ${fieldName} contains duplicate id ${id}`);
|
|
652
|
+
ids.set(id, row);
|
|
653
|
+
}
|
|
654
|
+
return ids;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function requireResolvedLinks(ids: string[], map: Map<string, JsonObject>, fieldName: string): void {
|
|
658
|
+
for (const id of ids) {
|
|
659
|
+
if (!map.has(id)) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function successfulLinkedRows(ids: string[], map: Map<string, JsonObject>, fieldName: string): JsonObject[] {
|
|
663
|
+
const rows: JsonObject[] = [];
|
|
664
|
+
for (const id of ids) {
|
|
665
|
+
const row = map.get(id);
|
|
666
|
+
if (!row) throw new Error(`qualityGate ${fieldName} references unknown id ${id}`);
|
|
667
|
+
requireSuccessfulRowOutcome(row, `${fieldName}.${id}`);
|
|
668
|
+
rows.push(row);
|
|
669
|
+
}
|
|
670
|
+
return rows;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function normalizedEvidenceKind(row: JsonObject): string {
|
|
674
|
+
return requiredStringField(row, "kind", "executorQa.artifactRefs[]").toLowerCase().replaceAll("_", "-");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function evidenceKindMatches(kind: string, words: string[]): boolean {
|
|
678
|
+
return words.some(word => kind.includes(word));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function validateSurfaceArtifactCompatibility(
|
|
682
|
+
surface: string,
|
|
683
|
+
artifactIds: string[],
|
|
684
|
+
artifactRefs: Map<string, JsonObject>,
|
|
685
|
+
fieldName: string,
|
|
686
|
+
): void {
|
|
687
|
+
const normalizedSurface = surface.toLowerCase().replaceAll("_", "-");
|
|
688
|
+
const kinds = artifactIds.map(id => normalizedEvidenceKind(artifactRefs.get(id)!));
|
|
689
|
+
const isGuiOrWeb = ["gui", "web", "browser", "ui", "visual"].some(word => normalizedSurface.includes(word));
|
|
690
|
+
if (isGuiOrWeb) {
|
|
691
|
+
const hasBrowser = kinds.some(kind =>
|
|
692
|
+
evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation"]),
|
|
693
|
+
);
|
|
694
|
+
const hasVisual = kinds.some(kind => evidenceKindMatches(kind, ["screenshot", "image", "visual"]));
|
|
695
|
+
if (!hasBrowser || !hasVisual) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`qualityGate ${fieldName} for GUI/web surfaces must reference browser automation plus screenshot or image-verdict artifacts`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const surfaceFamilies: Array<{ surface: string[]; evidence: string[]; label: string }> = [
|
|
703
|
+
{
|
|
704
|
+
surface: ["cli", "terminal", "command"],
|
|
705
|
+
evidence: ["cli", "log", "transcript", "terminal", "command", "test-report"],
|
|
706
|
+
label: "CLI",
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
surface: ["api", "package", "library", "sdk"],
|
|
710
|
+
evidence: ["api", "package", "consumer", "black-box", "test-report"],
|
|
711
|
+
label: "API/package",
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
surface: ["algorithm", "math", "mathematical", "equation"],
|
|
715
|
+
evidence: ["property", "boundary", "edge", "adversarial", "failure", "math", "algorithm", "test-report"],
|
|
716
|
+
label: "algorithm/math",
|
|
717
|
+
},
|
|
718
|
+
];
|
|
719
|
+
for (const family of surfaceFamilies) {
|
|
720
|
+
if (family.surface.some(word => normalizedSurface.includes(word))) {
|
|
721
|
+
if (!kinds.some(kind => evidenceKindMatches(kind, family.evidence))) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`qualityGate ${fieldName} for ${family.label} surfaces must reference compatible artifact kinds`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function isSubstantiveEvidence(value: unknown): boolean {
|
|
732
|
+
if (typeof value !== "string") return false;
|
|
733
|
+
const trimmed = value.trim();
|
|
734
|
+
if (trimmed.length < MIN_SUBSTANTIVE_EVIDENCE_CHARS) return false;
|
|
735
|
+
const words = trimmed.split(/\s+/).filter(word => /[a-z0-9]/i.test(word));
|
|
736
|
+
if (words.length < MIN_SUBSTANTIVE_EVIDENCE_WORDS) return false;
|
|
737
|
+
const normalized = trimmed.toLowerCase();
|
|
738
|
+
return !["todo", "tbd", "n/a", "na", "none", "placeholder", "empty", "stub"].includes(normalized);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function hasTypedVerifiedReceipt(value: unknown): boolean {
|
|
742
|
+
const receipt = qualityGateObject(value);
|
|
743
|
+
if (!receipt) return false;
|
|
744
|
+
const type = nonEmptyString(receipt.type) ?? nonEmptyString(receipt.kind) ?? nonEmptyString(receipt.receiptType);
|
|
745
|
+
const id = nonEmptyString(receipt.id) ?? nonEmptyString(receipt.receiptId) ?? nonEmptyString(receipt.ref);
|
|
746
|
+
const status = (nonEmptyString(receipt.status) ?? nonEmptyString(receipt.verdict) ?? "").toLowerCase();
|
|
747
|
+
return Boolean(type && id && (status === "verified" || status === "passed"));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function hasExistingNonEmptyArtifact(cwd: string, value: unknown): Promise<boolean> {
|
|
751
|
+
const artifactPath = nonEmptyString(value);
|
|
752
|
+
if (!artifactPath) return false;
|
|
753
|
+
const resolved = path.resolve(cwd, artifactPath);
|
|
754
|
+
try {
|
|
755
|
+
const file = Bun.file(resolved);
|
|
756
|
+
return (await file.exists()) && file.size > 0;
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (isEnoent(error)) return false;
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function requireSubstantiveArtifactEvidence(cwd: string, row: JsonObject, fieldName: string): Promise<void> {
|
|
764
|
+
if (isSubstantiveEvidence(row.inlineEvidence) || isSubstantiveEvidence(row.evidence)) return;
|
|
765
|
+
if (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt)) return;
|
|
766
|
+
if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
|
|
767
|
+
throw new Error(
|
|
768
|
+
`qualityGate ${fieldName} must reference an existing non-empty artifact path, substantive inlineEvidence, or a typed verifiedReceipt`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function validateArtifactRefs(cwd: string, executorQa: JsonObject): Promise<Map<string, JsonObject>> {
|
|
773
|
+
const rows = requireObjectArray(executorQa.artifactRefs, "executorQa.artifactRefs");
|
|
774
|
+
const idMap = buildRowIdMap(rows, "executorQa.artifactRefs");
|
|
775
|
+
for (const [index, row] of rows.entries()) {
|
|
776
|
+
const fieldName = `executorQa.artifactRefs[${index}]`;
|
|
777
|
+
requiredStringField(row, "kind", fieldName);
|
|
778
|
+
requiredStringField(row, "description", fieldName);
|
|
779
|
+
await requireSubstantiveArtifactEvidence(cwd, row, fieldName);
|
|
780
|
+
}
|
|
781
|
+
return idMap;
|
|
782
|
+
}
|
|
568
783
|
|
|
569
|
-
function
|
|
784
|
+
function validateSurfaceEvidence(
|
|
785
|
+
executorQa: JsonObject,
|
|
786
|
+
artifactRefs: Map<string, JsonObject>,
|
|
787
|
+
): Map<string, JsonObject> {
|
|
788
|
+
const rows = requireObjectArray(executorQa.surfaceEvidence, "executorQa.surfaceEvidence");
|
|
789
|
+
const idMap = buildRowIdMap(rows, "executorQa.surfaceEvidence");
|
|
790
|
+
for (const [index, row] of rows.entries()) {
|
|
791
|
+
const fieldName = `executorQa.surfaceEvidence[${index}]`;
|
|
792
|
+
const status = optionalStatusField(row, fieldName);
|
|
793
|
+
requiredStringField(row, "contractRef", fieldName);
|
|
794
|
+
if (status === NOT_APPLICABLE_STATUS) {
|
|
795
|
+
requiredStringField(row, "reason", fieldName);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const surface = requiredStringField(row, "surface", fieldName);
|
|
799
|
+
requireSuccessfulRowOutcome(row, fieldName);
|
|
800
|
+
requiredStringField(row, "invocation", fieldName);
|
|
801
|
+
if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
|
|
802
|
+
requiredStringField(row, "result", fieldName);
|
|
803
|
+
}
|
|
804
|
+
const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
|
|
805
|
+
requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
|
|
806
|
+
validateSurfaceArtifactCompatibility(surface, artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
|
|
807
|
+
}
|
|
808
|
+
return idMap;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function validateAdversarialCases(
|
|
812
|
+
executorQa: JsonObject,
|
|
813
|
+
artifactRefs: Map<string, JsonObject>,
|
|
814
|
+
): Map<string, JsonObject> {
|
|
815
|
+
const rows = requireObjectArray(executorQa.adversarialCases, "executorQa.adversarialCases");
|
|
816
|
+
const idMap = buildRowIdMap(rows, "executorQa.adversarialCases");
|
|
817
|
+
for (const [index, row] of rows.entries()) {
|
|
818
|
+
const fieldName = `executorQa.adversarialCases[${index}]`;
|
|
819
|
+
const status = optionalStatusField(row, fieldName);
|
|
820
|
+
if (status === NOT_APPLICABLE_STATUS) {
|
|
821
|
+
throw new Error(`qualityGate ${fieldName}.status must not be not_applicable`);
|
|
822
|
+
}
|
|
823
|
+
requireSuccessfulRowOutcome(row, fieldName);
|
|
824
|
+
requiredStringField(row, "contractRef", fieldName);
|
|
825
|
+
requiredStringField(row, "scenario", fieldName);
|
|
826
|
+
requiredStringField(row, "expectedBehavior", fieldName);
|
|
827
|
+
if (typeof row.verdict !== "string" || row.verdict.trim().length === 0) {
|
|
828
|
+
requiredStringField(row, "result", fieldName);
|
|
829
|
+
}
|
|
830
|
+
const artifactIds = requireStringLinks(row.artifactRefs, `${fieldName}.artifactRefs`);
|
|
831
|
+
requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
|
|
832
|
+
}
|
|
833
|
+
return idMap;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function validateContractCoverage(
|
|
837
|
+
executorQa: JsonObject,
|
|
838
|
+
surfaceEvidence: Map<string, JsonObject>,
|
|
839
|
+
adversarialCases: Map<string, JsonObject>,
|
|
840
|
+
artifactRefs: Map<string, JsonObject>,
|
|
841
|
+
): void {
|
|
842
|
+
const rows = requireObjectArray(executorQa.contractCoverage, "executorQa.contractCoverage");
|
|
843
|
+
buildRowIdMap(rows, "executorQa.contractCoverage");
|
|
844
|
+
let hasSuccessfulContractCoverage = false;
|
|
845
|
+
for (const [index, row] of rows.entries()) {
|
|
846
|
+
const fieldName = `executorQa.contractCoverage[${index}]`;
|
|
847
|
+
requiredStringField(row, "contractRef", fieldName);
|
|
848
|
+
const status = optionalStatusField(row, fieldName);
|
|
849
|
+
if (status === NOT_APPLICABLE_STATUS) {
|
|
850
|
+
requiredStringField(row, "reason", fieldName);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
requiredStringField(row, "obligation", fieldName);
|
|
854
|
+
if (!status) throw new Error(`qualityGate ${fieldName}.status must be a non-empty string`);
|
|
855
|
+
requireSuccessStatus(status, fieldName);
|
|
856
|
+
hasSuccessfulContractCoverage = true;
|
|
857
|
+
const surfaceIds = optionalStringLinks(row, "surfaceEvidenceRefs", fieldName);
|
|
858
|
+
const adversarialIds = optionalStringLinks(row, "adversarialCaseRefs", fieldName);
|
|
859
|
+
const artifactIds = optionalStringLinks(row, "artifactRefs", fieldName);
|
|
860
|
+
if (!surfaceIds && !adversarialIds && !artifactIds) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`qualityGate ${fieldName} must link to surfaceEvidenceRefs, adversarialCaseRefs, or artifactRefs`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
let successfulProofLinks = 0;
|
|
866
|
+
if (surfaceIds)
|
|
867
|
+
successfulProofLinks += successfulLinkedRows(
|
|
868
|
+
surfaceIds,
|
|
869
|
+
surfaceEvidence,
|
|
870
|
+
`${fieldName}.surfaceEvidenceRefs`,
|
|
871
|
+
).length;
|
|
872
|
+
if (adversarialIds) {
|
|
873
|
+
successfulProofLinks += successfulLinkedRows(
|
|
874
|
+
adversarialIds,
|
|
875
|
+
adversarialCases,
|
|
876
|
+
`${fieldName}.adversarialCaseRefs`,
|
|
877
|
+
).length;
|
|
878
|
+
}
|
|
879
|
+
if (artifactIds) {
|
|
880
|
+
requireResolvedLinks(artifactIds, artifactRefs, `${fieldName}.artifactRefs`);
|
|
881
|
+
successfulProofLinks += artifactIds.length;
|
|
882
|
+
}
|
|
883
|
+
if (successfulProofLinks === 0) {
|
|
884
|
+
throw new Error(`qualityGate ${fieldName} must link to at least one successful proof row or artifact`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!hasSuccessfulContractCoverage) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
"qualityGate executorQa.contractCoverage must include at least one row with status covered, passed, or verified",
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function validateExecutorQaRedTeamEvidence(cwd: string, executorQa: JsonObject): Promise<void> {
|
|
895
|
+
const artifactRefs = await validateArtifactRefs(cwd, executorQa);
|
|
896
|
+
const surfaceEvidence = validateSurfaceEvidence(executorQa, artifactRefs);
|
|
897
|
+
const adversarialCases = validateAdversarialCases(executorQa, artifactRefs);
|
|
898
|
+
validateContractCoverage(executorQa, surfaceEvidence, adversarialCases, artifactRefs);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function validateCompletionQualityGate(cwd: string, gate: JsonObject): Promise<void> {
|
|
570
902
|
const codeReview = qualityGateObject(gate.codeReview);
|
|
571
903
|
if (codeReview) {
|
|
572
904
|
throw new Error(
|
|
@@ -611,6 +943,7 @@ function validateCompletionQualityGate(gate: JsonObject): void {
|
|
|
611
943
|
}
|
|
612
944
|
requireNonEmptyString(executorQa.evidence, "executorQa.evidence");
|
|
613
945
|
requireEmptyBlockers(executorQa.blockers, "executorQa.blockers");
|
|
946
|
+
await validateExecutorQaRedTeamEvidence(cwd, executorQa);
|
|
614
947
|
if (iteration.status !== PASSED_STATUS || iteration.fullRerun !== true) {
|
|
615
948
|
throw new Error("qualityGate iteration must be passed with fullRerun true");
|
|
616
949
|
}
|
|
@@ -630,7 +963,7 @@ async function readRequiredCompletionQualityGate(cwd: string, value: string | un
|
|
|
630
963
|
const gate = await readStructuredValue(cwd, value);
|
|
631
964
|
const gateObject = qualityGateObject(gate);
|
|
632
965
|
if (!gateObject) throw new Error("qualityGate must be a JSON object");
|
|
633
|
-
validateCompletionQualityGate(gateObject);
|
|
966
|
+
await validateCompletionQualityGate(cwd, gateObject);
|
|
634
967
|
return gate;
|
|
635
968
|
}
|
|
636
969
|
|
|
@@ -896,11 +1229,7 @@ async function readBrief(cwd: string, args: readonly string[]): Promise<string>
|
|
|
896
1229
|
|
|
897
1230
|
function renderStatus(summary: UltragoalStatusSummary, json: boolean): string {
|
|
898
1231
|
if (json) return `${JSON.stringify(summary, null, 2)}\n`;
|
|
899
|
-
|
|
900
|
-
return `No ultragoal plan found at ${summary.paths.goalsPath}. Run \`gjc ultragoal create-goals --brief "..."\` first.\n`;
|
|
901
|
-
}
|
|
902
|
-
const current = summary.currentGoal ? ` Current: ${summary.currentGoal.id} (${summary.currentGoal.status}).` : "";
|
|
903
|
-
return `Ultragoal ${summary.status}: ${summary.counts.complete}/${summary.goals.length} complete.${current}\n`;
|
|
1232
|
+
return renderUltragoalStatusMarkdown(summary);
|
|
904
1233
|
}
|
|
905
1234
|
|
|
906
1235
|
function renderCompleteHandoff(
|
|
@@ -914,8 +1243,8 @@ function renderCompleteHandoff(
|
|
|
914
1243
|
`Ultragoal handoff: ${result.goal.id} — ${result.goal.title}`,
|
|
915
1244
|
`Objective: ${result.goal.objective}`,
|
|
916
1245
|
`GJC objective: ${result.plan.gjcObjective}`,
|
|
917
|
-
'Call goal({"op":"get"}); call goal({"op":"create","objective":"<printed objective>"}) only if no active GJC goal exists, then
|
|
918
|
-
|
|
1246
|
+
'Call goal({"op":"get"}); call goal({"op":"create","objective":"<printed objective>"}) only if no active GJC goal exists, then keep the GJC goal active while this Ultragoal story is verified and checkpointed.',
|
|
1247
|
+
'Before checkpointing complete, obtain a passing architectReview (architecture/product/code CLEAR + APPROVE) and executorQa (e2e/red-team passed with contractCoverage, surfaceEvidence, adversarialCases, and artifactRefs matrix evidence), then checkpoint with --quality-gate-json and a fresh active goal snapshot; record blockers instead of completing on any finding, plan/code mismatch, shallow evidence, or missing artifact link; call goal({"op":"complete"}) only after the final aggregate receipt exists.',
|
|
919
1248
|
"",
|
|
920
1249
|
].join("\n");
|
|
921
1250
|
}
|