ralphctl 0.4.2 → 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-TKPTT2UG.mjs → chunk-LCY32RW4.mjs} +621 -990
- 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-GL7MKLLS.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-ISHZM36X.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/repo-onboard.md +111 -0
- package/dist/{resolver-EOE5WUMV.mjs → resolver-PG2DZEBX.mjs} +3 -3
- package/dist/{sprint-OGOFEJJH.mjs → sprint-54DOSIJK.mjs} +3 -3
- package/dist/{start-76JKJQIH.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,843 +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));
|
|
3710
|
-
}
|
|
3711
|
-
if (this.children.size > 0) {
|
|
3712
|
-
log.warn(`Force-killing ${String(this.children.size)} remaining process(es)...`);
|
|
3713
|
-
this.killAll("SIGKILL");
|
|
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 });
|
|
3714
3627
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
* @internal
|
|
3720
|
-
*/
|
|
3721
|
-
dispose() {
|
|
3722
|
-
if (this.sigintHandler) {
|
|
3723
|
-
process.removeListener("SIGINT", this.sigintHandler);
|
|
3724
|
-
this.sigintHandler = null;
|
|
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 });
|
|
3725
3632
|
}
|
|
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
|
-
var CHECK_GATE_EXAMPLE = "Run the project's check gate \u2014 all pass (omit this step when the project has no check script)";
|
|
4243
|
-
function buildPlanCommon(projectToolingSection) {
|
|
4244
|
-
return composePrompt(loadPartial("plan-common"), {
|
|
4245
|
-
PLAN_COMMON_EXAMPLES: loadPartial("plan-common-examples"),
|
|
4246
|
-
PROJECT_TOOLING: projectToolingSection,
|
|
4247
|
-
CHECK_GATE_EXAMPLE
|
|
4248
|
-
});
|
|
4249
|
-
}
|
|
4250
|
-
function buildPlannerBase(projectToolingSection) {
|
|
4251
|
-
return {
|
|
4252
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4253
|
-
COMMON: buildPlanCommon(projectToolingSection),
|
|
4254
|
-
VALIDATION: loadPartial("validation-checklist"),
|
|
4255
|
-
SIGNALS: loadPartial("signals-planning"),
|
|
4256
|
-
CHECK_GATE_EXAMPLE
|
|
4257
|
-
};
|
|
4258
|
-
}
|
|
4259
|
-
function buildInteractivePrompt(context, outputFile, schema, projectToolingSection) {
|
|
4260
|
-
return composePrompt(loadTemplate("plan-interactive"), {
|
|
4261
|
-
...buildPlannerBase(projectToolingSection),
|
|
4262
|
-
CONTEXT: context,
|
|
4263
|
-
OUTPUT_FILE: outputFile,
|
|
4264
|
-
SCHEMA: schema
|
|
4265
|
-
});
|
|
4266
|
-
}
|
|
4267
|
-
function buildAutoPrompt(context, schema, projectToolingSection) {
|
|
4268
|
-
return composePrompt(loadTemplate("plan-auto"), {
|
|
4269
|
-
...buildPlannerBase(projectToolingSection),
|
|
4270
|
-
CONTEXT: context,
|
|
4271
|
-
SCHEMA: schema
|
|
4272
|
-
});
|
|
4273
|
-
}
|
|
4274
|
-
function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName, projectToolingSection = "") {
|
|
4275
|
-
let template = loadTemplate("task-execution");
|
|
4276
|
-
if (noCommit) {
|
|
4277
|
-
template = template.replace(/^[ \t]*\{\{COMMIT_STEP\}\}\n/m, "\n");
|
|
4278
|
-
template = template.replace(/^[ \t]*\{\{COMMIT_CONSTRAINT\}\}\n/m, "");
|
|
4279
|
-
}
|
|
4280
|
-
const commitStep = noCommit ? "" : " - **Before continuing:** Create a git commit with a descriptive message for the changes made.";
|
|
4281
|
-
const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.";
|
|
4282
|
-
return composePrompt(template, {
|
|
4283
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4284
|
-
SIGNALS: loadPartial("signals-task"),
|
|
4285
|
-
PROGRESS_FILE: progressFilePath,
|
|
4286
|
-
COMMIT_STEP: commitStep,
|
|
4287
|
-
COMMIT_CONSTRAINT: commitConstraint,
|
|
4288
|
-
CONTEXT_FILE: contextFileName,
|
|
4289
|
-
PROJECT_TOOLING: projectToolingSection
|
|
4290
|
-
});
|
|
4291
|
-
}
|
|
4292
|
-
function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
|
|
4293
|
-
const template = loadTemplate("ticket-refine");
|
|
4294
|
-
const issueContextSection = issueContext ? `<context>
|
|
4295
|
-
|
|
4296
|
-
${issueContext}
|
|
4297
|
-
|
|
4298
|
-
</context>` : "";
|
|
4299
|
-
return composePrompt(template, {
|
|
4300
|
-
TICKET: ticketContent,
|
|
4301
|
-
OUTPUT_FILE: outputFile,
|
|
4302
|
-
SCHEMA: schema,
|
|
4303
|
-
ISSUE_CONTEXT: issueContextSection
|
|
4304
3856
|
});
|
|
4305
3857
|
}
|
|
4306
|
-
function
|
|
4307
|
-
return
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
SCHEMA: schema
|
|
4315
|
-
});
|
|
4316
|
-
}
|
|
4317
|
-
function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema, projectToolingSection) {
|
|
4318
|
-
return composePrompt(loadTemplate("ideate-auto"), {
|
|
4319
|
-
...buildPlannerBase(projectToolingSection),
|
|
4320
|
-
IDEA_TITLE: ideaTitle,
|
|
4321
|
-
IDEA_DESCRIPTION: ideaDescription,
|
|
4322
|
-
PROJECT_NAME: projectName,
|
|
4323
|
-
REPOSITORIES: repositories,
|
|
4324
|
-
SCHEMA: schema
|
|
4325
|
-
});
|
|
4326
|
-
}
|
|
4327
|
-
function renderExtraDimensions(extras) {
|
|
4328
|
-
if (extras.length === 0) {
|
|
4329
|
-
return { section: "", passBar: "", assessment: "" };
|
|
4330
|
-
}
|
|
4331
|
-
const section = extras.map(
|
|
4332
|
-
(name) => `
|
|
4333
|
-
<dimension name="${name}" floor="false">
|
|
4334
|
-
Additional task-specific dimension flagged by the planner. Apply judgment to whether the implementation satisfies this dimension given the task's verification criteria and steps.
|
|
4335
|
-
</dimension>
|
|
4336
|
-
`
|
|
4337
|
-
).join("");
|
|
4338
|
-
const passBar = extras.map((name) => `
|
|
4339
|
-
- **${name}**: Task-specific dimension flagged by the planner`).join("");
|
|
4340
|
-
return {
|
|
4341
|
-
section,
|
|
4342
|
-
passBar,
|
|
4343
|
-
assessment: extras.map((name) => `
|
|
4344
|
-
**${name}**: PASS/FAIL \u2014 [one-line finding]`).join("")
|
|
4345
|
-
};
|
|
4346
|
-
}
|
|
4347
|
-
function buildEvaluatorPrompt(ctx) {
|
|
4348
|
-
const template = loadTemplate("task-evaluation");
|
|
4349
|
-
const descriptionSection = ctx.taskDescription ? `
|
|
4350
|
-
**Description:** ${ctx.taskDescription}` : "";
|
|
4351
|
-
const stepsSection = ctx.taskSteps.length > 0 ? `
|
|
4352
|
-
**Implementation Steps:**
|
|
4353
|
-
${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
|
|
4354
|
-
const criteriaSection = ctx.verificationCriteria.length > 0 ? `
|
|
4355
|
-
**Verification Criteria:**
|
|
4356
|
-
${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
|
|
4357
|
-
const checkSection = ctx.checkScriptSection ? `
|
|
4358
|
-
|
|
4359
|
-
${ctx.checkScriptSection}` : "";
|
|
4360
|
-
const extras = renderExtraDimensions(ctx.extraDimensions);
|
|
4361
|
-
const extraAssessmentPass = extras.assessment.replace(/PASS\/FAIL/g, "PASS");
|
|
4362
|
-
return composePrompt(template, {
|
|
4363
|
-
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
4364
|
-
SIGNALS: loadPartial("signals-evaluation"),
|
|
4365
|
-
TASK_NAME: ctx.taskName,
|
|
4366
|
-
TASK_DESCRIPTION_SECTION: descriptionSection,
|
|
4367
|
-
TASK_STEPS_SECTION: stepsSection,
|
|
4368
|
-
VERIFICATION_CRITERIA_SECTION: criteriaSection,
|
|
4369
|
-
PROJECT_PATH: ctx.projectPath,
|
|
4370
|
-
CHECK_SCRIPT_SECTION: checkSection,
|
|
4371
|
-
PROJECT_TOOLING: ctx.projectToolingSection,
|
|
4372
|
-
EXTRA_DIMENSIONS_SECTION: extras.section,
|
|
4373
|
-
EXTRA_DIMENSIONS_PASS_BAR: extras.passBar,
|
|
4374
|
-
EXTRA_DIMENSIONS_ASSESSMENT_PASS: extraAssessmentPass,
|
|
4375
|
-
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({});
|
|
4376
3866
|
});
|
|
4377
3867
|
}
|
|
4378
|
-
function
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
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
|
+
]);
|
|
4391
3881
|
}
|
|
4392
3882
|
|
|
4393
3883
|
// src/integration/ai/prompts/prompt-builder-adapter.ts
|
|
@@ -4595,8 +4085,8 @@ async function importTasksReplace(tasks, sprintId) {
|
|
|
4595
4085
|
|
|
4596
4086
|
// src/integration/cli/commands/ticket/refine-utils.ts
|
|
4597
4087
|
import { writeFile } from "fs/promises";
|
|
4598
|
-
import { join as
|
|
4599
|
-
import { Result as
|
|
4088
|
+
import { join as join2 } from "path";
|
|
4089
|
+
import { Result as Result2 } from "typescript-result";
|
|
4600
4090
|
function formatTicketForPrompt(ticket) {
|
|
4601
4091
|
const lines = [];
|
|
4602
4092
|
lines.push(`### ${formatTicketDisplay(ticket)}`);
|
|
@@ -4614,7 +4104,7 @@ function formatTicketForPrompt(ticket) {
|
|
|
4614
4104
|
}
|
|
4615
4105
|
function parseRequirementsFile(content) {
|
|
4616
4106
|
const jsonStr = extractJsonArray(content);
|
|
4617
|
-
const parseR =
|
|
4107
|
+
const parseR = Result2.try(() => JSON.parse(jsonStr));
|
|
4618
4108
|
if (!parseR.ok) {
|
|
4619
4109
|
throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
|
|
4620
4110
|
}
|
|
@@ -4634,7 +4124,7 @@ ${issues}`);
|
|
|
4634
4124
|
return result.data;
|
|
4635
4125
|
}
|
|
4636
4126
|
async function runAiSession(workingDir, prompt, ticketTitle) {
|
|
4637
|
-
const contextFile =
|
|
4127
|
+
const contextFile = join2(workingDir, "refine-context.md");
|
|
4638
4128
|
await writeFile(contextFile, prompt, "utf-8");
|
|
4639
4129
|
const provider = await getActiveProvider();
|
|
4640
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.`;
|
|
@@ -4689,132 +4179,6 @@ function parseEvaluationResult(output) {
|
|
|
4689
4179
|
return { passed: false, status: "malformed", output, dimensions };
|
|
4690
4180
|
}
|
|
4691
4181
|
|
|
4692
|
-
// src/integration/signals/parser.ts
|
|
4693
|
-
var SIGNAL_PATTERNS = {
|
|
4694
|
-
progress: /<progress>([\s\S]*?)<\/progress>/g,
|
|
4695
|
-
progressWithFiles: /<progress>([\s\S]*?)<\/progress>/,
|
|
4696
|
-
evaluation_passed: /<evaluation-passed>/,
|
|
4697
|
-
evaluation_failed: /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/,
|
|
4698
|
-
task_verified: /<task-verified>([\s\S]*?)<\/task-verified>/,
|
|
4699
|
-
task_complete: /<task-complete>/,
|
|
4700
|
-
task_blocked: /<task-blocked>([\s\S]*?)<\/task-blocked>/,
|
|
4701
|
-
note: /<note>([\s\S]*?)<\/note>/g
|
|
4702
|
-
};
|
|
4703
|
-
var DIMENSION_LINE2 = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/gi;
|
|
4704
|
-
function parseDimensionScores2(output) {
|
|
4705
|
-
const scores = [];
|
|
4706
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4707
|
-
DIMENSION_LINE2.lastIndex = 0;
|
|
4708
|
-
let match;
|
|
4709
|
-
while ((match = DIMENSION_LINE2.exec(output)) !== null) {
|
|
4710
|
-
const rawName = match[1];
|
|
4711
|
-
const verdict = match[2];
|
|
4712
|
-
const finding = match[3];
|
|
4713
|
-
if (!rawName || !verdict || !finding) continue;
|
|
4714
|
-
const name = rawName.toLowerCase();
|
|
4715
|
-
if (seen.has(name)) continue;
|
|
4716
|
-
seen.add(name);
|
|
4717
|
-
scores.push({
|
|
4718
|
-
dimension: name,
|
|
4719
|
-
passed: verdict.toUpperCase() === "PASS",
|
|
4720
|
-
finding: finding.trim()
|
|
4721
|
-
});
|
|
4722
|
-
}
|
|
4723
|
-
return scores;
|
|
4724
|
-
}
|
|
4725
|
-
var SignalParser = class {
|
|
4726
|
-
parseSignals(output) {
|
|
4727
|
-
const signals = [];
|
|
4728
|
-
const timestamp = /* @__PURE__ */ new Date();
|
|
4729
|
-
let progressMatch;
|
|
4730
|
-
while ((progressMatch = SIGNAL_PATTERNS.progress.exec(output)) !== null) {
|
|
4731
|
-
const summary = progressMatch[1]?.trim();
|
|
4732
|
-
if (summary) {
|
|
4733
|
-
const progressSignal = {
|
|
4734
|
-
type: "progress",
|
|
4735
|
-
summary,
|
|
4736
|
-
// Note: Phase 1 doesn't parse files attribute; added in Phase 2+
|
|
4737
|
-
timestamp
|
|
4738
|
-
};
|
|
4739
|
-
signals.push(progressSignal);
|
|
4740
|
-
}
|
|
4741
|
-
}
|
|
4742
|
-
if (output.includes("<evaluation-passed>")) {
|
|
4743
|
-
const dimensions = parseDimensionScores2(output);
|
|
4744
|
-
const evaluationSignal = {
|
|
4745
|
-
type: "evaluation",
|
|
4746
|
-
status: "passed",
|
|
4747
|
-
dimensions,
|
|
4748
|
-
timestamp
|
|
4749
|
-
};
|
|
4750
|
-
signals.push(evaluationSignal);
|
|
4751
|
-
} else {
|
|
4752
|
-
const failedMatch = SIGNAL_PATTERNS.evaluation_failed.exec(output);
|
|
4753
|
-
if (failedMatch?.[1]) {
|
|
4754
|
-
const critique = failedMatch[1].trim();
|
|
4755
|
-
const dimensions = parseDimensionScores2(output);
|
|
4756
|
-
const evaluationSignal = {
|
|
4757
|
-
type: "evaluation",
|
|
4758
|
-
status: dimensions.length > 0 ? "failed" : "malformed",
|
|
4759
|
-
dimensions,
|
|
4760
|
-
critique: dimensions.length > 0 ? critique : void 0,
|
|
4761
|
-
timestamp
|
|
4762
|
-
};
|
|
4763
|
-
signals.push(evaluationSignal);
|
|
4764
|
-
} else if (parseDimensionScores2(output).length > 0) {
|
|
4765
|
-
const dimensions = parseDimensionScores2(output);
|
|
4766
|
-
const evaluationSignal = {
|
|
4767
|
-
type: "evaluation",
|
|
4768
|
-
status: "failed",
|
|
4769
|
-
dimensions,
|
|
4770
|
-
timestamp
|
|
4771
|
-
};
|
|
4772
|
-
signals.push(evaluationSignal);
|
|
4773
|
-
}
|
|
4774
|
-
}
|
|
4775
|
-
const taskVerifiedMatch = SIGNAL_PATTERNS.task_verified.exec(output);
|
|
4776
|
-
if (taskVerifiedMatch?.[1]) {
|
|
4777
|
-
const verificationOutput = taskVerifiedMatch[1].trim();
|
|
4778
|
-
const verifiedSignal = {
|
|
4779
|
-
type: "task-verified",
|
|
4780
|
-
output: verificationOutput,
|
|
4781
|
-
timestamp
|
|
4782
|
-
};
|
|
4783
|
-
signals.push(verifiedSignal);
|
|
4784
|
-
}
|
|
4785
|
-
if (output.includes("<task-complete>")) {
|
|
4786
|
-
const completeSignal = {
|
|
4787
|
-
type: "task-complete",
|
|
4788
|
-
timestamp
|
|
4789
|
-
};
|
|
4790
|
-
signals.push(completeSignal);
|
|
4791
|
-
}
|
|
4792
|
-
const taskBlockedMatch = SIGNAL_PATTERNS.task_blocked.exec(output);
|
|
4793
|
-
if (taskBlockedMatch?.[1]) {
|
|
4794
|
-
const reason = taskBlockedMatch[1].trim();
|
|
4795
|
-
const blockedSignal = {
|
|
4796
|
-
type: "task-blocked",
|
|
4797
|
-
reason,
|
|
4798
|
-
timestamp
|
|
4799
|
-
};
|
|
4800
|
-
signals.push(blockedSignal);
|
|
4801
|
-
}
|
|
4802
|
-
let noteMatch;
|
|
4803
|
-
while ((noteMatch = SIGNAL_PATTERNS.note.exec(output)) !== null) {
|
|
4804
|
-
const text = noteMatch[1]?.trim();
|
|
4805
|
-
if (text) {
|
|
4806
|
-
const noteSignal = {
|
|
4807
|
-
type: "note",
|
|
4808
|
-
text,
|
|
4809
|
-
timestamp
|
|
4810
|
-
};
|
|
4811
|
-
signals.push(noteSignal);
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4814
|
-
return signals;
|
|
4815
|
-
}
|
|
4816
|
-
};
|
|
4817
|
-
|
|
4818
4182
|
// src/integration/ai/output/parser.ts
|
|
4819
4183
|
var signalParser = new SignalParser();
|
|
4820
4184
|
function parseExecutionResult(output) {
|
|
@@ -4965,8 +4329,8 @@ var AutoUserAdapter = class {
|
|
|
4965
4329
|
};
|
|
4966
4330
|
|
|
4967
4331
|
// src/integration/ai/project-tooling.ts
|
|
4968
|
-
import { existsSync
|
|
4969
|
-
import { join as
|
|
4332
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
4333
|
+
import { join as join3 } from "path";
|
|
4970
4334
|
var EMPTY_TOOLING = {
|
|
4971
4335
|
agents: [],
|
|
4972
4336
|
skills: [],
|
|
@@ -4977,7 +4341,7 @@ var EMPTY_TOOLING = {
|
|
|
4977
4341
|
};
|
|
4978
4342
|
function safeListDir(path, predicate) {
|
|
4979
4343
|
try {
|
|
4980
|
-
if (!
|
|
4344
|
+
if (!existsSync(path)) return [];
|
|
4981
4345
|
return readdirSync(path).filter(predicate).sort();
|
|
4982
4346
|
} catch {
|
|
4983
4347
|
return [];
|
|
@@ -4985,23 +4349,23 @@ function safeListDir(path, predicate) {
|
|
|
4985
4349
|
}
|
|
4986
4350
|
var EVALUATOR_DENYLISTED_AGENTS = /* @__PURE__ */ new Set(["implementer", "planner"]);
|
|
4987
4351
|
function detectAgents(projectPath) {
|
|
4988
|
-
const agentsDir =
|
|
4352
|
+
const agentsDir = join3(projectPath, ".claude", "agents");
|
|
4989
4353
|
return safeListDir(agentsDir, (name) => name.endsWith(".md")).map((name) => name.replace(/\.md$/, "")).filter((name) => !EVALUATOR_DENYLISTED_AGENTS.has(name));
|
|
4990
4354
|
}
|
|
4991
4355
|
function detectSkills(projectPath) {
|
|
4992
|
-
const skillsDir =
|
|
4356
|
+
const skillsDir = join3(projectPath, ".claude", "skills");
|
|
4993
4357
|
try {
|
|
4994
|
-
if (!
|
|
4358
|
+
if (!existsSync(skillsDir)) return [];
|
|
4995
4359
|
return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
4996
4360
|
} catch {
|
|
4997
4361
|
return [];
|
|
4998
4362
|
}
|
|
4999
4363
|
}
|
|
5000
4364
|
function detectMcpServers(projectPath) {
|
|
5001
|
-
const mcpFile =
|
|
5002
|
-
if (!
|
|
4365
|
+
const mcpFile = join3(projectPath, ".mcp.json");
|
|
4366
|
+
if (!existsSync(mcpFile)) return [];
|
|
5003
4367
|
try {
|
|
5004
|
-
const raw =
|
|
4368
|
+
const raw = readFileSync(mcpFile, "utf-8");
|
|
5005
4369
|
const parsed = JSON.parse(raw);
|
|
5006
4370
|
const servers = parsed.mcpServers;
|
|
5007
4371
|
if (!servers || typeof servers !== "object") return [];
|
|
@@ -5011,16 +4375,16 @@ function detectMcpServers(projectPath) {
|
|
|
5011
4375
|
}
|
|
5012
4376
|
}
|
|
5013
4377
|
function detectProjectTooling(projectPath) {
|
|
5014
|
-
if (!projectPath || !
|
|
4378
|
+
if (!projectPath || !existsSync(projectPath)) {
|
|
5015
4379
|
return EMPTY_TOOLING;
|
|
5016
4380
|
}
|
|
5017
4381
|
return {
|
|
5018
4382
|
agents: detectAgents(projectPath),
|
|
5019
4383
|
skills: detectSkills(projectPath),
|
|
5020
4384
|
mcpServers: detectMcpServers(projectPath),
|
|
5021
|
-
hasClaudeMd:
|
|
5022
|
-
hasAgentsMd:
|
|
5023
|
-
hasCopilotInstructions:
|
|
4385
|
+
hasClaudeMd: existsSync(join3(projectPath, "CLAUDE.md")),
|
|
4386
|
+
hasAgentsMd: existsSync(join3(projectPath, "AGENTS.md")),
|
|
4387
|
+
hasCopilotInstructions: existsSync(join3(projectPath, ".github", "copilot-instructions.md"))
|
|
5024
4388
|
};
|
|
5025
4389
|
}
|
|
5026
4390
|
function detectProjectToolingAcrossPaths(projectPaths) {
|
|
@@ -5131,7 +4495,7 @@ function describeMcpHint(name) {
|
|
|
5131
4495
|
}
|
|
5132
4496
|
|
|
5133
4497
|
// src/integration/external/lifecycle.ts
|
|
5134
|
-
import { spawnSync
|
|
4498
|
+
import { spawnSync } from "child_process";
|
|
5135
4499
|
var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5136
4500
|
function getHookTimeoutMs() {
|
|
5137
4501
|
const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
|
|
@@ -5144,7 +4508,7 @@ function getHookTimeoutMs() {
|
|
|
5144
4508
|
function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
5145
4509
|
assertSafeCwd(projectPath);
|
|
5146
4510
|
const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
|
|
5147
|
-
const result =
|
|
4511
|
+
const result = spawnSync(script, {
|
|
5148
4512
|
cwd: projectPath,
|
|
5149
4513
|
shell: true,
|
|
5150
4514
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -5158,9 +4522,9 @@ function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
|
|
|
5158
4522
|
|
|
5159
4523
|
// src/integration/ai/task-context.ts
|
|
5160
4524
|
import { execSync } from "child_process";
|
|
5161
|
-
import { Result as
|
|
4525
|
+
import { Result as Result3 } from "typescript-result";
|
|
5162
4526
|
function getRecentGitHistory(projectPath, count = 20) {
|
|
5163
|
-
const r =
|
|
4527
|
+
const r = Result3.try(() => {
|
|
5164
4528
|
assertSafeCwd(projectPath);
|
|
5165
4529
|
const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
|
|
5166
4530
|
cwd: projectPath,
|
|
@@ -5173,7 +4537,7 @@ function getRecentGitHistory(projectPath, count = 20) {
|
|
|
5173
4537
|
}
|
|
5174
4538
|
|
|
5175
4539
|
// src/integration/external/git.ts
|
|
5176
|
-
import { spawnSync as
|
|
4540
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5177
4541
|
var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
|
|
5178
4542
|
var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
|
|
5179
4543
|
function isValidBranchName(name) {
|
|
@@ -5186,7 +4550,7 @@ function isValidBranchName(name) {
|
|
|
5186
4550
|
}
|
|
5187
4551
|
function getCurrentBranch(cwd) {
|
|
5188
4552
|
assertSafeCwd(cwd);
|
|
5189
|
-
const result =
|
|
4553
|
+
const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
5190
4554
|
cwd,
|
|
5191
4555
|
encoding: "utf-8",
|
|
5192
4556
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5201,7 +4565,7 @@ function branchExists(cwd, name) {
|
|
|
5201
4565
|
if (!isValidBranchName(name)) {
|
|
5202
4566
|
throw new Error(`Invalid branch name: ${name}`);
|
|
5203
4567
|
}
|
|
5204
|
-
const result =
|
|
4568
|
+
const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
|
|
5205
4569
|
cwd,
|
|
5206
4570
|
encoding: "utf-8",
|
|
5207
4571
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5218,7 +4582,7 @@ function createAndCheckoutBranch(cwd, name) {
|
|
|
5218
4582
|
return;
|
|
5219
4583
|
}
|
|
5220
4584
|
if (branchExists(cwd, name)) {
|
|
5221
|
-
const result =
|
|
4585
|
+
const result = spawnSync2("git", ["checkout", name], {
|
|
5222
4586
|
cwd,
|
|
5223
4587
|
encoding: "utf-8",
|
|
5224
4588
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5227,7 +4591,7 @@ function createAndCheckoutBranch(cwd, name) {
|
|
|
5227
4591
|
throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
|
|
5228
4592
|
}
|
|
5229
4593
|
} else {
|
|
5230
|
-
const result =
|
|
4594
|
+
const result = spawnSync2("git", ["checkout", "-b", name], {
|
|
5231
4595
|
cwd,
|
|
5232
4596
|
encoding: "utf-8",
|
|
5233
4597
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5243,7 +4607,7 @@ function verifyCurrentBranch(cwd, expected) {
|
|
|
5243
4607
|
}
|
|
5244
4608
|
function getDefaultBranch(cwd) {
|
|
5245
4609
|
assertSafeCwd(cwd);
|
|
5246
|
-
const result =
|
|
4610
|
+
const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
5247
4611
|
cwd,
|
|
5248
4612
|
encoding: "utf-8",
|
|
5249
4613
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5264,7 +4628,7 @@ function getDefaultBranch(cwd) {
|
|
|
5264
4628
|
function getHeadSha(cwd) {
|
|
5265
4629
|
try {
|
|
5266
4630
|
assertSafeCwd(cwd);
|
|
5267
|
-
const result =
|
|
4631
|
+
const result = spawnSync2("git", ["rev-parse", "HEAD"], {
|
|
5268
4632
|
cwd,
|
|
5269
4633
|
encoding: "utf-8",
|
|
5270
4634
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5277,7 +4641,7 @@ function getHeadSha(cwd) {
|
|
|
5277
4641
|
}
|
|
5278
4642
|
function hasUncommittedChanges(cwd) {
|
|
5279
4643
|
assertSafeCwd(cwd);
|
|
5280
|
-
const result =
|
|
4644
|
+
const result = spawnSync2("git", ["status", "--porcelain"], {
|
|
5281
4645
|
cwd,
|
|
5282
4646
|
encoding: "utf-8",
|
|
5283
4647
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5289,7 +4653,7 @@ function hasUncommittedChanges(cwd) {
|
|
|
5289
4653
|
}
|
|
5290
4654
|
function autoCommit(cwd, message) {
|
|
5291
4655
|
assertSafeCwd(cwd);
|
|
5292
|
-
const add =
|
|
4656
|
+
const add = spawnSync2("git", ["add", "-A"], {
|
|
5293
4657
|
cwd,
|
|
5294
4658
|
encoding: "utf-8",
|
|
5295
4659
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5297,7 +4661,7 @@ function autoCommit(cwd, message) {
|
|
|
5297
4661
|
if (add.status !== 0) {
|
|
5298
4662
|
throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
|
|
5299
4663
|
}
|
|
5300
|
-
const commit =
|
|
4664
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
5301
4665
|
cwd,
|
|
5302
4666
|
encoding: "utf-8",
|
|
5303
4667
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5310,14 +4674,14 @@ function generateBranchName(sprintId) {
|
|
|
5310
4674
|
return `ralphctl/${sprintId}`;
|
|
5311
4675
|
}
|
|
5312
4676
|
function isGhAvailable() {
|
|
5313
|
-
const result =
|
|
4677
|
+
const result = spawnSync2("gh", ["--version"], {
|
|
5314
4678
|
encoding: "utf-8",
|
|
5315
4679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5316
4680
|
});
|
|
5317
4681
|
return result.status === 0;
|
|
5318
4682
|
}
|
|
5319
4683
|
function isGlabAvailable() {
|
|
5320
|
-
const result =
|
|
4684
|
+
const result = spawnSync2("glab", ["--version"], {
|
|
5321
4685
|
encoding: "utf-8",
|
|
5322
4686
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5323
4687
|
});
|
|
@@ -5392,6 +4756,265 @@ var DefaultExternalAdapter = class {
|
|
|
5392
4756
|
}
|
|
5393
4757
|
};
|
|
5394
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
|
+
|
|
5395
5018
|
// src/application/factories.ts
|
|
5396
5019
|
function createAiDeps(auto) {
|
|
5397
5020
|
return {
|
|
@@ -5451,6 +5074,22 @@ function createIdeatePipeline2(shared, idea, options = {}) {
|
|
|
5451
5074
|
options
|
|
5452
5075
|
);
|
|
5453
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
|
+
}
|
|
5454
5093
|
function createExecuteSprintPipeline2(shared, options = {}) {
|
|
5455
5094
|
const { aiSession, promptBuilder, parser, ui, external } = createAiDeps(false);
|
|
5456
5095
|
return createExecuteSprintPipeline(
|
|
@@ -5629,6 +5268,7 @@ async function sprintStartCommand(args) {
|
|
|
5629
5268
|
}
|
|
5630
5269
|
|
|
5631
5270
|
export {
|
|
5271
|
+
executePipeline,
|
|
5632
5272
|
getTasks,
|
|
5633
5273
|
saveTasks,
|
|
5634
5274
|
getTask,
|
|
@@ -5644,29 +5284,20 @@ export {
|
|
|
5644
5284
|
areAllTasksDone,
|
|
5645
5285
|
reorderByDependencies,
|
|
5646
5286
|
validateImportTasks,
|
|
5647
|
-
getCurrentBranch,
|
|
5648
|
-
branchExists,
|
|
5649
|
-
getDefaultBranch,
|
|
5650
|
-
isGhAvailable,
|
|
5651
|
-
isGlabAvailable,
|
|
5652
|
-
executePipeline,
|
|
5653
|
-
processLifecycleAdapter,
|
|
5654
|
-
resolveProvider,
|
|
5655
|
-
providerDisplayName,
|
|
5656
|
-
enterAltScreen,
|
|
5657
|
-
exitAltScreen,
|
|
5658
|
-
registerTuiInstance,
|
|
5659
|
-
withSuspendedTui,
|
|
5660
|
-
buildTicketRefinePrompt,
|
|
5661
5287
|
renderParsedTasksTable,
|
|
5662
5288
|
importTasks,
|
|
5663
5289
|
formatTicketForPrompt,
|
|
5664
5290
|
parseRequirementsFile,
|
|
5665
5291
|
runAiSession,
|
|
5666
|
-
|
|
5292
|
+
getCurrentBranch,
|
|
5293
|
+
branchExists,
|
|
5294
|
+
getDefaultBranch,
|
|
5295
|
+
isGhAvailable,
|
|
5296
|
+
isGlabAvailable,
|
|
5667
5297
|
createRefinePipeline2 as createRefinePipeline,
|
|
5668
5298
|
createPlanPipeline2 as createPlanPipeline,
|
|
5669
5299
|
createIdeatePipeline2 as createIdeatePipeline,
|
|
5300
|
+
createOnboardPipeline2 as createOnboardPipeline,
|
|
5670
5301
|
createExecuteSprintPipeline2 as createExecuteSprintPipeline,
|
|
5671
5302
|
parseSprintStartArgs,
|
|
5672
5303
|
sprintStartCommand
|