@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-CiJRX9Ps.js"></script>
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 = WorkItemCreateSchema.safeParse(request.body);
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,