ralphctl 0.4.1 → 0.4.3
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/README.md +13 -11
- package/dist/{add-CIM72NE3.mjs → add-MG26JWBP.mjs} +6 -6
- package/dist/{add-GX7P7XTT.mjs → add-ZZYL4BSF.mjs} +5 -4
- package/dist/chunk-2FT37OZX.mjs +1071 -0
- package/dist/{chunk-CTP2A436.mjs → chunk-D2HWXEHH.mjs} +9 -2
- package/dist/{chunk-JOQO4HMM.mjs → chunk-EGUFQNRB.mjs} +10 -10
- package/dist/{chunk-3HJNVQ7N.mjs → chunk-LCY32RW4.mjs} +621 -976
- package/dist/{chunk-NUYQK5MN.mjs → chunk-LDSG7G2T.mjs} +1 -1
- package/dist/{chunk-7JLZQICD.mjs → chunk-MDE6KPJQ.mjs} +6 -6
- package/dist/{chunk-3QBEBKMZ.mjs → chunk-Q4AVHUZL.mjs} +7 -7
- package/dist/{chunk-YCDUVPRT.mjs → chunk-RQGD5WS6.mjs} +4 -72
- package/dist/{chunk-D2YGPLIV.mjs → chunk-TDBEEHTS.mjs} +213 -8
- package/dist/{chunk-SM4GGZSU.mjs → chunk-WOMGKKZY.mjs} +152 -179
- package/dist/{chunk-FKMKOWLA.mjs → chunk-WZTY77GY.mjs} +75 -1
- package/dist/cli.mjs +68 -19
- package/dist/{create-7WFSCMP4.mjs → create-PQK6KKRD.mjs} +5 -5
- package/dist/{handle-BBAZJ44Y.mjs → handle-SYVCFI6Y.mjs} +1 -1
- package/dist/{mount-2N6H5CWA.mjs → mount-2ANLHHQE.mjs} +556 -318
- package/dist/{project-2IE7VWDB.mjs → project-JF47ZWMF.mjs} +2 -2
- package/dist/prompts/check-script-discover.md +69 -0
- package/dist/prompts/ideate-auto.md +26 -1
- package/dist/prompts/ideate.md +5 -1
- package/dist/prompts/plan-auto.md +30 -2
- package/dist/prompts/plan-common-examples.md +82 -0
- package/dist/prompts/plan-common.md +26 -78
- package/dist/prompts/plan-interactive.md +6 -2
- package/dist/prompts/repo-onboard.md +111 -0
- package/dist/prompts/sprint-feedback.md +6 -2
- package/dist/prompts/task-evaluation.md +25 -10
- package/dist/prompts/task-execution.md +13 -13
- package/dist/prompts/ticket-refine.md +4 -0
- package/dist/prompts/validation-checklist.md +4 -0
- package/dist/{resolver-EOE5WUMV.mjs → resolver-PG2DZEBX.mjs} +3 -3
- package/dist/{sprint-OGOFEJJH.mjs → sprint-54DOSIJK.mjs} +3 -3
- package/dist/{start-IUDCXIEA.mjs → start-2SZTBKGF.mjs} +7 -5
- package/package.json +6 -6
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ProviderAiSessionAdapter,
|
|
4
|
+
SignalParser,
|
|
5
|
+
buildAutoPrompt,
|
|
6
|
+
buildEvaluatorPrompt,
|
|
7
|
+
buildIdeateAutoPrompt,
|
|
8
|
+
buildIdeatePrompt,
|
|
9
|
+
buildInteractivePrompt,
|
|
10
|
+
buildRepoOnboardPrompt,
|
|
11
|
+
buildSprintFeedbackPrompt,
|
|
12
|
+
buildTaskExecutionPrompt,
|
|
13
|
+
buildTicketRefinePrompt,
|
|
14
|
+
getActiveProvider,
|
|
15
|
+
spawnInteractive
|
|
16
|
+
} from "./chunk-2FT37OZX.mjs";
|
|
2
17
|
import {
|
|
3
18
|
fetchIssueFromUrl,
|
|
4
19
|
formatIssueContext,
|
|
5
20
|
formatTicketDisplay,
|
|
6
21
|
truncate
|
|
7
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-EGUFQNRB.mjs";
|
|
8
23
|
import {
|
|
9
24
|
EXIT_ERROR,
|
|
10
|
-
EXIT_INTERRUPTED,
|
|
11
25
|
EXIT_NO_TASKS,
|
|
12
26
|
exitWithCode
|
|
13
27
|
} from "./chunk-CFUVE2BP.mjs";
|
|
@@ -15,17 +29,17 @@ import {
|
|
|
15
29
|
getPrompt,
|
|
16
30
|
getSharedDeps
|
|
17
31
|
} from "./chunk-747KW2RW.mjs";
|
|
32
|
+
import {
|
|
33
|
+
updateProject
|
|
34
|
+
} from "./chunk-LDSG7G2T.mjs";
|
|
18
35
|
import {
|
|
19
36
|
assertSprintStatus,
|
|
20
37
|
closeSprint,
|
|
21
|
-
getAiProvider,
|
|
22
38
|
getSprint,
|
|
23
39
|
resolveSprintId,
|
|
24
|
-
setAiProvider,
|
|
25
40
|
withFileLock
|
|
26
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-RQGD5WS6.mjs";
|
|
27
42
|
import {
|
|
28
|
-
emoji,
|
|
29
43
|
log,
|
|
30
44
|
printHeader,
|
|
31
45
|
renderTable,
|
|
@@ -35,13 +49,14 @@ import {
|
|
|
35
49
|
showSuccess,
|
|
36
50
|
showWarning,
|
|
37
51
|
terminalBell
|
|
38
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-WZTY77GY.mjs";
|
|
39
53
|
import {
|
|
40
54
|
ensureError,
|
|
41
55
|
unwrapOrThrow,
|
|
42
56
|
wrapAsync
|
|
43
57
|
} from "./chunk-IWXBJD2D.mjs";
|
|
44
58
|
import {
|
|
59
|
+
CURRENT_ONBOARDING_VERSION,
|
|
45
60
|
IdeateOutputSchema,
|
|
46
61
|
ImportTasksSchema,
|
|
47
62
|
RefinedRequirementsSchema,
|
|
@@ -53,12 +68,11 @@ import {
|
|
|
53
68
|
getTasksFilePath,
|
|
54
69
|
readValidatedJson,
|
|
55
70
|
writeValidatedJson
|
|
56
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-D2HWXEHH.mjs";
|
|
57
72
|
import {
|
|
58
73
|
BranchPreflightError,
|
|
59
74
|
DependencyCycleError,
|
|
60
75
|
DomainError,
|
|
61
|
-
IOError,
|
|
62
76
|
ParseError,
|
|
63
77
|
ProjectNotFoundError,
|
|
64
78
|
SpawnError,
|
|
@@ -2139,6 +2153,10 @@ ${repoPath}`);
|
|
|
2139
2153
|
case "note":
|
|
2140
2154
|
await this.signalHandler.handleNote(signal, ctx);
|
|
2141
2155
|
break;
|
|
2156
|
+
case "check-script-discovery":
|
|
2157
|
+
break;
|
|
2158
|
+
case "agents-md-proposal":
|
|
2159
|
+
break;
|
|
2142
2160
|
default: {
|
|
2143
2161
|
const _exhaustive = signal;
|
|
2144
2162
|
void _exhaustive;
|
|
@@ -3551,829 +3569,315 @@ function createExecuteSprintPipeline(deps, options = {}) {
|
|
|
3551
3569
|
]);
|
|
3552
3570
|
}
|
|
3553
3571
|
|
|
3554
|
-
// src/
|
|
3555
|
-
|
|
3572
|
+
// src/business/pipelines/steps/validate-agents-md.ts
|
|
3573
|
+
function validateAgentsMdStep(adapter) {
|
|
3574
|
+
return step("validate-agents-md", (ctx) => {
|
|
3575
|
+
const draft = ctx.agentsMdDraft;
|
|
3576
|
+
if (!draft || draft.trim().length === 0) {
|
|
3577
|
+
return Result.error(new ParseError("Project context file draft is empty \u2014 AI discovery produced no content."));
|
|
3578
|
+
}
|
|
3579
|
+
const { violations } = adapter.lintAgentsMd(draft);
|
|
3580
|
+
const partial = { agentsMdViolations: violations };
|
|
3581
|
+
return Result.ok(partial);
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3556
3584
|
|
|
3557
|
-
// src/
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
sigintHandler = null;
|
|
3574
|
-
sigtermHandler = null;
|
|
3575
|
-
constructor() {
|
|
3576
|
-
}
|
|
3577
|
-
/**
|
|
3578
|
-
* Get the singleton instance.
|
|
3579
|
-
*/
|
|
3580
|
-
static getInstance() {
|
|
3581
|
-
_ProcessManager.instance ??= new _ProcessManager();
|
|
3582
|
-
return _ProcessManager.instance;
|
|
3583
|
-
}
|
|
3584
|
-
/**
|
|
3585
|
-
* Reset the singleton for testing.
|
|
3586
|
-
* @internal
|
|
3587
|
-
*/
|
|
3588
|
-
static resetForTesting() {
|
|
3589
|
-
if (_ProcessManager.instance) {
|
|
3590
|
-
_ProcessManager.instance.dispose();
|
|
3591
|
-
_ProcessManager.instance = null;
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
/**
|
|
3595
|
-
* Register a child process for tracking.
|
|
3596
|
-
* Automatically installs signal handlers on first registration.
|
|
3597
|
-
* Throws an error if called during shutdown.
|
|
3598
|
-
*
|
|
3599
|
-
* @throws Error if called during shutdown
|
|
3600
|
-
*/
|
|
3601
|
-
registerChild(child) {
|
|
3602
|
-
if (this.exiting) {
|
|
3603
|
-
throw new Error("Cannot register child process during shutdown");
|
|
3604
|
-
}
|
|
3605
|
-
this.children.add(child);
|
|
3606
|
-
child.once("close", () => {
|
|
3607
|
-
this.children.delete(child);
|
|
3608
|
-
});
|
|
3609
|
-
if (!this.handlersInstalled) {
|
|
3610
|
-
this.installSignalHandlers();
|
|
3611
|
-
this.handlersInstalled = true;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
/**
|
|
3615
|
-
* Eagerly install signal handlers without requiring a child registration.
|
|
3616
|
-
* Call this at the top of execution loops so Ctrl+C works even before
|
|
3617
|
-
* the first AI process is spawned (e.g. while the spinner is visible).
|
|
3618
|
-
* Idempotent — safe to call multiple times.
|
|
3619
|
-
*/
|
|
3620
|
-
ensureHandlers() {
|
|
3621
|
-
if (!this.handlersInstalled) {
|
|
3622
|
-
this.installSignalHandlers();
|
|
3623
|
-
this.handlersInstalled = true;
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
|
-
/**
|
|
3627
|
-
* Check if a shutdown is in progress.
|
|
3628
|
-
* Used by execution loops to break immediately on Ctrl+C.
|
|
3629
|
-
*/
|
|
3630
|
-
isShuttingDown() {
|
|
3631
|
-
return this.exiting;
|
|
3632
|
-
}
|
|
3633
|
-
/**
|
|
3634
|
-
* Manually unregister a child process.
|
|
3635
|
-
* Normally not needed - children auto-unregister via event listeners.
|
|
3636
|
-
*/
|
|
3637
|
-
unregisterChild(child) {
|
|
3638
|
-
this.children.delete(child);
|
|
3639
|
-
}
|
|
3640
|
-
/**
|
|
3641
|
-
* Register a cleanup callback (for spinners, temp files, etc.).
|
|
3642
|
-
* Returns a deregister function.
|
|
3643
|
-
*/
|
|
3644
|
-
registerCleanup(callback) {
|
|
3645
|
-
this.cleanupCallbacks.add(callback);
|
|
3646
|
-
return () => {
|
|
3647
|
-
this.cleanupCallbacks.delete(callback);
|
|
3648
|
-
};
|
|
3649
|
-
}
|
|
3650
|
-
/**
|
|
3651
|
-
* Kill all tracked child processes with the given signal.
|
|
3652
|
-
* Catches errors (ESRCH = already dead, EPERM = permission denied).
|
|
3653
|
-
*/
|
|
3654
|
-
killAll(signal) {
|
|
3655
|
-
for (const child of this.children) {
|
|
3656
|
-
try {
|
|
3657
|
-
child.kill(signal);
|
|
3658
|
-
} catch (err) {
|
|
3659
|
-
const error = err;
|
|
3660
|
-
if (error.code === "ESRCH") {
|
|
3661
|
-
this.children.delete(child);
|
|
3662
|
-
} else if (error.code === "EPERM") {
|
|
3663
|
-
log.warn(`Permission denied killing process ${String(child.pid)}`);
|
|
3664
|
-
} else {
|
|
3665
|
-
log.error(`Error killing process ${String(child.pid)}: ${error.message}`);
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
/**
|
|
3671
|
-
* Graceful shutdown sequence:
|
|
3672
|
-
* 1. Run all cleanup callbacks (stop spinners)
|
|
3673
|
-
* 2. Send SIGINT to all children (what AI CLI processes expect)
|
|
3674
|
-
* 3. Wait up to 5 seconds for children to exit
|
|
3675
|
-
* 4. Send SIGKILL to any remaining children (force)
|
|
3676
|
-
* 5. Exit with code 130 (SIGINT) or 1 (force-quit)
|
|
3677
|
-
*
|
|
3678
|
-
* Double Ctrl+C: immediate SIGKILL + exit(1)
|
|
3679
|
-
*/
|
|
3680
|
-
async shutdown(signal) {
|
|
3681
|
-
if (signal === "SIGINT" && this.firstSigintAt) {
|
|
3682
|
-
const now = Date.now();
|
|
3683
|
-
if (now - this.firstSigintAt < FORCE_QUIT_WINDOW_MS) {
|
|
3684
|
-
log.warn("\n\nForce quit (double signal) \u2014 killing all processes immediately...");
|
|
3685
|
-
this.killAll("SIGKILL");
|
|
3686
|
-
process.exit(1);
|
|
3687
|
-
return;
|
|
3585
|
+
// src/business/pipelines/onboard.ts
|
|
3586
|
+
function providerInstructionsFileName(provider) {
|
|
3587
|
+
if (provider === "claude") return "CLAUDE.md";
|
|
3588
|
+
return ".github/copilot-instructions.md";
|
|
3589
|
+
}
|
|
3590
|
+
function loadProjectStep(deps) {
|
|
3591
|
+
return step("load-project", async (ctx) => {
|
|
3592
|
+
try {
|
|
3593
|
+
const project = await deps.persistence.getProject(ctx.projectName);
|
|
3594
|
+
const config = await deps.persistence.getConfig();
|
|
3595
|
+
if (!config.aiProvider) {
|
|
3596
|
+
return Result.error(
|
|
3597
|
+
new ParseError(
|
|
3598
|
+
"No AI provider configured \u2014 run `ralphctl config set provider <claude|copilot>` before onboarding."
|
|
3599
|
+
)
|
|
3600
|
+
);
|
|
3688
3601
|
}
|
|
3602
|
+
const partial = { project, provider: config.aiProvider };
|
|
3603
|
+
return Result.ok(partial);
|
|
3604
|
+
} catch (err) {
|
|
3605
|
+
if (err instanceof ProjectNotFoundError) return Result.error(err);
|
|
3606
|
+
return Result.error(new ParseError(err instanceof Error ? err.message : String(err)));
|
|
3689
3607
|
}
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
log.error(`Error in cleanup callback: ${err instanceof Error ? err.message : String(err)}`);
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
function selectRepoStep(deps, options) {
|
|
3611
|
+
return step("select-repo", async (ctx) => {
|
|
3612
|
+
const project = ctx.project;
|
|
3613
|
+
if (!project) return Result.error(new ParseError("Project not loaded."));
|
|
3614
|
+
const repos = project.repositories;
|
|
3615
|
+
if (repos.length === 0) return Result.error(new ParseError("Project has no repositories."));
|
|
3616
|
+
if (options.repo) {
|
|
3617
|
+
const match = repos.find((r) => r.name === options.repo);
|
|
3618
|
+
if (!match) {
|
|
3619
|
+
return Result.error(new ParseError(`No repository named "${options.repo}" in project "${project.name}".`));
|
|
3703
3620
|
}
|
|
3621
|
+
return Result.ok({ repo: match });
|
|
3704
3622
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3623
|
+
if (repos.length === 1) {
|
|
3624
|
+
const only = repos[0];
|
|
3625
|
+
if (!only) return Result.error(new ParseError("Project has no repositories."));
|
|
3626
|
+
return Result.ok({ repo: only });
|
|
3710
3627
|
}
|
|
3711
|
-
if (
|
|
3712
|
-
|
|
3713
|
-
|
|
3628
|
+
if (options.auto) {
|
|
3629
|
+
const first = repos[0];
|
|
3630
|
+
if (!first) return Result.error(new ParseError("Project has no repositories."));
|
|
3631
|
+
return Result.ok({ repo: first });
|
|
3714
3632
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3633
|
+
const choice = await deps.prompt.select({
|
|
3634
|
+
message: `Select a repository to onboard in "${project.name}":`,
|
|
3635
|
+
choices: repos.map((r) => ({ label: `${r.name} \u2014 ${r.path}`, value: r.id }))
|
|
3636
|
+
});
|
|
3637
|
+
const selected = repos.find((r) => r.id === choice);
|
|
3638
|
+
if (!selected) return Result.error(new ParseError("Invalid repository selection."));
|
|
3639
|
+
return Result.ok({ repo: selected });
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
function repoPreflightStep(deps) {
|
|
3643
|
+
return step("repo-preflight", (ctx) => {
|
|
3644
|
+
const repo = ctx.repo;
|
|
3645
|
+
const provider = ctx.provider;
|
|
3646
|
+
if (!repo) return Result.error(new ParseError("Repository not resolved."));
|
|
3647
|
+
if (!provider) return Result.error(new ParseError("AI provider not resolved."));
|
|
3648
|
+
const validation = deps.adapter.validateRepoPath(repo.path);
|
|
3649
|
+
if (!validation.exists) {
|
|
3650
|
+
return Result.error(new ParseError(`Repository path does not exist or is not a directory: ${repo.path}`));
|
|
3651
|
+
}
|
|
3652
|
+
if (!validation.isGitRepo) {
|
|
3653
|
+
return Result.error(new ParseError(`Repository is not a git repository: ${repo.path}`));
|
|
3654
|
+
}
|
|
3655
|
+
const existing = deps.adapter.readExistingInstructions(repo.path, provider);
|
|
3656
|
+
let mode;
|
|
3657
|
+
if (existing.content === null) {
|
|
3658
|
+
mode = "bootstrap";
|
|
3659
|
+
} else if (repo.onboardingVersion != null) {
|
|
3660
|
+
mode = "update";
|
|
3661
|
+
} else {
|
|
3662
|
+
mode = "adopt";
|
|
3729
3663
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
this.handlersInstalled = false;
|
|
3734
|
-
this.firstSigintAt = null;
|
|
3735
|
-
}
|
|
3736
|
-
/**
|
|
3737
|
-
* Install signal handlers for SIGINT and SIGTERM.
|
|
3738
|
-
* Uses process.on() (persistent) not process.once() (one-shot).
|
|
3739
|
-
* Stores handler references so dispose() can remove them.
|
|
3740
|
-
*/
|
|
3741
|
-
installSignalHandlers() {
|
|
3742
|
-
this.sigintHandler = () => {
|
|
3743
|
-
void this.shutdown("SIGINT");
|
|
3744
|
-
};
|
|
3745
|
-
this.sigtermHandler = () => {
|
|
3746
|
-
void this.shutdown("SIGTERM");
|
|
3747
|
-
};
|
|
3748
|
-
process.on("SIGINT", this.sigintHandler);
|
|
3749
|
-
process.on("SIGTERM", this.sigtermHandler);
|
|
3750
|
-
}
|
|
3751
|
-
};
|
|
3752
|
-
var processLifecycleAdapter = {
|
|
3753
|
-
ensureHandlers: () => {
|
|
3754
|
-
ProcessManager.getInstance().ensureHandlers();
|
|
3755
|
-
},
|
|
3756
|
-
isShuttingDown: () => ProcessManager.getInstance().isShuttingDown()
|
|
3757
|
-
};
|
|
3758
|
-
|
|
3759
|
-
// src/integration/ai/providers/claude.ts
|
|
3760
|
-
import { Result as Result2 } from "typescript-result";
|
|
3761
|
-
var claudeAdapter = {
|
|
3762
|
-
name: "claude",
|
|
3763
|
-
displayName: "Claude",
|
|
3764
|
-
binary: "claude",
|
|
3765
|
-
baseArgs: ["--permission-mode", "acceptEdits", "--effort", "xhigh"],
|
|
3766
|
-
experimental: false,
|
|
3767
|
-
buildInteractiveArgs(prompt, extraArgs = []) {
|
|
3768
|
-
return [...this.baseArgs, ...extraArgs, "--", prompt];
|
|
3769
|
-
},
|
|
3770
|
-
buildHeadlessArgs(extraArgs = []) {
|
|
3771
|
-
return ["-p", "--output-format", "json", ...this.baseArgs, ...extraArgs];
|
|
3772
|
-
},
|
|
3773
|
-
parseJsonOutput(stdout) {
|
|
3774
|
-
const jsonResult = Result2.try(() => JSON.parse(stdout));
|
|
3775
|
-
if (!jsonResult.ok) {
|
|
3776
|
-
return { result: stdout, sessionId: null, model: null };
|
|
3777
|
-
}
|
|
3778
|
-
const parsed = jsonResult.value;
|
|
3779
|
-
return {
|
|
3780
|
-
result: parsed.result ?? stdout,
|
|
3781
|
-
sessionId: parsed.session_id ?? null,
|
|
3782
|
-
model: parsed.model ?? null
|
|
3664
|
+
const partial = {
|
|
3665
|
+
mode,
|
|
3666
|
+
existingAgentsMd: existing.content
|
|
3783
3667
|
};
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
};
|
|
3805
|
-
|
|
3806
|
-
// src/integration/ai/providers/copilot.ts
|
|
3807
|
-
import { lstat, readdir, unlink } from "fs/promises";
|
|
3808
|
-
import { join as join2 } from "path";
|
|
3809
|
-
import { Result as Result3 } from "typescript-result";
|
|
3810
|
-
var copilotAdapter = {
|
|
3811
|
-
name: "copilot",
|
|
3812
|
-
displayName: "Copilot",
|
|
3813
|
-
binary: "copilot",
|
|
3814
|
-
experimental: true,
|
|
3815
|
-
baseArgs: ["--allow-all-tools"],
|
|
3816
|
-
buildInteractiveArgs(prompt, extraArgs = []) {
|
|
3817
|
-
return [...this.baseArgs, ...extraArgs, "-i", prompt];
|
|
3818
|
-
},
|
|
3819
|
-
buildHeadlessArgs(extraArgs = []) {
|
|
3820
|
-
return ["-p", "--output-format", "json", "--autopilot", "--no-ask-user", "--share", ...this.baseArgs, ...extraArgs];
|
|
3821
|
-
},
|
|
3822
|
-
parseJsonOutput(stdout) {
|
|
3823
|
-
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
3824
|
-
if (lines.length === 0) {
|
|
3825
|
-
return { result: "", sessionId: null, model: null };
|
|
3826
|
-
}
|
|
3827
|
-
const lastLine = lines.at(-1) ?? "";
|
|
3828
|
-
const jsonResult = Result3.try(() => JSON.parse(lastLine));
|
|
3829
|
-
if (jsonResult.ok) {
|
|
3830
|
-
const parsed = jsonResult.value;
|
|
3831
|
-
return {
|
|
3832
|
-
result: parsed.result ?? parsed.result_text ?? lastLine,
|
|
3833
|
-
sessionId: parsed.session_id ?? null,
|
|
3834
|
-
model: null
|
|
3835
|
-
};
|
|
3836
|
-
}
|
|
3837
|
-
return { result: stdout.trim(), sessionId: null, model: null };
|
|
3838
|
-
},
|
|
3839
|
-
buildResumeArgs(sessionId) {
|
|
3840
|
-
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
3841
|
-
throw new Error("Invalid session ID format");
|
|
3842
|
-
}
|
|
3843
|
-
return [`--resume=${sessionId}`];
|
|
3844
|
-
},
|
|
3845
|
-
async extractSessionId(cwd) {
|
|
3846
|
-
const filesResult = await wrapAsync(
|
|
3847
|
-
() => readdir(cwd),
|
|
3848
|
-
(err) => new IOError(`Failed to read directory: ${cwd}`, err instanceof Error ? err : void 0)
|
|
3849
|
-
);
|
|
3850
|
-
if (!filesResult.ok) return null;
|
|
3851
|
-
const files = filesResult.value;
|
|
3852
|
-
const shareFile = files.find((f) => /^copilot-session-[a-zA-Z0-9_][a-zA-Z0-9_-]*\.md$/.test(f));
|
|
3853
|
-
if (!shareFile) return null;
|
|
3854
|
-
const match = /^copilot-session-([a-zA-Z0-9_][a-zA-Z0-9_-]{0,127})\.md$/.exec(shareFile);
|
|
3855
|
-
if (!match?.[1]) return null;
|
|
3856
|
-
const filePath = join2(cwd, shareFile);
|
|
3857
|
-
const stat = await lstat(filePath).catch(() => null);
|
|
3858
|
-
if (stat?.isFile()) {
|
|
3859
|
-
await unlink(filePath).catch(() => {
|
|
3668
|
+
return Result.ok(partial);
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
function aiInventoryStep(deps) {
|
|
3672
|
+
return step("ai-inventory", async (ctx) => {
|
|
3673
|
+
const repo = ctx.repo;
|
|
3674
|
+
const mode = ctx.mode;
|
|
3675
|
+
const provider = ctx.provider;
|
|
3676
|
+
if (!repo || !mode || !provider)
|
|
3677
|
+
return Result.error(new ParseError("Preflight did not populate repo/mode/provider."));
|
|
3678
|
+
deps.logger.info(`Asking AI to inventory ${repo.name}...`);
|
|
3679
|
+
let result;
|
|
3680
|
+
try {
|
|
3681
|
+
result = await deps.adapter.discoverAgentsMd({
|
|
3682
|
+
repoPath: repo.path,
|
|
3683
|
+
mode,
|
|
3684
|
+
existingAgentsMd: ctx.existingAgentsMd ?? null,
|
|
3685
|
+
projectType: deps.adapter.inferProjectType(repo.path),
|
|
3686
|
+
checkScriptSuggestion: repo.checkScript ?? "",
|
|
3687
|
+
fileName: providerInstructionsFileName(provider)
|
|
3860
3688
|
});
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
return Result.error(new ParseError(`AI inventory failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
3861
3691
|
}
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
return
|
|
3873
|
-
},
|
|
3874
|
-
getSpawnEnv() {
|
|
3875
|
-
return {};
|
|
3876
|
-
}
|
|
3877
|
-
};
|
|
3878
|
-
|
|
3879
|
-
// src/integration/external/provider.ts
|
|
3880
|
-
async function resolveProvider() {
|
|
3881
|
-
const stored = await getAiProvider();
|
|
3882
|
-
if (stored) return stored;
|
|
3883
|
-
const choice = await getPrompt().select({
|
|
3884
|
-
message: `${emoji.donut} Which AI buddy should help with my homework?`,
|
|
3885
|
-
choices: [
|
|
3886
|
-
{ label: "Claude Code", value: "claude" },
|
|
3887
|
-
{ label: "GitHub Copilot", value: "copilot" }
|
|
3888
|
-
]
|
|
3692
|
+
if (!result.agentsMd) {
|
|
3693
|
+
return Result.error(
|
|
3694
|
+
new ParseError("AI returned no project context file proposal \u2014 try again, or edit the file manually.")
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
const partial = {
|
|
3698
|
+
agentsMdDraft: result.agentsMd,
|
|
3699
|
+
checkScriptDraft: result.checkScript,
|
|
3700
|
+
changes: result.changes
|
|
3701
|
+
};
|
|
3702
|
+
return Result.ok(partial);
|
|
3889
3703
|
});
|
|
3890
|
-
await setAiProvider(choice);
|
|
3891
|
-
return choice;
|
|
3892
|
-
}
|
|
3893
|
-
function providerDisplayName(provider) {
|
|
3894
|
-
return provider === "claude" ? "Claude" : "Copilot";
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
// src/integration/ai/providers/registry.ts
|
|
3898
|
-
function getProvider(provider) {
|
|
3899
|
-
switch (provider) {
|
|
3900
|
-
case "claude":
|
|
3901
|
-
return claudeAdapter;
|
|
3902
|
-
case "copilot":
|
|
3903
|
-
return copilotAdapter;
|
|
3904
|
-
}
|
|
3905
|
-
}
|
|
3906
|
-
async function getActiveProvider() {
|
|
3907
|
-
const provider = await resolveProvider();
|
|
3908
|
-
return getProvider(provider);
|
|
3909
3704
|
}
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
return { code: 1, error: `Failed to spawn ${provider.binary} CLI: ${result.error.message}` };
|
|
3923
|
-
}
|
|
3924
|
-
return { code: result.status ?? 1 };
|
|
3925
|
-
}
|
|
3926
|
-
async function spawnHeadless(options, provider) {
|
|
3927
|
-
assertSafeCwd(options.cwd);
|
|
3928
|
-
const p = provider ?? await getActiveProvider();
|
|
3929
|
-
return new Promise((resolve, reject) => {
|
|
3930
|
-
const allArgs = p.buildHeadlessArgs(options.args ?? []);
|
|
3931
|
-
if (options.resumeSessionId) {
|
|
3932
|
-
try {
|
|
3933
|
-
allArgs.push(...p.buildResumeArgs(options.resumeSessionId));
|
|
3934
|
-
} catch {
|
|
3935
|
-
reject(new SpawnError("Invalid session ID format", "", 1));
|
|
3936
|
-
return;
|
|
3705
|
+
function retryOnViolationStep(deps) {
|
|
3706
|
+
return step(
|
|
3707
|
+
"retry-agents-md-on-violation",
|
|
3708
|
+
async (ctx) => {
|
|
3709
|
+
const violations = ctx.agentsMdViolations ?? [];
|
|
3710
|
+
if (violations.length === 0) return Result.ok({});
|
|
3711
|
+
const repo = ctx.repo;
|
|
3712
|
+
const mode = ctx.mode;
|
|
3713
|
+
const provider = ctx.provider;
|
|
3714
|
+
const draft = ctx.agentsMdDraft;
|
|
3715
|
+
if (!repo || !mode || !provider || !draft) {
|
|
3716
|
+
return Result.error(new ParseError("Retry requires repo, mode, provider, and an existing draft."));
|
|
3937
3717
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3718
|
+
deps.logger.warn(
|
|
3719
|
+
`Project context file draft failed ${String(violations.length)} rule(s); asking AI for a fix...`
|
|
3720
|
+
);
|
|
3721
|
+
const violationSummary = violations.map((v) => `- [${v.rule}] ${v.message}`).join("\n");
|
|
3722
|
+
const feedbackContext = [
|
|
3723
|
+
ctx.existingAgentsMd ?? "",
|
|
3724
|
+
"",
|
|
3725
|
+
"---",
|
|
3726
|
+
"",
|
|
3727
|
+
"Your previous draft (below) violated these rules:",
|
|
3728
|
+
violationSummary,
|
|
3729
|
+
"",
|
|
3730
|
+
"Fix every violation and re-emit the full project context file plus check-script.",
|
|
3731
|
+
"",
|
|
3732
|
+
draft
|
|
3733
|
+
].join("\n");
|
|
3734
|
+
let retry;
|
|
3735
|
+
try {
|
|
3736
|
+
retry = await deps.adapter.discoverAgentsMd({
|
|
3737
|
+
repoPath: repo.path,
|
|
3738
|
+
mode,
|
|
3739
|
+
existingAgentsMd: feedbackContext,
|
|
3740
|
+
projectType: deps.adapter.inferProjectType(repo.path),
|
|
3741
|
+
checkScriptSuggestion: ctx.checkScriptDraft ?? repo.checkScript ?? "",
|
|
3742
|
+
fileName: providerInstructionsFileName(provider)
|
|
3743
|
+
});
|
|
3744
|
+
} catch (err) {
|
|
3745
|
+
return Result.error(new ParseError(`Retry failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
3957
3746
|
}
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
let rawStdout = "";
|
|
3962
|
-
let stderr = "";
|
|
3963
|
-
child.stdout.on("data", (data) => {
|
|
3964
|
-
if (rawStdout.length < MAX_STDOUT_SIZE) {
|
|
3965
|
-
rawStdout += data.toString();
|
|
3747
|
+
if (!retry.agentsMd) {
|
|
3748
|
+
deps.logger.warn("Retry produced no new proposal \u2014 keeping original draft.");
|
|
3749
|
+
return Result.ok({});
|
|
3966
3750
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
|
-
reject(new SpawnError(`Failed to spawn ${p.binary} CLI: ${err.message}`, "", 1));
|
|
3994
|
-
});
|
|
3751
|
+
const { violations: retryViolations } = deps.adapter.lintAgentsMd(retry.agentsMd);
|
|
3752
|
+
const partial = {
|
|
3753
|
+
agentsMdDraft: retry.agentsMd,
|
|
3754
|
+
checkScriptDraft: retry.checkScript ?? ctx.checkScriptDraft,
|
|
3755
|
+
agentsMdViolations: retryViolations
|
|
3756
|
+
};
|
|
3757
|
+
return Result.ok(partial);
|
|
3758
|
+
}
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
function checkDriftStep(deps) {
|
|
3762
|
+
return step("check-drift", (ctx) => {
|
|
3763
|
+
const draft = ctx.agentsMdDraft;
|
|
3764
|
+
const repo = ctx.repo;
|
|
3765
|
+
if (!draft || !repo) return Result.error(new ParseError("check-drift requires a draft and repo."));
|
|
3766
|
+
const warnings = deps.adapter.detectCommandDrift(draft, repo.path);
|
|
3767
|
+
const residual = ctx.agentsMdViolations ?? [];
|
|
3768
|
+
for (const v of residual) {
|
|
3769
|
+
warnings.push(`lint[${v.rule}]: ${v.message}`);
|
|
3770
|
+
}
|
|
3771
|
+
const alreadyCurrent = ctx.mode === "update" && warnings.length === 0 && (!ctx.changes || ctx.changes.trim().length === 0);
|
|
3772
|
+
const partial = {
|
|
3773
|
+
driftWarnings: warnings,
|
|
3774
|
+
alreadyCurrent
|
|
3775
|
+
};
|
|
3776
|
+
return Result.ok(partial);
|
|
3995
3777
|
});
|
|
3996
3778
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
}
|
|
4004
|
-
|
|
4005
|
-
return Math.floor(Math.random() * 1e3);
|
|
4006
|
-
}
|
|
4007
|
-
async function spawnWithRetry(options, retryOptions, provider) {
|
|
4008
|
-
const p = provider ?? await getActiveProvider();
|
|
4009
|
-
const maxRetries = retryOptions?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
4010
|
-
const totalTimeoutMs = retryOptions?.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
|
|
4011
|
-
const startTime = Date.now();
|
|
4012
|
-
let resumeSessionId = options.resumeSessionId;
|
|
4013
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4014
|
-
const elapsed = Date.now() - startTime;
|
|
4015
|
-
if (attempt > 0 && elapsed >= totalTimeoutMs) {
|
|
4016
|
-
throw new SpawnError(`Total retry timeout exceeded (${String(totalTimeoutMs)}ms)`, "", 1, resumeSessionId);
|
|
4017
|
-
}
|
|
4018
|
-
const r = await wrapAsync(async () => spawnHeadless({ ...options, resumeSessionId }, p), ensureError);
|
|
4019
|
-
if (r.ok) return r.value;
|
|
4020
|
-
const err = r.error;
|
|
4021
|
-
if (!(err instanceof SpawnError) || !err.rateLimited) {
|
|
4022
|
-
throw err;
|
|
4023
|
-
}
|
|
4024
|
-
if (err.sessionId) {
|
|
4025
|
-
resumeSessionId = err.sessionId;
|
|
4026
|
-
}
|
|
4027
|
-
if (attempt >= maxRetries) {
|
|
4028
|
-
throw err;
|
|
3779
|
+
function reviewAndConfirmStep(deps, options) {
|
|
3780
|
+
return step("review-and-confirm", async (ctx) => {
|
|
3781
|
+
if (ctx.alreadyCurrent || options.auto || options.dryRun) {
|
|
3782
|
+
const partial2 = {
|
|
3783
|
+
agentsMdFinal: ctx.agentsMdDraft,
|
|
3784
|
+
checkScriptFinal: ctx.checkScriptDraft ?? null
|
|
3785
|
+
};
|
|
3786
|
+
return Result.ok(partial2);
|
|
4029
3787
|
}
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
throw new Error("Max retries exceeded");
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
// src/integration/ui/tui/runtime/screen.ts
|
|
4038
|
-
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
4039
|
-
var LEAVE_ALT_SCREEN = "\x1B[?1049l";
|
|
4040
|
-
var HIDE_CURSOR = "\x1B[?25l";
|
|
4041
|
-
var SHOW_CURSOR = "\x1B[?25h";
|
|
4042
|
-
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
4043
|
-
var altScreenActive = false;
|
|
4044
|
-
var safetyNetsInstalled = false;
|
|
4045
|
-
function writeRaw(seq) {
|
|
4046
|
-
if (process.stdout.isTTY) process.stdout.write(seq);
|
|
4047
|
-
}
|
|
4048
|
-
function restore() {
|
|
4049
|
-
if (!altScreenActive) return;
|
|
4050
|
-
altScreenActive = false;
|
|
4051
|
-
writeRaw(SHOW_CURSOR);
|
|
4052
|
-
writeRaw(LEAVE_ALT_SCREEN);
|
|
4053
|
-
}
|
|
4054
|
-
function installSafetyNets() {
|
|
4055
|
-
if (safetyNetsInstalled) return;
|
|
4056
|
-
safetyNetsInstalled = true;
|
|
4057
|
-
process.on("exit", restore);
|
|
4058
|
-
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
4059
|
-
process.on(sig, () => {
|
|
4060
|
-
restore();
|
|
4061
|
-
process.kill(process.pid, sig);
|
|
3788
|
+
const fileName = ctx.provider ? providerInstructionsFileName(ctx.provider) : "project context file";
|
|
3789
|
+
const edited = await deps.prompt.editor({
|
|
3790
|
+
message: `Review ${fileName} (save to accept, cancel to abort):`,
|
|
3791
|
+
default: ctx.agentsMdDraft ?? ""
|
|
4062
3792
|
});
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
3793
|
+
if (edited === null) {
|
|
3794
|
+
return Result.error(new ParseError("User cancelled project context file review."));
|
|
3795
|
+
}
|
|
3796
|
+
const checkEdited = await deps.prompt.input({
|
|
3797
|
+
message: "Check script (optional; empty skips):",
|
|
3798
|
+
default: ctx.checkScriptDraft ?? ""
|
|
4068
3799
|
});
|
|
3800
|
+
const finalCheck = checkEdited.trim() === "" ? null : checkEdited.trim();
|
|
3801
|
+
const partial = {
|
|
3802
|
+
agentsMdFinal: edited,
|
|
3803
|
+
checkScriptFinal: finalCheck
|
|
3804
|
+
};
|
|
3805
|
+
return Result.ok(partial);
|
|
4069
3806
|
});
|
|
4070
3807
|
}
|
|
4071
|
-
function
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
activeInstance = instance;
|
|
4088
|
-
return () => {
|
|
4089
|
-
if (activeInstance === instance) {
|
|
4090
|
-
activeInstance = null;
|
|
4091
|
-
}
|
|
4092
|
-
};
|
|
4093
|
-
}
|
|
4094
|
-
async function withSuspendedTui(cb) {
|
|
4095
|
-
const instance = activeInstance;
|
|
4096
|
-
if (instance === null) {
|
|
4097
|
-
return cb();
|
|
4098
|
-
}
|
|
4099
|
-
exitAltScreen();
|
|
4100
|
-
try {
|
|
4101
|
-
return await cb();
|
|
4102
|
-
} finally {
|
|
4103
|
-
enterAltScreen();
|
|
4104
|
-
instance.clear();
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
4107
|
-
|
|
4108
|
-
// src/integration/ai/session/session-adapter.ts
|
|
4109
|
-
var ProviderAiSessionAdapter = class {
|
|
4110
|
-
provider = null;
|
|
4111
|
-
/** Lazily resolve and cache the active provider. */
|
|
4112
|
-
async getProvider() {
|
|
4113
|
-
this.provider ??= await getActiveProvider();
|
|
4114
|
-
return this.provider;
|
|
4115
|
-
}
|
|
4116
|
-
/** Public eager resolver — required before the sync getters can be used safely. */
|
|
4117
|
-
async ensureReady() {
|
|
4118
|
-
await this.getProvider();
|
|
4119
|
-
}
|
|
4120
|
-
async spawnInteractive(prompt, options) {
|
|
4121
|
-
const provider = await this.getProvider();
|
|
4122
|
-
await withSuspendedTui(() => {
|
|
4123
|
-
const result = spawnInteractive(
|
|
4124
|
-
prompt,
|
|
4125
|
-
{
|
|
4126
|
-
cwd: options.cwd,
|
|
4127
|
-
args: options.args,
|
|
4128
|
-
env: options.env
|
|
4129
|
-
},
|
|
4130
|
-
provider
|
|
3808
|
+
function writeArtifactsStep(deps, options) {
|
|
3809
|
+
return step("write-artifacts", async (ctx) => {
|
|
3810
|
+
if (options.dryRun || ctx.alreadyCurrent) {
|
|
3811
|
+
deps.logger.info(options.dryRun ? "Dry run \u2014 skipping writes." : "Already up to date \u2014 skipping writes.");
|
|
3812
|
+
return Result.ok({});
|
|
3813
|
+
}
|
|
3814
|
+
const repo = ctx.repo;
|
|
3815
|
+
const project = ctx.project;
|
|
3816
|
+
const provider = ctx.provider;
|
|
3817
|
+
const content = ctx.agentsMdFinal;
|
|
3818
|
+
if (!repo || !project || !provider || !content) {
|
|
3819
|
+
return Result.error(new ParseError("write-artifacts requires repo, project, provider, and final content."));
|
|
3820
|
+
}
|
|
3821
|
+
if (ctx.mode === "adopt") {
|
|
3822
|
+
deps.logger.warn(
|
|
3823
|
+
"Adopt mode \u2014 existing project context file left untouched. Review the proposed additions and apply them manually."
|
|
4131
3824
|
);
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
const provider = await this.getProvider();
|
|
4139
|
-
const result = await spawnHeadless(
|
|
4140
|
-
{
|
|
4141
|
-
cwd: options.cwd,
|
|
4142
|
-
args: options.args,
|
|
4143
|
-
env: options.env,
|
|
4144
|
-
prompt,
|
|
4145
|
-
resumeSessionId: options.resumeSessionId
|
|
4146
|
-
},
|
|
4147
|
-
provider
|
|
4148
|
-
);
|
|
4149
|
-
return {
|
|
4150
|
-
output: result.stdout,
|
|
4151
|
-
sessionId: result.sessionId ?? void 0,
|
|
4152
|
-
model: result.model ?? void 0
|
|
4153
|
-
};
|
|
4154
|
-
}
|
|
4155
|
-
async spawnWithRetry(prompt, options) {
|
|
4156
|
-
const provider = await this.getProvider();
|
|
4157
|
-
const result = await spawnWithRetry(
|
|
4158
|
-
{
|
|
4159
|
-
cwd: options.cwd,
|
|
4160
|
-
args: options.args,
|
|
4161
|
-
env: options.env,
|
|
4162
|
-
prompt,
|
|
4163
|
-
resumeSessionId: options.resumeSessionId
|
|
4164
|
-
},
|
|
4165
|
-
{ maxRetries: options.maxRetries },
|
|
4166
|
-
provider
|
|
4167
|
-
);
|
|
4168
|
-
return {
|
|
4169
|
-
output: result.stdout,
|
|
4170
|
-
sessionId: result.sessionId ?? void 0,
|
|
4171
|
-
model: result.model ?? void 0
|
|
4172
|
-
};
|
|
4173
|
-
}
|
|
4174
|
-
async resumeSession(sessionId, prompt, options) {
|
|
4175
|
-
const provider = await this.getProvider();
|
|
4176
|
-
const result = await spawnWithRetry(
|
|
4177
|
-
{
|
|
4178
|
-
cwd: options.cwd,
|
|
4179
|
-
args: options.args,
|
|
4180
|
-
env: options.env,
|
|
4181
|
-
prompt,
|
|
4182
|
-
resumeSessionId: sessionId
|
|
4183
|
-
},
|
|
4184
|
-
void 0,
|
|
4185
|
-
provider
|
|
4186
|
-
);
|
|
4187
|
-
return {
|
|
4188
|
-
output: result.stdout,
|
|
4189
|
-
sessionId: result.sessionId ?? void 0,
|
|
4190
|
-
model: result.model ?? void 0
|
|
4191
|
-
};
|
|
4192
|
-
}
|
|
4193
|
-
getProviderName() {
|
|
4194
|
-
if (!this.provider) {
|
|
4195
|
-
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
4196
|
-
}
|
|
4197
|
-
return this.provider.name;
|
|
4198
|
-
}
|
|
4199
|
-
getProviderDisplayName() {
|
|
4200
|
-
if (!this.provider) {
|
|
4201
|
-
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
3825
|
+
return Result.ok({
|
|
3826
|
+
driftWarnings: [
|
|
3827
|
+
...ctx.driftWarnings ?? [],
|
|
3828
|
+
"adopt-mode: authored file preserved; proposed additions not written \u2014 apply manually."
|
|
3829
|
+
]
|
|
3830
|
+
});
|
|
4202
3831
|
}
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
3832
|
+
try {
|
|
3833
|
+
const written = deps.adapter.writeProviderInstructions(repo.path, content, provider);
|
|
3834
|
+
const updatedRepos = project.repositories.map((r) => {
|
|
3835
|
+
if (r.id !== repo.id) return r;
|
|
3836
|
+
const next = {
|
|
3837
|
+
...r,
|
|
3838
|
+
onboardingVersion: CURRENT_ONBOARDING_VERSION
|
|
3839
|
+
};
|
|
3840
|
+
const cs = ctx.checkScriptFinal;
|
|
3841
|
+
if (cs && cs.length > 0) {
|
|
3842
|
+
next.checkScript = cs;
|
|
3843
|
+
} else if (cs === null) {
|
|
3844
|
+
delete next.checkScript;
|
|
3845
|
+
}
|
|
3846
|
+
return next;
|
|
3847
|
+
});
|
|
3848
|
+
await deps.updateProjectRepos(project.name, updatedRepos);
|
|
3849
|
+
const partial = {
|
|
3850
|
+
writtenPath: written.path
|
|
3851
|
+
};
|
|
3852
|
+
return Result.ok(partial);
|
|
3853
|
+
} catch (err) {
|
|
3854
|
+
return Result.error(new ParseError(`Write failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
4208
3855
|
}
|
|
4209
|
-
return this.provider.getSpawnEnv();
|
|
4210
|
-
}
|
|
4211
|
-
};
|
|
4212
|
-
|
|
4213
|
-
// src/integration/ai/prompts/loader.ts
|
|
4214
|
-
import { existsSync, readFileSync } from "fs";
|
|
4215
|
-
import { dirname, join as join3 } from "path";
|
|
4216
|
-
import { fileURLToPath } from "url";
|
|
4217
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
4218
|
-
function getPromptDir() {
|
|
4219
|
-
const bundled = join3(__dirname, "prompts");
|
|
4220
|
-
if (existsSync(bundled)) return bundled;
|
|
4221
|
-
return __dirname;
|
|
4222
|
-
}
|
|
4223
|
-
var promptDir = getPromptDir();
|
|
4224
|
-
function loadTemplate(name) {
|
|
4225
|
-
return readFileSync(join3(promptDir, `${name}.md`), "utf-8");
|
|
4226
|
-
}
|
|
4227
|
-
function loadPartial(name) {
|
|
4228
|
-
return loadTemplate(name).replace(/\s+$/, "");
|
|
4229
|
-
}
|
|
4230
|
-
var UNREPLACED_TOKEN_RE = /\{\{[A-Z_]+\}\}/g;
|
|
4231
|
-
function composePrompt(template, substitutions) {
|
|
4232
|
-
let result = template;
|
|
4233
|
-
for (const [key, value] of Object.entries(substitutions)) {
|
|
4234
|
-
result = result.replaceAll(`{{${key}}}`, value);
|
|
4235
|
-
}
|
|
4236
|
-
const remaining = result.match(UNREPLACED_TOKEN_RE);
|
|
4237
|
-
if (remaining) {
|
|
4238
|
-
throw new Error(`composePrompt: unreplaced placeholders: ${[...new Set(remaining)].join(", ")}`);
|
|
4239
|
-
}
|
|
4240
|
-
return result;
|
|
4241
|
-
}
|
|
4242
|
-
function buildPlanCommon(projectToolingSection) {
|
|
4243
|
-
return composePrompt(loadPartial("plan-common"), {
|
|
4244
|
-
PROJECT_TOOLING: projectToolingSection
|
|
4245
|
-
});
|
|
4246
|
-
}
|
|
4247
|
-
function buildPlannerBase(projectToolingSection) {
|
|
4248
|
-
return {
|
|
4249
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4250
|
-
COMMON: buildPlanCommon(projectToolingSection),
|
|
4251
|
-
VALIDATION: loadPartial("validation-checklist"),
|
|
4252
|
-
SIGNALS: loadPartial("signals-planning")
|
|
4253
|
-
};
|
|
4254
|
-
}
|
|
4255
|
-
function buildInteractivePrompt(context, outputFile, schema, projectToolingSection) {
|
|
4256
|
-
return composePrompt(loadTemplate("plan-interactive"), {
|
|
4257
|
-
...buildPlannerBase(projectToolingSection),
|
|
4258
|
-
CONTEXT: context,
|
|
4259
|
-
OUTPUT_FILE: outputFile,
|
|
4260
|
-
SCHEMA: schema
|
|
4261
|
-
});
|
|
4262
|
-
}
|
|
4263
|
-
function buildAutoPrompt(context, schema, projectToolingSection) {
|
|
4264
|
-
return composePrompt(loadTemplate("plan-auto"), {
|
|
4265
|
-
...buildPlannerBase(projectToolingSection),
|
|
4266
|
-
CONTEXT: context,
|
|
4267
|
-
SCHEMA: schema
|
|
4268
|
-
});
|
|
4269
|
-
}
|
|
4270
|
-
function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName, projectToolingSection = "") {
|
|
4271
|
-
const template = loadTemplate("task-execution");
|
|
4272
|
-
const commitStep = noCommit ? "" : "\n - **Before continuing:** Create a git commit with a descriptive message for the changes made.";
|
|
4273
|
-
const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.\n";
|
|
4274
|
-
return composePrompt(template, {
|
|
4275
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4276
|
-
SIGNALS: loadPartial("signals-task"),
|
|
4277
|
-
PROGRESS_FILE: progressFilePath,
|
|
4278
|
-
COMMIT_STEP: commitStep,
|
|
4279
|
-
COMMIT_CONSTRAINT: commitConstraint,
|
|
4280
|
-
CONTEXT_FILE: contextFileName,
|
|
4281
|
-
PROJECT_TOOLING: projectToolingSection
|
|
4282
|
-
});
|
|
4283
|
-
}
|
|
4284
|
-
function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
|
|
4285
|
-
const template = loadTemplate("ticket-refine");
|
|
4286
|
-
return composePrompt(template, {
|
|
4287
|
-
TICKET: ticketContent,
|
|
4288
|
-
OUTPUT_FILE: outputFile,
|
|
4289
|
-
SCHEMA: schema,
|
|
4290
|
-
ISSUE_CONTEXT: issueContext
|
|
4291
|
-
});
|
|
4292
|
-
}
|
|
4293
|
-
function buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositories, outputFile, schema, projectToolingSection) {
|
|
4294
|
-
return composePrompt(loadTemplate("ideate"), {
|
|
4295
|
-
...buildPlannerBase(projectToolingSection),
|
|
4296
|
-
IDEA_TITLE: ideaTitle,
|
|
4297
|
-
IDEA_DESCRIPTION: ideaDescription,
|
|
4298
|
-
PROJECT_NAME: projectName,
|
|
4299
|
-
REPOSITORIES: repositories,
|
|
4300
|
-
OUTPUT_FILE: outputFile,
|
|
4301
|
-
SCHEMA: schema
|
|
4302
|
-
});
|
|
4303
|
-
}
|
|
4304
|
-
function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema, projectToolingSection) {
|
|
4305
|
-
return composePrompt(loadTemplate("ideate-auto"), {
|
|
4306
|
-
...buildPlannerBase(projectToolingSection),
|
|
4307
|
-
IDEA_TITLE: ideaTitle,
|
|
4308
|
-
IDEA_DESCRIPTION: ideaDescription,
|
|
4309
|
-
PROJECT_NAME: projectName,
|
|
4310
|
-
REPOSITORIES: repositories,
|
|
4311
|
-
SCHEMA: schema
|
|
4312
3856
|
});
|
|
4313
3857
|
}
|
|
4314
|
-
function
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
`
|
|
4323
|
-
).join("");
|
|
4324
|
-
const passBar = extras.map((name) => `
|
|
4325
|
-
- **${name}**: Task-specific dimension flagged by the planner`).join("");
|
|
4326
|
-
return {
|
|
4327
|
-
section,
|
|
4328
|
-
passBar,
|
|
4329
|
-
assessment: extras.map((name) => `
|
|
4330
|
-
**${name}**: PASS/FAIL \u2014 [one-line finding]`).join("")
|
|
4331
|
-
};
|
|
4332
|
-
}
|
|
4333
|
-
function buildEvaluatorPrompt(ctx) {
|
|
4334
|
-
const template = loadTemplate("task-evaluation");
|
|
4335
|
-
const descriptionSection = ctx.taskDescription ? `
|
|
4336
|
-
**Description:** ${ctx.taskDescription}` : "";
|
|
4337
|
-
const stepsSection = ctx.taskSteps.length > 0 ? `
|
|
4338
|
-
**Implementation Steps:**
|
|
4339
|
-
${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
|
|
4340
|
-
const criteriaSection = ctx.verificationCriteria.length > 0 ? `
|
|
4341
|
-
**Verification Criteria:**
|
|
4342
|
-
${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
|
|
4343
|
-
const checkSection = ctx.checkScriptSection ? `
|
|
4344
|
-
|
|
4345
|
-
${ctx.checkScriptSection}` : "";
|
|
4346
|
-
const extras = renderExtraDimensions(ctx.extraDimensions);
|
|
4347
|
-
const extraAssessmentPass = extras.assessment.replace(/PASS\/FAIL/g, "PASS");
|
|
4348
|
-
return composePrompt(template, {
|
|
4349
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4350
|
-
SIGNALS: loadPartial("signals-evaluation"),
|
|
4351
|
-
TASK_NAME: ctx.taskName,
|
|
4352
|
-
TASK_DESCRIPTION_SECTION: descriptionSection,
|
|
4353
|
-
TASK_STEPS_SECTION: stepsSection,
|
|
4354
|
-
VERIFICATION_CRITERIA_SECTION: criteriaSection,
|
|
4355
|
-
PROJECT_PATH: ctx.projectPath,
|
|
4356
|
-
CHECK_SCRIPT_SECTION: checkSection,
|
|
4357
|
-
PROJECT_TOOLING: ctx.projectToolingSection,
|
|
4358
|
-
EXTRA_DIMENSIONS_SECTION: extras.section,
|
|
4359
|
-
EXTRA_DIMENSIONS_PASS_BAR: extras.passBar,
|
|
4360
|
-
EXTRA_DIMENSIONS_ASSESSMENT_PASS: extraAssessmentPass,
|
|
4361
|
-
EXTRA_DIMENSIONS_ASSESSMENT_MIXED: extras.assessment
|
|
3858
|
+
function verifyCheckScriptStep(deps) {
|
|
3859
|
+
return step("verify-check-script", (ctx) => {
|
|
3860
|
+
const cmd = ctx.checkScriptFinal;
|
|
3861
|
+
if (!cmd) return Result.ok({});
|
|
3862
|
+
if (!/^\S/.test(cmd)) {
|
|
3863
|
+
deps.logger.warn(`Check script looks malformed: ${cmd}`);
|
|
3864
|
+
}
|
|
3865
|
+
return Result.ok({});
|
|
4362
3866
|
});
|
|
4363
3867
|
}
|
|
4364
|
-
function
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
3868
|
+
function createOnboardPipeline(deps, options = {}) {
|
|
3869
|
+
return pipeline("onboard", [
|
|
3870
|
+
loadProjectStep(deps),
|
|
3871
|
+
selectRepoStep(deps, options),
|
|
3872
|
+
repoPreflightStep(deps),
|
|
3873
|
+
aiInventoryStep(deps),
|
|
3874
|
+
validateAgentsMdStep(deps.adapter),
|
|
3875
|
+
retryOnViolationStep(deps),
|
|
3876
|
+
checkDriftStep(deps),
|
|
3877
|
+
reviewAndConfirmStep(deps, options),
|
|
3878
|
+
writeArtifactsStep(deps, options),
|
|
3879
|
+
verifyCheckScriptStep(deps)
|
|
3880
|
+
]);
|
|
4377
3881
|
}
|
|
4378
3882
|
|
|
4379
3883
|
// src/integration/ai/prompts/prompt-builder-adapter.ts
|
|
@@ -4581,8 +4085,8 @@ async function importTasksReplace(tasks, sprintId) {
|
|
|
4581
4085
|
|
|
4582
4086
|
// src/integration/cli/commands/ticket/refine-utils.ts
|
|
4583
4087
|
import { writeFile } from "fs/promises";
|
|
4584
|
-
import { join as
|
|
4585
|
-
import { Result as
|
|
4088
|
+
import { join as join2 } from "path";
|
|
4089
|
+
import { Result as Result2 } from "typescript-result";
|
|
4586
4090
|
function formatTicketForPrompt(ticket) {
|
|
4587
4091
|
const lines = [];
|
|
4588
4092
|
lines.push(`### ${formatTicketDisplay(ticket)}`);
|
|
@@ -4600,7 +4104,7 @@ function formatTicketForPrompt(ticket) {
|
|
|
4600
4104
|
}
|
|
4601
4105
|
function parseRequirementsFile(content) {
|
|
4602
4106
|
const jsonStr = extractJsonArray(content);
|
|
4603
|
-
const parseR =
|
|
4107
|
+
const parseR = Result2.try(() => JSON.parse(jsonStr));
|
|
4604
4108
|
if (!parseR.ok) {
|
|
4605
4109
|
throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
|
|
4606
4110
|
}
|
|
@@ -4620,7 +4124,7 @@ ${issues}`);
|
|
|
4620
4124
|
return result.data;
|
|
4621
4125
|
}
|
|
4622
4126
|
async function runAiSession(workingDir, prompt, ticketTitle) {
|
|
4623
|
-
const contextFile =
|
|
4127
|
+
const contextFile = join2(workingDir, "refine-context.md");
|
|
4624
4128
|
await writeFile(contextFile, prompt, "utf-8");
|
|
4625
4129
|
const provider = await getActiveProvider();
|
|
4626
4130
|
const startPrompt = `I need help refining the requirements for "${ticketTitle}". The full context is in refine-context.md. Please read that file now and follow the instructions to help refine the ticket requirements.`;
|
|
@@ -4675,132 +4179,6 @@ function parseEvaluationResult(output) {
|
|
|
4675
4179
|
return { passed: false, status: "malformed", output, dimensions };
|
|
4676
4180
|
}
|
|
4677
4181
|
|
|
4678
|
-
// src/integration/signals/parser.ts
|
|
4679
|
-
var SIGNAL_PATTERNS = {
|
|
4680
|
-
progress: /<progress>([\s\S]*?)<\/progress>/g,
|
|
4681
|
-
progressWithFiles: /<progress>([\s\S]*?)<\/progress>/,
|
|
4682
|
-
evaluation_passed: /<evaluation-passed>/,
|
|
4683
|
-
evaluation_failed: /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/,
|
|
4684
|
-
task_verified: /<task-verified>([\s\S]*?)<\/task-verified>/,
|
|
4685
|
-
task_complete: /<task-complete>/,
|
|
4686
|
-
task_blocked: /<task-blocked>([\s\S]*?)<\/task-blocked>/,
|
|
4687
|
-
note: /<note>([\s\S]*?)<\/note>/g
|
|
4688
|
-
};
|
|
4689
|
-
var DIMENSION_LINE2 = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/gi;
|
|
4690
|
-
function parseDimensionScores2(output) {
|
|
4691
|
-
const scores = [];
|
|
4692
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4693
|
-
DIMENSION_LINE2.lastIndex = 0;
|
|
4694
|
-
let match;
|
|
4695
|
-
while ((match = DIMENSION_LINE2.exec(output)) !== null) {
|
|
4696
|
-
const rawName = match[1];
|
|
4697
|
-
const verdict = match[2];
|
|
4698
|
-
const finding = match[3];
|
|
4699
|
-
if (!rawName || !verdict || !finding) continue;
|
|
4700
|
-
const name = rawName.toLowerCase();
|
|
4701
|
-
if (seen.has(name)) continue;
|
|
4702
|
-
seen.add(name);
|
|
4703
|
-
scores.push({
|
|
4704
|
-
dimension: name,
|
|
4705
|
-
passed: verdict.toUpperCase() === "PASS",
|
|
4706
|
-
finding: finding.trim()
|
|
4707
|
-
});
|
|
4708
|
-
}
|
|
4709
|
-
return scores;
|
|
4710
|
-
}
|
|
4711
|
-
var SignalParser = class {
|
|
4712
|
-
parseSignals(output) {
|
|
4713
|
-
const signals = [];
|
|
4714
|
-
const timestamp = /* @__PURE__ */ new Date();
|
|
4715
|
-
let progressMatch;
|
|
4716
|
-
while ((progressMatch = SIGNAL_PATTERNS.progress.exec(output)) !== null) {
|
|
4717
|
-
const summary = progressMatch[1]?.trim();
|
|
4718
|
-
if (summary) {
|
|
4719
|
-
const progressSignal = {
|
|
4720
|
-
type: "progress",
|
|
4721
|
-
summary,
|
|
4722
|
-
// Note: Phase 1 doesn't parse files attribute; added in Phase 2+
|
|
4723
|
-
timestamp
|
|
4724
|
-
};
|
|
4725
|
-
signals.push(progressSignal);
|
|
4726
|
-
}
|
|
4727
|
-
}
|
|
4728
|
-
if (output.includes("<evaluation-passed>")) {
|
|
4729
|
-
const dimensions = parseDimensionScores2(output);
|
|
4730
|
-
const evaluationSignal = {
|
|
4731
|
-
type: "evaluation",
|
|
4732
|
-
status: "passed",
|
|
4733
|
-
dimensions,
|
|
4734
|
-
timestamp
|
|
4735
|
-
};
|
|
4736
|
-
signals.push(evaluationSignal);
|
|
4737
|
-
} else {
|
|
4738
|
-
const failedMatch = SIGNAL_PATTERNS.evaluation_failed.exec(output);
|
|
4739
|
-
if (failedMatch?.[1]) {
|
|
4740
|
-
const critique = failedMatch[1].trim();
|
|
4741
|
-
const dimensions = parseDimensionScores2(output);
|
|
4742
|
-
const evaluationSignal = {
|
|
4743
|
-
type: "evaluation",
|
|
4744
|
-
status: dimensions.length > 0 ? "failed" : "malformed",
|
|
4745
|
-
dimensions,
|
|
4746
|
-
critique: dimensions.length > 0 ? critique : void 0,
|
|
4747
|
-
timestamp
|
|
4748
|
-
};
|
|
4749
|
-
signals.push(evaluationSignal);
|
|
4750
|
-
} else if (parseDimensionScores2(output).length > 0) {
|
|
4751
|
-
const dimensions = parseDimensionScores2(output);
|
|
4752
|
-
const evaluationSignal = {
|
|
4753
|
-
type: "evaluation",
|
|
4754
|
-
status: "failed",
|
|
4755
|
-
dimensions,
|
|
4756
|
-
timestamp
|
|
4757
|
-
};
|
|
4758
|
-
signals.push(evaluationSignal);
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
const taskVerifiedMatch = SIGNAL_PATTERNS.task_verified.exec(output);
|
|
4762
|
-
if (taskVerifiedMatch?.[1]) {
|
|
4763
|
-
const verificationOutput = taskVerifiedMatch[1].trim();
|
|
4764
|
-
const verifiedSignal = {
|
|
4765
|
-
type: "task-verified",
|
|
4766
|
-
output: verificationOutput,
|
|
4767
|
-
timestamp
|
|
4768
|
-
};
|
|
4769
|
-
signals.push(verifiedSignal);
|
|
4770
|
-
}
|
|
4771
|
-
if (output.includes("<task-complete>")) {
|
|
4772
|
-
const completeSignal = {
|
|
4773
|
-
type: "task-complete",
|
|
4774
|
-
timestamp
|
|
4775
|
-
};
|
|
4776
|
-
signals.push(completeSignal);
|
|
4777
|
-
}
|
|
4778
|
-
const taskBlockedMatch = SIGNAL_PATTERNS.task_blocked.exec(output);
|
|
4779
|
-
if (taskBlockedMatch?.[1]) {
|
|
4780
|
-
const reason = taskBlockedMatch[1].trim();
|
|
4781
|
-
const blockedSignal = {
|
|
4782
|
-
type: "task-blocked",
|
|
4783
|
-
reason,
|
|
4784
|
-
timestamp
|
|
4785
|
-
};
|
|
4786
|
-
signals.push(blockedSignal);
|
|
4787
|
-
}
|
|
4788
|
-
let noteMatch;
|
|
4789
|
-
while ((noteMatch = SIGNAL_PATTERNS.note.exec(output)) !== null) {
|
|
4790
|
-
const text = noteMatch[1]?.trim();
|
|
4791
|
-
if (text) {
|
|
4792
|
-
const noteSignal = {
|
|
4793
|
-
type: "note",
|
|
4794
|
-
text,
|
|
4795
|
-
timestamp
|
|
4796
|
-
};
|
|
4797
|
-
signals.push(noteSignal);
|
|
4798
|
-
}
|
|
4799
|
-
}
|
|
4800
|
-
return signals;
|
|
4801
|
-
}
|
|
4802
|
-
};
|
|
4803
|
-
|
|
4804
4182
|
// src/integration/ai/output/parser.ts
|
|
4805
4183
|
var signalParser = new SignalParser();
|
|
4806
4184
|
function parseExecutionResult(output) {
|
|
@@ -4951,8 +4329,8 @@ var AutoUserAdapter = class {
|
|
|
4951
4329
|
};
|
|
4952
4330
|
|
|
4953
4331
|
// src/integration/ai/project-tooling.ts
|
|
4954
|
-
import { existsSync
|
|
4955
|
-
import { join as
|
|
4332
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
4333
|
+
import { join as join3 } from "path";
|
|
4956
4334
|
var EMPTY_TOOLING = {
|
|
4957
4335
|
agents: [],
|
|
4958
4336
|
skills: [],
|
|
@@ -4963,7 +4341,7 @@ var EMPTY_TOOLING = {
|
|
|
4963
4341
|
};
|
|
4964
4342
|
function safeListDir(path, predicate) {
|
|
4965
4343
|
try {
|
|
4966
|
-
if (!
|
|
4344
|
+
if (!existsSync(path)) return [];
|
|
4967
4345
|
return readdirSync(path).filter(predicate).sort();
|
|
4968
4346
|
} catch {
|
|
4969
4347
|
return [];
|
|
@@ -4971,23 +4349,23 @@ function safeListDir(path, predicate) {
|
|
|
4971
4349
|
}
|
|
4972
4350
|
var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
|
|
4973
4351
|
function detectAgents(projectPath) {
|
|
4974
|
-
const agentsDir =
|
|
4352
|
+
const agentsDir = join3(projectPath, ".claude", "agents");
|
|
4975
4353
|
return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
|
|
4976
4354
|
}
|
|
4977
4355
|
function detectSkills(projectPath) {
|
|
4978
|
-
const skillsDir =
|
|
4356
|
+
const skillsDir = join3(projectPath, ".claude", "skills");
|
|
4979
4357
|
try {
|
|
4980
|
-
if (!
|
|
4358
|
+
if (!existsSync(skillsDir)) return [];
|
|
4981
4359
|
return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
4982
4360
|
} catch {
|
|
4983
4361
|
return [];
|
|
4984
4362
|
}
|
|
4985
4363
|
}
|
|
4986
4364
|
function detectMcpServers(projectPath) {
|
|
4987
|
-
const mcpFile =
|
|
4988
|
-
if (!
|
|
4365
|
+
const mcpFile = join3(projectPath, ".mcp.json");
|
|
4366
|
+
if (!existsSync(mcpFile)) return [];
|
|
4989
4367
|
try {
|
|
4990
|
-
const raw =
|
|
4368
|
+
const raw = readFileSync(mcpFile, "utf-8");
|
|
4991
4369
|
const parsed = JSON.parse(raw);
|
|
4992
4370
|
const servers = parsed.mcpServers;
|
|
4993
4371
|
if (!servers || typeof servers !== "object") return [];
|
|
@@ -4997,16 +4375,16 @@ function detectMcpServers(projectPath) {
|
|
|
4997
4375
|
}
|
|
4998
4376
|
}
|
|
4999
4377
|
function detectProjectTooling(projectPath) {
|
|
5000
|
-
if (!projectPath || !
|
|
4378
|
+
if (!projectPath || !existsSync(projectPath)) {
|
|
5001
4379
|
return EMPTY_TOOLING;
|
|
5002
4380
|
}
|
|
5003
4381
|
return {
|
|
5004
4382
|
agents: detectAgents(projectPath),
|
|
5005
4383
|
skills: detectSkills(projectPath),
|
|
5006
4384
|
mcpServers: detectMcpServers(projectPath),
|
|
5007
|
-
hasClaudeMd:
|
|
5008
|
-
hasAgentsMd:
|
|
5009
|
-
hasCopilotInstructions:
|
|
4385
|
+
hasClaudeMd: existsSync(join3(projectPath, "CLAUDE.md")),
|
|
4386
|
+
hasAgentsMd: existsSync(join3(projectPath, "AGENTS.md")),
|
|
4387
|
+
hasCopilotInstructions: existsSync(join3(projectPath, ".github", "copilot-instructions.md"))
|
|
5010
4388
|
};
|
|
5011
4389
|
}
|
|
5012
4390
|
function detectProjectToolingAcrossPaths(projectPaths) {
|
|
@@ -5117,7 +4495,7 @@ function describeMcpHint(name) {
|
|
|
5117
4495
|
}
|
|
5118
4496
|
|
|
5119
4497
|
// src/integration/external/lifecycle.ts
|
|
5120
|
-
import { spawnSync
|
|
4498
|
+
import { spawnSync } from "child_process";
|
|
5121
4499
|
var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5122
4500
|
function getHookTimeoutMs() {
|
|
5123
4501
|
const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
|
|
@@ -5130,7 +4508,7 @@ function getHookTimeoutMs() {
|
|
|
5130
4508
|
function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
5131
4509
|
assertSafeCwd(projectPath);
|
|
5132
4510
|
const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
|
|
5133
|
-
const result =
|
|
4511
|
+
const result = spawnSync(script, {
|
|
5134
4512
|
cwd: projectPath,
|
|
5135
4513
|
shell: true,
|
|
5136
4514
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -5144,9 +4522,9 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
|
5144
4522
|
|
|
5145
4523
|
// src/integration/ai/task-context.ts
|
|
5146
4524
|
import { execSync } from "child_process";
|
|
5147
|
-
import { Result as
|
|
4525
|
+
import { Result as Result3 } from "typescript-result";
|
|
5148
4526
|
function getRecentGitHistory(projectPath, count = 20) {
|
|
5149
|
-
const r =
|
|
4527
|
+
const r = Result3.try(() => {
|
|
5150
4528
|
assertSafeCwd(projectPath);
|
|
5151
4529
|
const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
|
|
5152
4530
|
cwd: projectPath,
|
|
@@ -5159,7 +4537,7 @@ function getRecentGitHistory(projectPath, count = 20) {
|
|
|
5159
4537
|
}
|
|
5160
4538
|
|
|
5161
4539
|
// src/integration/external/git.ts
|
|
5162
|
-
import { spawnSync as
|
|
4540
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5163
4541
|
var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
|
|
5164
4542
|
var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
|
|
5165
4543
|
function isValidBranchName(name) {
|
|
@@ -5172,7 +4550,7 @@ function isValidBranchName(name) {
|
|
|
5172
4550
|
}
|
|
5173
4551
|
function getCurrentBranch(cwd) {
|
|
5174
4552
|
assertSafeCwd(cwd);
|
|
5175
|
-
const result =
|
|
4553
|
+
const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
5176
4554
|
cwd,
|
|
5177
4555
|
encoding: "utf-8",
|
|
5178
4556
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5187,7 +4565,7 @@ function branchExists(cwd, name) {
|
|
|
5187
4565
|
if (!isValidBranchName(name)) {
|
|
5188
4566
|
throw new Error(`Invalid branch name: ${name}`);
|
|
5189
4567
|
}
|
|
5190
|
-
const result =
|
|
4568
|
+
const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
|
|
5191
4569
|
cwd,
|
|
5192
4570
|
encoding: "utf-8",
|
|
5193
4571
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5204,7 +4582,7 @@ function createAndCheckoutBranch(cwd, name) {
|
|
|
5204
4582
|
return;
|
|
5205
4583
|
}
|
|
5206
4584
|
if (branchExists(cwd, name)) {
|
|
5207
|
-
const result =
|
|
4585
|
+
const result = spawnSync2("git", ["checkout", name], {
|
|
5208
4586
|
cwd,
|
|
5209
4587
|
encoding: "utf-8",
|
|
5210
4588
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5213,7 +4591,7 @@ function createAndCheckoutBranch(cwd, name) {
|
|
|
5213
4591
|
throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
|
|
5214
4592
|
}
|
|
5215
4593
|
} else {
|
|
5216
|
-
const result =
|
|
4594
|
+
const result = spawnSync2("git", ["checkout", "-b", name], {
|
|
5217
4595
|
cwd,
|
|
5218
4596
|
encoding: "utf-8",
|
|
5219
4597
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5229,7 +4607,7 @@ function verifyCurrentBranch(cwd, expected) {
|
|
|
5229
4607
|
}
|
|
5230
4608
|
function getDefaultBranch(cwd) {
|
|
5231
4609
|
assertSafeCwd(cwd);
|
|
5232
|
-
const result =
|
|
4610
|
+
const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
5233
4611
|
cwd,
|
|
5234
4612
|
encoding: "utf-8",
|
|
5235
4613
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5250,7 +4628,7 @@ function getDefaultBranch(cwd) {
|
|
|
5250
4628
|
function getHeadSha(cwd) {
|
|
5251
4629
|
try {
|
|
5252
4630
|
assertSafeCwd(cwd);
|
|
5253
|
-
const result =
|
|
4631
|
+
const result = spawnSync2("git", ["rev-parse", "HEAD"], {
|
|
5254
4632
|
cwd,
|
|
5255
4633
|
encoding: "utf-8",
|
|
5256
4634
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5263,7 +4641,7 @@ function getHeadSha(cwd) {
|
|
|
5263
4641
|
}
|
|
5264
4642
|
function hasUncommittedChanges(cwd) {
|
|
5265
4643
|
assertSafeCwd(cwd);
|
|
5266
|
-
const result =
|
|
4644
|
+
const result = spawnSync2("git", ["status", "--porcelain"], {
|
|
5267
4645
|
cwd,
|
|
5268
4646
|
encoding: "utf-8",
|
|
5269
4647
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5275,7 +4653,7 @@ function hasUncommittedChanges(cwd) {
|
|
|
5275
4653
|
}
|
|
5276
4654
|
function autoCommit(cwd, message) {
|
|
5277
4655
|
assertSafeCwd(cwd);
|
|
5278
|
-
const add =
|
|
4656
|
+
const add = spawnSync2("git", ["add", "-A"], {
|
|
5279
4657
|
cwd,
|
|
5280
4658
|
encoding: "utf-8",
|
|
5281
4659
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5283,7 +4661,7 @@ function autoCommit(cwd, message) {
|
|
|
5283
4661
|
if (add.status !== 0) {
|
|
5284
4662
|
throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
|
|
5285
4663
|
}
|
|
5286
|
-
const commit =
|
|
4664
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
5287
4665
|
cwd,
|
|
5288
4666
|
encoding: "utf-8",
|
|
5289
4667
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5296,14 +4674,14 @@ function generateBranchName(sprintId) {
|
|
|
5296
4674
|
return `ralphctl/${sprintId}`;
|
|
5297
4675
|
}
|
|
5298
4676
|
function isGhAvailable() {
|
|
5299
|
-
const result =
|
|
4677
|
+
const result = spawnSync2("gh", ["--version"], {
|
|
5300
4678
|
encoding: "utf-8",
|
|
5301
4679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5302
4680
|
});
|
|
5303
4681
|
return result.status === 0;
|
|
5304
4682
|
}
|
|
5305
4683
|
function isGlabAvailable() {
|
|
5306
|
-
const result =
|
|
4684
|
+
const result = spawnSync2("glab", ["--version"], {
|
|
5307
4685
|
encoding: "utf-8",
|
|
5308
4686
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5309
4687
|
});
|
|
@@ -5378,6 +4756,265 @@ var DefaultExternalAdapter = class {
|
|
|
5378
4756
|
}
|
|
5379
4757
|
};
|
|
5380
4758
|
|
|
4759
|
+
// src/integration/external/onboard-adapter.ts
|
|
4760
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
4761
|
+
import { join as join6 } from "path";
|
|
4762
|
+
|
|
4763
|
+
// src/integration/external/agents-md-linter.ts
|
|
4764
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
4765
|
+
import { join as join4 } from "path";
|
|
4766
|
+
var MAX_H2 = 7;
|
|
4767
|
+
var MAX_LINES = 300;
|
|
4768
|
+
var MIN_FLESCH = 40;
|
|
4769
|
+
var REQUIRED_H2_SECTIONS = [
|
|
4770
|
+
"Project Overview",
|
|
4771
|
+
"Build & Run",
|
|
4772
|
+
"Testing",
|
|
4773
|
+
"Architecture",
|
|
4774
|
+
"Implementation Style",
|
|
4775
|
+
"Security & Safety",
|
|
4776
|
+
"Performance Constraints"
|
|
4777
|
+
];
|
|
4778
|
+
function normalizeHeading(raw) {
|
|
4779
|
+
return raw.replace(/^#+\s*/, "").replace(/[*_`]/g, "").trim().toLowerCase();
|
|
4780
|
+
}
|
|
4781
|
+
function lintAgentsMd(content) {
|
|
4782
|
+
const violations = [];
|
|
4783
|
+
const lines = content.split("\n");
|
|
4784
|
+
if (lines.length >= MAX_LINES) {
|
|
4785
|
+
violations.push({
|
|
4786
|
+
rule: "max-lines",
|
|
4787
|
+
message: `Project context file is ${String(lines.length)} lines (must be under ${String(MAX_LINES)}).`
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
let inCodeFence = false;
|
|
4791
|
+
let h1Count = 0;
|
|
4792
|
+
let h2Count = 0;
|
|
4793
|
+
const h2Titles = [];
|
|
4794
|
+
for (const line of lines) {
|
|
4795
|
+
if (line.startsWith("```")) {
|
|
4796
|
+
inCodeFence = !inCodeFence;
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
if (inCodeFence) continue;
|
|
4800
|
+
const match = /^(#+)\s/.exec(line);
|
|
4801
|
+
if (!match) continue;
|
|
4802
|
+
const depth = match[1]?.length ?? 0;
|
|
4803
|
+
if (depth === 1) h1Count++;
|
|
4804
|
+
else if (depth === 2) {
|
|
4805
|
+
h2Count++;
|
|
4806
|
+
h2Titles.push(normalizeHeading(line));
|
|
4807
|
+
} else if (depth >= 4) {
|
|
4808
|
+
violations.push({
|
|
4809
|
+
rule: "no-h4-plus",
|
|
4810
|
+
message: `H${String(depth)} heading is too deep \u2014 keep structure flat (H1/H2/H3 only): "${line.trim()}"`
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
for (const required of REQUIRED_H2_SECTIONS) {
|
|
4815
|
+
if (!h2Titles.includes(required.toLowerCase())) {
|
|
4816
|
+
violations.push({
|
|
4817
|
+
rule: "required-section",
|
|
4818
|
+
message: `Missing required H2 section: "## ${required}".`
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
if (h1Count !== 1) {
|
|
4823
|
+
violations.push({
|
|
4824
|
+
rule: "single-h1",
|
|
4825
|
+
message: `Expected exactly one H1, found ${String(h1Count)}.`
|
|
4826
|
+
});
|
|
4827
|
+
}
|
|
4828
|
+
if (h2Count > MAX_H2) {
|
|
4829
|
+
violations.push({
|
|
4830
|
+
rule: "max-h2",
|
|
4831
|
+
message: `Too many H2 sections (${String(h2Count)}); keep at most ${String(MAX_H2)}.`
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
const flesch = fleschReadingEase(content);
|
|
4835
|
+
if (Number.isFinite(flesch) && flesch < MIN_FLESCH) {
|
|
4836
|
+
violations.push({
|
|
4837
|
+
rule: "readability",
|
|
4838
|
+
message: `Flesch score ${flesch.toFixed(1)} is below ${String(MIN_FLESCH)} \u2014 simplify long sentences.`
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
return { ok: violations.length === 0, violations };
|
|
4842
|
+
}
|
|
4843
|
+
function fleschReadingEase(content) {
|
|
4844
|
+
const prose = stripNonProse(content);
|
|
4845
|
+
const words = prose.match(/[A-Za-z][A-Za-z'-]*/g) ?? [];
|
|
4846
|
+
if (words.length === 0) return 100;
|
|
4847
|
+
const sentences = Math.max(1, (prose.match(/[.!?]+(?:\s|$)/g) ?? []).length);
|
|
4848
|
+
const syllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
|
|
4849
|
+
return 206.835 - 1.015 * (words.length / sentences) - 84.6 * (syllables / words.length);
|
|
4850
|
+
}
|
|
4851
|
+
function stripNonProse(content) {
|
|
4852
|
+
return content.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^#+\s+.*$/gm, " ").replace(/^\s*[-*+]\s+/gm, "");
|
|
4853
|
+
}
|
|
4854
|
+
function countSyllables(word) {
|
|
4855
|
+
const lower = word.toLowerCase();
|
|
4856
|
+
const groups = lower.match(/[aeiouy]+/g) ?? [];
|
|
4857
|
+
let count = groups.length;
|
|
4858
|
+
if (lower.at(-1) === "e" && count > 1) count--;
|
|
4859
|
+
return Math.max(1, count);
|
|
4860
|
+
}
|
|
4861
|
+
function detectCommandDrift(content, repoPath) {
|
|
4862
|
+
const warnings = [];
|
|
4863
|
+
const pkgPath = join4(repoPath, "package.json");
|
|
4864
|
+
if (!existsSync2(pkgPath)) return warnings;
|
|
4865
|
+
let scripts = {};
|
|
4866
|
+
try {
|
|
4867
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
4868
|
+
const parsed = JSON.parse(raw);
|
|
4869
|
+
if (isRecord(parsed) && isRecord(parsed["scripts"])) {
|
|
4870
|
+
const entries = Object.entries(parsed["scripts"]).filter((pair) => {
|
|
4871
|
+
return typeof pair[1] === "string";
|
|
4872
|
+
});
|
|
4873
|
+
scripts = Object.fromEntries(entries);
|
|
4874
|
+
}
|
|
4875
|
+
} catch {
|
|
4876
|
+
return warnings;
|
|
4877
|
+
}
|
|
4878
|
+
const re = /\b(?:npm|pnpm|yarn)\s+(?:run\s+)?([a-z][a-z0-9:_-]*)/gi;
|
|
4879
|
+
let match;
|
|
4880
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4881
|
+
while ((match = re.exec(content)) !== null) {
|
|
4882
|
+
const name = match[1];
|
|
4883
|
+
if (!name) continue;
|
|
4884
|
+
if (seen.has(name)) continue;
|
|
4885
|
+
seen.add(name);
|
|
4886
|
+
if (name === "install" || name === "test" || name === "start") continue;
|
|
4887
|
+
if (!(name in scripts)) {
|
|
4888
|
+
warnings.push(`Referenced script "${name}" not defined in package.json`);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
return warnings;
|
|
4892
|
+
}
|
|
4893
|
+
function isRecord(value) {
|
|
4894
|
+
return typeof value === "object" && value !== null;
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
// src/integration/external/agents-md-writer.ts
|
|
4898
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
|
|
4899
|
+
import { dirname, join as join5 } from "path";
|
|
4900
|
+
import { randomBytes } from "crypto";
|
|
4901
|
+
var RALPHCTL_MARKER = "<!-- managed by ralphctl onboard -->";
|
|
4902
|
+
function providerInstructionsPath(repoPath, provider) {
|
|
4903
|
+
if (provider === "claude") return join5(repoPath, "CLAUDE.md");
|
|
4904
|
+
return join5(repoPath, ".github", "copilot-instructions.md");
|
|
4905
|
+
}
|
|
4906
|
+
function readExistingProviderInstructions(repoPath, provider) {
|
|
4907
|
+
const path = providerInstructionsPath(repoPath, provider);
|
|
4908
|
+
if (!existsSync3(path)) return { content: null, authored: false };
|
|
4909
|
+
let content;
|
|
4910
|
+
try {
|
|
4911
|
+
content = readFileSync3(path, "utf-8");
|
|
4912
|
+
} catch {
|
|
4913
|
+
return { content: null, authored: false };
|
|
4914
|
+
}
|
|
4915
|
+
const managed = content.includes(RALPHCTL_MARKER);
|
|
4916
|
+
return { content, authored: !managed };
|
|
4917
|
+
}
|
|
4918
|
+
function writeProviderInstructionsAtomic(repoPath, content, provider) {
|
|
4919
|
+
const target = providerInstructionsPath(repoPath, provider);
|
|
4920
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
4921
|
+
const body = content.endsWith("\n") ? content : `${content}
|
|
4922
|
+
`;
|
|
4923
|
+
const stamped = body.includes(RALPHCTL_MARKER) ? body : `${body}
|
|
4924
|
+
${RALPHCTL_MARKER}
|
|
4925
|
+
`;
|
|
4926
|
+
const tempPath = `${target}.${randomBytes(6).toString("hex")}.tmp`;
|
|
4927
|
+
writeFileSync(tempPath, stamped, { encoding: "utf-8", mode: 420 });
|
|
4928
|
+
renameSync(tempPath, target);
|
|
4929
|
+
return { path: target };
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
// src/integration/ai/discover-agents-md.ts
|
|
4933
|
+
var DISCOVERY_TIMEOUT_MS = 12e4;
|
|
4934
|
+
async function discoverAgentsMdWithAi(ctx, aiSession, signalParser2) {
|
|
4935
|
+
const prompt = buildRepoOnboardPrompt(ctx);
|
|
4936
|
+
const session = aiSession.spawnHeadless(prompt, { cwd: ctx.repoPath });
|
|
4937
|
+
const timeout = new Promise((resolve) => {
|
|
4938
|
+
setTimeout(() => {
|
|
4939
|
+
resolve(null);
|
|
4940
|
+
}, DISCOVERY_TIMEOUT_MS).unref();
|
|
4941
|
+
});
|
|
4942
|
+
try {
|
|
4943
|
+
const result = await Promise.race([session, timeout]);
|
|
4944
|
+
if (!result) return { agentsMd: null, checkScript: null, changes: null };
|
|
4945
|
+
const signals = signalParser2.parseSignals(result.output);
|
|
4946
|
+
const agentsSignal = signals.find((s) => s.type === "agents-md-proposal");
|
|
4947
|
+
const checkSignal = signals.find((s) => s.type === "check-script-discovery");
|
|
4948
|
+
const changes = extractChanges(result.output);
|
|
4949
|
+
return {
|
|
4950
|
+
agentsMd: agentsSignal ? agentsSignal.content : null,
|
|
4951
|
+
checkScript: checkSignal ? checkSignal.command : null,
|
|
4952
|
+
changes
|
|
4953
|
+
};
|
|
4954
|
+
} catch {
|
|
4955
|
+
return { agentsMd: null, checkScript: null, changes: null };
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
function extractChanges(output) {
|
|
4959
|
+
const match = /<changes>([\s\S]*?)<\/changes>/.exec(output);
|
|
4960
|
+
if (!match?.[1]) return null;
|
|
4961
|
+
const body = match[1].trim();
|
|
4962
|
+
return body.length > 0 ? body : null;
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
// src/integration/external/onboard-adapter.ts
|
|
4966
|
+
var DefaultOnboardAdapter = class {
|
|
4967
|
+
constructor(aiSession, signalParser2) {
|
|
4968
|
+
this.aiSession = aiSession;
|
|
4969
|
+
this.signalParser = signalParser2;
|
|
4970
|
+
}
|
|
4971
|
+
aiSession;
|
|
4972
|
+
signalParser;
|
|
4973
|
+
readExistingInstructions(repoPath, provider) {
|
|
4974
|
+
return readExistingProviderInstructions(repoPath, provider);
|
|
4975
|
+
}
|
|
4976
|
+
validateRepoPath(path) {
|
|
4977
|
+
let exists;
|
|
4978
|
+
try {
|
|
4979
|
+
exists = existsSync4(path) && statSync(path).isDirectory();
|
|
4980
|
+
} catch {
|
|
4981
|
+
exists = false;
|
|
4982
|
+
}
|
|
4983
|
+
if (!exists) return { exists: false, isGitRepo: false };
|
|
4984
|
+
const isGitRepo = existsSync4(join6(path, ".git"));
|
|
4985
|
+
return { exists: true, isGitRepo };
|
|
4986
|
+
}
|
|
4987
|
+
lintAgentsMd(content) {
|
|
4988
|
+
return lintAgentsMd(content);
|
|
4989
|
+
}
|
|
4990
|
+
detectCommandDrift(content, repoPath) {
|
|
4991
|
+
return detectCommandDrift(content, repoPath);
|
|
4992
|
+
}
|
|
4993
|
+
async discoverAgentsMd(input) {
|
|
4994
|
+
return discoverAgentsMdWithAi(input, this.aiSession, this.signalParser);
|
|
4995
|
+
}
|
|
4996
|
+
inferProjectType(repoPath) {
|
|
4997
|
+
const checks = [
|
|
4998
|
+
["package.json", "node"],
|
|
4999
|
+
["pyproject.toml", "python"],
|
|
5000
|
+
["requirements.txt", "python"],
|
|
5001
|
+
["Cargo.toml", "rust"],
|
|
5002
|
+
["go.mod", "go"],
|
|
5003
|
+
["pom.xml", "java"],
|
|
5004
|
+
["build.gradle", "java"],
|
|
5005
|
+
["Makefile", "makefile"]
|
|
5006
|
+
];
|
|
5007
|
+
const hints = [];
|
|
5008
|
+
for (const [file, label] of checks) {
|
|
5009
|
+
if (existsSync4(join6(repoPath, file))) hints.push(label);
|
|
5010
|
+
}
|
|
5011
|
+
return hints.length === 0 ? "unknown" : hints.join(", ");
|
|
5012
|
+
}
|
|
5013
|
+
writeProviderInstructions(repoPath, content, provider) {
|
|
5014
|
+
return writeProviderInstructionsAtomic(repoPath, content, provider);
|
|
5015
|
+
}
|
|
5016
|
+
};
|
|
5017
|
+
|
|
5381
5018
|
// src/application/factories.ts
|
|
5382
5019
|
function createAiDeps(auto) {
|
|
5383
5020
|
return {
|
|
@@ -5437,6 +5074,22 @@ function createIdeatePipeline2(shared, idea, options = {}) {
|
|
|
5437
5074
|
options
|
|
5438
5075
|
);
|
|
5439
5076
|
}
|
|
5077
|
+
function createOnboardPipeline2(shared, options = {}) {
|
|
5078
|
+
const aiSession = new ProviderAiSessionAdapter();
|
|
5079
|
+
const adapter = new DefaultOnboardAdapter(aiSession, shared.signalParser);
|
|
5080
|
+
return createOnboardPipeline(
|
|
5081
|
+
{
|
|
5082
|
+
persistence: shared.persistence,
|
|
5083
|
+
adapter,
|
|
5084
|
+
logger: shared.logger,
|
|
5085
|
+
prompt: shared.prompt,
|
|
5086
|
+
updateProjectRepos: async (name, repositories) => {
|
|
5087
|
+
return updateProject(name, { repositories });
|
|
5088
|
+
}
|
|
5089
|
+
},
|
|
5090
|
+
options
|
|
5091
|
+
);
|
|
5092
|
+
}
|
|
5440
5093
|
function createExecuteSprintPipeline2(shared, options = {}) {
|
|
5441
5094
|
const { aiSession, promptBuilder, parser, ui, external } = createAiDeps(false);
|
|
5442
5095
|
return createExecuteSprintPipeline(
|
|
@@ -5615,6 +5268,7 @@ async function sprintStartCommand(args) {
|
|
|
5615
5268
|
}
|
|
5616
5269
|
|
|
5617
5270
|
export {
|
|
5271
|
+
executePipeline,
|
|
5618
5272
|
getTasks,
|
|
5619
5273
|
saveTasks,
|
|
5620
5274
|
getTask,
|
|
@@ -5630,29 +5284,20 @@ export {
|
|
|
5630
5284
|
areAllTasksDone,
|
|
5631
5285
|
reorderByDependencies,
|
|
5632
5286
|
validateImportTasks,
|
|
5633
|
-
getCurrentBranch,
|
|
5634
|
-
branchExists,
|
|
5635
|
-
getDefaultBranch,
|
|
5636
|
-
isGhAvailable,
|
|
5637
|
-
isGlabAvailable,
|
|
5638
|
-
executePipeline,
|
|
5639
|
-
processLifecycleAdapter,
|
|
5640
|
-
resolveProvider,
|
|
5641
|
-
providerDisplayName,
|
|
5642
|
-
enterAltScreen,
|
|
5643
|
-
exitAltScreen,
|
|
5644
|
-
registerTuiInstance,
|
|
5645
|
-
withSuspendedTui,
|
|
5646
|
-
buildTicketRefinePrompt,
|
|
5647
5287
|
renderParsedTasksTable,
|
|
5648
5288
|
importTasks,
|
|
5649
5289
|
formatTicketForPrompt,
|
|
5650
5290
|
parseRequirementsFile,
|
|
5651
5291
|
runAiSession,
|
|
5652
|
-
|
|
5292
|
+
getCurrentBranch,
|
|
5293
|
+
branchExists,
|
|
5294
|
+
getDefaultBranch,
|
|
5295
|
+
isGhAvailable,
|
|
5296
|
+
isGlabAvailable,
|
|
5653
5297
|
createRefinePipeline2 as createRefinePipeline,
|
|
5654
5298
|
createPlanPipeline2 as createPlanPipeline,
|
|
5655
5299
|
createIdeatePipeline2 as createIdeatePipeline,
|
|
5300
|
+
createOnboardPipeline2 as createOnboardPipeline,
|
|
5656
5301
|
createExecuteSprintPipeline2 as createExecuteSprintPipeline,
|
|
5657
5302
|
parseSprintStartArgs,
|
|
5658
5303
|
sprintStartCommand
|