@kaddo/cli 3.73.0 → 3.74.1
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.
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>admin</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-AAaxSvT4.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BPt-9--k.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
getWorkItemForEdit as coreGetWorkItemForEdit,
|
|
67
67
|
validateWorkItem as coreValidateWorkItem,
|
|
68
68
|
transitionWorkItem as coreTransitionWorkItem,
|
|
69
|
+
getWorkItemCaptureDefinition as coreGetCaptureDefinition,
|
|
69
70
|
WorkItemWriteError,
|
|
70
71
|
exists,
|
|
71
72
|
join,
|
|
@@ -129,9 +130,12 @@ function mapWriteError(err) {
|
|
|
129
130
|
if (err instanceof WorkItemWriteError) throw new CoreError(err.code, err.message);
|
|
130
131
|
throw err;
|
|
131
132
|
}
|
|
133
|
+
function getCaptureDefinition() {
|
|
134
|
+
return coreGetCaptureDefinition();
|
|
135
|
+
}
|
|
132
136
|
function createWorkItemAdmin(dir, body) {
|
|
133
137
|
try {
|
|
134
|
-
const res = coreCreateWorkItem(dir, { intent: body.intent, type: body.type });
|
|
138
|
+
const res = coreCreateWorkItem(dir, { intent: body.intent, type: body.type, answers: body.answers });
|
|
135
139
|
return { id: res.id, path: res.path, revision: res.revision, status: "draft" };
|
|
136
140
|
} catch (err) {
|
|
137
141
|
mapWriteError(err);
|
|
@@ -553,6 +557,19 @@ var WorkItemWriteResultSchema = z.object({
|
|
|
553
557
|
revision: z.string(),
|
|
554
558
|
status: z.string().optional()
|
|
555
559
|
});
|
|
560
|
+
var WorkItemCreateWithAnswersSchema = z.object({
|
|
561
|
+
intent: z.string().min(1),
|
|
562
|
+
type: z.string().min(1),
|
|
563
|
+
answers: z.record(z.string(), z.string()).optional()
|
|
564
|
+
});
|
|
565
|
+
var RefinementFeedbackSchema = z.object({
|
|
566
|
+
refinementId: z.string().min(1),
|
|
567
|
+
feedback: z.string().min(1)
|
|
568
|
+
});
|
|
569
|
+
var RefinementApplySchema = z.object({
|
|
570
|
+
refinementId: z.string().min(1),
|
|
571
|
+
expectedRevision: z.string().min(1)
|
|
572
|
+
});
|
|
556
573
|
var ErrorResponseSchema = z.object({
|
|
557
574
|
error: z.object({
|
|
558
575
|
code: z.string(),
|
|
@@ -560,11 +577,303 @@ var ErrorResponseSchema = z.object({
|
|
|
560
577
|
})
|
|
561
578
|
});
|
|
562
579
|
|
|
580
|
+
// src/refinement/service.ts
|
|
581
|
+
import crypto from "crypto";
|
|
582
|
+
import {
|
|
583
|
+
assembleRefinementContext,
|
|
584
|
+
getWorkItemAgentAssets,
|
|
585
|
+
normalizeAndValidateProposal,
|
|
586
|
+
applyRefinement,
|
|
587
|
+
WorkItemWriteError as WorkItemWriteError2
|
|
588
|
+
} from "@kaddo/cli/core";
|
|
589
|
+
|
|
590
|
+
// src/refinement/provider.ts
|
|
591
|
+
var RefinementProviderError = class extends Error {
|
|
592
|
+
code;
|
|
593
|
+
constructor(code, message) {
|
|
594
|
+
super(message);
|
|
595
|
+
this.name = "RefinementProviderError";
|
|
596
|
+
this.code = code;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/refinement/service.ts
|
|
601
|
+
var MAX_REPAIR_ATTEMPTS = 2;
|
|
602
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
603
|
+
var RefinementService = class {
|
|
604
|
+
constructor(provider, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
605
|
+
this.provider = provider;
|
|
606
|
+
this.timeoutMs = timeoutMs;
|
|
607
|
+
}
|
|
608
|
+
provider;
|
|
609
|
+
timeoutMs;
|
|
610
|
+
sessions = /* @__PURE__ */ new Map();
|
|
611
|
+
view(s) {
|
|
612
|
+
return { ...s };
|
|
613
|
+
}
|
|
614
|
+
async run(dir, workItemId, feedback, previous) {
|
|
615
|
+
const context = assembleRefinementContext(dir, workItemId);
|
|
616
|
+
if (context.workItem.status !== "draft") {
|
|
617
|
+
throw new RefinementProviderError("WORK_ITEM_NOT_EDITABLE", `A ${context.workItem.status} Work Item cannot be refined. Reopen it as Draft first.`);
|
|
618
|
+
}
|
|
619
|
+
const assets = getWorkItemAgentAssets();
|
|
620
|
+
const request = { context, assets, intent: context.workItem.intent, previousProposal: previous, feedback };
|
|
621
|
+
let lastErr;
|
|
622
|
+
for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
|
|
623
|
+
const ac = new AbortController();
|
|
624
|
+
const timer = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
625
|
+
try {
|
|
626
|
+
const result = await this.provider.refine(request, ac.signal);
|
|
627
|
+
const { validation } = normalizeAndValidateProposal(dir, workItemId, result.proposal);
|
|
628
|
+
const contextUsed = context.knowledge.map((k) => ({ id: k.id, title: k.title, layer: k.layer }));
|
|
629
|
+
return { context, proposal: result.proposal, validation, contextUsed, meta: { ...result.meta, repairAttempts: attempt } };
|
|
630
|
+
} catch (err) {
|
|
631
|
+
lastErr = err;
|
|
632
|
+
if (!(err instanceof RefinementProviderError && err.code === "INVALID_RESPONSE")) break;
|
|
633
|
+
} finally {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw lastErr instanceof Error ? lastErr : new RefinementProviderError("PROVIDER_ERROR", "Refinement failed.");
|
|
638
|
+
}
|
|
639
|
+
async start(dir, workItemId) {
|
|
640
|
+
const { context, proposal, validation, contextUsed, meta } = await this.run(dir, workItemId);
|
|
641
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
642
|
+
const session = {
|
|
643
|
+
refinementId: `ref_${crypto.randomBytes(8).toString("hex")}`,
|
|
644
|
+
workItemId,
|
|
645
|
+
sourceRevision: context.revision,
|
|
646
|
+
status: "ready-for-review",
|
|
647
|
+
intent: context.workItem.intent,
|
|
648
|
+
proposal,
|
|
649
|
+
validation,
|
|
650
|
+
contextUsed,
|
|
651
|
+
meta,
|
|
652
|
+
createdAt: now,
|
|
653
|
+
updatedAt: now
|
|
654
|
+
};
|
|
655
|
+
this.sessions.set(session.refinementId, session);
|
|
656
|
+
return this.view(session);
|
|
657
|
+
}
|
|
658
|
+
async feedback(dir, workItemId, refinementId, feedback) {
|
|
659
|
+
const session = this.get(refinementId, workItemId);
|
|
660
|
+
const { context, proposal, validation, contextUsed, meta } = await this.run(dir, workItemId, feedback, session.proposal);
|
|
661
|
+
session.sourceRevision = context.revision;
|
|
662
|
+
session.status = "ready-for-review";
|
|
663
|
+
session.proposal = proposal;
|
|
664
|
+
session.validation = validation;
|
|
665
|
+
session.contextUsed = contextUsed;
|
|
666
|
+
session.meta = meta;
|
|
667
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
668
|
+
return this.view(session);
|
|
669
|
+
}
|
|
670
|
+
apply(dir, workItemId, refinementId, expectedRevision) {
|
|
671
|
+
const session = this.get(refinementId, workItemId);
|
|
672
|
+
const current = assembleRefinementContext(dir, workItemId);
|
|
673
|
+
if (current.revision !== session.sourceRevision || expectedRevision !== session.sourceRevision) {
|
|
674
|
+
session.status = "stale";
|
|
675
|
+
throw new RefinementProviderError("WORK_ITEM_CONFLICT", "This Work Item changed while the refinement was running. The proposal has not been applied.");
|
|
676
|
+
}
|
|
677
|
+
let res;
|
|
678
|
+
try {
|
|
679
|
+
res = applyRefinement(dir, workItemId, session.proposal, session.sourceRevision);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (err instanceof WorkItemWriteError2) throw new RefinementProviderError(err.code, err.message);
|
|
682
|
+
throw err;
|
|
683
|
+
}
|
|
684
|
+
session.status = "applied";
|
|
685
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
+
return { id: workItemId, path: res.path, revision: res.revision, status: "draft" };
|
|
687
|
+
}
|
|
688
|
+
get(refinementId, workItemId) {
|
|
689
|
+
const s = this.sessions.get(refinementId);
|
|
690
|
+
if (!s || s.workItemId !== workItemId) throw new RefinementProviderError("REFINEMENT_NOT_FOUND", "Refinement session not found.");
|
|
691
|
+
return s;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/refinement/heuristic-provider.ts
|
|
696
|
+
function firstSentence(s) {
|
|
697
|
+
const m = s.trim().match(/^(.*?[.!?])(\s|$)/);
|
|
698
|
+
return (m ? m[1] : s.trim()).trim();
|
|
699
|
+
}
|
|
700
|
+
function mentionsNegation(feedback, module) {
|
|
701
|
+
const re = new RegExp(`(not|no)\\b[^.]*\\b${module}\\b|\\b${module}\\b[^.]*(not affected|no afecta|reviewed)`, "i");
|
|
702
|
+
return re.test(feedback);
|
|
703
|
+
}
|
|
704
|
+
var HeuristicRefinementProvider = class {
|
|
705
|
+
name = "heuristic";
|
|
706
|
+
async refine(request) {
|
|
707
|
+
const start = Date.now();
|
|
708
|
+
const { context, intent, previousProposal, feedback } = request;
|
|
709
|
+
const modules = context.modules;
|
|
710
|
+
const affected = new Set(previousProposal?.affectedModules ?? ["core"]);
|
|
711
|
+
if (feedback) {
|
|
712
|
+
for (const m of modules) {
|
|
713
|
+
if (new RegExp(`\\b${m}\\b`, "i").test(feedback)) {
|
|
714
|
+
if (mentionsNegation(feedback, m)) affected.delete(m);
|
|
715
|
+
else affected.add(m);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const affectedModules = modules.filter((m) => affected.has(m));
|
|
720
|
+
const moduleCoverage = modules.map(
|
|
721
|
+
(m) => affected.has(m) ? { id: m, status: "affected", reason: m === "core" ? "Backend behavior changes." : "User-facing change identified." } : { id: m, status: "unknown" }
|
|
722
|
+
);
|
|
723
|
+
const frontendAffected = affected.has("frontend");
|
|
724
|
+
const impactAnalysis = [
|
|
725
|
+
{ surface: "backend", status: "affected" },
|
|
726
|
+
{ surface: "frontend", status: frontendAffected ? "affected" : "unknown", ...frontendAffected ? {} : { question: "Is a user-facing surface involved?" } },
|
|
727
|
+
{ surface: "database", status: "reviewed-not-affected" },
|
|
728
|
+
{ surface: "feature-flags", status: "unknown", question: "Is this behavior controlled by a feature flag?" }
|
|
729
|
+
];
|
|
730
|
+
const summary = firstSentence(intent);
|
|
731
|
+
const proposal = {
|
|
732
|
+
outcome: {
|
|
733
|
+
actor: "User",
|
|
734
|
+
observableOutcome: summary,
|
|
735
|
+
currentBehavior: `Today: ${summary.toLowerCase()} is not yet supported as described.`,
|
|
736
|
+
targetBehavior: summary
|
|
737
|
+
},
|
|
738
|
+
journey: {
|
|
739
|
+
entryPoints: [frontendAffected ? "Public entry point" : "Application entry point"],
|
|
740
|
+
flow: ["Entry point", "Application logic", "Persistence", "Result"]
|
|
741
|
+
},
|
|
742
|
+
affectedModules,
|
|
743
|
+
moduleCoverage,
|
|
744
|
+
impactAnalysis,
|
|
745
|
+
scopeConfidence: {
|
|
746
|
+
level: "medium",
|
|
747
|
+
reasons: ["Primary behavior identified from the intent.", "Feature flag ownership not yet confirmed."]
|
|
748
|
+
},
|
|
749
|
+
scopeUnknowns: ["Is this behavior controlled by a feature flag?"],
|
|
750
|
+
acceptanceCriteria: [
|
|
751
|
+
`${summary}`,
|
|
752
|
+
"The change is covered by the affected modules above."
|
|
753
|
+
],
|
|
754
|
+
linkedDecisions: [],
|
|
755
|
+
relatedKnowledge: []
|
|
756
|
+
};
|
|
757
|
+
return { proposal, meta: { provider: this.name, durationMs: Date.now() - start, repairAttempts: 0 } };
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/refinement/anthropic-provider.ts
|
|
762
|
+
var API_URL = "https://api.anthropic.com/v1/messages";
|
|
763
|
+
var SCHEMA_HINT = `Return ONLY a JSON object (no prose, no code fences) with this shape \u2014 omit fields you cannot determine:
|
|
764
|
+
{
|
|
765
|
+
"title"?: string,
|
|
766
|
+
"outcome"?: { "actor"?: string, "observableOutcome"?: string, "currentBehavior"?: string, "targetBehavior"?: string },
|
|
767
|
+
"journey"?: { "entryPoints"?: string[], "flow"?: string[] },
|
|
768
|
+
"affectedModules"?: string[],
|
|
769
|
+
"moduleCoverage"?: [{ "id": string, "status": "affected"|"reviewed-not-affected"|"unknown"|"not-applicable", "reason"?: string }],
|
|
770
|
+
"impactAnalysis"?: [{ "surface": string, "status": "affected"|"reviewed-not-affected"|"unknown"|"not-applicable", "reason"?: string, "question"?: string }],
|
|
771
|
+
"scopeConfidence"?: { "level": "high"|"medium"|"low", "reasons"?: string[] },
|
|
772
|
+
"scopeUnknowns"?: string[],
|
|
773
|
+
"acceptanceCriteria"?: string[],
|
|
774
|
+
"linkedDecisions"?: string[],
|
|
775
|
+
"relatedKnowledge"?: string[]
|
|
776
|
+
}
|
|
777
|
+
Only reference module ids, decision ids and knowledge ids that appear in the provided context. Prefer "unknown" over inventing facts.`;
|
|
778
|
+
function extractJson(text) {
|
|
779
|
+
const start = text.indexOf("{");
|
|
780
|
+
const end = text.lastIndexOf("}");
|
|
781
|
+
if (start < 0 || end <= start) throw new RefinementProviderError("INVALID_RESPONSE", "The model did not return a JSON proposal.");
|
|
782
|
+
try {
|
|
783
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
784
|
+
} catch {
|
|
785
|
+
throw new RefinementProviderError("INVALID_RESPONSE", "The model returned a proposal that could not be parsed.");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
var AnthropicRefinementProvider = class {
|
|
789
|
+
name = "anthropic";
|
|
790
|
+
apiKey;
|
|
791
|
+
model;
|
|
792
|
+
constructor(apiKey, model) {
|
|
793
|
+
this.apiKey = apiKey;
|
|
794
|
+
this.model = model;
|
|
795
|
+
}
|
|
796
|
+
async refine(request, signal) {
|
|
797
|
+
const start = Date.now();
|
|
798
|
+
const { context, assets, intent, previousProposal, feedback } = request;
|
|
799
|
+
const system = [
|
|
800
|
+
assets.agentPrompt,
|
|
801
|
+
assets.skill ?? "",
|
|
802
|
+
"# Output format",
|
|
803
|
+
SCHEMA_HINT
|
|
804
|
+
].filter(Boolean).join("\n\n");
|
|
805
|
+
const userParts = [
|
|
806
|
+
`# Work Item intent
|
|
807
|
+
${intent}`,
|
|
808
|
+
`# Project
|
|
809
|
+
${JSON.stringify(context.project)}`,
|
|
810
|
+
`# Registered modules
|
|
811
|
+
${context.modules.join(", ")}`,
|
|
812
|
+
`# Known decisions
|
|
813
|
+
${context.decisions.map((d) => `${d.id} \u2014 ${d.title}`).join("\n") || "(none)"}`,
|
|
814
|
+
`# Knowledge
|
|
815
|
+
${context.knowledge.map((k) => `${k.id} \u2014 ${k.title} (${k.layer})`).join("\n") || "(none)"}`,
|
|
816
|
+
`# Current Work Item model
|
|
817
|
+
${JSON.stringify(context.workItem.current)}`
|
|
818
|
+
];
|
|
819
|
+
if (previousProposal) userParts.push(`# Previous proposal
|
|
820
|
+
${JSON.stringify(previousProposal)}`);
|
|
821
|
+
if (feedback) userParts.push(`# Human feedback (augments the original intent, does not replace it)
|
|
822
|
+
${feedback}`);
|
|
823
|
+
let res;
|
|
824
|
+
try {
|
|
825
|
+
res = await fetch(API_URL, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
signal,
|
|
828
|
+
headers: {
|
|
829
|
+
"content-type": "application/json",
|
|
830
|
+
"x-api-key": this.apiKey,
|
|
831
|
+
"anthropic-version": "2023-06-01"
|
|
832
|
+
},
|
|
833
|
+
body: JSON.stringify({
|
|
834
|
+
model: this.model,
|
|
835
|
+
max_tokens: 2048,
|
|
836
|
+
system,
|
|
837
|
+
messages: [{ role: "user", content: userParts.join("\n\n") }]
|
|
838
|
+
})
|
|
839
|
+
});
|
|
840
|
+
} catch (err) {
|
|
841
|
+
if (err.name === "AbortError") throw new RefinementProviderError("TIMEOUT", "The refinement timed out.");
|
|
842
|
+
throw new RefinementProviderError("PROVIDER_ERROR", "The refinement provider could not be reached.");
|
|
843
|
+
}
|
|
844
|
+
if (!res.ok) {
|
|
845
|
+
throw new RefinementProviderError("PROVIDER_ERROR", `The refinement provider returned an error (${res.status}).`);
|
|
846
|
+
}
|
|
847
|
+
const body = await res.json();
|
|
848
|
+
const text = (body.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("");
|
|
849
|
+
const proposal = extractJson(text);
|
|
850
|
+
return {
|
|
851
|
+
proposal,
|
|
852
|
+
meta: {
|
|
853
|
+
provider: this.name,
|
|
854
|
+
model: this.model,
|
|
855
|
+
durationMs: Date.now() - start,
|
|
856
|
+
inputTokens: body.usage?.input_tokens,
|
|
857
|
+
outputTokens: body.usage?.output_tokens
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// src/refinement/index.ts
|
|
864
|
+
function createRefinementService() {
|
|
865
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
866
|
+
const model = process.env.KADDO_REFINEMENT_MODEL || "claude-3-5-sonnet-latest";
|
|
867
|
+
const provider = key ? new AnthropicRefinementProvider(key, model) : new HeuristicRefinementProvider();
|
|
868
|
+
return new RefinementService(provider);
|
|
869
|
+
}
|
|
870
|
+
|
|
563
871
|
// src/server.ts
|
|
564
872
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
565
873
|
function statusForCode(code) {
|
|
566
874
|
switch (code) {
|
|
567
875
|
case "WORK_ITEM_NOT_FOUND":
|
|
876
|
+
case "REFINEMENT_NOT_FOUND":
|
|
568
877
|
return 404;
|
|
569
878
|
case "WORK_ITEM_CONFLICT":
|
|
570
879
|
case "WORK_ITEM_NOT_EDITABLE":
|
|
@@ -573,6 +882,11 @@ function statusForCode(code) {
|
|
|
573
882
|
case "INVALID_WORK_ITEM_ID":
|
|
574
883
|
case "INVALID_TRANSITION":
|
|
575
884
|
return 400;
|
|
885
|
+
case "TIMEOUT":
|
|
886
|
+
return 504;
|
|
887
|
+
case "PROVIDER_ERROR":
|
|
888
|
+
case "INVALID_RESPONSE":
|
|
889
|
+
return 502;
|
|
576
890
|
default:
|
|
577
891
|
return 500;
|
|
578
892
|
}
|
|
@@ -581,6 +895,7 @@ async function createAdminServer(opts) {
|
|
|
581
895
|
const { projectDir, storage, staticDir, host = "127.0.0.1", port = 4173 } = opts;
|
|
582
896
|
const app = Fastify({ logger: false });
|
|
583
897
|
const sessionManager = new SessionManager(storage);
|
|
898
|
+
const refinement = createRefinementService();
|
|
584
899
|
await app.register(fastifyCookie);
|
|
585
900
|
await app.register(fastifyCors, {
|
|
586
901
|
origin: `http://${host}:${port}`,
|
|
@@ -661,8 +976,9 @@ async function createAdminServer(opts) {
|
|
|
661
976
|
throw err;
|
|
662
977
|
}
|
|
663
978
|
};
|
|
979
|
+
app.get("/api/v1/admin/work-items-capture", coreRoute(() => getCaptureDefinition()));
|
|
664
980
|
app.post("/api/v1/admin/work-items", async (request, reply) => {
|
|
665
|
-
const parsed =
|
|
981
|
+
const parsed = WorkItemCreateWithAnswersSchema.safeParse(request.body);
|
|
666
982
|
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "Intent and type are required." } });
|
|
667
983
|
return writeHandler(reply, () => createWorkItemAdmin(projectDir, parsed.data));
|
|
668
984
|
});
|
|
@@ -687,6 +1003,29 @@ async function createAdminServer(opts) {
|
|
|
687
1003
|
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "expectedRevision is required." } });
|
|
688
1004
|
return writeHandler(reply, () => transitionWorkItemAdmin(projectDir, request.params.workItemId, "draft", parsed.data.expectedRevision));
|
|
689
1005
|
});
|
|
1006
|
+
const refineHandler = async (reply, fn) => {
|
|
1007
|
+
try {
|
|
1008
|
+
return await fn();
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
if (err instanceof RefinementProviderError || err instanceof CoreError) {
|
|
1011
|
+
return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
1012
|
+
}
|
|
1013
|
+
throw err;
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
app.post("/api/v1/admin/work-items/:workItemId/refinement", async (request, reply) => {
|
|
1017
|
+
return refineHandler(reply, () => refinement.start(projectDir, request.params.workItemId));
|
|
1018
|
+
});
|
|
1019
|
+
app.post("/api/v1/admin/work-items/:workItemId/refinement/feedback", async (request, reply) => {
|
|
1020
|
+
const parsed = RefinementFeedbackSchema.safeParse(request.body);
|
|
1021
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "refinementId and feedback are required." } });
|
|
1022
|
+
return refineHandler(reply, () => refinement.feedback(projectDir, request.params.workItemId, parsed.data.refinementId, parsed.data.feedback));
|
|
1023
|
+
});
|
|
1024
|
+
app.post("/api/v1/admin/work-items/:workItemId/refinement/apply", async (request, reply) => {
|
|
1025
|
+
const parsed = RefinementApplySchema.safeParse(request.body);
|
|
1026
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "refinementId and expectedRevision are required." } });
|
|
1027
|
+
return refineHandler(reply, () => refinement.apply(projectDir, request.params.workItemId, parsed.data.refinementId, parsed.data.expectedRevision));
|
|
1028
|
+
});
|
|
690
1029
|
app.get("/api/v1/admin/work-items/:workItemId", async (request, reply) => {
|
|
691
1030
|
try {
|
|
692
1031
|
return getWorkItemDetail(projectDir, request.params.workItemId);
|
|
@@ -853,11 +1192,14 @@ export {
|
|
|
853
1192
|
ProjectReadinessSchema,
|
|
854
1193
|
ProjectRouteSchema,
|
|
855
1194
|
ProjectSummarySchema,
|
|
1195
|
+
RefinementApplySchema,
|
|
1196
|
+
RefinementFeedbackSchema,
|
|
856
1197
|
RouteStepSchema,
|
|
857
1198
|
SQLiteAdminStorage,
|
|
858
1199
|
SessionManager,
|
|
859
1200
|
ValidationResultSchema,
|
|
860
1201
|
WorkItemCreateSchema,
|
|
1202
|
+
WorkItemCreateWithAnswersSchema,
|
|
861
1203
|
WorkItemDetailSchema,
|
|
862
1204
|
WorkItemEditModelSchema,
|
|
863
1205
|
WorkItemInputSchema,
|