tines 0.0.288 → 0.0.290

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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/dist/index.js +346 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -40,6 +40,9 @@ tines issues create <project> --title "…" -d @description.md
40
40
  tines issues create <project> --title "Linked" --blocked-by Other/12 --blocks Other/14 --duplicate-of Other/9
41
41
  tines issues show <project>/<number>
42
42
  tines issues move <project>/<number> <action> # a workflow transition
43
+ tines issues hold <project>/<number> # stop new admission, keep permission
44
+ tines issues release <project>/<number> # resume eligibility if permission remains on
45
+ tines issues cancel-run <project>/<number> <run-id> # request bounded cancellation
43
46
  tines issues transfer <project>/<number> --project <dest> # move to another project (keeps ID, record and old refs)
44
47
  tines issues transfer <ref> --project <dest> --dry-run # review only: no number allocated, nothing written
45
48
  tines issues transfer <ref> --project <dest> --inspect 0 # print any reviewed guidance item, including retained, in full
@@ -77,6 +80,20 @@ or narrow the list's filters.
77
80
  When a recurrence is also supplied, the relationships apply only to the first issue; later
78
81
  scheduled instances start without copied relationships.
79
82
 
83
+ For a shared project's recurring schedule, `schedules list` and `schedules show` report
84
+ your saved future-instance permission and its epoch. They are read receipts: the CLI and
85
+ API keys cannot turn personal permission on or off. Open the schedule in the browser to
86
+ choose. The first issue's permission is independent of the future schedule choice; future
87
+ permission starts off.
88
+
89
+ On a shared issue, `issues show` displays the safe member view. An accepted
90
+ member can use `issues comment`, repair their own human or run comment with
91
+ `comment-edit`/`comment-delete`, and use `issues move` from an awaiting-human
92
+ state. `issues move` submits the current exact decision witness once and does
93
+ not retry after a conflict. Keys cannot turn issue or future-schedule personal
94
+ permission on or off; open the issue or shared project in the browser. A saved
95
+ member choice does not enable member execution in this release.
96
+
80
97
  ## Workflow package files
81
98
 
82
99
  Export a workflow and its inheritance/context closure as canonical JSON, validate an edited
package/dist/index.js CHANGED
@@ -3747,12 +3747,15 @@ function action(type) {
3747
3747
  function otherRef(p) {
3748
3748
  return {
3749
3749
  kind: "other-ref",
3750
+ ...typeof p.other_project_id === "string" ? { project_id: p.other_project_id } : {},
3750
3751
  project_name: str(p.other_project_name),
3751
3752
  number: Number(p.other_number)
3752
3753
  };
3753
3754
  }
3754
3755
  function linkSegments(ev, p) {
3755
3756
  const lead = ev.type === "issue.link_added" ? "marked" : "unmarked";
3757
+ if (!Number.isFinite(Number(p.other_number)))
3758
+ return [text("changed an issue link on"), selfRef()];
3756
3759
  const duplicate = p.kind === "duplicate_of";
3757
3760
  if (duplicate && p.role === "target") {
3758
3761
  return [text(lead), otherRef(p), text("as a duplicate of"), selfRef()];
@@ -3795,6 +3798,14 @@ var DESCRIBERS = {
3795
3798
  });
3796
3799
  return segs;
3797
3800
  },
3801
+ "issue.personal_permission_changed": (_ev, p) => [
3802
+ text(`${p.value === "on" ? "allowed" : "stopped"} their agents on`),
3803
+ selfRef()
3804
+ ],
3805
+ "issue.agent_hold_changed": (_ev, p) => [
3806
+ text(p.held ? "held agents on" : "released the agent hold on"),
3807
+ selfRef()
3808
+ ],
3798
3809
  "issue.commented": () => [text("commented on"), selfRef()],
3799
3810
  "issue.comment_edited": () => [text("edited a comment on"), selfRef()],
3800
3811
  "issue.comment_deleted": () => [text("deleted a comment on"), selfRef()],
@@ -3816,6 +3827,10 @@ var DESCRIBERS = {
3816
3827
  "project.deleted": (ev, p) => projectSegments(ev, p),
3817
3828
  "project.archived": (ev, p) => projectSegments(ev, p),
3818
3829
  "project.unarchived": (ev, p) => projectSegments(ev, p),
3830
+ "project.sharing_started": () => [text("started sharing this project")],
3831
+ "project.invitation_created": () => [text("created a project invitation")],
3832
+ "project.member_joined": () => [text("joined this project")],
3833
+ "project.member_removed": () => [text("changed membership in this project")],
3819
3834
  "workflow.created": (ev, p) => [text(`${action(ev.type)} workflow`), name(p.name)],
3820
3835
  "workflow.updated": (ev, p) => [text(`${action(ev.type)} workflow`), name(p.name)],
3821
3836
  "workflow.deleted": (ev, p) => [text(`${action(ev.type)} workflow`), name(p.name)],
@@ -3826,6 +3841,11 @@ var DESCRIBERS = {
3826
3841
  ],
3827
3842
  "api_key.revoked": (_ev, p) => [text("revoked API key"), name(p.name)],
3828
3843
  "scheduled_task.created": (ev, p) => [text(`${action(ev.type)} schedule`), name(p.name)],
3844
+ "scheduled_task.personal_permission_changed": (_ev, p) => [
3845
+ text(
3846
+ `${p.value === "on" ? "allowed" : "stopped"} their agents on future issues from a schedule`
3847
+ )
3848
+ ],
3829
3849
  "scheduled_task.updated": (ev, p) => [text(`${action(ev.type)} schedule`), name(p.name)],
3830
3850
  "scheduled_task.deleted": (ev, p) => [text(`${action(ev.type)} schedule`), name(p.name)],
3831
3851
  "scheduled_task.skipped": (_ev, p) => {
@@ -4156,6 +4176,27 @@ function createApiClient(options) {
4156
4176
  createProject: (body) => request("POST", "/api/v1/projects", body),
4157
4177
  listStarters: () => get("/api/v1/projects/starters"),
4158
4178
  getProject: (id2) => get(`/api/v1/projects/${id2}`),
4179
+ getProjectPeople: (id2) => get(`/api/v1/projects/${id2}/people`),
4180
+ listProjectInvitations: (id2) => get(`/api/v1/projects/${id2}/invitations`),
4181
+ inviteProjectPerson: (id2, body) => request("POST", `/api/v1/projects/${id2}/invitations`, body),
4182
+ resendProjectInvitation: (id2, inviteId, expectedGeneration) => request(
4183
+ "POST",
4184
+ `/api/v1/projects/${id2}/invitations/${inviteId}/resend`,
4185
+ { expected_generation: expectedGeneration }
4186
+ ),
4187
+ cancelProjectInvitation: (id2, inviteId, expectedGeneration) => request("DELETE", `/api/v1/projects/${id2}/invitations/${inviteId}`, {
4188
+ expected_generation: expectedGeneration
4189
+ }),
4190
+ removeProjectMember: (id2, userId, expectedRevision) => request(
4191
+ "DELETE",
4192
+ `/api/v1/projects/${id2}/members/${userId}`,
4193
+ { expected_revision: expectedRevision }
4194
+ ),
4195
+ leaveProject: (id2, expectedRevision) => request(
4196
+ "POST",
4197
+ `/api/v1/projects/${id2}/leave`,
4198
+ { expected_revision: expectedRevision }
4199
+ ),
4159
4200
  updateProject: (id2, body) => request("PATCH", `/api/v1/projects/${id2}`, body),
4160
4201
  deleteProject: (id2, body) => request("DELETE", `/api/v1/projects/${id2}`, body),
4161
4202
  archiveProject: (id2) => request("POST", `/api/v1/projects/${id2}/archive`),
@@ -4197,6 +4238,10 @@ function createApiClient(options) {
4197
4238
  },
4198
4239
  getIssue: (id2) => get(`/api/v1/issues/${id2}`),
4199
4240
  getIssueByNumber: (projectId, number) => get(`/api/v1/projects/${projectId}/issues/${number}`),
4241
+ getIssueConsent: (id2) => get(`/api/v1/issues/${id2}/my-consent`),
4242
+ setIssueConsent: (id2, body) => request("PUT", `/api/v1/issues/${id2}/my-consent`, body),
4243
+ setIssueHold: (id2, body) => request("PUT", `/api/v1/issues/${id2}/agent-hold`, body),
4244
+ cancelIssueRun: (issueId, runId) => request("POST", `/api/v1/issues/${issueId}/runs/${runId}/cancel`, {}),
4200
4245
  updateIssue: (id2, body) => request("PATCH", `/api/v1/issues/${id2}`, body),
4201
4246
  transitionIssue: (id2, body) => request("POST", `/api/v1/issues/${id2}/transition`, body),
4202
4247
  /** Un-park: clears needs_attention, resets the attempt count. */
@@ -4215,6 +4260,8 @@ function createApiClient(options) {
4215
4260
  listSchedules: (filters = {}) => get(`/api/v1/schedules${query(filters)}`),
4216
4261
  listProjectSchedules: (projectId, page = {}) => get(`/api/v1/projects/${projectId}/schedules${query(page)}`),
4217
4262
  getSchedule: (id2) => get(`/api/v1/schedules/${id2}`),
4263
+ getScheduleConsent: (id2) => get(`/api/v1/schedules/${id2}/my-consent`),
4264
+ setScheduleConsent: (id2, body) => request("PUT", `/api/v1/schedules/${id2}/my-consent`, body),
4218
4265
  updateSchedule: (id2, body) => request("PATCH", `/api/v1/schedules/${id2}`, body),
4219
4266
  deleteSchedule: (id2) => request("DELETE", `/api/v1/schedules/${id2}`),
4220
4267
  /** Run now: creates an instance immediately (gate-respecting; 422 when blocked). */
@@ -4354,6 +4401,7 @@ function createApiClient(options) {
4354
4401
  // Supervisor settings
4355
4402
  getPreferences: () => get("/api/v1/preferences"),
4356
4403
  updatePreferences: (body) => request("PATCH", "/api/v1/preferences", body),
4404
+ acknowledgePermissionNotice: (version) => request("PUT", "/api/v1/preferences/disclosure", { version }),
4357
4405
  getSupervisorSettings: () => get("/api/v1/supervisor/settings"),
4358
4406
  getSupervisorQueue: (q = {}) => get(`/api/v1/supervisor/queue${query(q)}`),
4359
4407
  getSupervisorStats: (q = {}) => get(`/api/v1/supervisor/stats${query(q)}`),
@@ -5810,6 +5858,12 @@ function supportedEfforts(capabilities, model) {
5810
5858
  return capabilities.models.find((entry) => entry.model === model)?.efforts ?? null;
5811
5859
  }
5812
5860
 
5861
+ // ../shared/src/personal-permission.ts
5862
+ function personalPermissionLabel(role, value) {
5863
+ if (role === "owner" && value === "unset") return "on (owner default)";
5864
+ return value;
5865
+ }
5866
+
5813
5867
  // src/format.ts
5814
5868
  function timestamp(ms) {
5815
5869
  return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
@@ -6361,7 +6415,9 @@ async function resolveProject(api, ref) {
6361
6415
  const byName = items.filter((p) => p.name === ref);
6362
6416
  if (byName.length === 1) return byName[0];
6363
6417
  if (byName.length > 1) {
6364
- die(`project name "${ref}" is ambiguous; use an id: ${byName.map((p) => p.id).join(", ")}`);
6418
+ die(
6419
+ `project name "${ref}" is ambiguous; use an id: ${byName.map((p) => `${p.id}${p.owner ? ` (${p.owner.name})` : ""}`).join(", ")}`
6420
+ );
6365
6421
  }
6366
6422
  const have = items.map((p) => p.archived_at ? `${p.name} (archived)` : p.name).join(", ");
6367
6423
  die(`no project named "${ref}" (have: ${have || "none"})`);
@@ -7558,6 +7614,44 @@ function printIssueLinks(links) {
7558
7614
  }
7559
7615
  }
7560
7616
  function printIssueDetail(issue) {
7617
+ const shared = issue;
7618
+ if (shared.mode === "member") {
7619
+ console.log(`${shared.project?.name}/${shared.number} ${shared.title}`);
7620
+ console.log(
7621
+ `state: ${shared.state?.name} (${shared.state?.category}) workflow: ${shared.workflow?.name}`
7622
+ );
7623
+ console.log(`owner: ${shared.project?.owner.name} id: ${shared.id}`);
7624
+ if (shared.description) console.log(`
7625
+ ${shared.description}`);
7626
+ if (shared.artifacts?.length) {
7627
+ console.log("\nartifacts:");
7628
+ table(shared.artifacts.map((a) => [a.name, `v${a.current_version.version}`]));
7629
+ }
7630
+ if (shared.roster?.length) {
7631
+ console.log("\npeople and permission:");
7632
+ table(
7633
+ shared.roster.map((person) => [
7634
+ person.user.name,
7635
+ person.role,
7636
+ personalPermissionLabel(person.role, person.value)
7637
+ ])
7638
+ );
7639
+ }
7640
+ console.log(`your issue permission: ${shared.my_choice?.value ?? "unset"} (browser control)`);
7641
+ console.log(`latest run: ${shared.latest_run?.status ?? "none"}`);
7642
+ if (shared.comments?.length) {
7643
+ console.log("\ncomments:");
7644
+ for (const comment of shared.comments)
7645
+ console.log(
7646
+ `${comment.id} \xB7 ${comment.author.name}${comment.author.run ? ` via ${comment.author.run.name}` : ""}${comment.editor ? ` \xB7 edited by ${comment.editor.name}` : ""}
7647
+ ${comment.body}`
7648
+ );
7649
+ }
7650
+ console.log(
7651
+ "\nMembers may comment and take a current awaiting-human transition. Personal permission is chosen in the browser; member execution is unavailable."
7652
+ );
7653
+ return;
7654
+ }
7561
7655
  console.log(`${issue.project_name}/#${issue.number} ${issue.title}`);
7562
7656
  const dup = issue.duplicate_of;
7563
7657
  console.log(
@@ -7577,6 +7671,10 @@ function printIssueDetail(issue) {
7577
7671
  console.log(`labels: ${issue.labels.map((l) => l.name).join(", ")}`);
7578
7672
  }
7579
7673
  console.log(`id: ${issue.id}`);
7674
+ if (issue.schedule_origin)
7675
+ console.log(
7676
+ `created by schedule: ${issue.schedule_origin.schedule_name} [${issue.schedule_origin.schedule_id}] permission epoch: ${issue.schedule_origin.permission_epoch} (historical source; current issue permission is separate)`
7677
+ );
7580
7678
  printIssueLinks(issue.links);
7581
7679
  if (issue.description) {
7582
7680
  console.log(`
@@ -7732,13 +7830,13 @@ function register3(program3) {
7732
7830
  `${i.project_name}/${i.number}`,
7733
7831
  i.title,
7734
7832
  i.effective_state.name,
7735
- ageLabel2(new Date(i.state_entered_at).toISOString()),
7833
+ ageLabel2(new Date(i.state_entered_at ?? i.created_at).toISOString()),
7736
7834
  arrivedViaLabel(i.arrived_via),
7737
7835
  roundSummaryLabel(i.round_summary ?? null),
7738
7836
  [
7739
- i.open_blockers.length > 0 ? "blocked" : "",
7837
+ (i.open_blockers?.length ?? 0) > 0 ? "blocked" : "",
7740
7838
  i.duplicate_of ? "dup" : "",
7741
- ...i.labels.map((l) => `[${l.name}]`)
7839
+ ...(i.labels ?? []).map((l) => `[${l.name}]`)
7742
7840
  ].filter(Boolean).join(" ")
7743
7841
  ])
7744
7842
  ]);
@@ -7753,11 +7851,11 @@ function register3(program3) {
7753
7851
  // (and the filters above) go by the effective state.
7754
7852
  i.effective_state.name,
7755
7853
  i.effective_state.category,
7756
- timestamp(i.last_activity_at),
7854
+ timestamp(i.last_activity_at ?? i.updated_at),
7757
7855
  [
7758
- i.open_blockers.length > 0 ? "blocked" : "",
7856
+ (i.open_blockers?.length ?? 0) > 0 ? "blocked" : "",
7759
7857
  i.duplicate_of ? "dup" : "",
7760
- ...i.labels.map((l) => `[${l.name}]`)
7858
+ ...(i.labels ?? []).map((l) => `[${l.name}]`)
7761
7859
  ].filter(Boolean).join(" ")
7762
7860
  ])
7763
7861
  ]);
@@ -7868,14 +7966,87 @@ function register3(program3) {
7868
7966
  }
7869
7967
  );
7870
7968
  withCommon(
7871
- issues.command("move <ref> <action>").description('Take a transition on an issue by its action name (e.g. "approve")')
7969
+ issues.command("move <ref> <action>").description("Take a current transition (members: awaiting-human only; no permission change)")
7872
7970
  ).action(async (ref, action2, opts) => {
7873
7971
  const api = client(opts);
7874
7972
  const issue = await resolveIssue(api, ref);
7875
- const moved = await api.transitionIssue(issue.id, { action: action2 });
7973
+ let request = { action: action2 };
7974
+ const member = issue;
7975
+ if (member.mode === "member") {
7976
+ const transition = member.workflow.transitions?.find(
7977
+ (candidate) => candidate.name.toLowerCase() === action2.toLowerCase()
7978
+ );
7979
+ if (!transition || !member.my_choice || member.workflow_revision === void 0 || member.decision_revision === void 0)
7980
+ throw new Error(`No current transition named "${action2}"; refresh the issue`);
7981
+ request = {
7982
+ transition_id: transition.id,
7983
+ expected_state_id: issue.state.id,
7984
+ expected_decision_revision: member.decision_revision,
7985
+ expected_workflow_revision: member.workflow_revision,
7986
+ expected_consent_revision: member.my_choice.revision,
7987
+ expected_consent_epoch: member.my_choice.epoch
7988
+ };
7989
+ } else {
7990
+ const project = issue.project_id ? await api.getProject(issue.project_id) : null;
7991
+ if (project?.shared_at != null) {
7992
+ const transition = issue.allowed_transitions.find(
7993
+ (candidate) => candidate.name.toLowerCase() === action2.toLowerCase()
7994
+ );
7995
+ if (!transition)
7996
+ throw new Error(`No current transition named "${action2}"; refresh the issue`);
7997
+ const permission = await api.getIssueConsent(issue.id);
7998
+ request = {
7999
+ transition_id: transition.transition_id,
8000
+ expected_state_id: permission.issue_state.id,
8001
+ expected_decision_revision: permission.issue_state.decision_revision,
8002
+ expected_workflow_revision: permission.issue_state.workflow_revision,
8003
+ expected_consent_revision: permission.my_agents.revision,
8004
+ expected_consent_epoch: permission.my_agents.epoch
8005
+ };
8006
+ }
8007
+ }
8008
+ const moved = await api.transitionIssue(issue.id, request);
7876
8009
  if (opts.json) return printJson(moved);
8010
+ const movedMember = moved;
8011
+ console.log(
8012
+ `${moved.project_name ?? movedMember.project?.name}/#${moved.number}: ${issue.state.name} \u2192 ${moved.state.name} ("${action2}")`
8013
+ );
8014
+ if (member.mode === "member")
8015
+ console.log(
8016
+ "Personal permission is managed in the browser. Member execution is unavailable in this release."
8017
+ );
8018
+ else if (moved.permission_receipt?.message) console.log(moved.permission_receipt.message);
8019
+ });
8020
+ for (const held of [true, false]) {
8021
+ const verb = held ? "hold" : "release";
8022
+ withCommon(
8023
+ issues.command(`${verb} <ref>`).description(
8024
+ held ? "Hold an issue so approved agents cannot be admitted" : "Release an issue hold; existing permission choices stay unchanged"
8025
+ )
8026
+ ).action(async (ref, opts) => {
8027
+ const api = client(opts);
8028
+ const issue = await resolveIssue(api, ref);
8029
+ const consent = await api.getIssueConsent(issue.id);
8030
+ const receipt = await api.setIssueHold(issue.id, {
8031
+ held,
8032
+ expected_revision: consent.agent_hold.revision
8033
+ });
8034
+ if (opts.json) return printJson(receipt);
8035
+ console.log(`${issueRef(issue)}: ${receipt.message}`);
8036
+ if (receipt.released_assigned > 0) {
8037
+ console.log(`${receipt.released_assigned} assigned run(s) were released without a strike.`);
8038
+ }
8039
+ });
8040
+ }
8041
+ withCommon(
8042
+ issues.command("cancel-run <ref> <run-id>").description("Request cancellation of a run that belongs to this issue")
8043
+ ).action(async (ref, runId, opts) => {
8044
+ const api = client(opts);
8045
+ const issue = await resolveIssue(api, ref);
8046
+ const run3 = await api.cancelIssueRun(issue.id, runId);
8047
+ if (opts.json) return printJson(run3);
7877
8048
  console.log(
7878
- `${moved.project_name}/#${moved.number}: ${issue.state.name} \u2192 ${moved.state.name} ("${action2}")`
8049
+ `${issueRef(issue)}: ${run3.status === "canceled" ? "run canceled" : "cancellation requested; the run keeps its slot until it stops"}`
7879
8050
  );
7880
8051
  });
7881
8052
  withCommon(
@@ -7974,7 +8145,7 @@ function register3(program3) {
7974
8145
  const comment = await api.createComment(issue.id, { body });
7975
8146
  if (opts.json) return printJson(comment);
7976
8147
  console.log(
7977
- `commented on ${issue.project_name}/#${issue.number} as ${actorLabel(comment.actor)} (id ${comment.id})`
8148
+ `commented on ${issue.project_name ?? issue.project?.name}/#${issue.number} as ${actorLabel(comment.actor)} (id ${comment.id})`
7978
8149
  );
7979
8150
  });
7980
8151
  withCommon(
@@ -7987,7 +8158,9 @@ function register3(program3) {
7987
8158
  const issue = await resolveIssue(api, ref);
7988
8159
  const comment = await api.updateComment(issue.id, commentId, { body });
7989
8160
  if (opts.json) return printJson(comment);
7990
- console.log(`edited comment ${comment.id} on ${issue.project_name}/#${issue.number}`);
8161
+ console.log(
8162
+ `edited comment ${comment.id} on ${issue.project_name ?? issue.project?.name}/#${issue.number}`
8163
+ );
7991
8164
  }
7992
8165
  );
7993
8166
  withCommon(
@@ -7997,7 +8170,9 @@ function register3(program3) {
7997
8170
  const issue = await resolveIssue(api, ref);
7998
8171
  await api.deleteComment(issue.id, commentId);
7999
8172
  if (opts.json) return printJson({ id: commentId, deleted: true });
8000
- console.log(`deleted comment ${commentId} from ${issue.project_name}/#${issue.number}`);
8173
+ console.log(
8174
+ `deleted comment ${commentId} from ${issue.project_name ?? issue.project?.name}/#${issue.number}`
8175
+ );
8001
8176
  });
8002
8177
  withCommon(
8003
8178
  issues.command("label <ref> <name...>").description("Add labels to an issue (a label that does not exist yet is created)")
@@ -8857,6 +9032,89 @@ function registerEvents(program3) {
8857
9032
  // src/commands/projects.ts
8858
9033
  function register7(program3) {
8859
9034
  const projects = program3.command("projects").description("Manage projects");
9035
+ withCommon(
9036
+ projects.command("people <project>").description("List the owner and active members")
9037
+ ).action(async (ref, opts) => {
9038
+ const api = client(opts), project = await resolveProject(api, ref), people = await api.getProjectPeople(project.id);
9039
+ if (opts.json) return printJson(people);
9040
+ console.log(`${people.owner.name} [${people.owner.id}] \u2014 owner`);
9041
+ for (const person of people.members)
9042
+ console.log(`${person.name} [${person.id}] \u2014 member (revision ${person.revision})`);
9043
+ });
9044
+ withCommon(
9045
+ projects.command("invite <project> <email>").description("Invite someone to the whole project").option("--issue <ref>", "issue to open after acceptance").option(
9046
+ "--confirm-sharing",
9047
+ "acknowledge that first sharing gives members the whole project and restarts assigned work"
9048
+ )
9049
+ ).action(
9050
+ async (ref, email, opts) => {
9051
+ const api = client(opts), project = await resolveProject(api, ref);
9052
+ if (project.shared_at == null && !opts.confirmSharing)
9053
+ die(
9054
+ "First sharing gives access to the whole project. Your agents keep working on its issues and schedules unless you turn them off in the browser; members need their own permission. Assigned work is restarted and admitted work may finish. Rerun with --confirm-sharing to send the invite."
9055
+ );
9056
+ const issue = opts.issue ? await resolveIssue(api, opts.issue) : null;
9057
+ if (issue && issue.project_id !== project.id)
9058
+ die("--issue must name an issue in the invited project");
9059
+ const result = await api.inviteProjectPerson(project.id, {
9060
+ email,
9061
+ ...issue ? { landing_issue_id: issue.id } : {},
9062
+ confirm_sharing: opts.confirmSharing,
9063
+ expected_sharing_revision: project.sharing_revision ?? 0
9064
+ });
9065
+ if (opts.json) return printJson(result);
9066
+ console.log(
9067
+ `${result.delivery_status === "sent" ? "Invited" : "Invitation saved; delivery failed for"} ${result.email} [${result.id}]`
9068
+ );
9069
+ }
9070
+ );
9071
+ withCommon(
9072
+ projects.command("invite-resend <project> <invite-id>").description("Rotate and resend an invitation")
9073
+ ).action(async (ref, inviteId, opts) => {
9074
+ const api = client(opts), project = await resolveProject(api, ref);
9075
+ const invite = (await api.listProjectInvitations(project.id)).items.find(
9076
+ (item) => item.id === inviteId
9077
+ );
9078
+ if (!invite) die("No such invitation in this project");
9079
+ const result = await api.resendProjectInvitation(project.id, inviteId, invite.generation);
9080
+ if (opts.json) return printJson(result);
9081
+ console.log(`Invitation ${inviteId} rotated; delivery ${result.delivery_status}`);
9082
+ });
9083
+ withCommon(
9084
+ projects.command("invite-cancel <project> <invite-id>").description("Cancel an invitation")
9085
+ ).action(async (ref, inviteId, opts) => {
9086
+ const api = client(opts), project = await resolveProject(api, ref);
9087
+ const invite = (await api.listProjectInvitations(project.id)).items.find(
9088
+ (item) => item.id === inviteId
9089
+ );
9090
+ if (!invite) die("No such invitation in this project");
9091
+ await api.cancelProjectInvitation(project.id, inviteId, invite.generation);
9092
+ if (opts.json) return printJson({ canceled: true, id: inviteId });
9093
+ console.log(`Canceled invitation ${inviteId}`);
9094
+ });
9095
+ withCommon(
9096
+ projects.command("remove-member <project> <user-id>").description("Remove a project member")
9097
+ ).action(async (ref, userId, opts) => {
9098
+ const api = client(opts), project = await resolveProject(api, ref);
9099
+ const member = (await api.getProjectPeople(project.id)).members.find(
9100
+ (person) => person.id === userId
9101
+ );
9102
+ if (!member) die("No active member has that user ID");
9103
+ const result = await api.removeProjectMember(project.id, userId, member.revision);
9104
+ if (opts.json) return printJson(result);
9105
+ console.log(`Removed ${member.name} from ${project.name}`);
9106
+ });
9107
+ withCommon(projects.command("leave <project>").description("Leave a shared project")).action(
9108
+ async (ref, opts) => {
9109
+ const api = client(opts), project = await resolveProject(api, ref);
9110
+ const people = await api.getProjectPeople(project.id);
9111
+ const revision = people.members.find((person) => person.id === people.viewer_id)?.revision;
9112
+ if (!revision) die("No active membership to leave");
9113
+ const result = await api.leaveProject(project.id, revision);
9114
+ if (opts.json) return printJson(result);
9115
+ console.log(`Left ${project.name}`);
9116
+ }
9117
+ );
8860
9118
  withCommon(
8861
9119
  projects.command("starters").description("List built-in project starters and their inputs")
8862
9120
  ).action(async (opts) => {
@@ -9004,9 +9262,11 @@ function register7(program3) {
9004
9262
  const project = await resolveProject(api, ref);
9005
9263
  if (opts.json) return printJson(project);
9006
9264
  console.log(`${project.name} [${project.id}]`);
9265
+ if (project.viewer_role === "member")
9266
+ console.log(`shared by ${project.owner?.name ?? "project owner"} \u2014 read only`);
9007
9267
  if (project.description) console.log(project.description);
9008
- const defaultWorkflow = project.default_workflow_id ? (await api.getWorkflow(project.default_workflow_id)).name : "(standard)";
9009
- console.log(`
9268
+ const defaultWorkflow = project.viewer_role === "member" ? null : project.default_workflow_id ? (await api.getWorkflow(project.default_workflow_id)).name : "(standard)";
9269
+ if (defaultWorkflow) console.log(`
9010
9270
  default workflow: ${defaultWorkflow}`);
9011
9271
  console.log(
9012
9272
  `issues: ${project.issue_count} created: ${timestamp(project.created_at)} updated: ${timestamp(project.updated_at)}`
@@ -10361,6 +10621,14 @@ var RunTable = class {
10361
10621
  this.runs.delete(run3.runId);
10362
10622
  this.effects.persist();
10363
10623
  this.effects.release(run3, { keep: this.keep(outcome, run3), outcome });
10624
+ if (run3.cancellationToken) {
10625
+ const acknowledgement = this.effects.acknowledgeCancellation?.(run3);
10626
+ if (acknowledgement) {
10627
+ void acknowledgement.catch(
10628
+ (error) => this.effects.log(`cancellation acknowledgement for ${run3.runId} failed: ${String(error)}`)
10629
+ );
10630
+ }
10631
+ }
10364
10632
  }
10365
10633
  /**
10366
10634
  * Ends a run: flush logs, finish-report, clean up. On a run someone
@@ -10410,6 +10678,10 @@ var RunTable = class {
10410
10678
  run3.settled = true;
10411
10679
  return run3;
10412
10680
  }
10681
+ noteCancellationRequest(runId, token) {
10682
+ const run3 = this.runs.get(runId);
10683
+ if (run3) run3.cancellationToken = token;
10684
+ }
10413
10685
  };
10414
10686
  var LogBatcher = class {
10415
10687
  constructor(send, opts = {}) {
@@ -11024,6 +11296,7 @@ async function runDaemon(opts) {
11024
11296
  const ensureCli = () => opts.cliRefresh ? refresher.ensure().then(noteCli) : Promise.resolve(AMBIENT_CLI);
11025
11297
  const cliLabel = (cli) => cli.source === "ambient" ? `ambient PATH (${opts.cliRefresh ? "refresh failed" : "refresh disabled"})` : `tines ${cli.version ?? "unknown"} (daemon-managed${cli.source === "stale" ? ", last-good copy" : ""})`;
11026
11298
  log(`agent CLI: ${cliLabel(await ensureCli())}`);
11299
+ const pendingCancellationAcks = /* @__PURE__ */ new Map();
11027
11300
  const table2 = new RunTable(
11028
11301
  {
11029
11302
  finish: async (run3, status, error, judgment) => {
@@ -11101,6 +11374,9 @@ async function runDaemon(opts) {
11101
11374
  void uploadRawLog(run3);
11102
11375
  sweepKeptWorkspaces();
11103
11376
  },
11377
+ acknowledgeCancellation: async (run3) => {
11378
+ if (run3.cancellationToken) pendingCancellationAcks.set(run3.runId, run3.cancellationToken);
11379
+ },
11104
11380
  noteKept: (run3) => run3.batcher.append(`workspace kept at ${run3.workspace}
11105
11381
  `),
11106
11382
  persist: () => {
@@ -11502,6 +11778,12 @@ async function runDaemon(opts) {
11502
11778
  const res = await client2.pollRunner(creds.runner_id, {
11503
11779
  instance_id: instanceId,
11504
11780
  owned_runs: table2.ids(),
11781
+ ...pendingCancellationAcks.size > 0 ? {
11782
+ cancellation_acks: [...pendingCancellationAcks].slice(0, 100).map(([run_id, token]) => ({
11783
+ run_id,
11784
+ token
11785
+ }))
11786
+ } : {},
11505
11787
  // Kept for older servers. Opted-in daemons advertise only the last
11506
11788
  // accepted scheduling cap, never the higher machine ceiling.
11507
11789
  max_concurrent: effectiveConcurrency,
@@ -11521,6 +11803,14 @@ async function runDaemon(opts) {
11521
11803
  ...pendingUpdate ? { draining: true } : {}
11522
11804
  });
11523
11805
  failures = 0;
11806
+ for (const ack of res.cancellation_acks ?? []) {
11807
+ if (pendingCancellationAcks.get(ack.run_id) === ack.token) {
11808
+ pendingCancellationAcks.delete(ack.run_id);
11809
+ }
11810
+ }
11811
+ for (const request of res.cancel_requests ?? []) {
11812
+ table2.noteCancellationRequest(request.run_id, request.token);
11813
+ }
11524
11814
  for (const runId of res.cancels) killWithoutFinish(runId);
11525
11815
  let declinesChanged = false;
11526
11816
  for (const runId of res.released_assignments ?? []) {
@@ -12728,6 +13018,22 @@ async function resolveSchedule(api, ref) {
12728
13018
  return found;
12729
13019
  }
12730
13020
  function printScheduleDetail(s) {
13021
+ const shared = s;
13022
+ if (shared.project) {
13023
+ console.log(`${shared.project.name}/${shared.name} [${shared.id}]`);
13024
+ console.log(`owner: ${shared.project.owner.name} workflow: ${shared.workflow?.name}`);
13025
+ console.log(`cron: ${shared.recurrence?.cron} timezone: ${shared.recurrence?.timezone}`);
13026
+ console.log(
13027
+ `status: ${shared.recurrence?.enabled ? "enabled" : "paused"} start state: ${shared.workflow?.start_state_name}`
13028
+ );
13029
+ console.log(`
13030
+ title template: ${shared.title_template}`);
13031
+ if (shared.description_template)
13032
+ console.log(`description template:
13033
+ ${shared.description_template}`);
13034
+ console.log("\nThis shared schedule is read only in the current release.");
13035
+ return;
13036
+ }
12731
13037
  console.log(`${scheduleRef(s)} [${s.id}]${s.enabled ? "" : " (paused)"}`);
12732
13038
  console.log(`${recurrenceLabel(s)} (cron "${s.cron}")`);
12733
13039
  console.log(
@@ -12736,6 +13042,10 @@ function printScheduleDetail(s) {
12736
13042
  console.log(
12737
13043
  `next run: ${s.enabled ? timestamp(s.next_run_at) : "(paused)"} last run: ${s.last_run_at ? timestamp(s.last_run_at) : "never"} runs: ${s.run_count} open instances: ${s.open_instances}`
12738
13044
  );
13045
+ if (s.my_future_permission)
13046
+ console.log(
13047
+ `my agents on future issues: ${personalPermissionLabel("owner", s.my_future_permission.value)} permission epoch: ${s.my_future_permission.epoch} (change in the browser)`
13048
+ );
12739
13049
  console.log(`
12740
13050
  title template: ${s.title_template}`);
12741
13051
  if (s.description_template) {
@@ -12760,15 +13070,27 @@ function register10(program3) {
12760
13070
  printList(res, opts, (items) => {
12761
13071
  if (items.length === 0) return console.log("no schedules");
12762
13072
  table([
12763
- ["NAME", "RECURRENCE", "NEXT RUN", "LAST RUN", "OPEN", ""],
12764
- ...items.map((s) => [
12765
- scheduleRef(s),
12766
- recurrenceLabel(s),
12767
- s.enabled ? timestamp(s.next_run_at) : "\u2014",
12768
- s.last_run_at ? timestamp(s.last_run_at) : "never",
12769
- String(s.open_instances),
12770
- s.enabled ? "" : "(paused)"
12771
- ])
13073
+ ["NAME", "RECURRENCE", "NEXT RUN", "LAST RUN", "OPEN", "MY FUTURE", ""],
13074
+ ...items.map((s) => {
13075
+ const shared = s;
13076
+ return shared.project ? [
13077
+ `${shared.project.name}/${shared.name}`,
13078
+ shared.recurrence?.cron ?? "\u2014",
13079
+ "\u2014",
13080
+ "\u2014",
13081
+ "\u2014",
13082
+ shared.my_future_permission?.value ?? "unset",
13083
+ shared.recurrence?.enabled ? "" : "(paused)"
13084
+ ] : [
13085
+ scheduleRef(s),
13086
+ recurrenceLabel(s),
13087
+ s.enabled ? timestamp(s.next_run_at) : "\u2014",
13088
+ s.last_run_at ? timestamp(s.last_run_at) : "never",
13089
+ String(s.open_instances),
13090
+ s.my_future_permission ? personalPermissionLabel("owner", s.my_future_permission.value) : "\u2014",
13091
+ s.enabled ? "" : "(paused)"
13092
+ ];
13093
+ })
12772
13094
  ]);
12773
13095
  });
12774
13096
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.288",
3
+ "version": "0.0.290",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",