@dieulc/workflow 0.6.1 → 0.8.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/README.md +126 -40
- package/commit-policy.ts +43 -0
- package/edit-policy.ts +193 -14
- package/index.ts +3090 -1148
- package/model-default.ts +535 -0
- package/package.json +10 -3
- package/picker.ts +246 -0
- package/profiles.ts +98 -1
- package/questionnaire-layout.ts +340 -0
- package/questionnaire-view.ts +909 -0
- package/review-runtime.ts +260 -74
- package/review-ui.ts +27 -4
- package/review.ts +516 -16
- package/tab-editor.ts +147 -0
- package/tab-policy.ts +42 -0
- package/utils.ts +372 -69
package/edit-policy.ts
CHANGED
|
@@ -753,23 +753,44 @@ export function sanitizeWriteInstruction(raw: unknown): string {
|
|
|
753
753
|
* The gate consumes this and `parseWriteGateChoice` below, so a renamed
|
|
754
754
|
* label can never silently change the gate's behaviour (the unit test maps
|
|
755
755
|
* every real option through the parser).
|
|
756
|
+
*
|
|
757
|
+
* The labels state the scope they really have, because ONE answer now
|
|
758
|
+
* covers every gated write of the turn (see the turn-scoped helpers below):
|
|
759
|
+
* "Allow once (this turn)" allows the writes the dialog listed, and
|
|
760
|
+
* "Always allow …" persists a grant for every one of them.
|
|
756
761
|
*/
|
|
757
762
|
export function writeGateOptions(
|
|
758
|
-
kind:
|
|
763
|
+
kind: WriteGateOptionKind,
|
|
759
764
|
): readonly string[] {
|
|
760
|
-
return
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
765
|
+
return [
|
|
766
|
+
"Allow once (this turn)",
|
|
767
|
+
writeGateAlwaysLabel(kind),
|
|
768
|
+
"Deny",
|
|
769
|
+
WRITE_DENY_WITH_INSTRUCTIONS_LABEL,
|
|
770
|
+
];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* The "always allow" label for a set of items — singular, plural or mixed.
|
|
775
|
+
*
|
|
776
|
+
* The prefix "Always allow" is what `parseWriteGateChoice` matches on, so
|
|
777
|
+
* every variant parses to the same choice.
|
|
778
|
+
*/
|
|
779
|
+
export function writeGateAlwaysLabel(kind: WriteGateOptionKind): string {
|
|
780
|
+
switch (kind) {
|
|
781
|
+
case "path":
|
|
782
|
+
return "Always allow this path";
|
|
783
|
+
case "paths":
|
|
784
|
+
return "Always allow these paths";
|
|
785
|
+
case "command":
|
|
786
|
+
return "Always allow this command";
|
|
787
|
+
case "commands":
|
|
788
|
+
return "Always allow these commands";
|
|
789
|
+
case "mixed":
|
|
790
|
+
return "Always allow these";
|
|
791
|
+
default:
|
|
792
|
+
return "Always allow these";
|
|
793
|
+
}
|
|
773
794
|
}
|
|
774
795
|
|
|
775
796
|
/** What the ask dialog's answer means. */
|
|
@@ -855,6 +876,164 @@ export function buildWriteGuidanceMessage(input: {
|
|
|
855
876
|
);
|
|
856
877
|
}
|
|
857
878
|
|
|
879
|
+
// ── Turn-scoped gate helpers ───────────────────────────────────────────
|
|
880
|
+
//
|
|
881
|
+
// The gate asks ONE question per assistant turn — not one per tool call.
|
|
882
|
+
//
|
|
883
|
+
// WHY: Pi executes an assistant message's tool calls as a BATCH, and the
|
|
884
|
+
// extension's `tool_call` hook runs in the batch's PREPARATION phase: once
|
|
885
|
+
// per call, sequentially, before any tool in the batch runs
|
|
886
|
+
// (pi-agent-core/dist/agent-loop.js → executeToolCallsParallel →
|
|
887
|
+
// prepareToolCall → beforeToolCall). A model that batches two writes to the
|
|
888
|
+
// same file therefore produced two dialogs back to back, with no agent
|
|
889
|
+
// output in between — and the instruction typed into the first one could
|
|
890
|
+
// not reach the model before the second one opened, because a tool result is
|
|
891
|
+
// appended only after the whole batch and a steer message lands only after
|
|
892
|
+
// the turn's tool calls have run. The user had to retype the same sentence.
|
|
893
|
+
//
|
|
894
|
+
// So the gate collects the turn's gated writes, shows them all in one
|
|
895
|
+
// dialog, and applies the answer to every one of them.
|
|
896
|
+
|
|
897
|
+
/** One gated write covered by a single dialog answer. */
|
|
898
|
+
export interface WriteGateItem {
|
|
899
|
+
/** The tool that would write (edit, write, bash, powershell, …). */
|
|
900
|
+
tool: string;
|
|
901
|
+
/** Human target: the joined paths, or the command/tool key. */
|
|
902
|
+
target: string;
|
|
903
|
+
/** Short cause, e.g. "edits src/a.ts". */
|
|
904
|
+
why?: string;
|
|
905
|
+
/** Stable key a denial is remembered under (see `writeGateTargetKey`). */
|
|
906
|
+
key: string;
|
|
907
|
+
/** Path grants "always allow" would persist for this item. */
|
|
908
|
+
grantPaths?: string[];
|
|
909
|
+
/** Command (or tool) grant "always allow" would persist for this item. */
|
|
910
|
+
grantCommand?: string;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Which "always allow" label fits a set of items.
|
|
915
|
+
*
|
|
916
|
+
* "path"/"paths" — every item writes files; "command"/"commands" — every
|
|
917
|
+
* item runs a command (or a path-less tool, whose grant is stored in the
|
|
918
|
+
* command list); "mixed" — both in one turn.
|
|
919
|
+
*/
|
|
920
|
+
export type WriteGateOptionKind =
|
|
921
|
+
| "path"
|
|
922
|
+
| "paths"
|
|
923
|
+
| "command"
|
|
924
|
+
| "commands"
|
|
925
|
+
| "mixed";
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* The stable key a denial is remembered under.
|
|
929
|
+
*
|
|
930
|
+
* Path writes key on their normalized absolute path(s) (so the same file
|
|
931
|
+
* keys the same way whatever the model spelled it as), command writes on
|
|
932
|
+
* their allow-key, and a path-less tool on its tool key.
|
|
933
|
+
*/
|
|
934
|
+
export function writeGateTargetKey(
|
|
935
|
+
toolName: string,
|
|
936
|
+
classification: WriteCallClassification,
|
|
937
|
+
cwd: string,
|
|
938
|
+
): string {
|
|
939
|
+
const paths = classification.paths
|
|
940
|
+
.map((p) => normalizeWritePath(p, cwd))
|
|
941
|
+
.filter((p) => p.length > 0)
|
|
942
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
943
|
+
if (paths.length > 0) return `path:${paths.join("|")}`;
|
|
944
|
+
if (classification.key) return `cmd:${commandAllowKey(classification.key)}`;
|
|
945
|
+
return `tool:${toolAllowKey(toolName)}`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Build the item for one classified call.
|
|
950
|
+
*
|
|
951
|
+
* `grantPaths` / `grantCommand` are what "always allow" would persist for
|
|
952
|
+
* this item: the normalized paths for a file write, the allow-key for a
|
|
953
|
+
* command or a path-less tool.
|
|
954
|
+
*/
|
|
955
|
+
export function writeGateItemFromClassification(
|
|
956
|
+
toolName: string,
|
|
957
|
+
classification: WriteCallClassification,
|
|
958
|
+
cwd: string,
|
|
959
|
+
): WriteGateItem {
|
|
960
|
+
const tool = String(toolName ?? "");
|
|
961
|
+
const rawPaths = classification.paths.filter(
|
|
962
|
+
(p) => typeof p === "string" && p.trim().length > 0,
|
|
963
|
+
);
|
|
964
|
+
const grantPaths = rawPaths
|
|
965
|
+
.map((p) => normalizeWritePath(p, cwd))
|
|
966
|
+
.filter((p) => p.length > 0);
|
|
967
|
+
const item: WriteGateItem = {
|
|
968
|
+
tool,
|
|
969
|
+
target: rawPaths.length > 0 ? rawPaths.join(", ") : (classification.key ?? tool),
|
|
970
|
+
why: classification.reason,
|
|
971
|
+
key: writeGateTargetKey(tool, classification, cwd),
|
|
972
|
+
};
|
|
973
|
+
if (grantPaths.length > 0) item.grantPaths = grantPaths;
|
|
974
|
+
else if (classification.key) item.grantCommand = classification.key;
|
|
975
|
+
return item;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** Which "always allow" label fits a set of items. */
|
|
979
|
+
export function writeGateItemsKind(
|
|
980
|
+
items: readonly WriteGateItem[],
|
|
981
|
+
): WriteGateOptionKind {
|
|
982
|
+
const paths = items.filter((i) => (i.grantPaths?.length ?? 0) > 0);
|
|
983
|
+
const commands = items.filter((i) => Boolean(i.grantCommand));
|
|
984
|
+
if (paths.length > 0 && commands.length > 0) return "mixed";
|
|
985
|
+
if (paths.length > 0) return paths.length === 1 ? "path" : "paths";
|
|
986
|
+
if (commands.length > 0) return commands.length === 1 ? "command" : "commands";
|
|
987
|
+
return "path";
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* The human list of the writes a dialog covers:
|
|
992
|
+
* "edits src/a.ts; edits src/b.ts".
|
|
993
|
+
*/
|
|
994
|
+
export function describeWriteGateItems(
|
|
995
|
+
items: readonly WriteGateItem[],
|
|
996
|
+
): string {
|
|
997
|
+
return items
|
|
998
|
+
.map((item) => {
|
|
999
|
+
const why = String(item.why ?? "").trim();
|
|
1000
|
+
return why || String(item.target ?? "").trim();
|
|
1001
|
+
})
|
|
1002
|
+
.filter((text) => text.length > 0)
|
|
1003
|
+
.join("; ");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* The targets of a set of items, joined for the reason/guidance text:
|
|
1008
|
+
* "src/a.ts, src/b.ts".
|
|
1009
|
+
*/
|
|
1010
|
+
export function writeGateTargetsLabel(
|
|
1011
|
+
items: readonly WriteGateItem[],
|
|
1012
|
+
): string {
|
|
1013
|
+
return items
|
|
1014
|
+
.map((item) => String(item.target ?? "").trim())
|
|
1015
|
+
.filter((text) => text.length > 0)
|
|
1016
|
+
.join(", ");
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* The dialog title.
|
|
1021
|
+
*
|
|
1022
|
+
* One write keeps today's wording verbatim; several writes are listed, so
|
|
1023
|
+
* the user sees exactly what the answer covers.
|
|
1024
|
+
*/
|
|
1025
|
+
export function writeGateTitle(items: readonly WriteGateItem[]): string {
|
|
1026
|
+
if (items.length <= 1) {
|
|
1027
|
+
const only = items[0];
|
|
1028
|
+
const why = only?.why ? ` (${only.why})` : "";
|
|
1029
|
+
return `Write policy 'ask': allow this write${why}?`;
|
|
1030
|
+
}
|
|
1031
|
+
return (
|
|
1032
|
+
`Write policy 'ask': allow these ${items.length} writes? ` +
|
|
1033
|
+
`(${describeWriteGateItems(items)})`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
858
1037
|
// ── Argument completions (/write-policy) ────────────────────────────
|
|
859
1038
|
|
|
860
1039
|
/** One /write-policy subcommand with its usage hint. */
|