@kaddo/cli 3.74.1 → 3.75.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.
|
@@ -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-BpOMKXrf.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BPt-9--k.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
validateWorkItem as coreValidateWorkItem,
|
|
68
68
|
transitionWorkItem as coreTransitionWorkItem,
|
|
69
69
|
getWorkItemCaptureDefinition as coreGetCaptureDefinition,
|
|
70
|
+
buildRefinementHandoff as coreBuildRefinementHandoff,
|
|
70
71
|
WorkItemWriteError,
|
|
71
72
|
exists,
|
|
72
73
|
join,
|
|
@@ -128,11 +129,20 @@ function assertValidWorkItemId(workItemId) {
|
|
|
128
129
|
}
|
|
129
130
|
function mapWriteError(err) {
|
|
130
131
|
if (err instanceof WorkItemWriteError) throw new CoreError(err.code, err.message);
|
|
132
|
+
if (err instanceof WorkItemNotFoundError) throw new CoreError("WORK_ITEM_NOT_FOUND", "This Work Item does not exist in the current project.");
|
|
131
133
|
throw err;
|
|
132
134
|
}
|
|
133
135
|
function getCaptureDefinition() {
|
|
134
136
|
return coreGetCaptureDefinition();
|
|
135
137
|
}
|
|
138
|
+
function getRefinementHandoff(dir, workItemId) {
|
|
139
|
+
assertValidWorkItemId(workItemId);
|
|
140
|
+
try {
|
|
141
|
+
return coreBuildRefinementHandoff(dir, workItemId);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
mapWriteError(err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
136
146
|
function createWorkItemAdmin(dir, body) {
|
|
137
147
|
try {
|
|
138
148
|
const res = coreCreateWorkItem(dir, { intent: body.intent, type: body.type, answers: body.answers });
|
|
@@ -490,6 +500,7 @@ var LinkedDecisionSchema = z.object({
|
|
|
490
500
|
});
|
|
491
501
|
var LinkedKnowledgeSchema = z.object({ id: z.string(), title: z.string(), layer: z.string() });
|
|
492
502
|
var WorkItemDetailSchema = WorkItemListItemSchema.extend({
|
|
503
|
+
summary: z.string().nullable(),
|
|
493
504
|
actor: z.string().nullable(),
|
|
494
505
|
outcome: z.string().nullable(),
|
|
495
506
|
currentBehavior: z.string().nullable(),
|
|
@@ -507,7 +518,11 @@ var WorkItemDetailSchema = WorkItemListItemSchema.extend({
|
|
|
507
518
|
decisions: z.array(LinkedDecisionSchema),
|
|
508
519
|
relatedKnowledge: z.array(LinkedKnowledgeSchema),
|
|
509
520
|
source: z.object({ type: z.string(), id: z.string().optional(), inferred: z.boolean() }).passthrough(),
|
|
510
|
-
path: z.string()
|
|
521
|
+
path: z.string(),
|
|
522
|
+
refinement: z.object({
|
|
523
|
+
status: z.enum(["needs-refinement", "refined"]),
|
|
524
|
+
aspects: z.object({ outcome: z.boolean(), journey: z.boolean(), modules: z.boolean(), impact: z.boolean(), acceptance: z.boolean() })
|
|
525
|
+
})
|
|
511
526
|
});
|
|
512
527
|
var WorkItemInputSchema = z.object({
|
|
513
528
|
title: z.string(),
|
|
@@ -562,14 +577,6 @@ var WorkItemCreateWithAnswersSchema = z.object({
|
|
|
562
577
|
type: z.string().min(1),
|
|
563
578
|
answers: z.record(z.string(), z.string()).optional()
|
|
564
579
|
});
|
|
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
|
-
});
|
|
573
580
|
var ErrorResponseSchema = z.object({
|
|
574
581
|
error: z.object({
|
|
575
582
|
code: z.string(),
|
|
@@ -577,303 +584,11 @@ var ErrorResponseSchema = z.object({
|
|
|
577
584
|
})
|
|
578
585
|
});
|
|
579
586
|
|
|
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
|
-
|
|
871
587
|
// src/server.ts
|
|
872
588
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
873
589
|
function statusForCode(code) {
|
|
874
590
|
switch (code) {
|
|
875
591
|
case "WORK_ITEM_NOT_FOUND":
|
|
876
|
-
case "REFINEMENT_NOT_FOUND":
|
|
877
592
|
return 404;
|
|
878
593
|
case "WORK_ITEM_CONFLICT":
|
|
879
594
|
case "WORK_ITEM_NOT_EDITABLE":
|
|
@@ -882,11 +597,6 @@ function statusForCode(code) {
|
|
|
882
597
|
case "INVALID_WORK_ITEM_ID":
|
|
883
598
|
case "INVALID_TRANSITION":
|
|
884
599
|
return 400;
|
|
885
|
-
case "TIMEOUT":
|
|
886
|
-
return 504;
|
|
887
|
-
case "PROVIDER_ERROR":
|
|
888
|
-
case "INVALID_RESPONSE":
|
|
889
|
-
return 502;
|
|
890
600
|
default:
|
|
891
601
|
return 500;
|
|
892
602
|
}
|
|
@@ -895,7 +605,6 @@ async function createAdminServer(opts) {
|
|
|
895
605
|
const { projectDir, storage, staticDir, host = "127.0.0.1", port = 4173 } = opts;
|
|
896
606
|
const app = Fastify({ logger: false });
|
|
897
607
|
const sessionManager = new SessionManager(storage);
|
|
898
|
-
const refinement = createRefinementService();
|
|
899
608
|
await app.register(fastifyCookie);
|
|
900
609
|
await app.register(fastifyCors, {
|
|
901
610
|
origin: `http://${host}:${port}`,
|
|
@@ -1003,28 +712,15 @@ async function createAdminServer(opts) {
|
|
|
1003
712
|
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "expectedRevision is required." } });
|
|
1004
713
|
return writeHandler(reply, () => transitionWorkItemAdmin(projectDir, request.params.workItemId, "draft", parsed.data.expectedRevision));
|
|
1005
714
|
});
|
|
1006
|
-
|
|
715
|
+
app.get("/api/v1/admin/work-items/:workItemId/refinement-handoff", async (request, reply) => {
|
|
1007
716
|
try {
|
|
1008
|
-
return
|
|
717
|
+
return getRefinementHandoff(projectDir, request.params.workItemId);
|
|
1009
718
|
} catch (err) {
|
|
1010
|
-
if (err instanceof
|
|
719
|
+
if (err instanceof CoreError) {
|
|
1011
720
|
return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
1012
721
|
}
|
|
1013
722
|
throw err;
|
|
1014
723
|
}
|
|
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
724
|
});
|
|
1029
725
|
app.get("/api/v1/admin/work-items/:workItemId", async (request, reply) => {
|
|
1030
726
|
try {
|
|
@@ -1192,8 +888,6 @@ export {
|
|
|
1192
888
|
ProjectReadinessSchema,
|
|
1193
889
|
ProjectRouteSchema,
|
|
1194
890
|
ProjectSummarySchema,
|
|
1195
|
-
RefinementApplySchema,
|
|
1196
|
-
RefinementFeedbackSchema,
|
|
1197
891
|
RouteStepSchema,
|
|
1198
892
|
SQLiteAdminStorage,
|
|
1199
893
|
SessionManager,
|
package/dist/core.js
CHANGED
|
@@ -3686,9 +3686,6 @@ var RECOMMENDED_SKILLS = [...SKILL_GROUPS.delivery, ...SKILL_GROUPS.tech];
|
|
|
3686
3686
|
function skillInstallPath(id) {
|
|
3687
3687
|
return `knowledge/skills/${id}/skill.md`;
|
|
3688
3688
|
}
|
|
3689
|
-
function skillById(id) {
|
|
3690
|
-
return SKILLS.find((s) => s.id === id);
|
|
3691
|
-
}
|
|
3692
3689
|
|
|
3693
3690
|
// src/agents/responsibility.ts
|
|
3694
3691
|
var RESPONSIBILITY_MATRIX = {
|
|
@@ -7456,6 +7453,17 @@ function analyzeCrossRepoEvidence(input) {
|
|
|
7456
7453
|
|
|
7457
7454
|
// src/core/work-items.ts
|
|
7458
7455
|
import matter6 from "gray-matter";
|
|
7456
|
+
function computeRefinementStatus(wi) {
|
|
7457
|
+
const aspects = {
|
|
7458
|
+
outcome: Boolean(wi.currentBehavior?.trim() || wi.targetBehavior?.trim()),
|
|
7459
|
+
journey: Boolean(wi.entryPoints?.trim() || wi.endToEndFlow?.trim()),
|
|
7460
|
+
modules: wi.affectedModules.length > 0 || wi.moduleCoverage.length > 0,
|
|
7461
|
+
impact: wi.impactAnalysis.length > 0,
|
|
7462
|
+
acceptance: wi.acceptanceCriteria.length > 0
|
|
7463
|
+
};
|
|
7464
|
+
const refined = aspects.outcome && aspects.modules && aspects.acceptance;
|
|
7465
|
+
return { status: refined ? "refined" : "needs-refinement", aspects };
|
|
7466
|
+
}
|
|
7459
7467
|
var WorkItemNotFoundError = class extends Error {
|
|
7460
7468
|
constructor(workItemId) {
|
|
7461
7469
|
super(`Work Item "${workItemId}" was not found.`);
|
|
@@ -7531,8 +7539,9 @@ function getWorkItem(dir, workItemId) {
|
|
|
7531
7539
|
const fm = match.rawFrontmatter;
|
|
7532
7540
|
const knowledge = discoverKnowledge(dir).filter((a) => !a.isWorkItem);
|
|
7533
7541
|
const knowledgeById = new Map(knowledge.filter((k) => k.id).map((k) => [k.id, k]));
|
|
7534
|
-
|
|
7542
|
+
const detail = {
|
|
7535
7543
|
...base,
|
|
7544
|
+
summary: match.summary?.trim() || null,
|
|
7536
7545
|
actor: sectionText(sections, ["actor"]),
|
|
7537
7546
|
outcome: sectionText(sections, ["actor and outcome", "outcome", "expected result"]),
|
|
7538
7547
|
currentBehavior: sectionText(sections, ["current behavior", "current behaviour"]),
|
|
@@ -7552,6 +7561,7 @@ function getWorkItem(dir, workItemId) {
|
|
|
7552
7561
|
source: parseWorkItemSource(fm),
|
|
7553
7562
|
path: match.relPath
|
|
7554
7563
|
};
|
|
7564
|
+
return { ...detail, refinement: computeRefinementStatus(detail) };
|
|
7555
7565
|
}
|
|
7556
7566
|
function readBody(filePath) {
|
|
7557
7567
|
try {
|
|
@@ -8354,140 +8364,70 @@ function getWorkItemCaptureDefinition() {
|
|
|
8354
8364
|
questions
|
|
8355
8365
|
};
|
|
8356
8366
|
}
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
}
|
|
8362
|
-
function assembleRefinementContext(dir, workItemId) {
|
|
8363
|
-
const edit = getWorkItemForEdit(dir, workItemId);
|
|
8367
|
+
var RECOMMENDED_AGENT = "work-item-agent";
|
|
8368
|
+
var RECOMMENDED_SKILL = "work-item-refinement";
|
|
8369
|
+
function buildRefinementHandoff(dir, workItemId) {
|
|
8370
|
+
const wi = getWorkItem(dir, workItemId);
|
|
8364
8371
|
const config = loadConfig(dir);
|
|
8365
|
-
const
|
|
8366
|
-
|
|
8372
|
+
const projectName = config?.project.name ?? "this project";
|
|
8373
|
+
const mappedModules = loadMappedModules(dir).map((m) => m.id);
|
|
8374
|
+
const multirepo = mappedModules.length > 0;
|
|
8375
|
+
const lines = [
|
|
8376
|
+
`Refine Work Item ${wi.id} \u2014 "${wi.title}" \u2014 in project "${projectName}" using Kaddo.`,
|
|
8377
|
+
"",
|
|
8378
|
+
"Use a Kaddo-enabled agent with access to this repository. Drive the refinement with the",
|
|
8379
|
+
`canonical ${RECOMMENDED_AGENT} and the ${RECOMMENDED_SKILL} skill (via Kaddo MCP or skills).`,
|
|
8380
|
+
"",
|
|
8381
|
+
"Inspect the actual implementation before defining scope \u2014 do not guess affected modules from",
|
|
8382
|
+
"the Work Item title. Read the current behavior in the code first, then classify."
|
|
8383
|
+
];
|
|
8384
|
+
if (multirepo) {
|
|
8385
|
+
lines.push(
|
|
8386
|
+
"",
|
|
8387
|
+
`This is a multirepo project. Evaluate the scope across all relevant mapped modules (${mappedModules.join(", ")})`,
|
|
8388
|
+
"before finalizing affected_modules and module_coverage."
|
|
8389
|
+
);
|
|
8390
|
+
}
|
|
8391
|
+
lines.push(
|
|
8392
|
+
"",
|
|
8393
|
+
`Update the canonical Work Item ${wi.id} with:`,
|
|
8394
|
+
"- current and target behavior;",
|
|
8395
|
+
"- the end-to-end flow (journey);",
|
|
8396
|
+
"- affected modules;",
|
|
8397
|
+
"- module coverage;",
|
|
8398
|
+
"- impact analysis across the relevant surfaces;",
|
|
8399
|
+
"- scope confidence and open unknowns;",
|
|
8400
|
+
"- acceptance criteria;",
|
|
8401
|
+
"- relevant Knowledge / ADR relationships.",
|
|
8402
|
+
"",
|
|
8403
|
+
"Do not implement the Work Item. Do not run mutating Git operations."
|
|
8367
8404
|
);
|
|
8368
|
-
const decisions = knowledgeArtifacts.filter((a) => a.type === "adr" || /^adr-/i.test(a.id)).map((a) => ({ id: a.id, title: a.title || a.id }));
|
|
8369
|
-
const knowledge = knowledgeArtifacts.filter((a) => !(a.type === "adr" || /^adr-/i.test(a.id))).map((a) => ({ id: a.id || a.relPath, title: a.title || a.id, layer: a.layer, type: a.type || void 0, summary: a.summary || void 0 }));
|
|
8370
|
-
const modules = ["core", ...loadMappedModules(dir).map((m) => m.id)].filter((v, i, arr) => arr.indexOf(v) === i);
|
|
8371
8405
|
return {
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8406
|
+
workItemId: wi.id,
|
|
8407
|
+
title: wi.title,
|
|
8408
|
+
projectName,
|
|
8409
|
+
refinement: wi.refinement,
|
|
8410
|
+
recommendedAgent: RECOMMENDED_AGENT,
|
|
8411
|
+
recommendedSkill: RECOMMENDED_SKILL,
|
|
8412
|
+
text: lines.join("\n")
|
|
8378
8413
|
};
|
|
8379
8414
|
}
|
|
8380
|
-
function stripEdit(edit) {
|
|
8381
|
-
const { id: _i, status: _s, revision: _r, path: _p, editable: _e, editableReason: _er, ...input } = edit;
|
|
8382
|
-
return input;
|
|
8383
|
-
}
|
|
8384
|
-
var VALID_COVERAGE2 = /* @__PURE__ */ new Set(["affected", "reviewed-not-affected", "unknown", "not-applicable"]);
|
|
8385
|
-
var VALID_CONFIDENCE2 = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
8386
|
-
function str(v) {
|
|
8387
|
-
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
8388
|
-
}
|
|
8389
|
-
function strList(v) {
|
|
8390
|
-
return Array.isArray(v) ? v.map((x) => typeof x === "string" ? x.trim() : "").filter(Boolean) : [];
|
|
8391
|
-
}
|
|
8392
|
-
function normalizeAndValidateProposal(dir, workItemId, proposal) {
|
|
8393
|
-
const ctx = assembleRefinementContext(dir, workItemId);
|
|
8394
|
-
const knownModules = new Set(ctx.modules);
|
|
8395
|
-
const knownDecisions = new Set(ctx.decisions.map((d) => d.id));
|
|
8396
|
-
const knownKnowledge = new Set(ctx.knowledge.map((k) => k.id));
|
|
8397
|
-
const extraFindings = [];
|
|
8398
|
-
const input = { ...ctx.workItem.current };
|
|
8399
|
-
if (str(proposal.title)) input.title = str(proposal.title);
|
|
8400
|
-
const o = proposal.outcome ?? {};
|
|
8401
|
-
if (str(o.actor) !== void 0) input.actor = str(o.actor);
|
|
8402
|
-
if (str(o.observableOutcome) !== void 0) input.outcome = str(o.observableOutcome);
|
|
8403
|
-
if (str(o.currentBehavior) !== void 0) input.currentBehavior = str(o.currentBehavior);
|
|
8404
|
-
if (str(o.targetBehavior) !== void 0) input.targetBehavior = str(o.targetBehavior);
|
|
8405
|
-
const j = proposal.journey ?? {};
|
|
8406
|
-
if (j.entryPoints) input.entryPoints = strList(j.entryPoints).join("\n");
|
|
8407
|
-
if (j.flow) input.endToEndFlow = strList(j.flow).map((s) => `- ${s}`).join("\n");
|
|
8408
|
-
if (proposal.moduleCoverage) {
|
|
8409
|
-
input.moduleCoverage = proposal.moduleCoverage.filter((c) => {
|
|
8410
|
-
if (!knownModules.has(c.id)) {
|
|
8411
|
-
extraFindings.push({ level: "warning", message: `Proposed module "${c.id}" is not registered and was not included.` });
|
|
8412
|
-
return false;
|
|
8413
|
-
}
|
|
8414
|
-
return VALID_COVERAGE2.has(c.status);
|
|
8415
|
-
}).map((c) => ({ id: c.id, status: c.status, ...str(c.reason) ? { reason: str(c.reason) } : {} }));
|
|
8416
|
-
}
|
|
8417
|
-
const affected = /* @__PURE__ */ new Set();
|
|
8418
|
-
for (const m of proposal.affectedModules ?? []) if (knownModules.has(m)) affected.add(m);
|
|
8419
|
-
for (const c of input.moduleCoverage) if (c.status === "affected") affected.add(c.id);
|
|
8420
|
-
if (proposal.affectedModules || proposal.moduleCoverage) input.affectedModules = [...affected];
|
|
8421
|
-
if (proposal.impactAnalysis) {
|
|
8422
|
-
input.impactAnalysis = proposal.impactAnalysis.filter((s) => VALID_COVERAGE2.has(s.status) && str(s.surface)).map((s) => ({ surface: str(s.surface), status: s.status, ...str(s.reason) ? { reason: str(s.reason) } : {}, ...str(s.question) ? { question: str(s.question) } : {} }));
|
|
8423
|
-
}
|
|
8424
|
-
if (proposal.scopeConfidence && VALID_CONFIDENCE2.has(proposal.scopeConfidence.level)) {
|
|
8425
|
-
input.scopeConfidence = { level: proposal.scopeConfidence.level, reasons: strList(proposal.scopeConfidence.reasons) };
|
|
8426
|
-
}
|
|
8427
|
-
if (proposal.scopeUnknowns) input.scopeUnknowns = strList(proposal.scopeUnknowns);
|
|
8428
|
-
if (proposal.acceptanceCriteria) input.acceptanceCriteria = strList(proposal.acceptanceCriteria).map((t) => ({ text: t, checked: null }));
|
|
8429
|
-
if (proposal.linkedDecisions) {
|
|
8430
|
-
input.decisions = proposal.linkedDecisions.filter((id) => {
|
|
8431
|
-
if (!knownDecisions.has(id)) {
|
|
8432
|
-
extraFindings.push({ level: "warning", message: `Proposed decision "${id}" does not exist and was not linked.` });
|
|
8433
|
-
return false;
|
|
8434
|
-
}
|
|
8435
|
-
return true;
|
|
8436
|
-
});
|
|
8437
|
-
}
|
|
8438
|
-
if (proposal.relatedKnowledge) {
|
|
8439
|
-
input.relatedKnowledge = proposal.relatedKnowledge.filter((id) => {
|
|
8440
|
-
if (!knownKnowledge.has(id)) {
|
|
8441
|
-
extraFindings.push({ level: "warning", message: `Proposed knowledge "${id}" could not be resolved and was not linked.` });
|
|
8442
|
-
return false;
|
|
8443
|
-
}
|
|
8444
|
-
return true;
|
|
8445
|
-
});
|
|
8446
|
-
}
|
|
8447
|
-
const findings = [...extraFindings, ...evaluate(input, knownModules)];
|
|
8448
|
-
const blocking = findings.filter((f) => f.level === "blocking").length;
|
|
8449
|
-
const warning = findings.filter((f) => f.level === "warning").length;
|
|
8450
|
-
const fyi = findings.filter((f) => f.level === "fyi").length;
|
|
8451
|
-
return { input, validation: { findings, blocking, warning, fyi, canApply: true } };
|
|
8452
|
-
}
|
|
8453
|
-
function evaluate(input, knownModules) {
|
|
8454
|
-
const findings = [];
|
|
8455
|
-
for (const c of input.moduleCoverage) {
|
|
8456
|
-
if (c.status === "affected" && !input.affectedModules.includes(c.id)) {
|
|
8457
|
-
findings.push({ level: "blocking", message: `${c.id} is marked affected in module coverage but is missing from affected_modules.` });
|
|
8458
|
-
}
|
|
8459
|
-
}
|
|
8460
|
-
for (const m of input.affectedModules) {
|
|
8461
|
-
if (!knownModules.has(m)) findings.push({ level: "blocking", message: `Module "${m}" is not registered in this project.` });
|
|
8462
|
-
}
|
|
8463
|
-
if (!input.targetBehavior?.trim()) findings.push({ level: "warning", message: "Target behavior is not defined." });
|
|
8464
|
-
if (input.acceptanceCriteria.length === 0) findings.push({ level: "warning", message: "No acceptance criteria have been defined." });
|
|
8465
|
-
if (input.scopeConfidence?.level === "low") findings.push({ level: "warning", message: "Scope confidence is Low." });
|
|
8466
|
-
if (!input.scopeConfidence) findings.push({ level: "warning", message: "Scope confidence has not been assessed." });
|
|
8467
|
-
for (const s of input.impactAnalysis) if (s.status === "unknown") findings.push({ level: "fyi", message: `Impact on ${s.surface} is unknown.` });
|
|
8468
|
-
return findings;
|
|
8469
|
-
}
|
|
8470
|
-
function applyRefinement(dir, workItemId, proposal, expectedRevision) {
|
|
8471
|
-
const { input } = normalizeAndValidateProposal(dir, workItemId, proposal);
|
|
8472
|
-
return updateWorkItem(dir, workItemId, input, expectedRevision);
|
|
8473
|
-
}
|
|
8474
8415
|
export {
|
|
8475
8416
|
WorkItemNotFoundError,
|
|
8476
8417
|
WorkItemWriteError,
|
|
8477
8418
|
analyzeCrossRepoEvidence,
|
|
8478
8419
|
analyzeScopeCoverage,
|
|
8479
|
-
applyRefinement,
|
|
8480
|
-
assembleRefinementContext,
|
|
8481
8420
|
buildProjectExplanation,
|
|
8482
8421
|
buildProjectRoute,
|
|
8483
8422
|
buildReadinessReport,
|
|
8423
|
+
buildRefinementHandoff,
|
|
8424
|
+
computeRefinementStatus,
|
|
8484
8425
|
createWorkItem,
|
|
8485
8426
|
cwd,
|
|
8486
8427
|
discoverKnowledge,
|
|
8487
8428
|
discoverWorkItems,
|
|
8488
8429
|
exists,
|
|
8489
8430
|
getWorkItem,
|
|
8490
|
-
getWorkItemAgentAssets,
|
|
8491
8431
|
getWorkItemCaptureDefinition,
|
|
8492
8432
|
getWorkItemForEdit,
|
|
8493
8433
|
getWorkItems,
|
|
@@ -8500,7 +8440,6 @@ export {
|
|
|
8500
8440
|
lifecycleStateOf,
|
|
8501
8441
|
loadConfig,
|
|
8502
8442
|
loadMappedModules,
|
|
8503
|
-
normalizeAndValidateProposal,
|
|
8504
8443
|
readFile,
|
|
8505
8444
|
transitionWorkItem,
|
|
8506
8445
|
updateWorkItem,
|