@bridge4dev/runner 0.52.0 → 0.53.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.
- package/dist/adapters/claude.js +15 -32
- package/dist/adapters/codex.js +10 -26
- package/dist/adapters/types.d.ts +45 -1
- package/dist/adapters/types.js +53 -0
- package/dist/checkpoints.d.ts +62 -0
- package/dist/checkpoints.js +50 -1
- package/dist/git.d.ts +64 -0
- package/dist/git.js +487 -36
- package/dist/gitops.d.ts +5 -0
- package/dist/gitops.js +7 -8
- package/dist/policy.d.ts +38 -0
- package/dist/policy.js +228 -7
- package/dist/protocol.d.ts +17 -17
- package/dist/supervisor.d.ts +99 -3
- package/dist/supervisor.js +279 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/policy.d.ts
CHANGED
|
@@ -92,10 +92,40 @@ export interface PolicyContext extends AgentGitPolicy {
|
|
|
92
92
|
* what makes a change visible when this cannot prevent it.
|
|
93
93
|
*/
|
|
94
94
|
agentPromptFile?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Does this session work in the project folder itself, or in a worktree of
|
|
97
|
+
* its own? (#361 п. 5, ADR 0004)
|
|
98
|
+
*
|
|
99
|
+
* Only DIRECT sessions share a folder — and therefore share ONE current
|
|
100
|
+
* branch with every other session and person working in it. `git checkout
|
|
101
|
+
* <branch>` there is not a local move: it rewrites the working tree under
|
|
102
|
+
* everybody at once, which is the same class of act as `git reset --hard`.
|
|
103
|
+
*
|
|
104
|
+
* **`undefined` means «apply the rule».** Deliberately the opposite polarity
|
|
105
|
+
* to `SessionDescriptor.workMode` in `protocol.ts`, where a missing value
|
|
106
|
+
* means BRANCH when the folder is prepared. Different field, different
|
|
107
|
+
* question: there the safe reading is «build a worktree», here it is «ask
|
|
108
|
+
* first». Gotcha 193 — resolve «unknown» once, in the safe direction, and say
|
|
109
|
+
* out loud when two neighbours resolve it in opposite ways.
|
|
110
|
+
*/
|
|
111
|
+
workMode?: 'DIRECT' | 'BRANCH';
|
|
95
112
|
}
|
|
96
113
|
export interface PolicyDecision {
|
|
97
114
|
decision: 'allow' | 'deny' | 'ask';
|
|
98
115
|
reason: string;
|
|
116
|
+
/**
|
|
117
|
+
* A sentence written FOR THE PERSON answering the card (#361 п. 5).
|
|
118
|
+
*
|
|
119
|
+
* `reason` is a label — «strict mode», «command needs approval» — and it goes
|
|
120
|
+
* to the log and, on a denial, to the feed. A card carries neither: it shows
|
|
121
|
+
* a title, a description and the input, and the reason is dropped. So a rule
|
|
122
|
+
* that asks a question the human cannot answer without knowing WHY has to say
|
|
123
|
+
* why here, and the adapters put it on the card.
|
|
124
|
+
*
|
|
125
|
+
* Only set where there is something worth reading; absent means «the card's
|
|
126
|
+
* own title says enough».
|
|
127
|
+
*/
|
|
128
|
+
explain?: string;
|
|
99
129
|
}
|
|
100
130
|
export declare function maskString(value: string): string;
|
|
101
131
|
/** Deep-mask every string in a JSON-ish structure (payloads leaving the server). */
|
|
@@ -125,6 +155,14 @@ export declare function isGitInternalPath(p: string): boolean;
|
|
|
125
155
|
* DELIBERATELY switched something on — with the shipped defaults the first
|
|
126
156
|
* check refuses every push and the rest never run, which is byte for byte the
|
|
127
157
|
* behaviour of the four regexes it replaces.
|
|
158
|
+
*
|
|
159
|
+
* **A `deny` anywhere beats an `ask` anywhere.** Until #361 every decision here
|
|
160
|
+
* was a refusal, so returning the first one found in the first segment was the
|
|
161
|
+
* same thing as returning the worst one. With an `ask` in the set that stopped
|
|
162
|
+
* being true: `git checkout main && git push origin main` would have shown a
|
|
163
|
+
* card about the checkout, and confirming it would have run the push this
|
|
164
|
+
* function exists to refuse. So `deny` returns at once, `ask` is remembered,
|
|
165
|
+
* and the loop reads to the end.
|
|
128
166
|
*/
|
|
129
167
|
export declare function evaluateGitPolicy(command: string, ctx: PolicyContext): PolicyDecision | null;
|
|
130
168
|
export interface RecipeCommandContext {
|
package/dist/policy.js
CHANGED
|
@@ -517,6 +517,189 @@ function parsePush(segment) {
|
|
|
517
517
|
remoteExec,
|
|
518
518
|
};
|
|
519
519
|
}
|
|
520
|
+
const GIT_CHECKOUT = String.raw `\bgit\s+${FLAGS}(?:checkout|switch)\b`;
|
|
521
|
+
/** A word whose meaning only the shell knows — `$BRANCH`, `` `cat .b` ``, `\x`. */
|
|
522
|
+
const SHELL_UNRESOLVED = /[$`\\]/;
|
|
523
|
+
/**
|
|
524
|
+
* A REVISION expression — `main~1`, `v2^`, `abc123^{commit}`.
|
|
525
|
+
*
|
|
526
|
+
* Not a branch name (git's own ref grammar forbids `~` and `^` in one) and
|
|
527
|
+
* emphatically not a pathspec: checking one out leaves the folder on no branch
|
|
528
|
+
* at all. Grouping it with the pathspecs below, on the grounds that it «cannot
|
|
529
|
+
* be a branch», was exactly backwards — `git checkout main~1` detaches the
|
|
530
|
+
* shared folder for everybody in it, silently.
|
|
531
|
+
*/
|
|
532
|
+
const REVISION_EXPRESSION = /[~^]/;
|
|
533
|
+
/**
|
|
534
|
+
* An operand git could never read as a branch name — because it is a PATH.
|
|
535
|
+
*
|
|
536
|
+
* Not a guess about intent, a fact about the ref grammar: `*`, `?`, `[` and
|
|
537
|
+
* spaces are forbidden in a branch name, a leading `:` is git's own magic
|
|
538
|
+
* pathspec prefix, and `.`, `./x`, `../x`, `/x`, `x/` are paths by shape.
|
|
539
|
+
* A closing `]` is deliberately NOT here: `git check-ref-format --branch 'wip]1'`
|
|
540
|
+
* accepts it, so calling it impossible-in-a-branch would have waved through a
|
|
541
|
+
* real branch switch (QA #255).
|
|
542
|
+
* Everything else stays ambiguous, and git resolves ambiguity in favour of the
|
|
543
|
+
* BRANCH — which is why an ambiguous word asks rather than being waved through.
|
|
544
|
+
*
|
|
545
|
+
* Characters that are merely UNUSUAL in a branch name (a backslash, a control
|
|
546
|
+
* character, `@{`) are deliberately absent: they end up in the ambiguous pile,
|
|
547
|
+
* which asks. Every gap here has to fall on that side.
|
|
548
|
+
*/
|
|
549
|
+
function cannotBeBranch(operand) {
|
|
550
|
+
if (operand === '.' || operand === '..')
|
|
551
|
+
return true;
|
|
552
|
+
if (/^\.{1,2}\//.test(operand) || operand.startsWith('/'))
|
|
553
|
+
return true;
|
|
554
|
+
if (operand.startsWith(':'))
|
|
555
|
+
return true;
|
|
556
|
+
if (operand.endsWith('/'))
|
|
557
|
+
return true;
|
|
558
|
+
return /[*?[ ]/.test(operand);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The agent chose this word, so it is quoted back at a length WE choose.
|
|
562
|
+
*
|
|
563
|
+
* This string becomes the description of a permission card and travels in an
|
|
564
|
+
* event payload the gateway caps; a single implausibly long operand should cost
|
|
565
|
+
* a few characters of the sentence, never the card.
|
|
566
|
+
*/
|
|
567
|
+
function shortOperand(operand) {
|
|
568
|
+
return operand.length > 60 ? `${operand.slice(0, 60)}…` : operand;
|
|
569
|
+
}
|
|
570
|
+
const RESTORE_A_FILE = 'To put a file back use `git checkout -- <path>` or `git restore <path>`: those touch files only.';
|
|
571
|
+
const MOVES_THE_FOLDER = 'this session works in the project folder itself, so switching branches there changes the files under every other session and person working in it';
|
|
572
|
+
/**
|
|
573
|
+
* Does this segment move the shared folder onto another branch? (#361 п. 5)
|
|
574
|
+
*
|
|
575
|
+
* Parsed rather than pattern matched, for the same reason as `parsePush`: the
|
|
576
|
+
* difference between «restore this file» and «move everybody to another
|
|
577
|
+
* branch» is one `--` in the middle of the arguments, and no regex answers
|
|
578
|
+
* that. Synchronous and free of child processes — gotcha 196: this runs on
|
|
579
|
+
* every Bash call of every session on the machine.
|
|
580
|
+
*
|
|
581
|
+
* Returns null when the segment is not a checkout at all, or is one of the
|
|
582
|
+
* forms that provably only touches files.
|
|
583
|
+
*/
|
|
584
|
+
function parseCheckout(segment) {
|
|
585
|
+
if (!new RegExp(GIT_CHECKOUT).test(segment))
|
|
586
|
+
return null;
|
|
587
|
+
const tokens = words(segment);
|
|
588
|
+
const at = tokens.findIndex((token) => token === 'checkout' || token === 'switch');
|
|
589
|
+
if (at === -1) {
|
|
590
|
+
// The regex matched but the verb is hidden by a form this tokenizer does
|
|
591
|
+
// not model. Report the moving reading — «I could not parse it» must never
|
|
592
|
+
// become «go ahead».
|
|
593
|
+
return {
|
|
594
|
+
reason: `This git command could not be read well enough to tell whether it moves the folder onto another branch, and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (tokens[at] === 'switch') {
|
|
598
|
+
// `git switch` has no file form at all — it exists to move a branch.
|
|
599
|
+
return {
|
|
600
|
+
reason: `\`git switch\` moves the folder onto another branch, and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const rest = tokens.slice(at + 1);
|
|
604
|
+
/**
|
|
605
|
+
* `--` names paths only when there ARE paths after it.
|
|
606
|
+
*
|
|
607
|
+
* The obvious reading — «a `--` anywhere means this is a file command» — is
|
|
608
|
+
* wrong, and wrong in the direction that matters. Verified on git 2.43:
|
|
609
|
+
* `git checkout feature --` prints «Switched to branch 'feature'», and
|
|
610
|
+
* `git checkout -b tmp --` creates the branch and moves onto it. Git's own
|
|
611
|
+
* parser treats a trailing `--` as «the word before me is definitely a ref»,
|
|
612
|
+
* which is the opposite of what it looks like. So the separator is only a
|
|
613
|
+
* separator when it separates something.
|
|
614
|
+
*/
|
|
615
|
+
const dashDash = rest.indexOf('--');
|
|
616
|
+
if (dashDash !== -1 && dashDash < rest.length - 1)
|
|
617
|
+
return null;
|
|
618
|
+
const head = dashDash === -1 ? rest : rest.slice(0, dashDash);
|
|
619
|
+
let fileOnly = false;
|
|
620
|
+
let movingFlag = null;
|
|
621
|
+
const operands = [];
|
|
622
|
+
for (const token of head) {
|
|
623
|
+
if (token.startsWith('--')) {
|
|
624
|
+
const name = token.split('=')[0] ?? token;
|
|
625
|
+
if (name === '--ours' ||
|
|
626
|
+
name === '--theirs' ||
|
|
627
|
+
name === '--patch' ||
|
|
628
|
+
name === '--pathspec-from-file' ||
|
|
629
|
+
name === '--pathspec-file-nul') {
|
|
630
|
+
fileOnly = true;
|
|
631
|
+
}
|
|
632
|
+
else if (name === '--orphan' ||
|
|
633
|
+
name === '--detach' ||
|
|
634
|
+
name === '--track' ||
|
|
635
|
+
name === '--merge' ||
|
|
636
|
+
name === '--force') {
|
|
637
|
+
movingFlag ??= name;
|
|
638
|
+
}
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
// A lone `-` is an operand: «the branch I was on before».
|
|
642
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
643
|
+
for (const letter of token.slice(1)) {
|
|
644
|
+
if (letter === 'p')
|
|
645
|
+
fileOnly = true;
|
|
646
|
+
else if ('bBtmf'.includes(letter))
|
|
647
|
+
movingFlag ??= `-${letter}`;
|
|
648
|
+
}
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
operands.push(token);
|
|
652
|
+
}
|
|
653
|
+
if (fileOnly)
|
|
654
|
+
return null;
|
|
655
|
+
if (movingFlag) {
|
|
656
|
+
return {
|
|
657
|
+
reason: `\`git checkout ${shortOperand(movingFlag)}\` moves the folder onto another branch, and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// `git checkout` on its own restores nothing and moves nothing — but only
|
|
661
|
+
// when the arguments really are all of them. `echo feature | xargs git
|
|
662
|
+
// checkout` is a checkout whose operand is handed to it by the program in
|
|
663
|
+
// front of it, and this parser cannot see it: the segment ends with a bare
|
|
664
|
+
// `git checkout` and looks harmless. So «no operands» is only an answer when
|
|
665
|
+
// git is the thing being run, not an argument to something else.
|
|
666
|
+
if (operands.length === 0) {
|
|
667
|
+
const first = tokens.find((token) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(token));
|
|
668
|
+
if (first === 'git')
|
|
669
|
+
return null;
|
|
670
|
+
return {
|
|
671
|
+
reason: `This \`git checkout\` gets its arguments from another program, so the runner cannot tell whether it moves the folder onto another branch, and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
for (const operand of operands) {
|
|
675
|
+
if (SHELL_UNRESOLVED.test(operand)) {
|
|
676
|
+
return {
|
|
677
|
+
reason: `This \`git checkout\` names its target through the shell, so the runner cannot tell whether it is a file or a branch — and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (operand === '-') {
|
|
681
|
+
return {
|
|
682
|
+
reason: `\`git checkout -\` moves the folder back onto the previous branch, and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (/^(HEAD|@)([~^].*)?$/.test(operand) ||
|
|
686
|
+
/^[0-9a-f]{7,40}$/i.test(operand) ||
|
|
687
|
+
REVISION_EXPRESSION.test(operand)) {
|
|
688
|
+
return {
|
|
689
|
+
reason: `\`git checkout ${shortOperand(operand)}\` leaves the folder on no branch at all (detached HEAD), and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (cannotBeBranch(operand))
|
|
693
|
+
continue;
|
|
694
|
+
// A bare word. Git reads it as a BRANCH first and only falls back to a
|
|
695
|
+
// path, so «I meant the file» is not something this can assume — and the
|
|
696
|
+
// unambiguous spelling costs the agent three characters.
|
|
697
|
+
return {
|
|
698
|
+
reason: `\`git checkout ${shortOperand(operand)}\` is read by git as a branch first, and ${MOVES_THE_FOLDER}. ${RESTORE_A_FILE}`,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
520
703
|
/**
|
|
521
704
|
* The project's git rules, applied to one Bash command.
|
|
522
705
|
*
|
|
@@ -525,9 +708,18 @@ function parsePush(segment) {
|
|
|
525
708
|
* DELIBERATELY switched something on — with the shipped defaults the first
|
|
526
709
|
* check refuses every push and the rest never run, which is byte for byte the
|
|
527
710
|
* behaviour of the four regexes it replaces.
|
|
711
|
+
*
|
|
712
|
+
* **A `deny` anywhere beats an `ask` anywhere.** Until #361 every decision here
|
|
713
|
+
* was a refusal, so returning the first one found in the first segment was the
|
|
714
|
+
* same thing as returning the worst one. With an `ask` in the set that stopped
|
|
715
|
+
* being true: `git checkout main && git push origin main` would have shown a
|
|
716
|
+
* card about the checkout, and confirming it would have run the push this
|
|
717
|
+
* function exists to refuse. So `deny` returns at once, `ask` is remembered,
|
|
718
|
+
* and the loop reads to the end.
|
|
528
719
|
*/
|
|
529
720
|
export function evaluateGitPolicy(command, ctx) {
|
|
530
721
|
const policy = resolveGitPolicy(ctx);
|
|
722
|
+
let asked = null;
|
|
531
723
|
for (const segment of commandSegments(command)) {
|
|
532
724
|
if (!policy.allowDestructiveGit) {
|
|
533
725
|
/**
|
|
@@ -539,10 +731,14 @@ export function evaluateGitPolicy(command, ctx) {
|
|
|
539
731
|
* that went that way on 2026-07-28 existed nowhere else — and
|
|
540
732
|
* `git reset --hard` throws away every uncommitted edit in the tree.
|
|
541
733
|
*
|
|
542
|
-
* Denied rather than turned into a permission card
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
734
|
+
* Denied rather than turned into a permission card because there is no
|
|
735
|
+
* safe version of them, not because a card was impossible: this whole
|
|
736
|
+
* function runs BEFORE the trust branches, so an `ask` returned from here
|
|
737
|
+
* does reach the person even under AUTO (that is how #361 п. 5 works two
|
|
738
|
+
* rules below). What is true is that the safe LIST never sees them — Bash
|
|
739
|
+
* is allowed before it is consulted. An agent that needs to remove build
|
|
740
|
+
* output can name the paths; one that wants to undo its own work has
|
|
741
|
+
* `git checkout -- <file>`.
|
|
546
742
|
*/
|
|
547
743
|
if (new RegExp(String.raw `\bgit\s+${FLAGS}clean\b`).test(segment)) {
|
|
548
744
|
return {
|
|
@@ -557,6 +753,16 @@ export function evaluateGitPolicy(command, ctx) {
|
|
|
557
753
|
};
|
|
558
754
|
}
|
|
559
755
|
}
|
|
756
|
+
// #361 п. 5 — only where the folder is shared. In a BRANCH session the
|
|
757
|
+
// worktree belongs to this session alone and moving it costs nobody
|
|
758
|
+
// anything. See `PolicyContext.workMode` for why silence means «ask».
|
|
759
|
+
if (ctx.workMode !== 'BRANCH') {
|
|
760
|
+
const move = parseCheckout(segment);
|
|
761
|
+
// `reason` stays a LABEL — it goes to logs and, on a denial, to the feed.
|
|
762
|
+
// The sentence a person reads on the card travels in `explain`.
|
|
763
|
+
if (move)
|
|
764
|
+
asked ??= { decision: 'ask', reason: 'moves the shared folder', explain: move.reason };
|
|
765
|
+
}
|
|
560
766
|
const push = parsePush(segment);
|
|
561
767
|
if (!push)
|
|
562
768
|
continue;
|
|
@@ -648,7 +854,7 @@ export function evaluateGitPolicy(command, ctx) {
|
|
|
648
854
|
}
|
|
649
855
|
}
|
|
650
856
|
}
|
|
651
|
-
return
|
|
857
|
+
return asked;
|
|
652
858
|
}
|
|
653
859
|
// Commands considered safe enough to run without asking under NORMAL trust.
|
|
654
860
|
// Interpreters (node/python/…) and find (-exec) are NOT here: they execute
|
|
@@ -829,7 +1035,14 @@ export function evaluateRecipeCommand(command, ctx = {}) {
|
|
|
829
1035
|
* approval screen, not a side effect of this one.
|
|
830
1036
|
*/
|
|
831
1037
|
const git = evaluateGitPolicy(variant, { trustMode: 'STRICT', worktreePath: '' });
|
|
832
|
-
|
|
1038
|
+
// Only a REFUSAL stops a recipe. Since #361 this function can also return
|
|
1039
|
+
// «ask», and the first line of this doc comment is the answer to it: an
|
|
1040
|
+
// approved recipe does not ask, because a human already read this exact
|
|
1041
|
+
// command on the approval screen. Turning that ask into `{allowed: false}`
|
|
1042
|
+
// would hard-refuse every build script that starts with `git checkout main`
|
|
1043
|
+
// — a refusal invented by a rule about AGENTS, applied to a text a person
|
|
1044
|
+
// wrote and approved.
|
|
1045
|
+
if (git?.decision === 'deny') {
|
|
833
1046
|
// The sentence is rewritten, not passed through (QA-134 MINOR-2). The
|
|
834
1047
|
// generic one says «this project has «Принудительно запретить push»
|
|
835
1048
|
// switched on» — and a project that switched it OFF would be sent to look
|
|
@@ -1001,11 +1214,19 @@ export function evaluateToolUse(toolName, input, ctx) {
|
|
|
1001
1214
|
// and nothing after this line is consulted. That is exactly how «agents
|
|
1002
1215
|
// never push» quietly failed for a whole release (session 13), and it is
|
|
1003
1216
|
// the reason these rules did not simply move onto the safe list.
|
|
1217
|
+
// Both spellings are read to the end before anything is returned: a `deny`
|
|
1218
|
+
// in the dequoted form must beat an `ask` in the raw one, for the same
|
|
1219
|
+
// reason a later segment beats an earlier one (see `evaluateGitPolicy`).
|
|
1220
|
+
let gitAsk = null;
|
|
1004
1221
|
for (const variant of [command, dequote(command)]) {
|
|
1005
1222
|
const git = evaluateGitPolicy(variant, ctx);
|
|
1006
|
-
if (git)
|
|
1223
|
+
if (git?.decision === 'deny')
|
|
1007
1224
|
return git;
|
|
1225
|
+
if (git)
|
|
1226
|
+
gitAsk ??= git;
|
|
1008
1227
|
}
|
|
1228
|
+
if (gitAsk)
|
|
1229
|
+
return gitAsk;
|
|
1009
1230
|
if (trust === 'STRICT')
|
|
1010
1231
|
return { decision: 'ask', reason: 'strict mode' };
|
|
1011
1232
|
if (trust === 'AUTO')
|
package/dist/protocol.d.ts
CHANGED
|
@@ -242,6 +242,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
242
242
|
url: string;
|
|
243
243
|
token: string;
|
|
244
244
|
} | undefined;
|
|
245
|
+
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
245
246
|
pausedUntil?: string | null | undefined;
|
|
246
247
|
agentLatestVersion?: string | undefined;
|
|
247
248
|
skipAgentPrompt?: boolean | undefined;
|
|
@@ -252,7 +253,6 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
252
253
|
baseBranch?: string | undefined;
|
|
253
254
|
baseSha?: string | undefined;
|
|
254
255
|
} | undefined;
|
|
255
|
-
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
256
256
|
}, {
|
|
257
257
|
agent: "CLAUDE" | "CODEX";
|
|
258
258
|
id: string;
|
|
@@ -284,6 +284,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
284
284
|
token: string;
|
|
285
285
|
} | undefined;
|
|
286
286
|
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
287
|
+
workMode?: unknown;
|
|
287
288
|
model?: string | null | undefined;
|
|
288
289
|
effort?: string | null | undefined;
|
|
289
290
|
epoch?: number | undefined;
|
|
@@ -296,7 +297,6 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
296
297
|
skipAgentPrompt?: unknown;
|
|
297
298
|
branchHint?: unknown;
|
|
298
299
|
branchPlan?: unknown;
|
|
299
|
-
workMode?: unknown;
|
|
300
300
|
}>;
|
|
301
301
|
export type SessionDescriptor = z.infer<typeof SessionDescriptorSchema>;
|
|
302
302
|
/**
|
|
@@ -660,6 +660,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
660
660
|
url: string;
|
|
661
661
|
token: string;
|
|
662
662
|
} | undefined;
|
|
663
|
+
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
663
664
|
pausedUntil?: string | null | undefined;
|
|
664
665
|
agentLatestVersion?: string | undefined;
|
|
665
666
|
skipAgentPrompt?: boolean | undefined;
|
|
@@ -670,7 +671,6 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
670
671
|
baseBranch?: string | undefined;
|
|
671
672
|
baseSha?: string | undefined;
|
|
672
673
|
} | undefined;
|
|
673
|
-
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
674
674
|
}, {
|
|
675
675
|
agent: "CLAUDE" | "CODEX";
|
|
676
676
|
id: string;
|
|
@@ -702,6 +702,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
702
702
|
token: string;
|
|
703
703
|
} | undefined;
|
|
704
704
|
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
705
|
+
workMode?: unknown;
|
|
705
706
|
model?: string | null | undefined;
|
|
706
707
|
effort?: string | null | undefined;
|
|
707
708
|
epoch?: number | undefined;
|
|
@@ -714,12 +715,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
714
715
|
skipAgentPrompt?: unknown;
|
|
715
716
|
branchHint?: unknown;
|
|
716
717
|
branchPlan?: unknown;
|
|
717
|
-
workMode?: unknown;
|
|
718
718
|
}>, "many">;
|
|
719
719
|
}, "strip", z.ZodTypeAny, {
|
|
720
|
-
type: "hello_ack";
|
|
721
|
-
serverName: string;
|
|
722
|
-
serverId: string;
|
|
723
720
|
sessions: {
|
|
724
721
|
mode: "ask" | "plan" | "auto" | "full";
|
|
725
722
|
agent: "CLAUDE" | "CODEX";
|
|
@@ -758,6 +755,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
758
755
|
url: string;
|
|
759
756
|
token: string;
|
|
760
757
|
} | undefined;
|
|
758
|
+
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
761
759
|
pausedUntil?: string | null | undefined;
|
|
762
760
|
agentLatestVersion?: string | undefined;
|
|
763
761
|
skipAgentPrompt?: boolean | undefined;
|
|
@@ -768,13 +766,12 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
768
766
|
baseBranch?: string | undefined;
|
|
769
767
|
baseSha?: string | undefined;
|
|
770
768
|
} | undefined;
|
|
771
|
-
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
772
769
|
}[];
|
|
773
|
-
maxSessions?: number | undefined;
|
|
774
|
-
}, {
|
|
775
770
|
type: "hello_ack";
|
|
776
771
|
serverName: string;
|
|
777
772
|
serverId: string;
|
|
773
|
+
maxSessions?: number | undefined;
|
|
774
|
+
}, {
|
|
778
775
|
sessions: {
|
|
779
776
|
agent: "CLAUDE" | "CODEX";
|
|
780
777
|
id: string;
|
|
@@ -806,6 +803,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
806
803
|
token: string;
|
|
807
804
|
} | undefined;
|
|
808
805
|
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
806
|
+
workMode?: unknown;
|
|
809
807
|
model?: string | null | undefined;
|
|
810
808
|
effort?: string | null | undefined;
|
|
811
809
|
epoch?: number | undefined;
|
|
@@ -818,8 +816,10 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
818
816
|
skipAgentPrompt?: unknown;
|
|
819
817
|
branchHint?: unknown;
|
|
820
818
|
branchPlan?: unknown;
|
|
821
|
-
workMode?: unknown;
|
|
822
819
|
}[];
|
|
820
|
+
type: "hello_ack";
|
|
821
|
+
serverName: string;
|
|
822
|
+
serverId: string;
|
|
823
823
|
maxSessions?: unknown;
|
|
824
824
|
}>, z.ZodObject<{
|
|
825
825
|
type: z.ZodLiteral<"event_ack">;
|
|
@@ -1093,6 +1093,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1093
1093
|
url: string;
|
|
1094
1094
|
token: string;
|
|
1095
1095
|
} | undefined;
|
|
1096
|
+
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
1096
1097
|
pausedUntil?: string | null | undefined;
|
|
1097
1098
|
agentLatestVersion?: string | undefined;
|
|
1098
1099
|
skipAgentPrompt?: boolean | undefined;
|
|
@@ -1103,7 +1104,6 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1103
1104
|
baseBranch?: string | undefined;
|
|
1104
1105
|
baseSha?: string | undefined;
|
|
1105
1106
|
} | undefined;
|
|
1106
|
-
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
1107
1107
|
}, {
|
|
1108
1108
|
agent: "CLAUDE" | "CODEX";
|
|
1109
1109
|
id: string;
|
|
@@ -1135,6 +1135,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1135
1135
|
token: string;
|
|
1136
1136
|
} | undefined;
|
|
1137
1137
|
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
1138
|
+
workMode?: unknown;
|
|
1138
1139
|
model?: string | null | undefined;
|
|
1139
1140
|
effort?: string | null | undefined;
|
|
1140
1141
|
epoch?: number | undefined;
|
|
@@ -1147,7 +1148,6 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1147
1148
|
skipAgentPrompt?: unknown;
|
|
1148
1149
|
branchHint?: unknown;
|
|
1149
1150
|
branchPlan?: unknown;
|
|
1150
|
-
workMode?: unknown;
|
|
1151
1151
|
}>;
|
|
1152
1152
|
}, "strip", z.ZodTypeAny, {
|
|
1153
1153
|
type: "session_start";
|
|
@@ -1189,6 +1189,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1189
1189
|
url: string;
|
|
1190
1190
|
token: string;
|
|
1191
1191
|
} | undefined;
|
|
1192
|
+
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
1192
1193
|
pausedUntil?: string | null | undefined;
|
|
1193
1194
|
agentLatestVersion?: string | undefined;
|
|
1194
1195
|
skipAgentPrompt?: boolean | undefined;
|
|
@@ -1199,7 +1200,6 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1199
1200
|
baseBranch?: string | undefined;
|
|
1200
1201
|
baseSha?: string | undefined;
|
|
1201
1202
|
} | undefined;
|
|
1202
|
-
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
1203
1203
|
};
|
|
1204
1204
|
}, {
|
|
1205
1205
|
type: "session_start";
|
|
@@ -1234,6 +1234,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1234
1234
|
token: string;
|
|
1235
1235
|
} | undefined;
|
|
1236
1236
|
mode?: "ask" | "plan" | "auto" | "full" | undefined;
|
|
1237
|
+
workMode?: unknown;
|
|
1237
1238
|
model?: string | null | undefined;
|
|
1238
1239
|
effort?: string | null | undefined;
|
|
1239
1240
|
epoch?: number | undefined;
|
|
@@ -1246,7 +1247,6 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1246
1247
|
skipAgentPrompt?: unknown;
|
|
1247
1248
|
branchHint?: unknown;
|
|
1248
1249
|
branchPlan?: unknown;
|
|
1249
|
-
workMode?: unknown;
|
|
1250
1250
|
};
|
|
1251
1251
|
}>, z.ZodObject<{
|
|
1252
1252
|
type: z.ZodLiteral<"session_message">;
|
|
@@ -1406,8 +1406,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1406
1406
|
}, "strip", z.ZodTypeAny, {
|
|
1407
1407
|
type: "workspace_settings";
|
|
1408
1408
|
workspaceId: string;
|
|
1409
|
-
trustMode?: "STRICT" | "NORMAL" | "AUTO" | undefined;
|
|
1410
1409
|
agentAutoCommit?: boolean | undefined;
|
|
1410
|
+
trustMode?: "STRICT" | "NORMAL" | "AUTO" | undefined;
|
|
1411
1411
|
agentPushBan?: boolean | undefined;
|
|
1412
1412
|
agentProtectedBranches?: string[] | undefined;
|
|
1413
1413
|
agentAllowForcePush?: boolean | undefined;
|
|
@@ -1415,8 +1415,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1415
1415
|
}, {
|
|
1416
1416
|
type: "workspace_settings";
|
|
1417
1417
|
workspaceId: string;
|
|
1418
|
-
trustMode?: "STRICT" | "NORMAL" | "AUTO" | undefined;
|
|
1419
1418
|
agentAutoCommit?: boolean | undefined;
|
|
1419
|
+
trustMode?: "STRICT" | "NORMAL" | "AUTO" | undefined;
|
|
1420
1420
|
agentPushBan?: boolean | undefined;
|
|
1421
1421
|
agentProtectedBranches?: string[] | undefined;
|
|
1422
1422
|
agentAllowForcePush?: boolean | undefined;
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -359,6 +359,26 @@ export declare class Supervisor {
|
|
|
359
359
|
* would be a lock bought for nothing.
|
|
360
360
|
*/
|
|
361
361
|
private prepareWorkspace;
|
|
362
|
+
/**
|
|
363
|
+
* The working folder could not be built — say so in the feed, pin the fork
|
|
364
|
+
* point, and let the session go (#360).
|
|
365
|
+
*
|
|
366
|
+
* Both doors into `prepareWorkspace` end here. Three things happen in an
|
|
367
|
+
* order that is not free:
|
|
368
|
+
*
|
|
369
|
+
* 1. **The fork point is recorded first.** `git.ts` resolves it before it
|
|
370
|
+
* writes anything, precisely so that a failure still knows it. Without it
|
|
371
|
+
* the API keeps `base_sha = null` for the session's whole life, and
|
|
372
|
+
* «Continue» can never prove the leftover branch is the empty one this
|
|
373
|
+
* session created — which is the trap the ticket is about.
|
|
374
|
+
* 2. **The feed event goes out before the map entry is dropped**, because
|
|
375
|
+
* `sendEvent` needs it. Until now neither door sent one: a failed start
|
|
376
|
+
* was visible only as an `errorMessage` on the session row, and pressing
|
|
377
|
+
* «Continue» wipes that.
|
|
378
|
+
* 3. **Then the status, then the entry.** A FAILED session is over, and a
|
|
379
|
+
* leftover entry would hold one of the runner's few slots.
|
|
380
|
+
*/
|
|
381
|
+
private workspacePrepareFailed;
|
|
362
382
|
/**
|
|
363
383
|
* Spin the adapter up — for a fresh session, a resume-on-next-message, or a
|
|
364
384
|
* free CHAT session with no prompt at all (the agent boots, reports its
|
|
@@ -526,7 +546,8 @@ export declare class Supervisor {
|
|
|
526
546
|
/**
|
|
527
547
|
* Make room for one more agent process.
|
|
528
548
|
*
|
|
529
|
-
* Up to `maxSessions` agents run side by side
|
|
549
|
+
* Up to `maxSessions` agents run side by side — since session 16 most of them
|
|
550
|
+
* share the project folder rather than each having a worktree of its own. Over
|
|
530
551
|
* that, idle-but-resumable sessions (REVIEW / WAITING_INPUT) are parked —
|
|
531
552
|
* their provider session survives on disk and relaunches on the next message,
|
|
532
553
|
* so parking costs context nothing. Only mid-turn sessions (RUNNING /
|
|
@@ -754,10 +775,35 @@ export declare class Supervisor {
|
|
|
754
775
|
* Three reasons it declines, and each of them is a state in which a snapshot
|
|
755
776
|
* would be a lie rather than a restore point:
|
|
756
777
|
* - the machine's owner switched checkpoints off;
|
|
757
|
-
* -
|
|
778
|
+
* - THIS session is mid-turn, so its own tree is being written as we read it;
|
|
758
779
|
* - a repo-mutating command holds the repository.
|
|
780
|
+
*
|
|
781
|
+
* «A neighbour in the same folder is working» used to be a fourth reason, and
|
|
782
|
+
* it was the wrong one (#310): in DIRECT mode the folder is shared by design,
|
|
783
|
+
* so that rule silently switched restore points off for everybody the moment
|
|
784
|
+
* a second session opened. The neighbours are recorded on the point instead —
|
|
785
|
+
* the conversation can always be rewound to it, the files cannot.
|
|
786
|
+
*
|
|
787
|
+
* Every refusal is now audible. A restore point that was never taken is
|
|
788
|
+
* invisible until the day somebody reaches for it, and «the button is not
|
|
789
|
+
* there» is not a sentence anybody can act on.
|
|
759
790
|
*/
|
|
760
791
|
private captureCheckpoint;
|
|
792
|
+
/**
|
|
793
|
+
* Say something once per BUSY PERIOD, not once per message (#310).
|
|
794
|
+
*
|
|
795
|
+
* A folder held by a neighbour stays held for minutes, and a session mid-turn
|
|
796
|
+
* can be sent three follow-up notes inside one answer. Keying this on the
|
|
797
|
+
* message seq would have counted each of those as its own turn and said the
|
|
798
|
+
* same sentence three times — the noise the frequency policy exists to
|
|
799
|
+
* prevent. The set is cleared when the session next comes to rest
|
|
800
|
+
* (`reportStatus`), which is exactly when the reason stops being true.
|
|
801
|
+
*
|
|
802
|
+
* A SET of keys, not the last one said: two different reasons can both come
|
|
803
|
+
* up inside one period, and remembering only the most recent would let them
|
|
804
|
+
* take turns re-announcing each other.
|
|
805
|
+
*/
|
|
806
|
+
private noticeOncePerTurn;
|
|
761
807
|
/**
|
|
762
808
|
* Deliver messages that raced session start (already journaled).
|
|
763
809
|
*
|
|
@@ -887,7 +933,15 @@ export declare class Supervisor {
|
|
|
887
933
|
private stopWorkUnderPause;
|
|
888
934
|
/** Live model / interaction-mode switch (persisted for the next relaunch). */
|
|
889
935
|
private applySettings;
|
|
890
|
-
/**
|
|
936
|
+
/**
|
|
937
|
+
* Is a turn (or a question the agent is parked on) in flight right now?
|
|
938
|
+
*
|
|
939
|
+
* The same status list as `holdsTheTree` plus one term: an open question
|
|
940
|
+
* parks the session without any of those statuses, and a live settings change
|
|
941
|
+
* must not land under it. Reading the shared array rather than spelling the
|
|
942
|
+
* statuses out again is the whole point of Р8 — a fourth waiting status has
|
|
943
|
+
* to arrive in one place, not four.
|
|
944
|
+
*/
|
|
891
945
|
private isMidTurn;
|
|
892
946
|
/** The three live setters, in the order that lets an explicit pick win. */
|
|
893
947
|
private applyLiveSettings;
|
|
@@ -976,8 +1030,50 @@ export declare class Supervisor {
|
|
|
976
1030
|
* has to branch on the type, so "I did not check" cannot compile.
|
|
977
1031
|
*/
|
|
978
1032
|
private requireSession;
|
|
1033
|
+
/**
|
|
1034
|
+
* The statuses in which a session is holding its working tree (#310).
|
|
1035
|
+
*
|
|
1036
|
+
* One array, read by all three predicates below. There used to be one
|
|
1037
|
+
* literal, then two would have been needed, and a third would have been
|
|
1038
|
+
* written the day somebody added a waiting state — which is how «the agent
|
|
1039
|
+
* is busy» and «the folder is busy» come to disagree about what busy means.
|
|
1040
|
+
* (`rewindActionsLive` in the dashboard is a fourth reader and a deliberate
|
|
1041
|
+
* mirror: the runner does not depend on `@devbridge/shared`.)
|
|
1042
|
+
*/
|
|
1043
|
+
private static readonly MID_TURN_STATUSES;
|
|
1044
|
+
/**
|
|
1045
|
+
* Is a stop still in flight over this session's live process?
|
|
1046
|
+
*
|
|
1047
|
+
* Three terms, written out in three places before this: an open cycle, that
|
|
1048
|
+
* cycle owning the process that is running NOW, and the cycle not yet
|
|
1049
|
+
* settled. #373 is what makes it load-bearing — the resting status is
|
|
1050
|
+
* published before the process dies, so this is the difference between «the
|
|
1051
|
+
* session is quiet» and «the session is still writing files».
|
|
1052
|
+
*/
|
|
1053
|
+
private static stopInFlight;
|
|
1054
|
+
private static holdsTheTree;
|
|
979
1055
|
/** A session actively mid-turn in this worktree — git writes must wait. */
|
|
980
1056
|
private isWorktreeBusy;
|
|
1057
|
+
/**
|
|
1058
|
+
* Is THIS session mid-turn? (#310)
|
|
1059
|
+
*
|
|
1060
|
+
* The question `isWorktreeBusy` was answering in three places where the right
|
|
1061
|
+
* question was this one. They are the same question only when a folder holds
|
|
1062
|
+
* exactly one session — and DIRECT mode, the default since session 16, is
|
|
1063
|
+
* precisely the arrangement in which it holds several. A restore point is a
|
|
1064
|
+
* snapshot of this session's own conversation; a neighbour typing in the same
|
|
1065
|
+
* folder is a reason to MARK it, not to refuse to take it.
|
|
1066
|
+
*/
|
|
1067
|
+
private isSessionMidTurn;
|
|
1068
|
+
/**
|
|
1069
|
+
* Everybody else who is working in this folder right now (#310).
|
|
1070
|
+
*
|
|
1071
|
+
* Includes a neighbour whose stop is still running: after #373 the resting
|
|
1072
|
+
* status is published BEFORE the process is actually gone, so a session that
|
|
1073
|
+
* reports IDLE while its stop cycle still owns a live process is still
|
|
1074
|
+
* writing files. Reading `lastReported` alone would call that folder quiet.
|
|
1075
|
+
*/
|
|
1076
|
+
private busyNeighbours;
|
|
981
1077
|
private static readonly EVENT_PAYLOAD_CAP;
|
|
982
1078
|
/**
|
|
983
1079
|
* Journal an event, put it on the wire, and return it.
|