@kaddo/cli 3.74.0 → 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-AAaxSvT4.js"></script>
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
- const refineHandler = async (reply, fn) => {
715
+ app.get("/api/v1/admin/work-items/:workItemId/refinement-handoff", async (request, reply) => {
1007
716
  try {
1008
- return await fn();
717
+ return getRefinementHandoff(projectDir, request.params.workItemId);
1009
718
  } catch (err) {
1010
- if (err instanceof RefinementProviderError || err instanceof CoreError) {
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,