| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- using Inversion;
- using Spry;
- using Spry.Authentication;
- using Statum;
- using ClankerSaddle.Services;
- namespace ClankerSaddle {
- /** Sealed project id carried by the project actions. */
- public class ProjectActionPrivate : Object {
- public int64 project_id { get; set; }
- }
- /** Sealed link identity carried by {@link RemoveProjectLinkAction}. */
- public class ProjectLinkActionPrivate : Object {
- public int64 project_id { get; set; }
- public int64 link_id { get; set; }
- }
- /** Sealed note identity carried by {@link DeleteProjectNoteAction}. */
- public class ProjectNoteActionPrivate : Object {
- public int64 project_id { get; set; }
- public int64 note_id { get; set; }
- }
- /**
- * The title prefix of every understanding refresh task (goal 24):
- * {@link RefreshUnderstandingAction} creates them and refuses a
- * second refresh while one is still live.
- */
- public const string UNDERSTANDING_REFRESH_PREFIX = "Understanding refresh:";
- /**
- * Creates a project from the projects form (name and git URL
- * required); default_branch falls back to 'main'.
- */
- public class CreateProjectAction : StatumAction {
- private ProjectService projects = inject<ProjectService>();
- private UserService users = inject<UserService>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- string name = Forms.field(request, "name");
- string git_url = Forms.field(request, "git_url");
- if (name.length == 0 || git_url.length == 0) {
- return directives().notify("error", "Name and git URL are required").navigate("/projects");
- }
- try {
- var project = yield projects.create_project(name, git_url,
- Forms.field(request, "default_branch"), Forms.field(request, "summary"));
- return directives().notify("success", @"Project '$(project.name)' created")
- .navigate(@"/projects/$(project.id)");
- } catch (Error e) {
- return directives().notify("error", @"Could not create the project: $(e.message)").navigate("/projects");
- }
- }
- }
- /**
- * Saves the editable fields of a project; the id comes from the sealed
- * {@link ProjectActionPrivate} blob (the hidden form field is a
- * fallback). An empty summary clears the stored one.
- */
- public class UpdateProjectAction : TypedStatumAction<ProjectActionPrivate> {
- private ProjectService projects = inject<ProjectService>();
- private UserService users = inject<UserService>();
- protected override Type private_type { get { return typeof(ProjectActionPrivate); } }
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- var ctx = request_private;
- int64 project_id = ctx != null ? ((!)ctx).project_id : Forms.integer(request, "project_id", 0);
- if (project_id <= 0) {
- return directives().navigate("/projects");
- }
- try {
- yield projects.update_project(project_id,
- Forms.optional(request, "name"), Forms.optional(request, "git_url"),
- Forms.optional(request, "default_branch"),
- request.form != null ? Forms.field(request, "summary") : null);
- return directives().notify("success", "Project saved")
- .navigate(@"/projects/$(project_id)");
- } catch (Error e) {
- return directives().notify("error", @"Could not save the project: $(e.message)")
- .navigate(@"/projects/$(project_id)");
- }
- }
- }
- /**
- * Links another project to this one; the parent id is sealed, the
- * target and relationship come from the form selects.
- */
- public class AddProjectLinkAction : TypedStatumAction<ProjectActionPrivate> {
- private ProjectService projects = inject<ProjectService>();
- private UserService users = inject<UserService>();
- protected override Type private_type { get { return typeof(ProjectActionPrivate); } }
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- var ctx = request_private;
- int64 project_id = ctx != null ? ((!)ctx).project_id : Forms.integer(request, "project_id", 0);
- int64 linked_id = Forms.integer(request, "linked_project_id", 0);
- string relationship = Forms.field(request, "relationship");
- if (project_id <= 0 || linked_id <= 0) {
- return directives().notify("error", "Pick a project to link").navigate(@"/projects/$(project_id)");
- }
- try {
- yield projects.add_link(project_id, linked_id, relationship);
- return directives().notify("success", "Link added").navigate(@"/projects/$(project_id)");
- } catch (Error e) {
- return directives().notify("error", @"Could not add the link: $(e.message)")
- .navigate(@"/projects/$(project_id)");
- }
- }
- }
- /** Removes a project link; both ids come from the sealed private blob. */
- public class RemoveProjectLinkAction : TypedStatumAction<ProjectLinkActionPrivate> {
- private ProjectService projects = inject<ProjectService>();
- private UserService users = inject<UserService>();
- protected override Type private_type { get { return typeof(ProjectLinkActionPrivate); } }
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- var ctx = request_private;
- if (ctx == null) {
- return directives().navigate("/projects");
- }
- try {
- yield projects.remove_link(((!)ctx).link_id);
- return directives().notify("success", "Link removed")
- .navigate(@"/projects/$(((!)ctx).project_id)");
- } catch (Error e) {
- return directives().notify("error", @"Could not remove the link: $(e.message)")
- .navigate(@"/projects/$(((!)ctx).project_id)");
- }
- }
- }
- /**
- * Deletes a project note; both ids come from the sealed private blob.
- * Notes are LLM-authored (`saddle note` via the callback endpoint) —
- * the web UI only curates them.
- */
- public class DeleteProjectNoteAction : TypedStatumAction<ProjectNoteActionPrivate> {
- private ProjectService projects = inject<ProjectService>();
- private UserService users = inject<UserService>();
- protected override Type private_type { get { return typeof(ProjectNoteActionPrivate); } }
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- var ctx = request_private;
- if (ctx == null) {
- return directives().navigate("/projects");
- }
- try {
- yield projects.delete_note(((!)ctx).note_id);
- return directives().notify("success", "Note deleted")
- .navigate(@"/projects/$(((!)ctx).project_id)");
- } catch (Error e) {
- return directives().notify("error", @"Could not delete the note: $(e.message)")
- .navigate(@"/projects/$(((!)ctx).project_id)");
- }
- }
- }
- /**
- * Spins up the project's understanding refresh task (goal 24): a
- * QUEUED investigate task whose agent explores the project, walks
- * the git history since the current document's commit when one
- * exists, and saves an UPDATED markdown understanding via the
- * `save_understanding` tool. Refuses while another refresh task
- * for the project is still live — one explorer at a time.
- */
- public class RefreshUnderstandingAction : TypedStatumAction<ProjectActionPrivate> {
- private ProjectService projects = inject<ProjectService>();
- private UnderstandingService understandings = inject<UnderstandingService>();
- private TaskService tasks = inject<TaskService>();
- private UserService users = inject<UserService>();
- protected override Type private_type { get { return typeof(ProjectActionPrivate); } }
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- var ctx = request_private;
- int64 project_id = ctx != null ? ((!)ctx).project_id : Forms.integer(request, "project_id", 0);
- if (project_id <= 0) {
- return directives().navigate("/projects");
- }
- var project = yield projects.get_project(project_id);
- if (project == null) {
- return directives().notify("error", "That project no longer exists").navigate("/projects");
- }
- // One live explorer at a time: a non-terminal task whose
- // title carries the refresh prefix blocks a new refresh.
- foreach (var task in yield tasks.list_tasks(null, project_id)) {
- if (!task.title.has_prefix(UNDERSTANDING_REFRESH_PREFIX)) {
- continue;
- }
- if (task.status == STATUS_DONE || task.status == STATUS_FAILED) {
- continue;
- }
- return directives().notify("error",
- @"An understanding refresh (#$(task.id)) is already running for this project")
- .navigate(@"/projects/$(project_id)");
- }
- var current = yield understandings.current_understanding(project_id);
- string old_hash = current != null ? ((!)current).commit_hash : "";
- string old_body = current != null ? ((!)current).body : "";
- var brief = new StringBuilder();
- brief.append("MANDATE — you are the project's understanding explorer.\n\n");
- brief.append("Explore the project's structure and produce a markdown understanding ");
- brief.append("document covering the project layout and anything unusual that would ");
- brief.append("be useful to know for anyone working on it.\n");
- if (old_body.length > 0) {
- brief.append("\nA previous understanding document exists. Do NOT start from ");
- brief.append("scratch: review the git history since the old commit hash ");
- brief.append("(git log <old>..HEAD, plus diffs of the significant changes) and ");
- brief.append("produce an UPDATED document that keeps what still holds and ");
- brief.append("rewrites what changed.\n");
- }
- brief.append("\nThe finishing move: call the save_understanding tool exactly ONCE ");
- brief.append("with the new document and the git HEAD hash of the explored checkout ");
- brief.append("(run `git rev-parse HEAD`), then finish(status=done) with a short ");
- brief.append("summary. You are in investigate mode — do not edit any files.\n");
- brief.append("\nPrevious commit hash: ");
- brief.append(old_hash.length > 0 ? old_hash : "none — first refresh");
- if (old_body.length > 0) {
- brief.append("\n\nPrevious document:\n\n```markdown\n");
- brief.append(old_body);
- brief.append("\n```\n");
- }
- var notes = yield projects.list_notes(project_id);
- brief.append("\nProject notes from other agents:\n");
- if (notes.length == 0) {
- brief.append("(none)\n");
- } else {
- foreach (var note in notes) {
- brief.append("\n---\n");
- brief.append(note.body);
- brief.append("\n");
- }
- }
- try {
- var task = yield tasks.create_task(project_id,
- @"$(UNDERSTANDING_REFRESH_PREFIX) $(((!)project).name)",
- brief.str, 5, null, null, STATUS_QUEUED,
- TASK_KIND_TASK, "investigate", CREATOR_HUMAN);
- yield tasks.add_system_comment(task.id,
- @"Understanding refresh queued against commit $(old_hash.length > 0 ? old_hash : "(none)")");
- return directives().notify("success",
- @"Refresh queued — the explorer task is #$(task.id)")
- .navigate(@"/projects/$(project_id)");
- } catch (Error e) {
- return directives().notify("error", @"Could not queue the refresh: $(e.message)")
- .navigate(@"/projects/$(project_id)");
- }
- }
- }
- }
|