edsger 0.57.0 → 0.59.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.
Files changed (36) hide show
  1. package/dist/api/cross-product.js +0 -1
  2. package/dist/api/issues/issue-utils.js +0 -1
  3. package/dist/api/issues/update-issue.js +1 -1
  4. package/dist/commands/agent-workflow/chat-worker.js +1 -1
  5. package/dist/commands/checklists/index.js +1 -1
  6. package/dist/commands/product-techniques/index.d.ts +15 -0
  7. package/dist/commands/product-techniques/index.js +37 -0
  8. package/dist/commands/workflow/executors/phase-executor.js +1 -1
  9. package/dist/index.js +24 -1
  10. package/dist/phases/analyze-logs/index.js +1 -1
  11. package/dist/phases/bug-fixing/context-fetcher.js +4 -2
  12. package/dist/phases/find-features/index.js +1 -1
  13. package/dist/phases/output-contracts.js +47 -36
  14. package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
  15. package/dist/phases/pr-shared/agent-utils.js +48 -4
  16. package/dist/phases/product-techniques/index.d.ts +52 -0
  17. package/dist/phases/product-techniques/index.js +268 -0
  18. package/dist/phases/product-techniques/mcp-server.d.ts +41 -0
  19. package/dist/phases/product-techniques/mcp-server.js +96 -0
  20. package/dist/phases/product-techniques/prompts.d.ts +19 -0
  21. package/dist/phases/product-techniques/prompts.js +66 -0
  22. package/dist/phases/product-techniques/types.d.ts +13 -0
  23. package/dist/phases/product-techniques/types.js +13 -0
  24. package/dist/phases/screen-flow/index.js +73 -17
  25. package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
  26. package/dist/phases/screen-flow/mcp-server.js +262 -0
  27. package/dist/phases/screen-flow/prompts.js +3 -1
  28. package/dist/phases/screen-flow/theme.js +23 -12
  29. package/dist/phases/screen-flow/types.js +30 -15
  30. package/dist/services/branches.js +3 -3
  31. package/dist/services/phase-hooks/hook-executor.js +1 -1
  32. package/dist/services/phase-ratings.js +1 -1
  33. package/dist/services/product-logs.js +1 -1
  34. package/dist/services/pull-requests.js +3 -3
  35. package/package.json +1 -1
  36. package/vitest.config.ts +1 -0
@@ -56,7 +56,6 @@ export async function listAllReadyIssues(verbose) {
56
56
  const allIssues = (data || []).map((row) => {
57
57
  // The `product` join lands as a nested object; lift its name onto
58
58
  // the row to keep the wire shape stable.
59
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- shape from PostgREST join
60
59
  const r = row;
61
60
  return {
62
61
  ...r,
@@ -41,7 +41,6 @@ export async function claimNextIssue(productId, verbose) {
41
41
  }
42
42
  // RPC returns rows with `issue_*` prefixed column names — unwrap to
43
43
  // the IssueInfo shape callers already consume.
44
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- raw RPC row
45
44
  const r = row;
46
45
  const issue = {
47
46
  id: r.issue_id,
@@ -91,7 +91,7 @@ export async function markWorkflowPhaseCompleted(issueId, phaseName, verbose) {
91
91
  // Fall through to MCP
92
92
  }
93
93
  }
94
- if (issue == null) {
94
+ if (!issue) {
95
95
  const response = (await callMcpEndpoint('issues/get', {
96
96
  issue_id: issueId,
97
97
  }));
@@ -354,7 +354,7 @@ function startRealtime() {
354
354
  registerChannel(payload.new);
355
355
  // A channel created with messages already in it (unlikely but
356
356
  // possible if INSERTs race) deserves an immediate sweep.
357
- const id = payload.new.id;
357
+ const { id } = payload.new;
358
358
  if (id) {
359
359
  void claimAndProcess(id);
360
360
  }
@@ -116,7 +116,7 @@ export async function runChecklists(options) {
116
116
  ? createChecklistsMcpServer({ kind: 'team', teamId: teamId })
117
117
  : createChecklistsMcpServer({
118
118
  kind: 'product',
119
- productId: productId,
119
+ productId,
120
120
  });
121
121
  const promptParts = isTeamScope
122
122
  ? [
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CLI command: edsger product-techniques <productId> --techniques-id <id>
3
+ *
4
+ * Clones the product's repo, asks Claude to write a catalogue of the
5
+ * techniques the repo uses, and stores the markdown body in
6
+ * product_techniques. The desktop UI creates a pending row first then
7
+ * invokes the CLI with --techniques-id; the CLI flips status running →
8
+ * success/failed and populates the content/summary fields.
9
+ */
10
+ export interface ProductTechniquesCliOptions {
11
+ techniquesId: string;
12
+ guidance?: string;
13
+ verbose?: boolean;
14
+ }
15
+ export declare function runProductTechniques(productId: string, options: ProductTechniquesCliOptions): Promise<void>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * CLI command: edsger product-techniques <productId> --techniques-id <id>
3
+ *
4
+ * Clones the product's repo, asks Claude to write a catalogue of the
5
+ * techniques the repo uses, and stores the markdown body in
6
+ * product_techniques. The desktop UI creates a pending row first then
7
+ * invokes the CLI with --techniques-id; the CLI flips status running →
8
+ * success/failed and populates the content/summary fields.
9
+ */
10
+ import { runProductTechniquesPhase } from '../../phases/product-techniques/index.js';
11
+ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
12
+ export async function runProductTechniques(productId, options) {
13
+ const { techniquesId, guidance, verbose } = options;
14
+ if (!productId) {
15
+ throw new Error('Product ID is required for product-techniques');
16
+ }
17
+ if (!techniquesId) {
18
+ throw new Error('--techniques-id is required (the pending product_techniques row id)');
19
+ }
20
+ logInfo(`Starting techniques generation for product ${productId}`);
21
+ const result = await runProductTechniquesPhase({
22
+ productId,
23
+ techniquesId,
24
+ guidance,
25
+ verbose,
26
+ });
27
+ if (result.status === 'success') {
28
+ logSuccess(result.message);
29
+ if (result.summary) {
30
+ logInfo(`\nSummary: ${result.summary}`);
31
+ }
32
+ }
33
+ else {
34
+ logError(result.message);
35
+ process.exit(1);
36
+ }
37
+ }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { checkApprovalBeforePhaseExecution } from '../../../api/issues/approval-checker.js';
5
5
  import { updateIssueStatusForPhase } from '../../../api/issues/index.js';
6
- import { logIssuePhaseEvent, } from '../../../services/audit-logs.js';
6
+ import { logIssuePhaseEvent } from '../../../services/audit-logs.js';
7
7
  import { getChecklistsForPhase, processChecklistItemResultsFromResponse, processChecklistResultsFromResponse, validateChecklistsForPhase, validateRequiredChecklistResults, } from '../../../services/checklist.js';
8
8
  import { runHooksForPhase } from '../../../services/phase-hooks/index.js';
9
9
  import { logDebug, logInfo, logWarning } from '../../../utils/logger.js';
package/dist/index.js CHANGED
@@ -19,17 +19,18 @@ import { runFindBugs } from './commands/find-bugs/index.js';
19
19
  import { runFindFeatures } from './commands/find-features/index.js';
20
20
  import { parseCategoriesOption, runFindSmells, } from './commands/find-smells/index.js';
21
21
  import { runGrowthAnalysis } from './commands/growth-analysis/index.js';
22
- import { runScreenFlow } from './commands/screen-flow/index.js';
23
22
  import { runInit } from './commands/init/index.js';
24
23
  import { runIntelligence } from './commands/intelligence/index.js';
25
24
  import { runIssueAnalysisCommand } from './commands/issue-analysis/index.js';
26
25
  import { runPRResolve } from './commands/pr-resolve/index.js';
27
26
  import { runPRReview } from './commands/pr-review/index.js';
27
+ import { runProductTechniques } from './commands/product-techniques/index.js';
28
28
  import { runProductTestCases } from './commands/product-test-cases/index.js';
29
29
  import { runQualityBenchmarkCli } from './commands/quality-benchmark/index.js';
30
30
  import { runRefactor } from './commands/refactor/refactor.js';
31
31
  import { runReleaseSyncCommand } from './commands/release-sync/index.js';
32
32
  import { runRunSheetCommand } from './commands/run-sheet/index.js';
33
+ import { runScreenFlow } from './commands/screen-flow/index.js';
33
34
  import { runSmokeTestCommand } from './commands/smoke-test/index.js';
34
35
  import { runSyncGithubIssues } from './commands/sync-github-issues/index.js';
35
36
  import { runSyncSentryIssues } from './commands/sync-sentry-issues/index.js';
@@ -694,6 +695,28 @@ program
694
695
  }
695
696
  });
696
697
  // ============================================================
698
+ // Subcommand: edsger product-techniques <productId>
699
+ // ============================================================
700
+ program
701
+ .command('product-techniques <productId>')
702
+ .description("Generate a catalogue of the techniques a product's repo uses (languages, frameworks, patterns, state idioms, build & deploy, notable bits). Writes the result to the pending product_techniques row identified by --techniques-id.")
703
+ .requiredOption('--techniques-id <id>', 'Pending product_techniques row id to populate')
704
+ .option('-g, --guidance <text>', 'Human direction for the AI (focus areas, exclusions)')
705
+ .option('-v, --verbose', 'Verbose output')
706
+ .action(async (productId, opts) => {
707
+ try {
708
+ await runProductTechniques(productId, {
709
+ techniquesId: opts.techniquesId,
710
+ guidance: opts.guidance,
711
+ verbose: opts.verbose,
712
+ });
713
+ }
714
+ catch (error) {
715
+ logError(error instanceof Error ? error.message : String(error));
716
+ process.exit(1);
717
+ }
718
+ });
719
+ // ============================================================
697
720
  // Subcommand: edsger pr-resolve <productId>
698
721
  // ============================================================
699
722
  program
@@ -5,11 +5,11 @@
5
5
  * user's session logs (only if the latest log is >1 hour old), and creates
6
6
  * product_user_feedbacks for any issues or improvement suggestions found.
7
7
  */
8
+ import { callMcpEndpoint } from '../../api/mcp-client.js';
8
9
  import { getProduct } from '../../api/products.js';
9
10
  import { getPendingLogsByUser, markLogsAnalyzed, } from '../../services/product-logs.js';
10
11
  import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
11
12
  import { logError, logInfo, logSuccess } from '../../utils/logger.js';
12
- import { callMcpEndpoint } from '../../api/mcp-client.js';
13
13
  import { executeLogAnalysis } from './agent.js';
14
14
  import { createAnalyzeLogsSystemPrompt, createAnalyzeLogsUserPrompt, } from './prompts.js';
15
15
  /**
@@ -41,8 +41,10 @@ export async function fetchBugFixingContext(issueId, verbose) {
41
41
  // Fall through to MCP
42
42
  }
43
43
  }
44
- if (raw == null) {
45
- const testReportResult = (await callMcpEndpoint('test_reports/latest', { issue_id: issueId }));
44
+ if (!raw) {
45
+ const testReportResult = (await callMcpEndpoint('test_reports/latest', {
46
+ issue_id: issueId,
47
+ }));
46
48
  raw = testReportResult.test_report ?? null;
47
49
  }
48
50
  if (raw) {
@@ -267,7 +267,7 @@ async function fetchIntelligenceReports(productId, focusReportId) {
267
267
  if (error) {
268
268
  throw new Error(error.message);
269
269
  }
270
- reports = (data || []);
270
+ reports = data || [];
271
271
  usedSdk = true;
272
272
  }
273
273
  catch {
@@ -897,46 +897,57 @@ You MUST end your response with a JSON object containing the code refine results
897
897
  \`\`\`
898
898
  `,
899
899
  'screen-flow': `
900
- **CRITICAL — Output Format**:
900
+ **CRITICAL — How to return the result**:
901
901
 
902
- After finishing your investigation, emit a single fenced code block tagged \`screen_flow\` containing the structured extraction. Do not emit any other JSON blocks.
902
+ Return the extraction by calling the MCP tool
903
+ \`mcp__screen-flow__submit_screen_flow\` **exactly once** with three arguments:
903
904
 
904
- \`\`\`screen_flow
905
- {
906
- "summary": "1-3 sentence narrative of what kind of app this is and its primary user flows",
907
- "nodes": [
908
- {
909
- "slug": "login",
910
- "name": "Login",
911
- "route": "/signin",
912
- "file": "src/pages/Login.tsx",
913
- "kind": "page",
914
- "layout": "centered",
915
- "header": { "title": "Sign in", "actions": [{ "label": "Sign up", "variant": "ghost" }] },
916
- "body": [
917
- {
918
- "type": "form",
919
- "submitLabel": "Sign in",
920
- "fields": [
921
- { "label": "Email", "kind": "email", "required": true },
922
- { "label": "Password", "kind": "password", "required": true }
923
- ]
924
- }
925
- ]
926
- }
905
+ - \`summary\` — 1-3 sentence narrative of what kind of app this is and its primary user flows
906
+ - \`nodes\` — array of ScreenSchema objects (every user-facing screen, modal, drawer, tab, or named state)
907
+ - \`edges\` array of ScreenEdge objects (transitions between screens)
908
+
909
+ The tool validates the arguments against the schema. If it returns an error,
910
+ fix the issue it describes and call the tool again. After a successful call,
911
+ end your turn — do not also paste the same data as a fenced text block.
912
+
913
+ You can also call \`mcp__screen-flow__record_progress({ phase, message })\` at
914
+ each phase boundary (detection / routing / screens / transitions / submission)
915
+ to keep the user informed during long runs. This is observability only — it
916
+ does not affect the extraction.
917
+
918
+ ScreenSchema fields:
919
+ - \`slug\` (unique within the flow), \`name\`, \`route?\`, \`file?\`
920
+ - \`kind\`: one of \`page\`, \`modal\`, \`drawer\`, \`tab\`, \`state\`
921
+ - \`layout\`: one of \`centered\`, \`sidebar\`, \`split\`, \`list-detail\`, \`tabs\`, \`stacked\`
922
+ - \`header?\`: \`{ title, subtitle?, back?, actions?: [{ label, variant?, icon? }] }\`
923
+ - \`body\`: array of sections; each section \`type\` is one of \`form\`, \`list\`, \`card-grid\`, \`table\`, \`kanban\`, \`text\`, \`image\`, \`chart\`, \`stats\`, \`empty-state\`, \`tabs\`, \`custom\`
924
+
925
+ ScreenEdge fields:
926
+ - \`fromSlug\`, \`toSlug\` (both MUST appear in nodes), \`triggerLabel\`, \`triggerFile?\`
927
+ - \`kind\`: one of \`navigate\`, \`modal\`, \`redirect\`, \`back\`
928
+
929
+ Schematic example of the tool call:
930
+
931
+ \`\`\`
932
+ submit_screen_flow({
933
+ summary: "Two-screen demo: sign in then land on home.",
934
+ nodes: [
935
+ { slug: "login", name: "Login", route: "/signin", file: "src/pages/Login.tsx",
936
+ kind: "page", layout: "centered",
937
+ header: { title: "Sign in", actions: [{ label: "Sign up", variant: "ghost" }] },
938
+ body: [{ type: "form", submitLabel: "Sign in", fields: [
939
+ { label: "Email", kind: "email", required: true },
940
+ { label: "Password", kind: "password", required: true }
941
+ ]}]
942
+ },
943
+ { slug: "home", name: "Home", route: "/", file: "src/pages/Home.tsx",
944
+ kind: "page", layout: "sidebar", body: [] }
927
945
  ],
928
- "edges": [
929
- {
930
- "fromSlug": "login",
931
- "toSlug": "home",
932
- "triggerLabel": "Submit credentials",
933
- "triggerFile": "src/pages/Login.tsx",
934
- "kind": "navigate"
935
- }
946
+ edges: [
947
+ { fromSlug: "login", toSlug: "home", triggerLabel: "Submit credentials",
948
+ triggerFile: "src/pages/Login.tsx", kind: "navigate" }
936
949
  ]
937
- }
950
+ })
938
951
  \`\`\`
939
-
940
- All node \`slug\` values must be unique. Every \`fromSlug\` / \`toSlug\` in edges must reference a slug that appears in \`nodes\`. Section \`type\` values are restricted to: \`form\`, \`list\`, \`card-grid\`, \`table\`, \`kanban\`, \`text\`, \`image\`, \`chart\`, \`stats\`, \`empty-state\`, \`tabs\`, \`custom\`. Edge \`kind\` values are restricted to: \`navigate\`, \`modal\`, \`redirect\`, \`back\`.
941
952
  `,
942
953
  };
@@ -24,16 +24,24 @@ export declare function createPromptGenerator(prompt: string): AsyncGenerator<{
24
24
  }>;
25
25
  /**
26
26
  * Extract text content from assistant message content array.
27
+ *
28
+ * When `verbose`, also surfaces tool_use / tool_result blocks via
29
+ * logDebug so it's visible whether the agent is making MCP / file /
30
+ * bash calls — without these, a long-running session looks frozen
31
+ * between text emissions.
27
32
  */
28
33
  export declare function extractTextFromContent(content: any[], verbose?: boolean): string;
29
34
  /**
30
35
  * Try to parse a JSON result from agent response text.
31
- * Looks for ```json code blocks first, then falls back to raw JSON parsing.
32
- * Returns the parsed object or null on failure.
36
+ * Tries a custom fenceTag (e.g. ```screen_flow) first when provided, then
37
+ * ```json, then falls back to raw JSON parsing. Returns the parsed object or
38
+ * null on failure.
33
39
  */
34
- export declare function tryParseJsonFromResponse(responseText: string): unknown | null;
40
+ export declare function tryParseJsonFromResponse(responseText: string, fenceTag?: string): unknown | null;
35
41
  /**
36
42
  * Extract a specific keyed result from agent response.
37
43
  * e.g., tryExtractResult(text, 'review_result') extracts the review_result key.
44
+ * The key is also tried as the fenced code-block tag so phases whose output
45
+ * contract uses a custom fence (e.g. ```screen_flow) parse correctly.
38
46
  */
39
47
  export declare function tryExtractResult(responseText: string, key: string): unknown | null;
@@ -23,6 +23,11 @@ export async function* createPromptGenerator(prompt) {
23
23
  }
24
24
  /**
25
25
  * Extract text content from assistant message content array.
26
+ *
27
+ * When `verbose`, also surfaces tool_use / tool_result blocks via
28
+ * logDebug so it's visible whether the agent is making MCP / file /
29
+ * bash calls — without these, a long-running session looks frozen
30
+ * between text emissions.
26
31
  */
27
32
  export function extractTextFromContent(
28
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -33,16 +38,50 @@ content, verbose) {
33
38
  text += `${item.text}\n`;
34
39
  logDebug(item.text, verbose);
35
40
  }
41
+ else if (verbose && item.type === 'tool_use') {
42
+ logDebug(`→ ${item.name}(${previewJson(item.input)})`, verbose);
43
+ }
44
+ else if (verbose && item.type === 'tool_result') {
45
+ const preview = Array.isArray(item.content)
46
+ ? item.content
47
+ .filter((c) => c?.type === 'text')
48
+ .map((c) => c.text ?? '')
49
+ .join(' ')
50
+ : String(item.content ?? '');
51
+ const flag = item.is_error ? '✗' : '←';
52
+ logDebug(`${flag} ${truncate(preview, 200)}`, verbose);
53
+ }
36
54
  }
37
55
  return text;
38
56
  }
57
+ function previewJson(value, max = 200) {
58
+ try {
59
+ return truncate(JSON.stringify(value), max);
60
+ }
61
+ catch {
62
+ return truncate(String(value), max);
63
+ }
64
+ }
65
+ function truncate(text, max) {
66
+ if (text.length <= max) {
67
+ return text;
68
+ }
69
+ return `${text.slice(0, max - 1)}…`;
70
+ }
39
71
  /**
40
72
  * Try to parse a JSON result from agent response text.
41
- * Looks for ```json code blocks first, then falls back to raw JSON parsing.
42
- * Returns the parsed object or null on failure.
73
+ * Tries a custom fenceTag (e.g. ```screen_flow) first when provided, then
74
+ * ```json, then falls back to raw JSON parsing. Returns the parsed object or
75
+ * null on failure.
43
76
  */
44
- export function tryParseJsonFromResponse(responseText) {
77
+ export function tryParseJsonFromResponse(responseText, fenceTag = 'json') {
45
78
  try {
79
+ if (fenceTag !== 'json') {
80
+ const taggedMatch = responseText.match(new RegExp(`\`\`\`${escapeRegExp(fenceTag)}\\s*\\n([\\s\\S]*?)\\n\\s*\`\`\``));
81
+ if (taggedMatch) {
82
+ return JSON.parse(taggedMatch[1]);
83
+ }
84
+ }
46
85
  const jsonBlockMatch = responseText.match(/```json\s*\n([\s\S]*?)\n\s*```/);
47
86
  return jsonBlockMatch
48
87
  ? JSON.parse(jsonBlockMatch[1])
@@ -55,9 +94,11 @@ export function tryParseJsonFromResponse(responseText) {
55
94
  /**
56
95
  * Extract a specific keyed result from agent response.
57
96
  * e.g., tryExtractResult(text, 'review_result') extracts the review_result key.
97
+ * The key is also tried as the fenced code-block tag so phases whose output
98
+ * contract uses a custom fence (e.g. ```screen_flow) parse correctly.
58
99
  */
59
100
  export function tryExtractResult(responseText, key) {
60
- const parsed = tryParseJsonFromResponse(responseText);
101
+ const parsed = tryParseJsonFromResponse(responseText, key);
61
102
  if (parsed &&
62
103
  typeof parsed === 'object' &&
63
104
  key in parsed) {
@@ -66,3 +107,6 @@ export function tryExtractResult(responseText, key) {
66
107
  // If top-level has the expected shape, return the whole thing
67
108
  return parsed;
68
109
  }
110
+ function escapeRegExp(value) {
111
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
112
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * product-techniques phase: clone the product's repo, ask Claude (via the
3
+ * `submit_techniques` MCP tool) to write a catalogue of the techniques the
4
+ * repo uses, and persist the result to product_techniques via the Supabase
5
+ * SDK.
6
+ *
7
+ * Mirrors the screen-flow pattern. Production-grade behaviours layered on
8
+ * top of the basic generate-and-write loop:
9
+ *
10
+ * - Heartbeat: `last_heartbeat_at` is refreshed on every assistant message
11
+ * so the reader can detect stalled / crashed runs (see services/db/
12
+ * product-techniques.ts for the lazy reaper).
13
+ * - Cancellation-safe writes: markRunning / markSuccess / markFailed only
14
+ * touch rows whose status is in {pending, running}. If the user clicked
15
+ * Stop and the row is now 'cancelled', the final write no-ops.
16
+ * - Tool-based submission: validated server-side via Zod + content checks
17
+ * (mcp-server.ts). Falls back to fenced JSON parsing for resilience.
18
+ */
19
+ import type { SupabaseClient } from '@supabase/supabase-js';
20
+ export interface ProductTechniquesOptions {
21
+ productId: string;
22
+ techniquesId: string;
23
+ guidance?: string;
24
+ verbose?: boolean;
25
+ }
26
+ export interface ProductTechniquesResult {
27
+ status: 'success' | 'error' | 'cancelled';
28
+ message: string;
29
+ summary?: string;
30
+ }
31
+ export declare function runProductTechniquesPhase(options: ProductTechniquesOptions): Promise<ProductTechniquesResult>;
32
+ /**
33
+ * Claim the row by flipping `pending` → `running`. Returns true on success
34
+ * (we won the claim) and false when the row has already moved on (e.g. user
35
+ * cancelled before the CLI started). Bounded by the status filter so we
36
+ * can't accidentally resurrect a 'cancelled' row.
37
+ */
38
+ export declare function markRunning(supabase: SupabaseClient, techniquesId: string): Promise<boolean>;
39
+ /**
40
+ * Touch the heartbeat. Best-effort — if it fails (network blip, RLS), the
41
+ * agent loop keeps running; the reader treats this row as stale and marks
42
+ * it failed on next read, which is the correct behaviour.
43
+ */
44
+ export declare function heartbeat(supabase: SupabaseClient, techniquesId: string): Promise<void>;
45
+ /**
46
+ * Write failure status iff the row is still in an active state. Returns
47
+ * true if the row was actually updated (so the caller knows whether the
48
+ * agent's verdict made it to the DB). Returns false when the row has
49
+ * already been cancelled or otherwise resolved by someone else.
50
+ */
51
+ export declare function markFailed(supabase: SupabaseClient, techniquesId: string, errorMessage: string): Promise<boolean>;
52
+ export declare function markSuccess(supabase: SupabaseClient, techniquesId: string, summary: string, content: string): Promise<boolean>;