@openpome/local-gateway 0.38.0-alpha.0 → 0.41.0-alpha.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/connectors/work-item-registry.d.ts.map +1 -1
- package/dist/connectors/work-item-registry.js +4 -3
- package/dist/connectors/work-item-registry.js.map +1 -1
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +724 -21
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createDefaultWorkItemSourceRegistry, createJiraCloudOAuthLogin, exchang
|
|
|
16
16
|
const execAsync = promisify(exec);
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
18
|
const jiraOAuthCredentialAccount = "jira-cloud/oauth";
|
|
19
|
+
const jiraApiTokenCredentialAccount = "jira-cloud/api-token";
|
|
19
20
|
const githubOAuthCredentialAccount = "github/oauth";
|
|
20
21
|
const openAiCredentialAccount = "model/openai/api-key";
|
|
21
22
|
const anthropicCredentialAccount = "model/anthropic/api-key";
|
|
@@ -41,7 +42,7 @@ const maxWorkspaceScanRepositories = 200;
|
|
|
41
42
|
export function getGatewayHealth() {
|
|
42
43
|
return {
|
|
43
44
|
status: "ok",
|
|
44
|
-
version: "0.
|
|
45
|
+
version: "0.41.0-alpha.0"
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
export async function initOpenPome() {
|
|
@@ -453,11 +454,17 @@ export async function startTaskSession(key, env = process.env) {
|
|
|
453
454
|
updatedAt: now
|
|
454
455
|
};
|
|
455
456
|
const events = createSessionStartEvents(session, resolution.workItem, workspaceCandidate, now);
|
|
457
|
+
const repositoryKnowledge = workspaceCandidate?.workspace.path
|
|
458
|
+
? await buildRepositoryKnowledge(workspaceCandidate.workspace, now)
|
|
459
|
+
: undefined;
|
|
460
|
+
const intelligence = buildWorkItemIntelligenceReport(resolution.workItem, workspaceCandidate, repositoryKnowledge);
|
|
456
461
|
await writeActiveTaskSession(paths.homeDirectory, {
|
|
457
462
|
version: 1,
|
|
458
463
|
session,
|
|
459
464
|
workItem: resolution.workItem,
|
|
460
465
|
workspaceCandidate,
|
|
466
|
+
intelligence,
|
|
467
|
+
repositoryKnowledge,
|
|
461
468
|
events,
|
|
462
469
|
approvalHistory: []
|
|
463
470
|
});
|
|
@@ -465,6 +472,8 @@ export async function startTaskSession(key, env = process.env) {
|
|
|
465
472
|
session,
|
|
466
473
|
workItem: resolution.workItem,
|
|
467
474
|
workspaceCandidate,
|
|
475
|
+
intelligence,
|
|
476
|
+
repositoryKnowledge,
|
|
468
477
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory)
|
|
469
478
|
};
|
|
470
479
|
}
|
|
@@ -483,6 +492,8 @@ export async function getTaskSessionStatus() {
|
|
|
483
492
|
session: persisted.session,
|
|
484
493
|
workItem: persisted.workItem,
|
|
485
494
|
workspaceCandidate: persisted.workspaceCandidate,
|
|
495
|
+
intelligence: persisted.intelligence,
|
|
496
|
+
repositoryKnowledge: persisted.repositoryKnowledge,
|
|
486
497
|
plan: persisted.plan,
|
|
487
498
|
planApproval: persisted.planApproval,
|
|
488
499
|
events: persisted.events ?? [],
|
|
@@ -501,12 +512,12 @@ export async function getTaskSessionStatus() {
|
|
|
501
512
|
}
|
|
502
513
|
export async function getAssistantDecision() {
|
|
503
514
|
const status = await getTaskSessionStatus();
|
|
515
|
+
const jira = await getJiraAuthStatus();
|
|
504
516
|
if (!status.active || !status.session || !status.workItem) {
|
|
505
|
-
const jira = await getJiraAuthStatus();
|
|
506
517
|
if (!jira.configured) {
|
|
507
518
|
return buildAssistantDecision(status, "connect_jira", "Connect Jira", "OpenPome needs Jira access before it can show assigned stories.", [
|
|
508
519
|
"pome onboard",
|
|
509
|
-
"pome auth jira
|
|
520
|
+
"pome auth jira token",
|
|
510
521
|
"pome demo"
|
|
511
522
|
], [jira.detail]);
|
|
512
523
|
}
|
|
@@ -515,6 +526,16 @@ export async function getAssistantDecision() {
|
|
|
515
526
|
"pome start <KEY>"
|
|
516
527
|
]);
|
|
517
528
|
}
|
|
529
|
+
if (!jira.configured && process.env["OPENPOME_DEMO"] !== "1") {
|
|
530
|
+
return buildAssistantDecision(status, "connect_jira", "Connect Jira", "OpenPome needs Jira connected before continuing this story as real work.", [
|
|
531
|
+
"pome auth jira token",
|
|
532
|
+
"pome reset",
|
|
533
|
+
"pome demo"
|
|
534
|
+
], [
|
|
535
|
+
jira.detail,
|
|
536
|
+
"If this is an old demo/session, run `pome reset` and start from `pome work` after connecting Jira."
|
|
537
|
+
]);
|
|
538
|
+
}
|
|
518
539
|
if (!status.plan) {
|
|
519
540
|
return buildAssistantDecision(status, "create_plan", "Create implementation plan", "Build a repo-aware implementation plan from the latest Jira story.", [
|
|
520
541
|
"pome plan"
|
|
@@ -1653,6 +1674,49 @@ export async function getJiraAuthStatus(env = process.env) {
|
|
|
1653
1674
|
...status
|
|
1654
1675
|
};
|
|
1655
1676
|
}
|
|
1677
|
+
export async function configureJiraApiTokenAuth(input, env = process.env) {
|
|
1678
|
+
const baseUrl = normalizeJiraBaseUrl(input.baseUrl);
|
|
1679
|
+
const email = input.email.trim();
|
|
1680
|
+
const apiToken = input.apiToken.trim();
|
|
1681
|
+
if (!baseUrl) {
|
|
1682
|
+
throw new Error("Jira site URL is required. Example: https://your-company.atlassian.net");
|
|
1683
|
+
}
|
|
1684
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(email)) {
|
|
1685
|
+
throw new Error("Jira email is required. Use the email address you use to sign in to Atlassian.");
|
|
1686
|
+
}
|
|
1687
|
+
if (!apiToken) {
|
|
1688
|
+
throw new Error("Jira API token is required.");
|
|
1689
|
+
}
|
|
1690
|
+
const store = createCredentialStore();
|
|
1691
|
+
if (!store.isAvailable()) {
|
|
1692
|
+
throw new Error(`Credential store is unavailable: ${store.backend}. Use OPENPOME_JIRA_BASE_URL, OPENPOME_JIRA_EMAIL, and OPENPOME_JIRA_API_TOKEN environment variables instead.`);
|
|
1693
|
+
}
|
|
1694
|
+
const credential = {
|
|
1695
|
+
baseUrl,
|
|
1696
|
+
email,
|
|
1697
|
+
apiToken,
|
|
1698
|
+
storedAt: new Date().toISOString()
|
|
1699
|
+
};
|
|
1700
|
+
await setJsonCredential(store, jiraApiTokenCredentialAccount, credential);
|
|
1701
|
+
const source = await createJiraSource({
|
|
1702
|
+
...env,
|
|
1703
|
+
OPENPOME_JIRA_BASE_URL: baseUrl,
|
|
1704
|
+
OPENPOME_JIRA_EMAIL: email,
|
|
1705
|
+
OPENPOME_JIRA_API_TOKEN: apiToken
|
|
1706
|
+
});
|
|
1707
|
+
const reachability = await source.checkReachability();
|
|
1708
|
+
if (reachability.status !== "ok" && reachability.status !== "reachable") {
|
|
1709
|
+
throw new Error(`Jira credentials were saved, but Jira was not reachable yet. ${reachability.detail}`);
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
provider: "jira-cloud",
|
|
1713
|
+
stored: true,
|
|
1714
|
+
mode: "api-token",
|
|
1715
|
+
baseUrl,
|
|
1716
|
+
email,
|
|
1717
|
+
detail: "Jira connected. OpenPome can now load your assigned stories."
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1656
1720
|
export function createJiraOAuthLogin(env = process.env) {
|
|
1657
1721
|
const clientId = env["OPENPOME_JIRA_OAUTH_CLIENT_ID"];
|
|
1658
1722
|
const redirectUri = env["OPENPOME_JIRA_OAUTH_REDIRECT_URI"] ?? "http://127.0.0.1:48731/auth/jira/callback";
|
|
@@ -1764,10 +1828,15 @@ async function createJiraSource(env) {
|
|
|
1764
1828
|
const paths = getOpenPomePaths();
|
|
1765
1829
|
const localConfig = await readConfigIfPresent(paths.configFile);
|
|
1766
1830
|
const selectedBoardScope = getActiveJiraBoardScope(localConfig);
|
|
1831
|
+
const storedApiToken = await readStoredJiraApiToken();
|
|
1767
1832
|
const storedOAuth = await refreshStoredJiraOAuthIfNeeded(await readStoredJiraOAuth(), env);
|
|
1833
|
+
const connectorCredentials = {
|
|
1834
|
+
...(storedApiToken ? { [jiraApiTokenCredentialAccount]: storedApiToken } : {}),
|
|
1835
|
+
...(storedOAuth ? { [jiraOAuthCredentialAccount]: storedOAuth } : {})
|
|
1836
|
+
};
|
|
1768
1837
|
return workItemSourceRegistry.getActiveSource(env, {
|
|
1769
1838
|
activeScope: selectedBoardScope,
|
|
1770
|
-
connectorCredentials:
|
|
1839
|
+
connectorCredentials: Object.keys(connectorCredentials).length ? connectorCredentials : undefined
|
|
1771
1840
|
});
|
|
1772
1841
|
}
|
|
1773
1842
|
function normalizeModelProviderId(provider) {
|
|
@@ -1867,6 +1936,13 @@ async function readStoredJiraOAuth() {
|
|
|
1867
1936
|
}
|
|
1868
1937
|
return getJsonCredential(store, jiraOAuthCredentialAccount);
|
|
1869
1938
|
}
|
|
1939
|
+
async function readStoredJiraApiToken() {
|
|
1940
|
+
const store = createCredentialStore();
|
|
1941
|
+
if (!store.isAvailable()) {
|
|
1942
|
+
return undefined;
|
|
1943
|
+
}
|
|
1944
|
+
return getJsonCredential(store, jiraApiTokenCredentialAccount);
|
|
1945
|
+
}
|
|
1870
1946
|
async function readStoredGitHubOAuth() {
|
|
1871
1947
|
const store = createCredentialStore();
|
|
1872
1948
|
if (!store.isAvailable()) {
|
|
@@ -2064,6 +2140,13 @@ function getOpenPomePaths() {
|
|
|
2064
2140
|
configFile: join(homeDirectory, "config.json")
|
|
2065
2141
|
};
|
|
2066
2142
|
}
|
|
2143
|
+
function normalizeJiraBaseUrl(value) {
|
|
2144
|
+
const trimmed = value.trim().replace(/\/+$/u, "");
|
|
2145
|
+
if (!trimmed) {
|
|
2146
|
+
return "";
|
|
2147
|
+
}
|
|
2148
|
+
return /^https?:\/\//iu.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
2149
|
+
}
|
|
2067
2150
|
async function readConfigIfPresent(configFile) {
|
|
2068
2151
|
try {
|
|
2069
2152
|
const content = await readFile(configFile, "utf8");
|
|
@@ -2285,6 +2368,255 @@ async function readPackageScripts(packageFile) {
|
|
|
2285
2368
|
throw error;
|
|
2286
2369
|
}
|
|
2287
2370
|
}
|
|
2371
|
+
async function buildRepositoryKnowledge(workspace, now) {
|
|
2372
|
+
if (!workspace.path) {
|
|
2373
|
+
return undefined;
|
|
2374
|
+
}
|
|
2375
|
+
const workspacePath = workspace.path;
|
|
2376
|
+
const trackedFiles = await listWorkspaceFilesForKnowledge(workspacePath);
|
|
2377
|
+
const packageManager = detectPackageManager(workspacePath);
|
|
2378
|
+
const manifests = trackedFiles
|
|
2379
|
+
.filter((filePath) => basename(filePath) === "package.json")
|
|
2380
|
+
.filter((filePath) => !isGeneratedWorkspacePath(filePath) && !isSensitiveWorkspacePath(filePath))
|
|
2381
|
+
.sort((left, right) => left.localeCompare(right))
|
|
2382
|
+
.slice(0, 80);
|
|
2383
|
+
const rootScripts = await readPackageScripts(join(workspacePath, "package.json"));
|
|
2384
|
+
const packageNames = await readPackageNamesFromManifests(workspacePath, manifests);
|
|
2385
|
+
const codeowners = await readRepositoryCodeowners(workspacePath);
|
|
2386
|
+
const knowledgeFile = getRepositoryKnowledgeFile(workspacePath);
|
|
2387
|
+
const pathMap = buildRepositoryPathMap(trackedFiles);
|
|
2388
|
+
const knowledge = {
|
|
2389
|
+
schemaVersion: 1,
|
|
2390
|
+
createdAt: await readExistingRepositoryKnowledgeCreatedAt(knowledgeFile, now),
|
|
2391
|
+
updatedAt: now,
|
|
2392
|
+
workspace: {
|
|
2393
|
+
name: workspace.name,
|
|
2394
|
+
path: workspacePath,
|
|
2395
|
+
remoteUrls: workspace.remoteUrls,
|
|
2396
|
+
currentBranch: workspace.currentBranch
|
|
2397
|
+
},
|
|
2398
|
+
packageMap: {
|
|
2399
|
+
packageManager,
|
|
2400
|
+
packageNames: packageNames.length ? packageNames : workspace.packageNames ?? [],
|
|
2401
|
+
manifests,
|
|
2402
|
+
scripts: await buildRepositoryScriptMap(workspacePath, manifests, rootScripts),
|
|
2403
|
+
buildCommands: buildRepositoryCommands(packageManager, rootScripts, ["build", "compile"]),
|
|
2404
|
+
testCommands: buildRepositoryCommands(packageManager, rootScripts, ["test", "test:unit", "test:integration", "test:e2e"]),
|
|
2405
|
+
lintCommands: buildRepositoryCommands(packageManager, rootScripts, ["lint", "lint:fix"]),
|
|
2406
|
+
typecheckCommands: buildRepositoryCommands(packageManager, rootScripts, ["typecheck", "tsc"]),
|
|
2407
|
+
validateCommands: buildRepositoryCommands(packageManager, rootScripts, ["validate", "check", "ci"])
|
|
2408
|
+
},
|
|
2409
|
+
pathMap,
|
|
2410
|
+
moduleBoundaries: buildRepositoryModuleBoundaries(trackedFiles, packageNames),
|
|
2411
|
+
ownership: {
|
|
2412
|
+
codeownersFiles: codeowners.codeownersFiles,
|
|
2413
|
+
owners: codeowners.owners,
|
|
2414
|
+
signals: codeowners.signals
|
|
2415
|
+
},
|
|
2416
|
+
knowledgeFile
|
|
2417
|
+
};
|
|
2418
|
+
await mkdir(dirname(knowledgeFile), { recursive: true });
|
|
2419
|
+
await writeFile(knowledgeFile, `${JSON.stringify(knowledge, null, 2)}\n`, "utf8");
|
|
2420
|
+
return knowledge;
|
|
2421
|
+
}
|
|
2422
|
+
async function readExistingRepositoryKnowledgeCreatedAt(knowledgeFile, fallback) {
|
|
2423
|
+
try {
|
|
2424
|
+
const content = await readFile(knowledgeFile, "utf8");
|
|
2425
|
+
const parsed = JSON.parse(content);
|
|
2426
|
+
return typeof parsed.createdAt === "string" ? parsed.createdAt : fallback;
|
|
2427
|
+
}
|
|
2428
|
+
catch (error) {
|
|
2429
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2430
|
+
return fallback;
|
|
2431
|
+
}
|
|
2432
|
+
if (error instanceof SyntaxError) {
|
|
2433
|
+
return fallback;
|
|
2434
|
+
}
|
|
2435
|
+
throw error;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function getRepositoryKnowledgeFile(workspacePath) {
|
|
2439
|
+
return join(workspacePath, ".pome", "knowledge", "repository.json");
|
|
2440
|
+
}
|
|
2441
|
+
async function readPackageNamesFromManifests(workspacePath, manifests) {
|
|
2442
|
+
const names = [];
|
|
2443
|
+
for (const manifest of manifests.slice(0, 80)) {
|
|
2444
|
+
const name = await readPackageName(join(workspacePath, manifest));
|
|
2445
|
+
if (name) {
|
|
2446
|
+
names.push(name);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return uniqueStrings(names).slice(0, 80);
|
|
2450
|
+
}
|
|
2451
|
+
async function buildRepositoryScriptMap(workspacePath, manifests, rootScripts) {
|
|
2452
|
+
const scripts = { ...rootScripts };
|
|
2453
|
+
for (const manifest of manifests.filter((filePath) => filePath !== "package.json").slice(0, 40)) {
|
|
2454
|
+
const packageName = await readPackageName(join(workspacePath, manifest));
|
|
2455
|
+
const packageScripts = await readPackageScripts(join(workspacePath, manifest));
|
|
2456
|
+
const prefix = packageName ?? dirname(manifest);
|
|
2457
|
+
for (const [scriptName, script] of Object.entries(packageScripts)) {
|
|
2458
|
+
scripts[`${prefix}:${scriptName}`] = script;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return Object.fromEntries(Object.entries(scripts).slice(0, 120));
|
|
2462
|
+
}
|
|
2463
|
+
function buildRepositoryCommands(packageManager, scripts, preferredScriptNames) {
|
|
2464
|
+
return preferredScriptNames
|
|
2465
|
+
.filter((scriptName) => Boolean(scripts[scriptName]))
|
|
2466
|
+
.map((scriptName) => buildPackageScriptCommand(packageManager, scriptName))
|
|
2467
|
+
.slice(0, 8);
|
|
2468
|
+
}
|
|
2469
|
+
function buildRepositoryPathMap(files) {
|
|
2470
|
+
const source = [];
|
|
2471
|
+
const tests = [];
|
|
2472
|
+
const config = [];
|
|
2473
|
+
const generated = [];
|
|
2474
|
+
const sensitive = [];
|
|
2475
|
+
const docs = [];
|
|
2476
|
+
for (const filePath of files.slice(0, 2000)) {
|
|
2477
|
+
const normalized = filePath.replace(/\\/gu, "/");
|
|
2478
|
+
if (isSensitiveWorkspacePath(normalized)) {
|
|
2479
|
+
sensitive.push(normalized);
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
if (isGeneratedWorkspacePath(normalized)) {
|
|
2483
|
+
generated.push(normalized);
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
if (isDocumentationWorkspacePath(normalized)) {
|
|
2487
|
+
docs.push(normalized);
|
|
2488
|
+
}
|
|
2489
|
+
if (isConfigWorkspacePath(normalized)) {
|
|
2490
|
+
config.push(normalized);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
if (isTestLikeFile(normalized)) {
|
|
2494
|
+
tests.push(normalized);
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
if (isSourceWorkspacePath(normalized)) {
|
|
2498
|
+
source.push(normalized);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return {
|
|
2502
|
+
source: uniqueStrings(source).slice(0, 160),
|
|
2503
|
+
tests: uniqueStrings(tests).slice(0, 120),
|
|
2504
|
+
config: uniqueStrings(config).slice(0, 80),
|
|
2505
|
+
generated: uniqueStrings(generated).slice(0, 80),
|
|
2506
|
+
sensitive: uniqueStrings(sensitive).slice(0, 40),
|
|
2507
|
+
docs: uniqueStrings(docs).slice(0, 40)
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
function buildRepositoryModuleBoundaries(files, packageNames) {
|
|
2511
|
+
const boundaries = new Map();
|
|
2512
|
+
for (const filePath of files) {
|
|
2513
|
+
if (basename(filePath) === "package.json") {
|
|
2514
|
+
const directory = dirname(filePath);
|
|
2515
|
+
const normalizedDirectory = directory === "." ? "." : directory;
|
|
2516
|
+
const packageName = packageNames.find((name) => normalizeModuleName(name).includes(basename(normalizedDirectory).toLowerCase()));
|
|
2517
|
+
boundaries.set(normalizedDirectory, {
|
|
2518
|
+
name: packageName ?? (basename(normalizedDirectory) || "root"),
|
|
2519
|
+
path: normalizedDirectory,
|
|
2520
|
+
kind: inferModuleBoundaryKind(normalizedDirectory),
|
|
2521
|
+
reason: "Package manifest defines a build or runtime boundary."
|
|
2522
|
+
});
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
const topLevel = filePath.split("/")[0] ?? "";
|
|
2526
|
+
if (!topLevel || topLevel.startsWith(".")) {
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
if (/^(apps|services|packages|connectors|src|lib)$/u.test(topLevel)) {
|
|
2530
|
+
const parts = filePath.split("/");
|
|
2531
|
+
const boundaryPath = topLevel === "src" || topLevel === "lib" ? topLevel : parts.slice(0, 2).join("/");
|
|
2532
|
+
if (boundaryPath && !boundaries.has(boundaryPath)) {
|
|
2533
|
+
boundaries.set(boundaryPath, {
|
|
2534
|
+
name: basename(boundaryPath),
|
|
2535
|
+
path: boundaryPath,
|
|
2536
|
+
kind: inferModuleBoundaryKind(boundaryPath),
|
|
2537
|
+
reason: `Repository path suggests a ${inferModuleBoundaryKind(boundaryPath)} boundary.`
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
return [...boundaries.values()]
|
|
2543
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
2544
|
+
.slice(0, 80);
|
|
2545
|
+
}
|
|
2546
|
+
function inferModuleBoundaryKind(path) {
|
|
2547
|
+
if (path.startsWith("apps/")) {
|
|
2548
|
+
return "application";
|
|
2549
|
+
}
|
|
2550
|
+
if (path.startsWith("services/")) {
|
|
2551
|
+
return "service";
|
|
2552
|
+
}
|
|
2553
|
+
if (path.startsWith("packages/")) {
|
|
2554
|
+
return "package";
|
|
2555
|
+
}
|
|
2556
|
+
if (path.startsWith("connectors/")) {
|
|
2557
|
+
return "connector";
|
|
2558
|
+
}
|
|
2559
|
+
if (path === "src" || path.startsWith("src/") || path === "lib" || path.startsWith("lib/")) {
|
|
2560
|
+
return "source";
|
|
2561
|
+
}
|
|
2562
|
+
return "module";
|
|
2563
|
+
}
|
|
2564
|
+
function normalizeModuleName(value) {
|
|
2565
|
+
return value.toLowerCase().replace(/^@[^/]+\//u, "").replace(/[^a-z0-9]+/gu, "-");
|
|
2566
|
+
}
|
|
2567
|
+
async function readRepositoryCodeowners(workspacePath) {
|
|
2568
|
+
const codeownersFiles = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"];
|
|
2569
|
+
const foundFiles = [];
|
|
2570
|
+
const owners = [];
|
|
2571
|
+
const signals = [];
|
|
2572
|
+
for (const filePath of codeownersFiles) {
|
|
2573
|
+
const content = await readOptionalTextFile(join(workspacePath, filePath), 32_000);
|
|
2574
|
+
if (!content) {
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
foundFiles.push(filePath);
|
|
2578
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
2579
|
+
const trimmed = line.trim();
|
|
2580
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
const parts = trimmed.split(/\s+/u);
|
|
2584
|
+
const pathPattern = parts[0];
|
|
2585
|
+
const lineOwners = parts.slice(1).filter((owner) => owner.startsWith("@") || owner.includes("@"));
|
|
2586
|
+
owners.push(...lineOwners);
|
|
2587
|
+
if (pathPattern && lineOwners.length) {
|
|
2588
|
+
signals.push(`${pathPattern}: ${lineOwners.slice(0, 4).join(", ")}`);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return {
|
|
2593
|
+
codeownersFiles: foundFiles,
|
|
2594
|
+
owners: uniqueStrings(owners).slice(0, 40),
|
|
2595
|
+
signals: uniqueStrings(signals).slice(0, 40)
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
function isSourceWorkspacePath(filePath) {
|
|
2599
|
+
const lower = filePath.toLowerCase();
|
|
2600
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs|swift|scala|sh|sql)$/u.test(lower)
|
|
2601
|
+
&& !isConfigWorkspacePath(lower)
|
|
2602
|
+
&& !isGeneratedWorkspacePath(lower);
|
|
2603
|
+
}
|
|
2604
|
+
function isConfigWorkspacePath(filePath) {
|
|
2605
|
+
const lower = filePath.toLowerCase();
|
|
2606
|
+
return /(^|\/)(package\.json|pnpm-workspace\.yaml|tsconfig[^/]*\.json|vite\.config\.[cm]?[jt]s|vitest\.config\.[cm]?[jt]s|jest\.config\.[cm]?[jt]s|eslint\.config\.[cm]?[jt]s|\.eslintrc[^/]*|\.prettierrc[^/]*|dockerfile|docker-compose\.ya?ml|makefile|cargo\.toml|go\.mod|pom\.xml|gradle\.properties|build\.gradle|pyproject\.toml|requirements\.txt|ruff\.toml)$/u.test(lower)
|
|
2607
|
+
|| /(^|\/)(\.github\/workflows\/.+\.ya?ml|config\/.+\.(json|ya?ml|toml|ini))$/u.test(lower);
|
|
2608
|
+
}
|
|
2609
|
+
function isGeneratedWorkspacePath(filePath) {
|
|
2610
|
+
const lower = filePath.toLowerCase();
|
|
2611
|
+
return /(^|\/)(dist|build|coverage|out|target|\.next|\.nuxt|\.turbo|\.cache|vendor|node_modules|generated|__generated__)\//u.test(lower)
|
|
2612
|
+
|| /\.(tsbuildinfo|min\.js|min\.css|map)$/u.test(lower)
|
|
2613
|
+
|| /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?)$/u.test(lower);
|
|
2614
|
+
}
|
|
2615
|
+
function isDocumentationWorkspacePath(filePath) {
|
|
2616
|
+
const lower = filePath.toLowerCase();
|
|
2617
|
+
return /(^|\/)(readme|changelog|license|contributing)(\.[a-z0-9]+)?$/u.test(lower)
|
|
2618
|
+
|| lower.startsWith("docs/");
|
|
2619
|
+
}
|
|
2288
2620
|
async function readWorkspaceReadmeKeywords(directory) {
|
|
2289
2621
|
const readmeFiles = ["README.md", "README.txt", "readme.md"];
|
|
2290
2622
|
const keywords = [];
|
|
@@ -2737,6 +3069,7 @@ function selectArchivedTaskSession(sessions, sessionId) {
|
|
|
2737
3069
|
}
|
|
2738
3070
|
function buildPlanningContext(session) {
|
|
2739
3071
|
const workspace = session.workspaceCandidate?.workspace;
|
|
3072
|
+
const knowledge = session.repositoryKnowledge;
|
|
2740
3073
|
const missingRequirementSignals = detectMissingRequirementSignals(session.workItem);
|
|
2741
3074
|
const context = [
|
|
2742
3075
|
`Work item type: ${session.workItem.type}`,
|
|
@@ -2759,7 +3092,17 @@ function buildPlanningContext(session) {
|
|
|
2759
3092
|
workspace?.readmeKeywords?.length ? `README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2760
3093
|
workspace?.codeownersKeywords?.length ? `Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2761
3094
|
workspace?.recentBranches?.length ? `Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
2762
|
-
workspace?.recentCommitRefs?.length ? `Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined
|
|
3095
|
+
workspace?.recentCommitRefs?.length ? `Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined,
|
|
3096
|
+
knowledge ? `Repository knowledge file: ${knowledge.knowledgeFile}` : undefined,
|
|
3097
|
+
knowledge ? `Repository package manager: ${knowledge.packageMap.packageManager}` : undefined,
|
|
3098
|
+
knowledge?.packageMap.packageNames.length ? `Repository packages: ${knowledge.packageMap.packageNames.slice(0, 12).join(", ")}` : undefined,
|
|
3099
|
+
knowledge?.moduleBoundaries.length
|
|
3100
|
+
? `Module boundaries: ${knowledge.moduleBoundaries.slice(0, 12).map((boundary) => `${boundary.kind}:${boundary.path}`).join(", ")}`
|
|
3101
|
+
: undefined,
|
|
3102
|
+
knowledge ? `Path map: ${knowledge.pathMap.source.length} source, ${knowledge.pathMap.tests.length} test, ${knowledge.pathMap.config.length} config, ${knowledge.pathMap.generated.length} generated, ${knowledge.pathMap.sensitive.length} sensitive paths` : undefined,
|
|
3103
|
+
knowledge?.ownership.owners.length ? `Code owners: ${knowledge.ownership.owners.slice(0, 12).join(", ")}` : undefined,
|
|
3104
|
+
knowledge?.packageMap.validateCommands.length ? `Validation commands: ${knowledge.packageMap.validateCommands.join(", ")}` : undefined,
|
|
3105
|
+
knowledge?.packageMap.testCommands.length ? `Test commands: ${knowledge.packageMap.testCommands.join(", ")}` : undefined
|
|
2763
3106
|
];
|
|
2764
3107
|
return context.filter((item) => Boolean(item));
|
|
2765
3108
|
}
|
|
@@ -2813,6 +3156,307 @@ function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
|
2813
3156
|
missingInfo
|
|
2814
3157
|
};
|
|
2815
3158
|
}
|
|
3159
|
+
function buildWorkItemIntelligenceReport(workItem, workspaceCandidate, repositoryKnowledge) {
|
|
3160
|
+
const acceptanceCriteria = extractAcceptanceCriteria(workItem);
|
|
3161
|
+
const missingQuestions = detectMissingRequirementSignals(workItem);
|
|
3162
|
+
const linkedReferences = summarizeLinkedReferences(workItem);
|
|
3163
|
+
const likelyFiles = inferLikelyFileHints(workItem, workspaceCandidate, repositoryKnowledge);
|
|
3164
|
+
const dependencies = inferDependencySignals(workItem, workspaceCandidate, repositoryKnowledge);
|
|
3165
|
+
const risks = inferWorkItemRisks(workItem, workspaceCandidate, acceptanceCriteria, likelyFiles);
|
|
3166
|
+
return {
|
|
3167
|
+
summary: summarizeWorkItem(workItem),
|
|
3168
|
+
acceptanceCriteria,
|
|
3169
|
+
missingQuestions,
|
|
3170
|
+
affectedRepositories: workspaceCandidate
|
|
3171
|
+
? [{
|
|
3172
|
+
name: workspaceCandidate.workspace.name,
|
|
3173
|
+
path: workspaceCandidate.workspace.path,
|
|
3174
|
+
reasons: workspaceCandidate.reasons.slice(0, 6)
|
|
3175
|
+
}]
|
|
3176
|
+
: [],
|
|
3177
|
+
likelyFiles,
|
|
3178
|
+
linkedReferences,
|
|
3179
|
+
dependencies,
|
|
3180
|
+
risks,
|
|
3181
|
+
testStrategy: inferTestStrategy(workItem, workspaceCandidate, likelyFiles, repositoryKnowledge),
|
|
3182
|
+
deliveryChecklist: buildDeliveryChecklist(workItem)
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function summarizeWorkItem(workItem) {
|
|
3186
|
+
const displayType = `${workItem.type.charAt(0).toUpperCase()}${workItem.type.slice(1)}`;
|
|
3187
|
+
const fragments = [
|
|
3188
|
+
`${displayType} ${workItem.key}`,
|
|
3189
|
+
`is currently ${workItem.status}`,
|
|
3190
|
+
workItem.priority ? `with ${workItem.priority} priority` : undefined,
|
|
3191
|
+
`and asks for: ${workItem.title}`
|
|
3192
|
+
].filter((item) => Boolean(item));
|
|
3193
|
+
return `${fragments.join(" ")}.`;
|
|
3194
|
+
}
|
|
3195
|
+
function extractAcceptanceCriteria(workItem) {
|
|
3196
|
+
const description = workItem.description?.trim();
|
|
3197
|
+
if (!description) {
|
|
3198
|
+
return [];
|
|
3199
|
+
}
|
|
3200
|
+
const lines = description
|
|
3201
|
+
.split(/\r?\n/u)
|
|
3202
|
+
.map((line) => line.trim())
|
|
3203
|
+
.filter(Boolean);
|
|
3204
|
+
const criteria = [];
|
|
3205
|
+
let collecting = false;
|
|
3206
|
+
for (const line of lines) {
|
|
3207
|
+
const normalized = line.toLowerCase();
|
|
3208
|
+
if (/\b(acceptance criteria|success criteria|definition of done|expected result|expected behavior|validation)\b/u.test(normalized)) {
|
|
3209
|
+
collecting = true;
|
|
3210
|
+
const inline = line.split(/:\s*/u).slice(1).join(":").trim();
|
|
3211
|
+
if (inline) {
|
|
3212
|
+
criteria.push(cleanCriteriaLine(inline));
|
|
3213
|
+
}
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
if (collecting) {
|
|
3217
|
+
if (/^[a-z][a-z ]{1,30}:$/u.test(normalized) && !/\b(given|when|then|should|must|verify|validate|done|expected)\b/u.test(normalized)) {
|
|
3218
|
+
collecting = false;
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
if (/^[-*•\d.)\s]+/u.test(line) || /\b(given|when|then|should|must|verify|validate|done when|expected)\b/u.test(normalized)) {
|
|
3222
|
+
criteria.push(cleanCriteriaLine(line));
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
else if (/\b(given\b.*\bwhen\b.*\bthen|should|must|verify|validate|done when|expected)\b/su.test(normalized)) {
|
|
3226
|
+
criteria.push(cleanCriteriaLine(line));
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
return uniqueStrings(criteria).slice(0, 8);
|
|
3230
|
+
}
|
|
3231
|
+
function cleanCriteriaLine(line) {
|
|
3232
|
+
return line
|
|
3233
|
+
.replace(/^[-*•\d.)\s]+/u, "")
|
|
3234
|
+
.trim();
|
|
3235
|
+
}
|
|
3236
|
+
function summarizeLinkedReferences(workItem) {
|
|
3237
|
+
return (workItem.links ?? []).map((link) => ({
|
|
3238
|
+
kind: link.kind,
|
|
3239
|
+
title: link.title ?? link.url,
|
|
3240
|
+
url: link.url
|
|
3241
|
+
})).slice(0, 10);
|
|
3242
|
+
}
|
|
3243
|
+
function inferLikelyFileHints(workItem, workspaceCandidate, repositoryKnowledge) {
|
|
3244
|
+
const hints = [];
|
|
3245
|
+
const workspace = workspaceCandidate?.workspace;
|
|
3246
|
+
const tokens = tokenizePatchSearchText([
|
|
3247
|
+
workItem.key,
|
|
3248
|
+
workItem.title,
|
|
3249
|
+
workItem.description,
|
|
3250
|
+
...(workItem.labels ?? []),
|
|
3251
|
+
...(workItem.components ?? []),
|
|
3252
|
+
...(repositoryKnowledge?.packageMap.packageNames ?? []),
|
|
3253
|
+
...(repositoryKnowledge?.moduleBoundaries.map((boundary) => `${boundary.name} ${boundary.path}`) ?? [])
|
|
3254
|
+
].filter((value) => Boolean(value)).join(" "));
|
|
3255
|
+
for (const link of workItem.links ?? []) {
|
|
3256
|
+
if (link.kind !== "code" && link.kind !== "pull_request") {
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
const path = inferCodePathFromUrl(link.url);
|
|
3260
|
+
if (path) {
|
|
3261
|
+
hints.push({
|
|
3262
|
+
path,
|
|
3263
|
+
reason: `${link.kind === "pull_request" ? "Linked pull request" : "Linked code"} references this path.`
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
for (const component of workItem.components ?? []) {
|
|
3268
|
+
const slug = slugifyPathSegment(component);
|
|
3269
|
+
if (slug) {
|
|
3270
|
+
hints.push({
|
|
3271
|
+
path: `**/${slug}/**`,
|
|
3272
|
+
reason: `Jira component "${component}" should narrow the code area.`
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
for (const label of workItem.labels ?? []) {
|
|
3277
|
+
const slug = slugifyPathSegment(label);
|
|
3278
|
+
if (slug) {
|
|
3279
|
+
hints.push({
|
|
3280
|
+
path: `**/${slug}/**`,
|
|
3281
|
+
reason: `Jira label "${label}" may map to package, module, or test names.`
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
const title = workItem.title.toLowerCase();
|
|
3286
|
+
if (/\b(test|spec|validation|qa)\b/u.test(title)) {
|
|
3287
|
+
hints.push({ path: "**/*.{test,spec}.*", reason: "Story title mentions tests or validation." });
|
|
3288
|
+
}
|
|
3289
|
+
if (/\b(api|endpoint|controller|route)\b/u.test(title)) {
|
|
3290
|
+
hints.push({ path: "**/{api,routes,controllers}/**", reason: "Story title mentions API or endpoint work." });
|
|
3291
|
+
}
|
|
3292
|
+
if (/\b(ui|screen|page|component|button|form)\b/u.test(title)) {
|
|
3293
|
+
hints.push({ path: "**/{components,pages,app,src}/**", reason: "Story title mentions UI or component work." });
|
|
3294
|
+
}
|
|
3295
|
+
if (/\b(config|setting|env|feature flag|flag)\b/u.test(title)) {
|
|
3296
|
+
hints.push({ path: "**/{config,.github,scripts}/**", reason: "Story title mentions configuration or flags." });
|
|
3297
|
+
}
|
|
3298
|
+
if (workspace?.packageNames?.length) {
|
|
3299
|
+
for (const packageName of workspace.packageNames.slice(0, 4)) {
|
|
3300
|
+
hints.push({
|
|
3301
|
+
path: packageName,
|
|
3302
|
+
reason: "Workspace package metadata is relevant to this task."
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (repositoryKnowledge) {
|
|
3307
|
+
for (const boundary of rankKnowledgeModuleBoundaries(repositoryKnowledge, tokens).slice(0, 5)) {
|
|
3308
|
+
hints.push({
|
|
3309
|
+
path: boundary.path,
|
|
3310
|
+
reason: `Repository knowledge identifies ${boundary.kind} boundary "${boundary.name}" as relevant.`
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
for (const filePath of rankKnowledgePaths(repositoryKnowledge, tokens).slice(0, 8)) {
|
|
3314
|
+
hints.push({
|
|
3315
|
+
path: filePath,
|
|
3316
|
+
reason: "Repository knowledge matched this tracked path to the story text, labels, components, or plan signals."
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (workspace?.path && hints.length === 0) {
|
|
3321
|
+
hints.push({
|
|
3322
|
+
path: workspace.path,
|
|
3323
|
+
reason: "Selected workspace is the current best code context; repository knowledge will narrow files further."
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
return uniqueFileHints(hints).slice(0, 10);
|
|
3327
|
+
}
|
|
3328
|
+
function rankKnowledgeModuleBoundaries(repositoryKnowledge, tokens) {
|
|
3329
|
+
return repositoryKnowledge.moduleBoundaries
|
|
3330
|
+
.map((boundary) => ({
|
|
3331
|
+
boundary,
|
|
3332
|
+
score: scoreKnowledgeText(`${boundary.name} ${boundary.path} ${boundary.kind}`, tokens)
|
|
3333
|
+
}))
|
|
3334
|
+
.filter((candidate) => candidate.score > 0)
|
|
3335
|
+
.sort((left, right) => right.score - left.score || left.boundary.path.localeCompare(right.boundary.path))
|
|
3336
|
+
.map((candidate) => candidate.boundary);
|
|
3337
|
+
}
|
|
3338
|
+
function rankKnowledgePaths(repositoryKnowledge, tokens) {
|
|
3339
|
+
return [
|
|
3340
|
+
...repositoryKnowledge.pathMap.source,
|
|
3341
|
+
...repositoryKnowledge.pathMap.tests,
|
|
3342
|
+
...repositoryKnowledge.pathMap.config
|
|
3343
|
+
]
|
|
3344
|
+
.map((filePath) => ({
|
|
3345
|
+
filePath,
|
|
3346
|
+
score: scoreKnowledgeText(filePath, tokens)
|
|
3347
|
+
+ (isTestLikeFile(filePath) ? 3 : 0)
|
|
3348
|
+
+ (isConfigWorkspacePath(filePath) ? 1 : 0)
|
|
3349
|
+
}))
|
|
3350
|
+
.filter((candidate) => candidate.score > 0)
|
|
3351
|
+
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath))
|
|
3352
|
+
.map((candidate) => candidate.filePath);
|
|
3353
|
+
}
|
|
3354
|
+
function scoreKnowledgeText(value, tokens) {
|
|
3355
|
+
const lower = value.toLowerCase();
|
|
3356
|
+
let score = 0;
|
|
3357
|
+
for (const token of tokens) {
|
|
3358
|
+
if (token.length >= 3 && lower.includes(token)) {
|
|
3359
|
+
score += token.includes("/") ? 8 : 4;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
return score;
|
|
3363
|
+
}
|
|
3364
|
+
function inferDependencySignals(workItem, workspaceCandidate, repositoryKnowledge) {
|
|
3365
|
+
const workspace = workspaceCandidate?.workspace;
|
|
3366
|
+
const signals = [
|
|
3367
|
+
workItem.parentKey ? `Parent work item: ${workItem.parentKey}` : undefined,
|
|
3368
|
+
workItem.subtasks?.length ? `Subtasks: ${workItem.subtasks.map((subtask) => `${subtask.key} ${subtask.status}`).join(", ")}` : undefined,
|
|
3369
|
+
workItem.components?.length ? `Components: ${workItem.components.join(", ")}` : undefined,
|
|
3370
|
+
workItem.labels?.length ? `Labels: ${workItem.labels.join(", ")}` : undefined,
|
|
3371
|
+
workspace?.packageNames?.length ? `Workspace packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
3372
|
+
workspace?.codeownersKeywords?.length ? `Ownership signals: ${workspace.codeownersKeywords.slice(0, 8).join(", ")}` : undefined,
|
|
3373
|
+
workspace?.recentCommitRefs?.length ? `Recent related work refs: ${workspace.recentCommitRefs.slice(0, 8).join(", ")}` : undefined,
|
|
3374
|
+
repositoryKnowledge?.moduleBoundaries.length
|
|
3375
|
+
? `Module boundaries: ${repositoryKnowledge.moduleBoundaries.slice(0, 8).map((boundary) => `${boundary.kind}:${boundary.path}`).join(", ")}`
|
|
3376
|
+
: undefined,
|
|
3377
|
+
repositoryKnowledge?.ownership.owners.length ? `Code owners: ${repositoryKnowledge.ownership.owners.slice(0, 8).join(", ")}` : undefined,
|
|
3378
|
+
repositoryKnowledge?.packageMap.validateCommands.length
|
|
3379
|
+
? `Validation commands: ${repositoryKnowledge.packageMap.validateCommands.join(", ")}`
|
|
3380
|
+
: undefined
|
|
3381
|
+
];
|
|
3382
|
+
return signals.filter((signal) => Boolean(signal)).slice(0, 10);
|
|
3383
|
+
}
|
|
3384
|
+
function inferWorkItemRisks(workItem, workspaceCandidate, acceptanceCriteria, likelyFiles) {
|
|
3385
|
+
const risks = [
|
|
3386
|
+
!workspaceCandidate ? "No codebase is selected yet; implementation cannot safely start." : undefined,
|
|
3387
|
+
workspaceCandidate && workspaceCandidate.confidence < 0.6 ? "Codebase match is weak; confirm workspace before editing files." : undefined,
|
|
3388
|
+
acceptanceCriteria.length === 0 ? "Acceptance criteria are not explicit; confirm completion rules before coding." : undefined,
|
|
3389
|
+
likelyFiles.length === 0 ? "No likely files were identified; repository knowledge should be built before a broad change." : undefined,
|
|
3390
|
+
/high|highest|blocker|critical/u.test((workItem.priority ?? "").toLowerCase()) ? `Priority is ${workItem.priority}; keep the change small and validation evidence strong.` : undefined,
|
|
3391
|
+
workItem.type === "bug" && detectMissingRequirementSignals(workItem).some((signal) => signal.includes("reproduction")) ? "Bug report lacks a clear reproduction path." : undefined,
|
|
3392
|
+
(workItem.subtasks?.length ?? 0) > 0 ? "Subtasks may affect delivery order and Jira completion update." : undefined
|
|
3393
|
+
];
|
|
3394
|
+
return risks.filter((risk) => Boolean(risk)).slice(0, 8);
|
|
3395
|
+
}
|
|
3396
|
+
function inferTestStrategy(workItem, workspaceCandidate, likelyFiles, repositoryKnowledge) {
|
|
3397
|
+
const strategy = [
|
|
3398
|
+
workspaceCandidate?.workspace.path ? "Discover validation commands from the selected workspace." : "Resolve a workspace before selecting tests.",
|
|
3399
|
+
repositoryKnowledge?.packageMap.validateCommands.length
|
|
3400
|
+
? `Prefer repository validation: ${repositoryKnowledge.packageMap.validateCommands.slice(0, 3).join(", ")}.`
|
|
3401
|
+
: undefined,
|
|
3402
|
+
repositoryKnowledge?.packageMap.testCommands.length
|
|
3403
|
+
? `Run test command candidates: ${repositoryKnowledge.packageMap.testCommands.slice(0, 3).join(", ")}.`
|
|
3404
|
+
: undefined,
|
|
3405
|
+
repositoryKnowledge?.pathMap.tests.length
|
|
3406
|
+
? "Repository knowledge found related test paths; run the narrowest affected test before broad validation."
|
|
3407
|
+
: undefined,
|
|
3408
|
+
likelyFiles.some((file) => /\b(test|spec)\b/u.test(file.path)) ? "Run the related test file candidate first." : undefined,
|
|
3409
|
+
workItem.type === "bug" ? "Add or run a regression test that reproduces the reported behavior." : undefined,
|
|
3410
|
+
workItem.type === "story" || workItem.type === "task" ? "Run the smallest relevant test plus the workspace validation command." : undefined,
|
|
3411
|
+
"Capture test evidence before PR creation.",
|
|
3412
|
+
"If validation fails, use the approved AI retry loop before broadening the patch."
|
|
3413
|
+
];
|
|
3414
|
+
return strategy.filter((item) => Boolean(item));
|
|
3415
|
+
}
|
|
3416
|
+
function buildDeliveryChecklist(workItem) {
|
|
3417
|
+
return [
|
|
3418
|
+
`Keep ${workItem.key} in branch, commit, and PR title when possible.`,
|
|
3419
|
+
"Summarize implementation scope in the PR body.",
|
|
3420
|
+
"Include validation evidence from approved test runs.",
|
|
3421
|
+
"Call out risks, missing context, or manual QA needs.",
|
|
3422
|
+
"Post the Jira update only after reviewing the prepared message."
|
|
3423
|
+
];
|
|
3424
|
+
}
|
|
3425
|
+
function inferCodePathFromUrl(value) {
|
|
3426
|
+
try {
|
|
3427
|
+
const url = new URL(value);
|
|
3428
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
3429
|
+
const markerIndex = parts.findIndex((part) => part === "blob" || part === "tree");
|
|
3430
|
+
if (markerIndex >= 0 && parts.length > markerIndex + 2) {
|
|
3431
|
+
return parts.slice(markerIndex + 2).join("/");
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
catch {
|
|
3435
|
+
// Non-URL code references are allowed below.
|
|
3436
|
+
}
|
|
3437
|
+
const trimmed = value.trim();
|
|
3438
|
+
if (/^[\w./-]+\.[a-z0-9]+(?::\d+)?$/iu.test(trimmed) && !trimmed.includes("://")) {
|
|
3439
|
+
return trimmed.replace(/:\d+$/u, "");
|
|
3440
|
+
}
|
|
3441
|
+
return undefined;
|
|
3442
|
+
}
|
|
3443
|
+
function slugifyPathSegment(value) {
|
|
3444
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "");
|
|
3445
|
+
return slug.length >= 3 ? slug : undefined;
|
|
3446
|
+
}
|
|
3447
|
+
function uniqueFileHints(values) {
|
|
3448
|
+
const seen = new Set();
|
|
3449
|
+
const result = [];
|
|
3450
|
+
for (const value of values) {
|
|
3451
|
+
const key = value.path.toLowerCase();
|
|
3452
|
+
if (seen.has(key)) {
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
seen.add(key);
|
|
3456
|
+
result.push(value);
|
|
3457
|
+
}
|
|
3458
|
+
return result;
|
|
3459
|
+
}
|
|
2816
3460
|
function hasExplicitAcceptanceCriteria(workItem) {
|
|
2817
3461
|
const text = [workItem.title, workItem.description].filter(Boolean).join("\n").toLowerCase();
|
|
2818
3462
|
return /\b(acceptance criteria|acceptance|criteria|given\b.*\bwhen\b.*\bthen|expected result|definition of done|done when|should|expected behavior|success criteria|verify|validation)\b/su.test(text);
|
|
@@ -3097,6 +3741,19 @@ const sensitiveContentPatterns = [
|
|
|
3097
3741
|
];
|
|
3098
3742
|
async function collectPatchContextFiles(workspacePath, session) {
|
|
3099
3743
|
const candidates = [];
|
|
3744
|
+
const tokens = tokenizePatchSearchText([
|
|
3745
|
+
session.workItem.key,
|
|
3746
|
+
session.workItem.title,
|
|
3747
|
+
session.workItem.description,
|
|
3748
|
+
session.plan?.summary,
|
|
3749
|
+
...(session.plan?.steps.map((step) => `${step.title} ${step.detail ?? ""}`) ?? []),
|
|
3750
|
+
...(session.workItem.labels ?? []),
|
|
3751
|
+
...(session.workItem.components ?? []),
|
|
3752
|
+
...(session.workspaceCandidate?.workspace.packageNames ?? []),
|
|
3753
|
+
...(session.workspaceCandidate?.workspace.readmeKeywords ?? []),
|
|
3754
|
+
...(session.repositoryKnowledge?.packageMap.packageNames ?? []),
|
|
3755
|
+
...(session.repositoryKnowledge?.moduleBoundaries.map((boundary) => `${boundary.name} ${boundary.path}`) ?? [])
|
|
3756
|
+
].filter((value) => Boolean(value)).join(" "));
|
|
3100
3757
|
for (const filePath of session.plan?.filesLikelyChanged ?? []) {
|
|
3101
3758
|
const normalized = normalizeWorkspaceRelativePath(workspacePath, filePath, "skip");
|
|
3102
3759
|
if (normalized && normalized !== ".") {
|
|
@@ -3108,18 +3765,33 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
3108
3765
|
}
|
|
3109
3766
|
}
|
|
3110
3767
|
candidates.push({ filePath: "package.json", score: 24, reason: "Package metadata helps infer scripts, package boundaries, and runtime." }, { filePath: "README.md", score: 18, reason: "README gives repository purpose and local validation hints." }, { filePath: "AGENTS.md", score: 18, reason: "Agent instructions constrain safe implementation style." }, { filePath: "CODEOWNERS", score: 14, reason: "Ownership metadata helps identify relevant domains and review paths." });
|
|
3768
|
+
if (session.repositoryKnowledge) {
|
|
3769
|
+
for (const boundary of rankKnowledgeModuleBoundaries(session.repositoryKnowledge, tokens).slice(0, 8)) {
|
|
3770
|
+
candidates.push({
|
|
3771
|
+
filePath: boundary.path,
|
|
3772
|
+
score: 34,
|
|
3773
|
+
reason: `Repository knowledge selected ${boundary.kind} boundary "${boundary.name}".`
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
for (const filePath of rankKnowledgePaths(session.repositoryKnowledge, tokens).slice(0, 30)) {
|
|
3777
|
+
candidates.push({
|
|
3778
|
+
filePath,
|
|
3779
|
+
score: 32 + scorePatchContextFile(filePath, tokens, new Set()),
|
|
3780
|
+
reason: "Repository knowledge ranked this tracked file for the current story."
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3783
|
+
for (const filePath of [
|
|
3784
|
+
...session.repositoryKnowledge.pathMap.config.slice(0, 12),
|
|
3785
|
+
...session.repositoryKnowledge.pathMap.tests.slice(0, 12)
|
|
3786
|
+
]) {
|
|
3787
|
+
candidates.push({
|
|
3788
|
+
filePath,
|
|
3789
|
+
score: 18,
|
|
3790
|
+
reason: "Repository knowledge includes this as relevant configuration or validation context."
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3111
3794
|
const trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
3112
|
-
const tokens = tokenizePatchSearchText([
|
|
3113
|
-
session.workItem.key,
|
|
3114
|
-
session.workItem.title,
|
|
3115
|
-
session.workItem.description,
|
|
3116
|
-
session.plan?.summary,
|
|
3117
|
-
...(session.plan?.steps.map((step) => `${step.title} ${step.detail ?? ""}`) ?? []),
|
|
3118
|
-
...(session.workItem.labels ?? []),
|
|
3119
|
-
...(session.workItem.components ?? []),
|
|
3120
|
-
...(session.workspaceCandidate?.workspace.packageNames ?? []),
|
|
3121
|
-
...(session.workspaceCandidate?.workspace.readmeKeywords ?? [])
|
|
3122
|
-
].filter((value) => Boolean(value)).join(" "));
|
|
3123
3795
|
const planHints = new Set((session.plan?.filesLikelyChanged ?? [])
|
|
3124
3796
|
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
3125
3797
|
.filter((filePath) => Boolean(filePath)));
|
|
@@ -3180,10 +3852,21 @@ async function listTrackedWorkspaceFiles(workspacePath) {
|
|
|
3180
3852
|
.slice(0, 1000);
|
|
3181
3853
|
return trackedFiles.length > 0 ? trackedFiles : listWorkspaceFilesFallback(workspacePath);
|
|
3182
3854
|
}
|
|
3183
|
-
async function
|
|
3855
|
+
async function listWorkspaceFilesForKnowledge(workspacePath) {
|
|
3856
|
+
const output = await runGit(workspacePath, ["ls-files"]);
|
|
3857
|
+
const trackedFiles = output
|
|
3858
|
+
.split(/\r?\n/u)
|
|
3859
|
+
.map((line) => line.trim())
|
|
3860
|
+
.filter(Boolean)
|
|
3861
|
+
.filter((filePath) => !filePath.startsWith(".git/") && !filePath.startsWith("node_modules/") && !filePath.startsWith(".pome/"))
|
|
3862
|
+
.slice(0, 2000);
|
|
3863
|
+
return trackedFiles.length > 0 ? trackedFiles : listWorkspaceFilesFallback(workspacePath, { includeGenerated: true, includeSensitive: true });
|
|
3864
|
+
}
|
|
3865
|
+
async function listWorkspaceFilesFallback(workspacePath, options = {}) {
|
|
3184
3866
|
const collected = [];
|
|
3185
3867
|
const queue = ["."];
|
|
3186
|
-
const ignoredDirectories = new Set([".git", "
|
|
3868
|
+
const ignoredDirectories = new Set([".git", ".pome", "node_modules", "vendor"]);
|
|
3869
|
+
const generatedDirectories = new Set(["dist", "build", "coverage", ".next", ".turbo", ".cache", "out", "target", "generated", "__generated__"]);
|
|
3187
3870
|
while (queue.length > 0 && collected.length < 1000) {
|
|
3188
3871
|
const current = queue.shift() ?? ".";
|
|
3189
3872
|
const absoluteCurrent = resolve(workspacePath, current);
|
|
@@ -3197,12 +3880,15 @@ async function listWorkspaceFilesFallback(workspacePath) {
|
|
|
3197
3880
|
for await (const entry of directory) {
|
|
3198
3881
|
const relativePath = current === "." ? entry.name : `${current}/${entry.name}`;
|
|
3199
3882
|
if (entry.isDirectory()) {
|
|
3200
|
-
|
|
3883
|
+
const shouldSkipGenerated = !options.includeGenerated && generatedDirectories.has(entry.name);
|
|
3884
|
+
const shouldSkipSensitive = !options.includeSensitive && isSensitiveWorkspacePath(relativePath);
|
|
3885
|
+
if (!ignoredDirectories.has(entry.name) && !shouldSkipGenerated && !shouldSkipSensitive) {
|
|
3201
3886
|
queue.push(relativePath);
|
|
3202
3887
|
}
|
|
3203
3888
|
continue;
|
|
3204
3889
|
}
|
|
3205
|
-
|
|
3890
|
+
const shouldSkipSensitive = !options.includeSensitive && isSensitiveWorkspacePath(relativePath);
|
|
3891
|
+
if (entry.isFile() && !shouldSkipSensitive) {
|
|
3206
3892
|
collected.push(relativePath);
|
|
3207
3893
|
if (collected.length >= 1000) {
|
|
3208
3894
|
break;
|
|
@@ -3276,6 +3962,7 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
3276
3962
|
...(plan?.missingInfo ?? [])
|
|
3277
3963
|
])).slice(0, 8);
|
|
3278
3964
|
const workspace = session.workspaceCandidate?.workspace;
|
|
3965
|
+
const knowledge = session.repositoryKnowledge;
|
|
3279
3966
|
return [
|
|
3280
3967
|
"You are OpenPome's implementation engine.",
|
|
3281
3968
|
failedTestContext.length
|
|
@@ -3322,6 +4009,18 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
3322
4009
|
workspace?.recentBranches?.length ? `- Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
3323
4010
|
workspace?.recentCommitRefs?.length ? `- Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined,
|
|
3324
4011
|
"",
|
|
4012
|
+
knowledge ? "Repository knowledge:" : undefined,
|
|
4013
|
+
knowledge ? `- Knowledge file: ${knowledge.knowledgeFile}` : undefined,
|
|
4014
|
+
knowledge ? `- Package manager: ${knowledge.packageMap.packageManager}` : undefined,
|
|
4015
|
+
knowledge?.packageMap.packageNames.length ? `- Packages: ${knowledge.packageMap.packageNames.slice(0, 12).join(", ")}` : undefined,
|
|
4016
|
+
knowledge?.moduleBoundaries.length
|
|
4017
|
+
? `- Module boundaries: ${knowledge.moduleBoundaries.slice(0, 12).map((boundary) => `${boundary.kind}:${boundary.path}`).join("; ")}`
|
|
4018
|
+
: undefined,
|
|
4019
|
+
knowledge ? `- Path map: ${knowledge.pathMap.source.length} source, ${knowledge.pathMap.tests.length} test, ${knowledge.pathMap.config.length} config, ${knowledge.pathMap.generated.length} generated, ${knowledge.pathMap.sensitive.length} sensitive` : undefined,
|
|
4020
|
+
knowledge?.ownership.owners.length ? `- Code owners: ${knowledge.ownership.owners.slice(0, 12).join(", ")}` : undefined,
|
|
4021
|
+
knowledge?.packageMap.validateCommands.length ? `- Validation commands: ${knowledge.packageMap.validateCommands.join(", ")}` : undefined,
|
|
4022
|
+
knowledge?.packageMap.testCommands.length ? `- Test commands: ${knowledge.packageMap.testCommands.join(", ")}` : undefined,
|
|
4023
|
+
knowledge ? "" : undefined,
|
|
3325
4024
|
"Readable context files:",
|
|
3326
4025
|
context || "No source files were safely included. You may propose small new files only if the task clearly asks for them."
|
|
3327
4026
|
].filter((line) => Boolean(line)).join("\n");
|
|
@@ -3502,7 +4201,9 @@ async function discoverTestCommandCandidates(workspacePath, session) {
|
|
|
3502
4201
|
async function findRelatedTestFiles(workspacePath, session) {
|
|
3503
4202
|
let trackedFiles = [];
|
|
3504
4203
|
try {
|
|
3505
|
-
trackedFiles =
|
|
4204
|
+
trackedFiles = session.repositoryKnowledge?.pathMap.tests.length
|
|
4205
|
+
? session.repositoryKnowledge.pathMap.tests
|
|
4206
|
+
: await listTrackedWorkspaceFiles(workspacePath);
|
|
3506
4207
|
}
|
|
3507
4208
|
catch {
|
|
3508
4209
|
return [];
|
|
@@ -3518,6 +4219,8 @@ async function findRelatedTestFiles(workspacePath, session) {
|
|
|
3518
4219
|
session.workItem.description,
|
|
3519
4220
|
...(session.workItem.labels ?? []),
|
|
3520
4221
|
...(session.workItem.components ?? []),
|
|
4222
|
+
...(session.repositoryKnowledge?.packageMap.packageNames ?? []),
|
|
4223
|
+
...(session.repositoryKnowledge?.moduleBoundaries.map((boundary) => `${boundary.name} ${boundary.path}`) ?? []),
|
|
3521
4224
|
session.plan?.summary
|
|
3522
4225
|
].filter((value) => Boolean(value)).join(" "))
|
|
3523
4226
|
]);
|