flowviant 0.76.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/lib/work.mjs +129 -0
  2. package/package.json +2 -2
package/bin/lib/work.mjs CHANGED
@@ -3712,6 +3712,31 @@ export function createWorkManager({
3712
3712
  await postAgentTurn({ turnId, outcome: 'nothing' });
3713
3713
  return;
3714
3714
  }
3715
+ /**
3716
+ * A TASK TURN WITH NO CARD IS REFUSED, NOT IMPROVISED.
3717
+ *
3718
+ * The kickoff below branches on `job.kind === 'task' && job.task` and falls
3719
+ * through to the HUMAN kickoff otherwise — and a task turn's body is empty
3720
+ * by design, because the daemon composes the prompt from the card's own
3721
+ * spec. So a card deleted while its agent held it spawned a real CLI on a
3722
+ * prompt whose entire content was "a member of this project said: (nothing).
3723
+ * Carry on, and end with the JSON object." Nothing on either side refused
3724
+ * it, and an ordinary gesture reached it: the board's Delete.
3725
+ *
3726
+ * The server now declines to hand out such a job at all. This is the
3727
+ * backstop, and it is worth having on its own: it costs one comparison and
3728
+ * it is the layer that cannot be skipped by a server on an older deploy.
3729
+ * `nothing` is the honest outcome — the agent goes to Stuck saying the turn
3730
+ * produced nothing, which is exactly what happened.
3731
+ */
3732
+ if (job.kind === 'task' && !job.task) {
3733
+ await postAgentTurn({
3734
+ turnId,
3735
+ outcome: 'nothing',
3736
+ answer: 'the card this turn was for no longer exists',
3737
+ });
3738
+ return;
3739
+ }
3715
3740
 
3716
3741
  await inPlace(place, false, async () => {
3717
3742
  const dir = placeWtFor(place);
@@ -4163,6 +4188,110 @@ export function createWorkManager({
4163
4188
  await postAgentMerge({ agentId, ok: true, sha: tip || undefined });
4164
4189
  return;
4165
4190
  }
4191
+ /**
4192
+ * THIS PROJECT MERGES THROUGH A PULL REQUEST.
4193
+ *
4194
+ * PR mode existed for sessions and was simply not honoured for agents:
4195
+ * nothing read the project's merge mode on this path and the direct
4196
+ * merge below ran unconditionally. On the repos PR mode exists FOR —
4197
+ * branch protection refuses a direct push of a merge commit — every
4198
+ * agent approval failed at the push and came back to review carrying
4199
+ * git's refusal, forever.
4200
+ *
4201
+ * PUSH, CREATE, MERGE, in that order, and each step's reasoning is the
4202
+ * session PR job's: push first because GitHub merges the REMOTE tip;
4203
+ * `--fill` titles from the branch's own commits so nothing is invented;
4204
+ * `--merge` and never squash, because the cards' receipts are commit
4205
+ * shas and a squash rewrites them off base, orphaning every receipt and
4206
+ * blinding the landed observer's trailer read. Adopt only an OPEN PR:
4207
+ * gh's branch finder falls back to the most recent merged one, and
4208
+ * adopting a dead PR would report success over work that never moves.
4209
+ */
4210
+ if (job.prMode) {
4211
+ try {
4212
+ execFileSync('gh', ['auth', 'status'], { stdio: ['ignore', 'pipe', 'pipe'] });
4213
+ } catch (e) {
4214
+ await postAgentMerge({
4215
+ agentId,
4216
+ ok: false,
4217
+ detail:
4218
+ e?.code === 'ENOENT'
4219
+ ? 'this project merges through pull requests, and the GitHub CLI (gh) is not installed on this machine'
4220
+ : `this project merges through pull requests, and gh is not signed in here: ${ghFirstLine(e)}`,
4221
+ });
4222
+ return;
4223
+ }
4224
+ const prBase = baseBranchName(baseRef());
4225
+ if (branch === prBase) {
4226
+ await postAgentMerge({
4227
+ agentId,
4228
+ ok: false,
4229
+ detail: `this agent is on the base branch (${prBase}) — there is nothing to open a pull request from`,
4230
+ });
4231
+ return;
4232
+ }
4233
+ try {
4234
+ git(['push', '-u', 'origin', branch], wt);
4235
+ } catch (e) {
4236
+ await postAgentMerge({ agentId, ok: false, detail: ghFirstLine(e) });
4237
+ return;
4238
+ }
4239
+ let prUrl = null;
4240
+ try {
4241
+ const j = JSON.parse(
4242
+ execFileSync('gh', ['pr', 'view', branch, '--json', 'url,state'], {
4243
+ cwd: repoRoot,
4244
+ stdio: ['ignore', 'pipe', 'pipe'],
4245
+ }).toString()
4246
+ );
4247
+ if (j?.state === 'OPEN' && typeof j?.url === 'string') prUrl = j.url.trim();
4248
+ } catch {
4249
+ /* no PR for this branch at all — created below */
4250
+ }
4251
+ if (!prUrl) {
4252
+ try {
4253
+ const out = execFileSync(
4254
+ 'gh',
4255
+ // baseBranchName, not baseRef: gh 422s on a remote-tracking name.
4256
+ ['pr', 'create', '--head', branch, '--base', prBase, '--fill'],
4257
+ { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] }
4258
+ )
4259
+ .toString()
4260
+ .trim();
4261
+ prUrl = out.split('\n').filter(Boolean).pop() ?? null;
4262
+ } catch (e) {
4263
+ await postAgentMerge({ agentId, ok: false, detail: ghFirstLine(e) });
4264
+ return;
4265
+ }
4266
+ }
4267
+ try {
4268
+ execFileSync('gh', ['pr', 'merge', branch, '--merge'], {
4269
+ cwd: repoRoot,
4270
+ stdio: ['ignore', 'pipe', 'pipe'],
4271
+ });
4272
+ } catch (e) {
4273
+ const line = ghFirstLine(e);
4274
+ // Already merged is a SUCCESS: a re-offered job, or somebody merged
4275
+ // it in the browser. The observer closes the cards either way.
4276
+ if (!/already merged/i.test(line)) {
4277
+ await postAgentMerge({
4278
+ agentId,
4279
+ ok: false,
4280
+ detail:
4281
+ prUrl && PR_URL_RE.test(prUrl) ? `${line} — the pull request is at ${prUrl}` : line,
4282
+ });
4283
+ return;
4284
+ }
4285
+ }
4286
+ // THE TIP, not a merge commit: the merge commit was made on GitHub and
4287
+ // this machine has not fetched it. It identifies the state we asked to
4288
+ // be merged, which is what the receipt is for — the cards themselves
4289
+ // close when the landed observer sees the commits arrive on base.
4290
+ await postAgentMerge({ agentId, ok: true, sha: tip });
4291
+ onRepoChanged();
4292
+ landed.observe();
4293
+ return;
4294
+ }
4166
4295
  try {
4167
4296
  shipMergeOutward({
4168
4297
  tip,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.76.0",
3
+ "version": "0.77.0",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,4 +40,4 @@
40
40
  "bugs": {
41
41
  "url": "https://github.com/flowviant/cli/issues"
42
42
  }
43
- }
43
+ }