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.
- package/dist/api/cross-product.js +0 -1
- package/dist/api/issues/issue-utils.js +0 -1
- package/dist/api/issues/update-issue.js +1 -1
- package/dist/commands/agent-workflow/chat-worker.js +1 -1
- package/dist/commands/checklists/index.js +1 -1
- package/dist/commands/product-techniques/index.d.ts +15 -0
- package/dist/commands/product-techniques/index.js +37 -0
- package/dist/commands/workflow/executors/phase-executor.js +1 -1
- package/dist/index.js +24 -1
- package/dist/phases/analyze-logs/index.js +1 -1
- package/dist/phases/bug-fixing/context-fetcher.js +4 -2
- package/dist/phases/find-features/index.js +1 -1
- package/dist/phases/output-contracts.js +47 -36
- package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
- package/dist/phases/pr-shared/agent-utils.js +48 -4
- package/dist/phases/product-techniques/index.d.ts +52 -0
- package/dist/phases/product-techniques/index.js +268 -0
- package/dist/phases/product-techniques/mcp-server.d.ts +41 -0
- package/dist/phases/product-techniques/mcp-server.js +96 -0
- package/dist/phases/product-techniques/prompts.d.ts +19 -0
- package/dist/phases/product-techniques/prompts.js +66 -0
- package/dist/phases/product-techniques/types.d.ts +13 -0
- package/dist/phases/product-techniques/types.js +13 -0
- package/dist/phases/screen-flow/index.js +73 -17
- package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
- package/dist/phases/screen-flow/mcp-server.js +262 -0
- package/dist/phases/screen-flow/prompts.js +3 -1
- package/dist/phases/screen-flow/theme.js +23 -12
- package/dist/phases/screen-flow/types.js +30 -15
- package/dist/services/branches.js +3 -3
- package/dist/services/phase-hooks/hook-executor.js +1 -1
- package/dist/services/phase-ratings.js +1 -1
- package/dist/services/product-logs.js +1 -1
- package/dist/services/pull-requests.js +3 -3
- package/package.json +1 -1
- 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,
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
45
|
-
const testReportResult = (await callMcpEndpoint('test_reports/latest', {
|
|
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) {
|
|
@@ -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 —
|
|
900
|
+
**CRITICAL — How to return the result**:
|
|
901
901
|
|
|
902
|
-
|
|
902
|
+
Return the extraction by calling the MCP tool
|
|
903
|
+
\`mcp__screen-flow__submit_screen_flow\` **exactly once** with three arguments:
|
|
903
904
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
929
|
-
{
|
|
930
|
-
"
|
|
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
|
-
*
|
|
32
|
-
* Returns the parsed object or
|
|
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
|
-
*
|
|
42
|
-
* Returns the parsed object or
|
|
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>;
|